--- 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('&', '&').replace('<', '<')
+ 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'