--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/baseviews.py Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,995 @@
+"""Set of HTML generic base views:
+
+* noresult, final
+* primary, sidebox
+* secondary, oneline, incontext, outofcontext, text
+* list
+* xml, rss
+
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from time import timezone
+
+from rql import nodes
+
+from logilab.common.decorators import cached
+from logilab.mtconverter import html_escape, TransformError
+
+from cubicweb import Unauthorized, NoSelectableObject, typed_eid
+from cubicweb.common.selectors import (yes_selector, anyrset_selector, accept_selector,
+ onelinerset_selector, searchstate_selector,
+ req_form_params_selector, accept_rset_selector)
+from cubicweb.common.uilib import (cut, printable_value, UnicodeCSVWriter,
+ ajax_replace_url, rql_for_eid)
+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
+
+_ = unicode
+
+
+class NullView(AnyRsetView):
+ """default view when no result has been found"""
+ id = 'null'
+ __select__ = classmethod(yes_selector)
+ def call(self, **kwargs):
+ pass
+ cell_call = call
+
+
+class NoResultView(EmptyRsetView):
+ """default view when no result has been found"""
+ id = 'noresult'
+
+ def call(self, **kwargs):
+ self.w(u'<div class="searchMessage"><strong>%s</strong></div>\n'
+ % self.req._('No result matching query'))
+
+
+class FinalView(AnyRsetView):
+ """display values without any transformation (i.e. get a number for
+ entities)
+ """
+ id = 'final'
+
+ def cell_call(self, row, col, props=None, displaytime=False):
+ etype = self.rset.description[row][col]
+ value = self.rset.rows[row][col]
+ if etype == 'String':
+ entity, rtype = self.rset.related_entity(row, col)
+ if entity is not None:
+ # yes !
+ self.w(entity.printable_value(rtype, value))
+ return
+ if etype in ('Time', 'Interval'):
+ _ = self.req._
+ # value is DateTimeDelta but we have no idea about what is the
+ # reference date here, so we can only approximate years and months
+ if value.days > 730: # 2 years
+ self.w(_('%d years') % (value.days // 365))
+ elif value.days > 60: # 2 months
+ self.w(_('%d months') % (value.days // 30))
+ elif value.days > 14: # 2 weeks
+ self.w(_('%d weeks') % (value.days // 7))
+ elif value.days > 2:
+ self.w(_('%s days') % int(value.days))
+ elif value.hours > 2:
+ self.w(_('%s hours') % int(value.hours))
+ elif value.minutes >= 2:
+ self.w(_('%s minutes') % int(value.minutes))
+ else:
+ self.w(_('%s seconds') % int(value.seconds))
+ return
+ self.wdata(printable_value(self.req, etype, value, props, displaytime=displaytime))
+
+
+class EditableFinalView(FinalView):
+ """same as FinalView but enables inplace-edition when possible"""
+ id = 'editable-final'
+
+ def cell_call(self, row, col, props=None, displaytime=False):
+ etype = self.rset.description[row][col]
+ value = self.rset.rows[row][col]
+ entity, rtype = self.rset.related_entity(row, col)
+ if entity is not None:
+ self.w(entity.view('reledit', rtype=rtype))
+ else:
+ super(EditableFinalView, self).cell_call(row, col, props, displaytime)
+
+PRIMARY_SKIP_RELS = set(['is', 'is_instance_of', 'identity',
+ 'owned_by', 'created_by',
+ 'in_state', 'wf_info_for', 'require_permission',
+ 'from_entity', 'to_entity',
+ 'see_also'])
+
+class PrimaryView(EntityView):
+ """the full view of an non final entity"""
+ id = 'primary'
+ title = _('primary')
+ show_attr_label = True
+ show_rel_label = True
+ skip_none = True
+ skip_attrs = ('eid', 'creation_date', 'modification_date')
+ skip_rels = ()
+ main_related_section = True
+
+ def html_headers(self):
+ """return a list of html headers (eg something to be inserted between
+ <head> and </head> of the returned page
+
+ by default primary views are indexed
+ """
+ return []
+
+ def cell_call(self, row, col):
+ self.row = row
+ self.render_entity(self.complete_entity(row, col))
+
+ def render_entity(self, entity):
+ """return html to display the given entity"""
+ siderelations = []
+ self.render_entity_title(entity)
+ self.render_entity_metadata(entity)
+ # entity's attributes and relations, excluding meta data
+ # if the entity isn't meta itself
+ self.w(u'<table border="0" width="100%">')
+ self.w(u'<tr>')
+ self.w(u'<td style="width:75%" valign="top">')
+ 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>')
+ if self.main_related_section:
+ self.render_entity_relations(entity, siderelations)
+ self.w(u'</td>')
+ # side boxes
+ self.w(u'<td valign="top">')
+ self.render_side_related(entity, siderelations)
+ self.w(u'</td>')
+ self.w(u'<td valign="top">')
+ self.w(u'</td>')
+ self.w(u'</tr>')
+ self.w(u'</table>')
+ self.w(u'<div class="navcontentbottom">')
+ for comp in self.vreg.possible_vobjects('contentnavigation',
+ self.req, self.rset,
+ view=self, context='navcontentbottom'):
+ 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
+ if attr in self.skip_attrs:
+ continue
+ yield rschema, targetschema
+
+ def iter_relations(self, entity):
+ skip = set(self.skip_rels)
+ skip.update(PRIMARY_SKIP_RELS)
+ for rschema, targetschemas, x in entity.e_schema.relation_definitions():
+ if rschema.type in skip:
+ continue
+ yield rschema, targetschemas, x
+
+ def render_entity_title(self, entity):
+ title = self.content_title(entity) # deprecate content_title?
+ if title:
+ self.w(u'<h1><span class="etype">%s</span> %s</h1>'
+ % (entity.dc_type().capitalize(), title))
+
+ def content_title(self, entity):
+ """default implementation return an empty string"""
+ return u''
+
+ def render_entity_metadata(self, entity):
+ entity.view('metadata', w=self.w)
+ summary = self.summary(entity) # deprecate summary?
+ if summary:
+ self.w(u'<div class="summary">%s</div>' % summary)
+
+ def summary(self, entity):
+ """default implementation return an empty string"""
+ return u''
+
+
+ def render_entity_attributes(self, entity, siderelations):
+ for rschema, targetschema in self.iter_attributes(entity):
+ attr = rschema.type
+ if targetschema.type in ('Password', 'Bytes'):
+ continue
+ try:
+ wdg = entity.get_widget(attr)
+ except Exception, ex:
+ value = entity.printable_value(attr, entity[attr], targetschema.type)
+ else:
+ value = wdg.render(entity)
+ if self.skip_none and (value is None or value == ''):
+ continue
+ if rschema.meta:
+ continue
+ self._render_related_entities(entity, rschema, value)
+
+ def render_entity_relations(self, entity, siderelations):
+ if hasattr(self, 'get_side_boxes_defs'):
+ return
+ eschema = entity.e_schema
+ maxrelated = self.req.property_value('navigation.related-limit')
+ for rschema, targetschemas, x in self.iter_relations(entity):
+ try:
+ related = entity.related(rschema.type, x, limit=maxrelated+1)
+ except Unauthorized:
+ continue
+ if not related:
+ continue
+ if self.is_side_related(rschema, eschema):
+ siderelations.append((rschema, related, x))
+ continue
+ self._render_related_entities(entity, rschema, related, x)
+
+ def render_side_related(self, entity, siderelations):
+ """display side related relations:
+ non-meta in a first step, meta in a second step
+ """
+ if hasattr(self, 'get_side_boxes_defs'):
+ for label, rset in self.get_side_boxes_defs(entity):
+ if rset:
+ self.w(u'<div class="sideRelated">')
+ self.wview('sidebox', rset, title=label)
+ self.w(u'</div>')
+ elif siderelations:
+ self.w(u'<div class="sideRelated">')
+ for relatedinfos in siderelations:
+ # if not relatedinfos[0].meta:
+ # 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'):
+ try:
+ box.dispatch(w=self.w, col=entity.col, row=entity.row)
+ except NotImplementedError:
+ # much probably a context insensitive box, which only implements
+ # .call() and not cell_call()
+ box.dispatch(w=self.w)
+
+ def is_side_related(self, rschema, eschema):
+ return rschema.meta and \
+ not rschema.schema_relation() == eschema.schema_entity()
+
+ def _render_related_entities(self, entity, rschema, related,
+ role='subject'):
+ if rschema.is_final():
+ value = related
+ show_label = self.show_attr_label
+ else:
+ if not related:
+ return
+ show_label = self.show_rel_label
+ # if not too many entities, show them all in a list
+ maxrelated = self.req.property_value('navigation.related-limit')
+ if related.rowcount <= maxrelated:
+ if related.rowcount == 1:
+ value = self.view('incontext', related, row=0)
+ elif 1 < related.rowcount <= 5:
+ value = self.view('csv', related)
+ else:
+ value = '<div>' + self.view('simplelist', related) + '</div>'
+ # else show links to display related entities
+ else:
+ rql = related.printable_rql()
+ related.limit(maxrelated)
+ value = '<div>' + self.view('simplelist', related)
+ value += '[<a href="%s">%s</a>]' % (self.build_url(rql=rql),
+ self.req._('see them all'))
+ value += '</div>'
+ label = display_name(self.req, rschema.type, role)
+ self.field(label, value, show_label=show_label, w=self.w, tr=False)
+
+
+class SideBoxView(EntityView):
+ """side box usually displaying some related entities in a primary view"""
+ id = 'sidebox'
+
+ def call(self, boxclass='sideBox', title=u''):
+ """display a list of entities by calling their <item_vid> view
+ """
+ if title:
+ self.w(u'<div class="sideBoxTitle"><span>%s</span></div>' % title)
+ self.w(u'<div class="%s"><div class="sideBoxBody">' % boxclass)
+ # if not too much entities, show them all in a list
+ maxrelated = self.req.property_value('navigation.related-limit')
+ if self.rset.rowcount <= maxrelated:
+ if len(self.rset) == 1:
+ self.wview('incontext', self.rset, row=0)
+ elif 1 < len(self.rset) < 5:
+ self.wview('csv', self.rset)
+ else:
+ self.wview('simplelist', self.rset)
+ # else show links to display related entities
+ else:
+ self.rset.limit(maxrelated)
+ rql = self.rset.printable_rql(encoded=False)
+ self.wview('simplelist', self.rset)
+ self.w(u'[<a href="%s">%s</a>]' % (self.build_url(rql=rql),
+ self.req._('see them all')))
+ self.w(u'</div>\n</div>\n')
+
+
+
+class SecondaryView(EntityView):
+ id = 'secondary'
+ title = _('secondary')
+
+ def cell_call(self, row, col):
+ """the secondary view for an entity
+ secondary = icon + view(oneline)
+ """
+ entity = self.entity(row, col)
+ self.w(u' ')
+ self.wview('oneline', self.rset, row=row, col=col)
+
+class OneLineView(EntityView):
+ id = 'oneline'
+ title = _('oneline')
+
+ def cell_call(self, row, col):
+ """the one line view for an entity: linked text view
+ """
+ entity = self.entity(row, col)
+ self.w(u'<a href="%s">' % html_escape(entity.absolute_url()))
+ self.w(html_escape(self.view('text', self.rset, row=row, col=col)))
+ self.w(u'</a>')
+
+class TextView(EntityView):
+ """the simplest text view for an entity
+ """
+ id = 'text'
+ title = _('text')
+ accepts = 'Any',
+ def call(self, **kwargs):
+ """the view is called for an entire result set, by default loop
+ other rows of the result set and call the same view on the
+ particular row
+
+ Views applicable on None result sets have to override this method
+ """
+ rset = self.rset
+ if rset is None:
+ raise NotImplementedError, self
+ for i in xrange(len(rset)):
+ self.wview(self.id, rset, row=i, **kwargs)
+ if len(rset) > 1:
+ self.w(u"\n")
+
+ def cell_call(self, row, col=0, **kwargs):
+ entity = self.entity(row, col)
+ self.w(cut(entity.dc_title(),
+ self.req.property_value('navigation.short-line-size')))
+
+class MetaDataView(EntityView):
+ """paragraph view of some metadata"""
+ id = 'metadata'
+ accepts = 'Any',
+ show_eid = True
+
+ def cell_call(self, row, col):
+ _ = self.req._
+ entity = self.entity(row, col)
+ self.w(u'<div class="metadata">')
+ if self.show_eid:
+ self.w(u'#%s - ' % entity.eid)
+ if entity.modification_date != entity.creation_date:
+ self.w(u'<span>%s</span> ' % _('latest update on'))
+ self.w(u'<span class="value">%s</span>, '
+ % self.format_date(entity.modification_date))
+ # entities from external source may not have a creation date (eg ldap)
+ if entity.creation_date:
+ self.w(u'<span>%s</span> ' % _('created on'))
+ self.w(u'<span class="value">%s</span>'
+ % self.format_date(entity.creation_date))
+ if entity.creator:
+ creatoreid = entity.creator.eid
+ self.w(u' <span>%s</span> ' % _('by'))
+ self.w(u'<span class="value">%s</span>' % entity.creator.name())
+ else:
+ creatoreid = None
+ try:
+ owners = ','.join(u.name() for u in entity.owned_by
+ if u.eid != creatoreid)
+ if owners:
+ self.w(u', <span>%s</span> ' % _('owned by'))
+ self.w(u'<span class="value">%s</span>' % owners)
+ except Unauthorized:
+ pass
+ self.w(u'</div>')
+
+
+# new default views for finner control in general views , to use instead of
+# oneline / secondary
+
+class InContextTextView(TextView):
+ id = 'textincontext'
+ title = None # not listed as a possible view
+ def cell_call(self, row, col):
+ entity = self.entity(row, col)
+ self.w(entity.dc_title())
+
+class OutOfContextTextView(InContextTextView):
+ id = 'textoutofcontext'
+
+ def cell_call(self, row, col):
+ entity = self.entity(row, col)
+ self.w(entity.dc_long_title())
+
+
+class InContextView(EntityView):
+ id = 'incontext'
+
+ def cell_call(self, row, col):
+ entity = self.entity(row, col)
+ desc = cut(entity.dc_description(), 50)
+ self.w(u'<a href="%s" title="%s">' % (html_escape(entity.absolute_url()),
+ html_escape(desc)))
+ self.w(html_escape(self.view('textincontext', self.rset, row=row, col=col)))
+ self.w(u'</a>')
+
+
+class OutOfContextView(EntityView):
+ id = 'outofcontext'
+
+ def cell_call(self, row, col):
+ self.w(u'<a href="%s">' % self.entity(row, col).absolute_url())
+ self.w(html_escape(self.view('textoutofcontext', self.rset, row=row, col=col)))
+ self.w(u'</a>')
+
+class NotClickableInContextView(EntityView):
+ id = 'incontext'
+ accepts = ('State',)
+ def cell_call(self, row, col):
+ self.w(html_escape(self.view('textincontext', self.rset, row=row, col=col)))
+
+## class NotClickableOutOfContextView(EntityView):
+## id = 'outofcontext'
+## accepts = ('State',)
+## def cell_call(self, row, col):
+## self.w(html_escape(self.view('textoutofcontext', self.rset, row=row)))
+
+
+# list and table related views ################################################
+
+class ListView(EntityView):
+ id = 'list'
+ title = _('list')
+ item_vid = 'listitem'
+
+ def call(self, klass=None, title=None, subvid=None, listid=None, **kwargs):
+ """display a list of entities by calling their <item_vid> view
+
+ :param listid: the DOM id to use for the root element
+ """
+ if subvid is None and 'subvid' in self.req.form:
+ subvid = self.req.form.pop('subvid') # consume it
+ if listid:
+ listid = u' id="%s"' % listid
+ else:
+ listid = u''
+ if title:
+ self.w(u'<div%s class="%s"><h4>%s</h4>\n' % (listid, klass or 'section', title))
+ self.w(u'<ul>\n')
+ else:
+ self.w(u'<ul%s class="%s">\n' % (listid, klass or 'section'))
+ for i in xrange(self.rset.rowcount):
+ self.cell_call(row=i, col=0, vid=subvid, **kwargs)
+ self.w(u'</ul>\n')
+ if title:
+ self.w(u'</div>\n')
+
+ def cell_call(self, row, col=0, vid=None, **kwargs):
+ self.w(u'<li>')
+ self.wview(self.item_vid, self.rset, row=row, col=col, vid=vid, **kwargs)
+ self.w(u'</li>\n')
+
+ def url(self):
+ """overrides url method so that by default, the view list is called
+ with sorted entities
+ """
+ coltypes = self.rset.column_types(0)
+ # don't want to generate the rql if there is some restriction on
+ # something else than the entity type
+ if len(coltypes) == 1:
+ # XXX norestriction is not correct here. For instance, in cases like
+ # Any P,N WHERE P is Project, P name N
+ # norestriction should equal True
+ restr = self.rset.syntax_tree().children[0].where
+ norestriction = (isinstance(restr, nodes.Relation) and
+ restr.is_types_restriction())
+ if norestriction:
+ etype = iter(coltypes).next()
+ return self.build_url(etype.lower(), vid=self.id)
+ if len(self.rset) == 1:
+ entity = self.rset.get_entity(0, 0)
+ return self.build_url(entity.rest_path(), vid=self.id)
+ return self.build_url(rql=self.rset.printable_rql(), vid=self.id)
+
+
+class ListItemView(EntityView):
+ id = 'listitem'
+
+ @property
+ def redirect_vid(self):
+ if self.req.search_state[0] == 'normal':
+ return 'outofcontext'
+ return 'outofcontext-search'
+
+ def cell_call(self, row, col, vid=None, **kwargs):
+ if not vid:
+ vid = self.redirect_vid
+ try:
+ self.wview(vid, self.rset, row=row, col=col, **kwargs)
+ except NoSelectableObject:
+ if vid == self.redirect_vid:
+ raise
+ kwargs.pop('done', None)
+ self.wview(self.redirect_vid, self.rset, row=row, col=col, **kwargs)
+
+
+class SimpleListView(ListItemView):
+ """list without bullets"""
+ id = 'simplelist'
+ redirect_vid = 'incontext'
+
+
+class CSVView(SimpleListView):
+ id = 'csv'
+ redirect_vid = 'incontext'
+
+ def call(self, **kwargs):
+ rset = self.rset
+ for i in xrange(len(rset)):
+ self.cell_call(i, 0, vid=kwargs.get('vid'))
+ if i < rset.rowcount-1:
+ self.w(u", ")
+
+
+class TreeItemView(ListItemView):
+ accepts = ('Any',)
+ id = 'treeitem'
+
+ def cell_call(self, row, col):
+ self.wview('incontext', self.rset, row=row, col=col)
+
+
+# xml and xml/rss views #######################################################
+
+class XmlView(EntityView):
+ id = 'xml'
+ title = _('xml')
+ templatable = False
+ content_type = 'text/xml'
+ xml_root = 'rset'
+ item_vid = 'xmlitem'
+
+ def cell_call(self, row, col):
+ 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
+ """
+ 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):
+ self.cell_call(i, 0)
+ self.w(u'</%s>\n' % self.xml_root)
+
+
+class XmlItemView(EntityView):
+ id = 'xmlitem'
+
+ def cell_call(self, row, col):
+ """ element as an item for an xml feed """
+ entity = self.complete_entity(row, col)
+ self.w(u'<%s>\n' % (entity.e_schema))
+ for rschema, attrschema in entity.e_schema.attribute_definitions():
+ attr = rschema.type
+ try:
+ value = entity[attr]
+ except KeyError:
+ # Bytes
+ continue
+ if value is not None:
+ if attrschema == 'Bytes':
+ from base64 import b64encode
+ value = '<![CDATA[%s]]>' % b64encode(value.getvalue())
+ elif isinstance(value, basestring):
+ value = value.replace('&', '&').replace('<', '<')
+ self.w(u' <%s>%s</%s>\n' % (attr, value, attr))
+ self.w(u'</%s>\n' % (entity.e_schema))
+
+
+class RssView(XmlView):
+ id = 'rss'
+ title = _('rss')
+ templatable = False
+ content_type = 'text/xml'
+ http_cache_manager = MaxAgeHTTPCacheManager
+ cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
+
+ def cell_call(self, row, col):
+ self.wview('rssitem', self.rset, row=row, col=col)
+
+ def call(self):
+ """display a list of entities by calling their <item_vid> view"""
+ req = self.req
+ self.w(u'<?xml version="1.0" encoding="%s"?>\n' % req.encoding)
+ self.w(u'''<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns="http://purl.org/rss/1.0/"
+>''')
+ self.w(u' <channel rdf:about="%s">\n' % html_escape(req.url()))
+ self.w(u' <title>%s RSS Feed</title>\n' % html_escape(self.page_title()))
+ self.w(u' <description>%s</description>\n' % html_escape(req.form.get('vtitle', '')))
+ params = req.form.copy()
+ params.pop('vid', None)
+ self.w(u' <link>%s</link>\n' % html_escape(self.build_url(**params)))
+ self.w(u' <items>\n')
+ self.w(u' <rdf:Seq>\n')
+ for entity in self.rset.entities():
+ self.w(u' <rdf:li resource="%s" />\n' % html_escape(entity.absolute_url()))
+ self.w(u' </rdf:Seq>\n')
+ self.w(u' </items>\n')
+ self.w(u' </channel>\n')
+ for i in xrange(self.rset.rowcount):
+ self.cell_call(i, 0)
+ self.w(u'</rdf:RDF>')
+
+
+class RssItemView(EntityView):
+ id = 'rssitem'
+ date_format = '%%Y-%%m-%%dT%%H:%%M%+03i:00' % (timezone / 3600)
+
+ def cell_call(self, row, col):
+ entity = self.complete_entity(row, col)
+ self.w(u'<item rdf:about="%s">\n' % html_escape(entity.absolute_url()))
+ self._marker('title', entity.dc_long_title())
+ self._marker('link', entity.absolute_url())
+ self._marker('description', entity.dc_description())
+ self._marker('dc:date', entity.dc_date(self.date_format))
+ self._marker('author', entity.dc_authors())
+ self.w(u'</item>\n')
+
+ def _marker(self, marker, value):
+ if value:
+ self.w(u' <%s>%s</%s>\n' % (marker, html_escape(value), marker))
+
+
+class CSVMixIn(object):
+ """mixin class for CSV views"""
+ templatable = False
+ content_type = "text/comma-separated-values"
+ binary = True # avoid unicode assertion
+ csv_params = {'dialect': 'excel',
+ 'quotechar': '"',
+ 'delimiter': ';',
+ 'lineterminator': '\n'}
+
+ def set_request_content_type(self):
+ """overriden to set a .csv filename"""
+ self.req.set_content_type(self.content_type, filename='cubicwebexport.csv')
+
+ def csvwriter(self, **kwargs):
+ params = self.csv_params.copy()
+ params.update(kwargs)
+ return UnicodeCSVWriter(self.w, self.req.encoding, **params)
+
+
+class CSVRsetView(CSVMixIn, AnyRsetView):
+ """dumps rset in CSV"""
+ id = 'csvexport'
+ title = _('csv export')
+
+ def call(self):
+ writer = self.csvwriter()
+ writer.writerow(self.get_headers_labels())
+ descr = self.rset.description
+ for rowindex, row in enumerate(self.rset):
+ csvrow = []
+ for colindex, val in enumerate(row):
+ etype = descr[rowindex][colindex]
+ if val is not None and not self.schema.eschema(etype).is_final():
+ # csvrow.append(val) # val is eid in that case
+ content = self.view('textincontext', self.rset,
+ row=rowindex, col=colindex)
+ else:
+ content = self.view('final', self.rset, displaytime=True,
+ row=rowindex, col=colindex)
+ csvrow.append(content)
+ writer.writerow(csvrow)
+
+ def get_headers_labels(self):
+ rqlstdescr = self.rset.syntax_tree().get_description()[0] # XXX missing Union support
+ labels = []
+ for colindex, attr in enumerate(rqlstdescr):
+ # compute column header
+ if colindex == 0 or attr == 'Any': # find a better label
+ label = ','.join(display_name(self.req, et)
+ for et in self.rset.column_types(colindex))
+ else:
+ label = display_name(self.req, attr)
+ labels.append(label)
+ return labels
+
+
+class CSVEntityView(CSVMixIn, EntityView):
+ """dumps rset's entities (with full set of attributes) in CSV"""
+ id = 'ecsvexport'
+ title = _('csv entities export')
+
+ def call(self):
+ """
+ the generated CSV file will have a table per entity type
+ found in the resultset. ('table' here only means empty
+ lines separation between table contents)
+ """
+ req = self.req
+ rows_by_type = {}
+ writer = self.csvwriter()
+ rowdef_by_type = {}
+ for index in xrange(len(self.rset)):
+ entity = self.complete_entity(index)
+ if entity.e_schema not in rows_by_type:
+ rowdef_by_type[entity.e_schema] = [rs for rs, as in entity.e_schema.attribute_definitions()
+ if as.type != 'Bytes']
+ rows_by_type[entity.e_schema] = [[display_name(req, rschema.type)
+ for rschema in rowdef_by_type[entity.e_schema]]]
+ rows = rows_by_type[entity.e_schema]
+ rows.append([entity.printable_value(rs.type, format='text/plain')
+ for rs in rowdef_by_type[entity.e_schema]])
+ for etype, rows in rows_by_type.items():
+ writer.writerows(rows)
+ # use two empty lines as separator
+ writer.writerows([[], []])
+
+
+## Work in progress ###########################################################
+
+class SearchForAssociationView(EntityView):
+ """view called by the edition view when the user asks
+ to search for something to link to the edited eid
+ """
+ id = 'search-associate'
+ title = _('search for association')
+ __selectors__ = (onelinerset_selector, searchstate_selector, accept_selector)
+ accepts = ('Any',)
+ search_states = ('linksearch',)
+
+ def cell_call(self, row, col):
+ rset, vid, divid, paginate = self.filter_box_context_info()
+ self.w(u'<div id="%s">' % divid)
+ self.pagination(self.req, rset, w=self.w)
+ self.wview(vid, rset)
+ self.w(u'</div>')
+
+ @cached
+ def filter_box_context_info(self):
+ entity = self.entity(0, 0)
+ role, eid, rtype, etype = self.req.search_state[1]
+ assert entity.eid == typed_eid(eid)
+ # the default behaviour is to fetch all unrelated entities and display
+ # them. Use fetch_order and not fetch_unrelated_order as sort method
+ # since the latter is mainly there to select relevant items in the combo
+ # box, it doesn't give interesting result in this context
+ rql = entity.unrelated_rql(rtype, etype, role,
+ ordermethod='fetch_order',
+ vocabconstraints=False)
+ rset = self.req.execute(rql, {'x' : entity.eid}, 'x')
+ #vid = vid_from_rset(self.req, rset, self.schema)
+ return rset, 'list', "search-associate-content", True
+
+
+class OutOfContextSearch(EntityView):
+ id = 'outofcontext-search'
+ def cell_call(self, row, col):
+ entity = self.entity(row, col)
+ erset = entity.as_rset()
+ if linksearch_match(self.req, erset):
+ self.w(u'<a href="%s" title="%s">%s</a> <a href="%s" title="%s">[...]</a>' % (
+ html_escape(linksearch_select_url(self.req, erset)),
+ self.req._('select this entity'),
+ html_escape(entity.view('textoutofcontext')),
+ html_escape(entity.absolute_url(vid='primary')),
+ self.req._('view detail for this entity')))
+ else:
+ entity.view('outofcontext', w=self.w)
+
+
+class EditRelationView(EntityView):
+ """Note: This is work in progress
+
+ This view is part of the edition view refactoring.
+ It is still too big and cluttered with strange logic, but it's a start
+
+ The main idea is to be able to call an edition view for a specific
+ relation. For example :
+ self.wview('editrelation', person_rset, rtype='firstname')
+ self.wview('editrelation', person_rset, rtype='works_for')
+ """
+ id = 'editrelation'
+
+ __selectors__ = (req_form_params_selector,)
+ form_params = ('rtype',)
+
+ # TODO: inlineview, multiple edit, (widget view ?)
+ def cell_call(self, row, col, rtype=None, role='subject', targettype=None,
+ showlabel=True):
+ self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
+ entity = self.entity(row, col)
+ rtype = self.req.form.get('rtype', rtype)
+ showlabel = self.req.form.get('showlabel', showlabel)
+ assert rtype is not None, "rtype is mandatory for 'edirelation' view"
+ targettype = self.req.form.get('targettype', targettype)
+ role = self.req.form.get('role', role)
+ mode = entity.rtags.get_mode(rtype, targettype, role)
+ if mode == 'create':
+ return
+ category = entity.rtags.get_category(rtype, targettype, role)
+ if category in ('generated', 'metadata'):
+ return
+ elif category in ('primary', 'secondary'):
+ if hasattr(entity, '%s_format' % rtype):
+ formatwdg = entity.get_widget('%s_format' % rtype, role)
+ self.w(formatwdg.edit_render(entity))
+ self.w(u'<br/>')
+ wdg = entity.get_widget(rtype, role)
+ if showlabel:
+ self.w(u'%s' % wdg.render_label(entity))
+ self.w(u'%s %s %s' %
+ (wdg.render_error(entity), wdg.edit_render(entity),
+ wdg.render_help(entity),))
+ elif category == 'generic':
+ self._render_generic_relation(entity, rtype, role)
+ else:
+ self.error("oops, wrong category %s", category)
+
+ def _render_generic_relation(self, entity, relname, role):
+ text = self.req.__('add %s %s %s' % (entity.e_schema, relname, role))
+ # pending operations
+ operations = self.req.get_pending_operations(entity, relname, role)
+ if operations['insert'] or operations['delete'] or 'unfold' in self.req.form:
+ self.w(u'<h3>%s</h3>' % text)
+ self._render_generic_relation_form(operations, entity, relname, role)
+ else:
+ divid = "%s%sreledit" % (relname, role)
+ url = ajax_replace_url(divid, rql_for_eid(entity.eid), 'editrelation',
+ {'unfold' : 1, 'relname' : relname, 'role' : role})
+ self.w(u'<a href="%s">%s</a>' % (url, text))
+ self.w(u'<div id="%s"></div>' % divid)
+
+
+ def _build_opvalue(self, entity, relname, target, role):
+ if role == 'subject':
+ return '%s:%s:%s' % (entity.eid, relname, target)
+ else:
+ return '%s:%s:%s' % (target, relname, entity.eid)
+
+
+ def _render_generic_relation_form(self, operations, entity, relname, role):
+ rqlexec = self.req.execute
+ for optype, targets in operations.items():
+ for target in targets:
+ self._render_pending(optype, entity, relname, target, role)
+ opvalue = self._build_opvalue(entity, relname, target, role)
+ self.w(u'<a href="javascript: addPendingDelete(\'%s\', %s);">-</a> '
+ % (opvalue, entity.eid))
+ rset = rqlexec('Any X WHERE X eid %(x)s', {'x': target}, 'x')
+ self.wview('oneline', rset)
+ # now, unrelated ones
+ self._render_unrelated_selection(entity, relname, role)
+
+ def _render_pending(self, optype, entity, relname, target, role):
+ opvalue = self._build_opvalue(entity, relname, target, role)
+ self.w(u'<input type="hidden" name="__%s" value="%s" />'
+ % (optype, opvalue))
+ if optype == 'insert':
+ checktext = '-'
+ else:
+ checktext = '+'
+ rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': target}, 'x')
+ self.w(u"""[<a href="javascript: cancelPending%s('%s:%s:%s')">%s</a>"""
+ % (optype.capitalize(), relname, target, role,
+ self.view('oneline', rset)))
+
+ def _render_unrelated_selection(self, entity, relname, role):
+ rschema = self.schema.rschema(relname)
+ if role == 'subject':
+ targettypes = rschema.objects(entity.e_schema)
+ else:
+ targettypes = rschema.subjects(entity.e_schema)
+ self.w(u'<select onselect="addPendingInsert(this.selected.value);">')
+ for targettype in targettypes:
+ unrelated = entity.unrelated(relname, targettype, role) # XXX limit
+ for rowindex, row in enumerate(unrelated):
+ teid = row[0]
+ opvalue = self._build_opvalue(entity, relname, teid, role)
+ self.w(u'<option name="__insert" value="%s>%s</option>'
+ % (opvalue, self.view('text', unrelated, row=rowindex)))
+ self.w(u'</select>')
+
+
+class TextSearchResultView(EntityView):
+ """this view is used to display full-text search
+
+ It tries to highlight part of data where the search word appears.
+
+ XXX: finish me (fixed line width, fixed number of lines, CSS, etc.)
+ """
+ id = 'tsearch'
+
+
+ def cell_call(self, row, col, **kwargs):
+ entity = self.complete_entity(row, col)
+ self.w(entity.view('incontext'))
+ searched = self.rset.searched_text()
+ if searched is None:
+ return
+ searched = searched.lower()
+ highlighted = '<b>%s</b>' % searched
+ for attr in entity.e_schema.indexable_attributes():
+ try:
+ value = html_escape(entity.printable_value(attr, format='text/plain').lower())
+ except TransformError, ex:
+ continue
+ except:
+ continue
+ if searched in value:
+ contexts = []
+ for ctx in value.split(searched):
+ if len(ctx) > 30:
+ contexts.append(u'...' + ctx[-30:])
+ else:
+ contexts.append(ctx)
+ value = u'\n' + highlighted.join(contexts)
+ self.w(value.replace('\n', '<br/>'))
+
+
+class EntityRelationView(EntityView):
+ accepts = ()
+ vid = 'list'
+
+ def cell_call(self, row, col):
+ if self.target == 'object':
+ role = 'subject'
+ else:
+ role = 'object'
+ rset = self.rset.get_entity(row, col).related(self.rtype, role)
+ self.w(u'<h1>%s</h1>' % self.req._(self.title).capitalize())
+ self.w(u'<div class="mainInfo">')
+ self.wview(self.vid, rset, 'noresult')
+ self.w(u'</div>')
+
+
+class TooltipView(OneLineView):
+ """A entity view used in a tooltip"""
+ id = 'tooltip'
+ title = None # don't display in possible views
+ def cell_call(self, row, col):
+ self.wview('oneline', self.rset, row=row, col=col)
+
+try:
+ from cubicweb.web.views.tableview import TableView
+ from logilab.common.deprecation import class_moved
+ TableView = class_moved(TableView)
+except ImportError:
+ pass # gae has no tableview module (yet)