(merge)
authorJulien Jehannet <julien.jehannet@logilab.fr>
Fri, 30 Jan 2009 15:55:03 +0100
changeset 539 6ed63265764e
parent 538 da51759027d0 (current diff)
parent 537 f16da6c874da (diff)
child 540 e5c97f6f119d
(merge)
--- a/common/uilib.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/common/uilib.py	Fri Jan 30 15:55:03 2009 +0100
@@ -4,7 +4,7 @@
 contains some functions designed to help implementation of cubicweb user interface
 
 :organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
@@ -213,6 +213,21 @@
     
 # HTML generation helper functions ############################################
 
+def simple_sgml_tag(tag, content=None, **attrs):
+    """generation of a simple sgml tag (eg without children tags) easier
+
+    content and attributes will be escaped
+    """
+    value = u'<%s' % tag
+    if attrs:
+        value += u' ' + u' '.join(u'%s="%s"' % (attr, html_escape(unicode(value)))
+                                  for attr, value in attrs.items())
+    if content:
+        value += u'>%s</%s>' % (html_escape(unicode(content)), tag)
+    else:
+        value += u'/>'
+    return value
+
 def tooltipize(text, tooltip, url=None):
     """make an HTML tooltip"""
     url = url or '#'
--- a/devtools/testlib.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/devtools/testlib.py	Fri Jan 30 15:55:03 2009 +0100
@@ -93,28 +93,24 @@
     #  SaxOnlyValidator : guarantees XML is well formed
     #  None : do not try to validate anything
     # validators used must be imported from from.devtools.htmlparser
-    validators = {
-        # maps vid : validator name
-        'hcal' : SaxOnlyValidator,
-        'rss' : SaxOnlyValidator,
-        'rssitem' : None,
-        'xml' : SaxOnlyValidator,
-        'xmlitem' : None,
-        'xbel' : SaxOnlyValidator,
-        'xbelitem' : None,
-        'vcard' : None,
-        'fulltext': None,
-        'fullthreadtext': None,
-        'fullthreadtext_descending': None,
-        'text' : None,
-        'treeitemview': None,
-        'textincontext' : None,
-        'textoutofcontext' : None,
-        'combobox' : None,
-        'csvexport' : None,
-        'ecsvexport' : None,
-        'owl' : SaxOnlyValidator, # XXX
-        'owlabox' : SaxOnlyValidator, # XXX
+    content_type_validators = {
+        # maps MIME type : validator name
+        #
+        # do not set html validators here, we need HTMLValidator for html
+        # snippets
+        #'text/html': DTDValidator,
+        #'application/xhtml+xml': DTDValidator,
+        'application/xml': SaxOnlyValidator,
+        'text/xml': SaxOnlyValidator,
+        'text/plain': None,
+        'text/comma-separated-values': None,
+        'text/x-vcard': None,
+        'text/calendar': None,
+        'application/json': None,
+        'image/png': None,
+        }
+    vid_validators = {
+        # maps vid : validator name (override content_type_validators)
         }
     valmap = {None: None, 'dtd': DTDValidator, 'xml': SaxOnlyValidator}
     no_auto_populate = ()
@@ -165,21 +161,24 @@
         self.commit()
 
     @nocoverage
-    def _check_html(self, output, vid, template='main'):
+    def _check_html(self, output, view, template='main'):
         """raises an exception if the HTML is invalid"""
-        if template is None:
-            default_validator = HTMLValidator
-        else:
-            default_validator = DTDValidator
-        validatorclass = self.validators.get(vid, default_validator)
+        try:
+            validatorclass = self.vid_validators[view.id]
+        except KeyError:
+            if template is None:
+                default_validator = HTMLValidator
+            else:
+                default_validator = DTDValidator
+            validatorclass = self.content_type_validators.get(view.content_type,
+                                                              default_validator)
         if validatorclass is None:
             return None
         validator = validatorclass()
-        output = output.strip()
-        return validator.parse_string(output)
+        return validator.parse_string(output.strip())
 
 
-    def view(self, vid, rset, req=None, template='main', htmlcheck=True, **kwargs):
+    def view(self, vid, rset, req=None, template='main', **kwargs):
         """This method tests the view `vid` on `rset` using `template`
 
         If no error occured while rendering the view, the HTML is analyzed
@@ -196,8 +195,6 @@
         #     print 
         req.form['vid'] = vid
         view = self.vreg.select_view(vid, req, rset, **kwargs)
-        if view.content_type not in ('application/xml', 'application/xhtml+xml', 'text/html'):
-            htmlcheck = False
         # set explicit test description
         if rset is not None:
             self.set_description("testing %s, mod=%s (%s)" % (vid, view.__module__, rset.printable_rql()))
@@ -211,13 +208,13 @@
             # patch TheMainTemplate.process_rql to avoid recomputing resultset
             TheMainTemplate._select_view_and_rset = lambda *a, **k: (view, rset)
         try:
-            return self._test_view(viewfunc, vid, htmlcheck, template, **kwargs)
+            return self._test_view(viewfunc, view, template, **kwargs)
         finally:
             if template == 'main':
                 TheMainTemplate._select_view_and_rset = _select_view_and_rset
 
 
-    def _test_view(self, viewfunc, vid, htmlcheck=True, template='main', **kwargs):
+    def _test_view(self, viewfunc, view, template='main', **kwargs):
         """this method does the actual call to the view
 
         If no error occured while rendering the view, the HTML is analyzed
@@ -229,10 +226,7 @@
         output = None
         try:
             output = viewfunc(**kwargs)
-            if htmlcheck:
-                return self._check_html(output, vid, template)
-            else:
-                return output
+            return self._check_html(output, view, template)
         except (SystemExit, KeyboardInterrupt):
             raise
         except:
@@ -240,19 +234,16 @@
             # is not an AssertionError
             klass, exc, tcbk = sys.exc_info()
             try:
-                msg = '[%s in %s] %s' % (klass, vid, exc)
+                msg = '[%s in %s] %s' % (klass, view.id, exc)
             except:
-                msg = '[%s in %s] undisplayable exception' % (klass, vid)
+                msg = '[%s in %s] undisplayable exception' % (klass, view.id)
             if output is not None:
                 position = getattr(exc, "position", (0,))[0]
                 if position:
                     # define filter
-                    
-                    
                     output = output.splitlines()
                     width = int(log(len(output), 10)) + 1
                     line_template = " %" + ("%i" % width) + "i: %s"
-
                     # XXX no need to iterate the whole file except to get
                     # the line number
                     output = '\n'.join(line_template % (idx + 1, line)
@@ -286,23 +277,26 @@
         """returns the list of views that can be applied on `rset`"""
         req = rset.req
         only_once_vids = ('primary', 'secondary', 'text')
-        skipped = ('restriction', 'cell')
         req.data['ex'] = ValueError("whatever")
         for vid, views in self.vreg.registry('views').items():
             if vid[0] == '_':
                 continue
-            try:
-                view = self.vreg.select(views, req, rset)
-                if view.id in skipped:
-                    continue
-                if view.category == 'startupview':
+            if rset.rowcount > 1 and vid in only_once_vids:
+                continue
+            views = [view for view in views
+                     if view.category != 'startupview'
+                     and not issubclass(view, NotificationView)]
+            if views:
+                try:
+                    view = self.vreg.select(views, req, rset)
+                    if view.linkable():
+                        yield view
+                    else:
+                        not_selected(self.vreg, view)
+                    # else the view is expected to be used as subview and should
+                    # not be tested directly
+                except NoSelectableObject:
                     continue
-                if rset.rowcount > 1 and view.id in only_once_vids:
-                    continue
-                if not isinstance(view, NotificationView):
-                    yield view
-            except NoSelectableObject:
-                continue
 
     def list_actions_for(self, rset):
         """returns the list of actions that can be applied on `rset`"""
@@ -310,22 +304,21 @@
         for action in self.vreg.possible_objects('actions', req, rset):
             yield action
 
-        
     def list_boxes_for(self, rset):
         """returns the list of boxes that can be applied on `rset`"""
         req = rset.req
         for box in self.vreg.possible_objects('boxes', req, rset):
             yield box
             
-        
     def list_startup_views(self):
         """returns the list of startup views"""
         req = self.request()
         for view in self.vreg.possible_views(req, None):
-            if view.category != 'startupview':
-                continue
-            yield view.id
-
+            if view.category == 'startupview':
+                yield view.id
+            else:
+                not_selected(self.vreg, view)
+                
     def _test_everything_for(self, rset):
         """this method tries to find everything that can be tested
         for `rset` and yields a callable test (as needed in generative tests)
@@ -339,7 +332,7 @@
             backup_rset = rset._prepare_copy(rset.rows, rset.description)
             yield InnerTest(self._testname(rset, view.id, 'view'),
                             self.view, view.id, rset,
-                            rset.req.reset_headers(), 'main', not view.binary)
+                            rset.req.reset_headers(), 'main')
             # We have to do this because some views modify the
             # resultset's syntax tree
             rset = backup_rset
@@ -352,8 +345,6 @@
         for box in self.list_boxes_for(rset):
             yield InnerTest(self._testname(rset, box.id, 'box'), box.dispatch)
 
-
-
     @staticmethod
     def _testname(rset, objid, objtype):
         return '%s_%s_%s' % ('_'.join(rset.column_types(0)), objid, objtype)
@@ -394,4 +385,36 @@
                 rset2 = rset.limit(limit=1, offset=row)
                 yield rset2
 
+def not_selected(vreg, vobject):
+    try:
+        vreg._selected[vobject.__class__] -= 1
+    except (KeyError, AttributeError):
+        pass
         
+def vreg_instrumentize(testclass):
+    from cubicweb.devtools.apptest import TestEnvironment
+    env = testclass._env = TestEnvironment('data', configcls=testclass.configcls,
+                                           requestcls=testclass.requestcls)
+    vreg = env.vreg
+    vreg._selected = {}
+    orig_select = vreg.__class__.select
+    def instr_select(self, *args, **kwargs):
+        selected = orig_select(self, *args, **kwargs)
+        try:
+            self._selected[selected.__class__] += 1
+        except KeyError:
+            self._selected[selected.__class__] = 1
+        except AttributeError:
+            pass # occurs on vreg used to restore database
+        return selected
+    vreg.__class__.select = instr_select
+
+def print_untested_objects(testclass, skipregs=('hooks', 'etypes')):
+    vreg = testclass._env.vreg
+    for registry, vobjectsdict in vreg.items():
+        if registry in skipregs:
+            continue
+        for vobjects in vobjectsdict.values():
+            for vobject in vobjects:
+                if not vreg._selected.get(vobject):
+                    print 'not tested', registry, vobject
--- a/web/box.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/web/box.py	Fri Jan 30 15:55:03 2009 +0100
@@ -158,8 +158,7 @@
     condition = None
     
     def call(self, row=0, col=0, **kwargs):
-        """classes inheriting from EntityBoxTemplate should defined cell_call,
-        """
+        """classes inheriting from EntityBoxTemplate should define cell_call"""
         self.cell_call(row, col, **kwargs)
 
 
--- a/web/component.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/web/component.py	Fri Jan 30 15:55:03 2009 +0100
@@ -56,7 +56,10 @@
     condition = None
     
     def call(self, view):
-        raise RuntimeError()
+        return self.cell_call(0, 0, view)
+
+    def cell_call(self, row, col, view):
+        raise NotImplementedError()
 
     
 class NavigationComponent(VComponent):
@@ -145,17 +148,17 @@
         """
         return None
     
-    def call(self, view=None):
+    def cell_call(self, row, col, view=None):
         rql = self.rql()
         if rql is None:
-            entity = self.rset.get_entity(0, 0)
+            entity = self.rset.get_entity(row, col)
             if self.target == 'object':
                 role = 'subject'
             else:
                 role = 'object'
             rset = entity.related(self.rtype, role)
         else:
-            eid = self.rset[0][0]
+            eid = self.rset[row][col]
             rset = self.req.execute(self.rql(), {'x': eid}, 'x')
         if not rset.rowcount:
             return
--- a/web/views/basecomponents.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/web/views/basecomponents.py	Fri Jan 30 15:55:03 2009 +0100
@@ -138,9 +138,9 @@
     target = 'subject'
     title = _('Workflow history')
 
-    def call(self, view=None):
+    def cell_call(self, row, col, view=None):
         _ = self.req._
-        eid = self.rset[0][0]
+        eid = self.rset[row][col]
         sel = 'Any FS,TS,WF,D'
         rql = ' ORDERBY D DESC WHERE WF wf_info_for X,'\
               'WF from_state FS, WF to_state TS, WF comment C,'\
--- a/web/views/baseviews.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/web/views/baseviews.py	Fri Jan 30 15:55:03 2009 +0100
@@ -13,19 +13,20 @@
 """
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
 from time import timezone
 
 from rql import nodes
 
 from logilab.common.decorators import cached
-from logilab.mtconverter import html_escape, TransformError
+from logilab.mtconverter import TransformError, html_escape, xml_escape
 
 from cubicweb import Unauthorized, NoSelectableObject, typed_eid
 from cubicweb.common.selectors import (yes, nonempty_rset, accept,
                                        one_line_rset, match_search_state, 
                                        match_form_params, accept_rset)
 from cubicweb.common.uilib import (cut, printable_value,  UnicodeCSVWriter,
-                                   ajax_replace_url, rql_for_eid)
+                                   ajax_replace_url, rql_for_eid, simple_sgml_tag)
 from cubicweb.common.view import EntityView, AnyRsetView, EmptyRsetView
 from cubicweb.web.httpcache import MaxAgeHTTPCacheManager
 from cubicweb.web.views import vid_from_rset, linksearch_select_url, linksearch_match
@@ -143,12 +144,7 @@
         self.w(u'<div class="mainInfo">')
         self.render_entity_attributes(entity, siderelations)
         self.w(u'</div>')
-        self.w(u'<div class="navcontenttop">')
-        for comp in self.vreg.possible_vobjects('contentnavigation',
-                                                self.req, self.rset,
-                                                view=self, context='navcontenttop'):
-            comp.dispatch(w=self.w, view=self)
-        self.w(u'</div>')
+        self.content_navigation_components('navcontenttop')
         if self.main_related_section:
             self.render_entity_relations(entity, siderelations)
         self.w(u'</td>')
@@ -158,13 +154,21 @@
         self.w(u'</td>')
         self.w(u'</tr>')
         self.w(u'</table>')        
-        self.w(u'<div class="navcontentbottom">')
+        self.content_navigation_components('navcontentbottom')
+
+    def content_navigation_components(self, context):
+        self.w(u'<div class="%s">' % context)
         for comp in self.vreg.possible_vobjects('contentnavigation',
-                                                self.req, self.rset,
-                                                view=self, context='navcontentbottom'):
-            comp.dispatch(w=self.w, view=self)
+                                                self.req, self.rset, row=self.row,
+                                                view=self, context=context):
+            try:
+                comp.dispatch(w=self.w, row=self.row, view=self)
+            except NotImplementedError:
+                warn('component %s doesnt implement cell_call, please update'
+                     % comp.__class__, DeprecationWarning)
+                comp.dispatch(w=self.w, view=self)
         self.w(u'</div>')
-
+        
     def iter_attributes(self, entity):
         for rschema, targetschema in entity.e_schema.attribute_definitions():
             attr = rschema.type
@@ -251,11 +255,11 @@
                 #    continue
                 self._render_related_entities(entity, *relatedinfos)
             self.w(u'</div>')
-        for box in self.vreg.possible_vobjects('boxes', self.req, entity.rset,
-                                               col=entity.col, row=entity.row,
-                                               view=self, context='incontext'):
+        for box in self.vreg.possible_vobjects('boxes', self.req, self.rset,
+                                               row=self.row, view=self,
+                                               context='incontext'):
             try:
-                box.dispatch(w=self.w, col=entity.col, row=entity.row)
+                box.dispatch(w=self.w, row=self.row)
             except NotImplementedError:
                 # much probably a context insensitive box, which only implements
                 # .call() and not cell_call()
@@ -350,10 +354,10 @@
         self.w(u'</a>')
 
 class TextView(EntityView):
-    """the simplest text view for an entity
-    """
+    """the simplest text view for an entity"""
     id = 'text'
     title = _('text')
+    content_type = 'text/plain'
     accepts = 'Any',
     def call(self, **kwargs):
         """the view is called for an entire result set, by default loop
@@ -571,8 +575,7 @@
         self.wview(self.item_vid, self.rset, row=row, col=col)
         
     def call(self):
-        """display a list of entities by calling their <item_vid> view
-        """
+        """display a list of entities by calling their <item_vid> view"""
         self.w(u'<?xml version="1.0" encoding="%s"?>\n' % self.req.encoding)
         self.w(u'<%s size="%s">\n' % (self.xml_root, len(self.rset)))
         for i in xrange(self.rset.rowcount):
@@ -599,7 +602,7 @@
                     from base64 import b64encode
                     value = '<![CDATA[%s]]>' % b64encode(value.getvalue())
                 elif isinstance(value, basestring):
-                    value = value.replace('&', '&amp;').replace('<', '&lt;')
+                    value = xml_escape(value)
                 self.w(u'  <%s>%s</%s>\n' % (attr, value, attr))
         self.w(u'</%s>\n' % (entity.e_schema))
 
@@ -619,21 +622,25 @@
         eschema = self.schema.eschema
         labels = self.columns_labels(False)
         w(u'<?xml version="1.0" encoding="%s"?>\n' % self.req.encoding)
-        w(u'<%s>\n' % self.xml_root)
+        w(u'<%s query="%s">\n' % (self.xml_root, html_escape(rset.printable_rql())))
         for rowindex, row in enumerate(self.rset):
             w(u' <row>\n')
             for colindex, val in enumerate(row):
                 etype = descr[rowindex][colindex]
                 tag = labels[colindex]
+                attrs = {}
+                if '(' in tag:
+                    attrs['expr'] = tag
+                    tag = 'funccall'
                 if val is not None and not eschema(etype).is_final():
+                    attrs['eid'] = val
                     # csvrow.append(val) # val is eid in that case
-                    content = self.view('textincontext', rset, 
-                                        row=rowindex, col=colindex)
-                    w(u'  <%s eid="%s">%s</%s>\n' % (tag, val, html_escape(content), tag))
+                    val = self.view('textincontext', rset,
+                                    row=rowindex, col=colindex)
                 else:
-                    content = self.view('final', rset, displaytime=True,
-                                        row=rowindex, col=colindex)
-                    w(u'  <%s>%s</%s>\n' % (tag, html_escape(content), tag))
+                    val = self.view('final', rset, displaytime=True,
+                                    row=rowindex, col=colindex)
+                w(simple_sgml_tag(tag, val, **attrs))
             w(u' </row>\n')
         w(u'</%s>\n' % self.xml_root)
     
--- a/web/views/calendar.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/web/views/calendar.py	Fri Jan 30 15:55:03 2009 +0100
@@ -118,7 +118,7 @@
     accepts_interfaces = (ICalendarable,)
     need_navigation = False
     title = _('hCalendar')
-    templatable = False
+    #templatable = False
     id = 'hcal'
 
     def call(self):
--- a/web/views/euser.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/web/views/euser.py	Fri Jan 30 15:55:03 2009 +0100
@@ -1,17 +1,18 @@
 """Specific views for users
 
 :organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
 
 from logilab.common.decorators import cached
+from logilab.mtconverter import html_escape
 
 from cubicweb.schema import display_name
 from cubicweb.web import INTERNAL_FIELD_VALUE
 from cubicweb.web.form import EntityForm
-from cubicweb.web.views.baseviews import PrimaryView
+from cubicweb.web.views.baseviews import PrimaryView, EntityView
 
 class EUserPrimaryView(PrimaryView):
     accepts = ('EUser',)
@@ -32,6 +33,36 @@
                                  'todo_by', 'bookmarked_by',
                                  ]
 
+class FoafView(EntityView):
+    id = 'foaf'
+    accepts = ('EUser',)
+    title = _('foaf')
+    templatable = False
+    content_type = 'text/xml'
+
+    def call(self):
+        self.w(u'<?xml version="1.0" encoding="%s"?>\n' % self.req.encoding)
+        self.w(u'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n')
+        self.w(u'xmlns:foaf="http://xmlns.com/foaf/0.1/">\n')
+        for i in xrange(self.rset.rowcount):
+            self.cell_call(i, 0)
+        self.w(u'</rdf:RDF>\n')
+
+    def cell_call(self, row, col):
+        entity = self.complete_entity(row, col)
+        self.w(u'<foaf:Person>\n')
+        self.w(u'<foaf:name>%s</foaf:name>\n' % html_escape(entity.dc_long_title()))
+        if entity.surname:
+            self.w(u'<foaf:surname>%s</foaf:surname>\n'
+                   % html_escape(entity.surname))
+        if entity.firstname:
+            self.w(u'<foaf:firstname>%s</foaf:firstname>\n'
+                   % html_escape(entity.firstname))
+        emailaddr = entity.get_email()
+        if emailaddr:
+            self.w(u'<foaf:mbox>%s</foaf:mbox>\n' % html_escape(emailaddr))
+        self.w(u'</foaf:Person>\n')
+
 
 class EditGroups(EntityForm):
     """displays a simple euser / egroups editable table"""
--- a/web/views/owl.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/web/views/owl.py	Fri Jan 30 15:55:03 2009 +0100
@@ -30,6 +30,7 @@
     id = 'owl'
     title = _('owl')
     templatable =False
+    content_type = 'application/xml' # 'text/xml'
 
     def call(self):
         skipmeta = int(self.req.form.get('skipmeta', True))
@@ -176,11 +177,10 @@
     title = _('owlabox')
     templatable =False
     accepts = ('Any',)
+    content_type = 'application/xml' # 'text/xml'
     
     def call(self):
 
-        rql = ('Any X')
-        rset = self.req.execute(rql)
         skipmeta = int(self.req.form.get('skipmeta', True))
         self.w(u'''<?xml version="1.0" encoding="UTF-8"?>
         <!DOCTYPE rdf:RDF [
--- a/web/views/tableview.py	Fri Jan 30 15:17:22 2009 +0100
+++ b/web/views/tableview.py	Fri Jan 30 15:55:03 2009 +0100
@@ -325,7 +325,7 @@
         self.w(u'</div>\n')
 
 
-class EditableInitiableTableView(InitialTableView):
+class EditableInitialTableTableView(InitialTableView):
     id = 'editable-initialtable'
     finalview = 'editable-final'