diff -r 000000000000 -r b97547f5f1fa web/views/baseviews.py --- /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'
%s
\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 + and 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'') + self.w(u'') + self.w(u'') + # side boxes + self.w(u'') + self.w(u'') + self.w(u'') + self.w(u'
') + self.w(u'
') + self.render_entity_attributes(entity, siderelations) + self.w(u'
') + self.w(u'') + if self.main_related_section: + self.render_entity_relations(entity, siderelations) + self.w(u'
') + self.render_side_related(entity, siderelations) + self.w(u'') + self.w(u'
') + self.w(u'') + + 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'

%s %s

' + % (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'
%s
' % 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'
') + self.wview('sidebox', rset, title=label) + self.w(u'
') + elif siderelations: + self.w(u'
') + for relatedinfos in siderelations: + # if not relatedinfos[0].meta: + # continue + self._render_related_entities(entity, *relatedinfos) + 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'): + 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 = '
' + self.view('simplelist', related) + '
' + # else show links to display related entities + else: + rql = related.printable_rql() + related.limit(maxrelated) + value = '
' + self.view('simplelist', related) + value += '[%s]' % (self.build_url(rql=rql), + self.req._('see them all')) + value += '
' + 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 view + """ + if title: + self.w(u'
%s
' % title) + self.w(u'
' % 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'[%s]' % (self.build_url(rql=rql), + self.req._('see them all'))) + self.w(u'
\n
\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'' % html_escape(entity.absolute_url())) + self.w(html_escape(self.view('text', self.rset, row=row, col=col))) + self.w(u'') + +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'
') + if self.show_eid: + self.w(u'#%s - ' % entity.eid) + if entity.modification_date != entity.creation_date: + self.w(u'%s ' % _('latest update on')) + self.w(u'%s, ' + % 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'%s ' % _('created on')) + self.w(u'%s' + % self.format_date(entity.creation_date)) + if entity.creator: + creatoreid = entity.creator.eid + self.w(u' %s ' % _('by')) + self.w(u'%s' % 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', %s ' % _('owned by')) + self.w(u'%s' % owners) + except Unauthorized: + pass + self.w(u'
') + + +# 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'' % (html_escape(entity.absolute_url()), + html_escape(desc))) + self.w(html_escape(self.view('textincontext', self.rset, row=row, col=col))) + self.w(u'') + + +class OutOfContextView(EntityView): + id = 'outofcontext' + + def cell_call(self, row, col): + self.w(u'' % self.entity(row, col).absolute_url()) + self.w(html_escape(self.view('textoutofcontext', self.rset, row=row, col=col))) + self.w(u'') + +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 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'

%s

\n' % (listid, klass or 'section', title)) + self.w(u'
    \n') + else: + self.w(u'\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'
\n') + if title: + self.w(u'\n') + + def cell_call(self, row, col=0, vid=None, **kwargs): + self.w(u'
  • ') + self.wview(self.item_vid, self.rset, row=row, col=col, vid=vid, **kwargs) + self.w(u'
  • \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 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): + self.cell_call(i, 0) + self.w(u'\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 = '' % b64encode(value.getvalue()) + elif isinstance(value, basestring): + value = value.replace('&', '&').replace('<', '<') + self.w(u' <%s>%s\n' % (attr, value, attr)) + self.w(u'\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 view""" + req = self.req + self.w(u'\n' % req.encoding) + self.w(u'''''') + self.w(u' \n' % html_escape(req.url())) + self.w(u' %s RSS Feed\n' % html_escape(self.page_title())) + self.w(u' %s\n' % html_escape(req.form.get('vtitle', ''))) + params = req.form.copy() + params.pop('vid', None) + self.w(u' %s\n' % html_escape(self.build_url(**params))) + self.w(u' \n') + self.w(u' \n') + for entity in self.rset.entities(): + self.w(u' \n' % html_escape(entity.absolute_url())) + self.w(u' \n') + self.w(u' \n') + self.w(u' \n') + for i in xrange(self.rset.rowcount): + self.cell_call(i, 0) + self.w(u'') + + +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'\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'\n') + + def _marker(self, marker, value): + if value: + self.w(u' <%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'
    ' % divid) + self.pagination(self.req, rset, w=self.w) + self.wview(vid, rset) + self.w(u'
    ') + + @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'%s [...]' % ( + 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'
    ') + 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'

    %s

    ' % 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'%s' % (url, text)) + self.w(u'
    ' % 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'- ' + % (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'' + % (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"""[%s""" + % (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'