# HG changeset patch # User Julien Jehannet # Date 1233327303 -3600 # Node ID 6ed63265764ee8ceb4a8bd4f6ce5bb244b1f07a2 # Parent da51759027d0da6d784a43f0d23d7d401c8c6cc1# Parent f16da6c874daf98edbf71f87e78c8846174fb996 (merge) diff -r da51759027d0 -r 6ed63265764e common/uilib.py --- 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' % (html_escape(unicode(content)), tag) + else: + value += u'/>' + return value + def tooltipize(text, tooltip, url=None): """make an HTML tooltip""" url = url or '#' diff -r da51759027d0 -r 6ed63265764e devtools/testlib.py --- 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 diff -r da51759027d0 -r 6ed63265764e web/box.py --- 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) diff -r da51759027d0 -r 6ed63265764e web/component.py --- 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 diff -r da51759027d0 -r 6ed63265764e web/views/basecomponents.py --- 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,'\ diff -r da51759027d0 -r 6ed63265764e web/views/baseviews.py --- 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'
') self.render_entity_attributes(entity, siderelations) self.w(u'
') - self.w(u'') + self.content_navigation_components('navcontenttop') if self.main_related_section: self.render_entity_relations(entity, siderelations) self.w(u'') @@ -158,13 +154,21 @@ self.w(u'') self.w(u'') self.w(u'') - self.w(u'') - 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'') 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 view - """ + """display a list of entities by calling their view""" self.w(u'\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 = '' % b64encode(value.getvalue()) elif isinstance(value, basestring): - value = value.replace('&', '&').replace('<', '<') + value = xml_escape(value) self.w(u' <%s>%s\n' % (attr, value, attr)) self.w(u'\n' % (entity.e_schema)) @@ -619,21 +622,25 @@ eschema = self.schema.eschema labels = self.columns_labels(False) w(u'\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' \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\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\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' \n') w(u'\n' % self.xml_root) diff -r da51759027d0 -r 6ed63265764e web/views/calendar.py --- 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): diff -r da51759027d0 -r 6ed63265764e web/views/euser.py --- 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'\n' % self.req.encoding) + self.w(u'\n') + for i in xrange(self.rset.rowcount): + self.cell_call(i, 0) + self.w(u'\n') + + def cell_call(self, row, col): + entity = self.complete_entity(row, col) + self.w(u'\n') + self.w(u'%s\n' % html_escape(entity.dc_long_title())) + if entity.surname: + self.w(u'%s\n' + % html_escape(entity.surname)) + if entity.firstname: + self.w(u'%s\n' + % html_escape(entity.firstname)) + emailaddr = entity.get_email() + if emailaddr: + self.w(u'%s\n' % html_escape(emailaddr)) + self.w(u'\n') + class EditGroups(EntityForm): """displays a simple euser / egroups editable table""" diff -r da51759027d0 -r 6ed63265764e web/views/owl.py --- 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''' \n') -class EditableInitiableTableView(InitialTableView): +class EditableInitialTableTableView(InitialTableView): id = 'editable-initialtable' finalview = 'editable-final'