web/views/baseviews.py
changeset 0 b97547f5f1fa
child 137 7e45cf48c2f1
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """Set of HTML generic base views:
       
     2 
       
     3 * noresult, final
       
     4 * primary, sidebox
       
     5 * secondary, oneline, incontext, outofcontext, text
       
     6 * list
       
     7 * xml, rss
       
     8 
       
     9 
       
    10 :organization: Logilab
       
    11 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
    12 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
    13 """
       
    14 __docformat__ = "restructuredtext en"
       
    15 
       
    16 from time import timezone
       
    17 
       
    18 from rql import nodes
       
    19 
       
    20 from logilab.common.decorators import cached
       
    21 from logilab.mtconverter import html_escape, TransformError
       
    22 
       
    23 from cubicweb import Unauthorized, NoSelectableObject, typed_eid
       
    24 from cubicweb.common.selectors import (yes_selector, anyrset_selector, accept_selector,
       
    25                                     onelinerset_selector, searchstate_selector, 
       
    26                                     req_form_params_selector, accept_rset_selector)
       
    27 from cubicweb.common.uilib import (cut, printable_value,  UnicodeCSVWriter,
       
    28                                 ajax_replace_url, rql_for_eid)
       
    29 from cubicweb.common.view import EntityView, AnyRsetView, EmptyRsetView
       
    30 from cubicweb.web.httpcache import MaxAgeHTTPCacheManager
       
    31 from cubicweb.web.views import vid_from_rset, linksearch_select_url, linksearch_match
       
    32 
       
    33 _ = unicode
       
    34 
       
    35 
       
    36 class NullView(AnyRsetView):
       
    37     """default view when no result has been found"""
       
    38     id = 'null'
       
    39     __select__ = classmethod(yes_selector)
       
    40     def call(self, **kwargs):
       
    41         pass
       
    42     cell_call = call
       
    43 
       
    44 
       
    45 class NoResultView(EmptyRsetView):
       
    46     """default view when no result has been found"""
       
    47     id = 'noresult'
       
    48     
       
    49     def call(self, **kwargs):
       
    50         self.w(u'<div class="searchMessage"><strong>%s</strong></div>\n'
       
    51                % self.req._('No result matching query'))
       
    52 
       
    53 
       
    54 class FinalView(AnyRsetView):
       
    55     """display values without any transformation (i.e. get a number for
       
    56     entities) 
       
    57     """
       
    58     id = 'final'
       
    59             
       
    60     def cell_call(self, row, col, props=None, displaytime=False):
       
    61         etype = self.rset.description[row][col]
       
    62         value = self.rset.rows[row][col]
       
    63         if etype == 'String':
       
    64             entity, rtype = self.rset.related_entity(row, col)
       
    65             if entity is not None:
       
    66                 # yes !
       
    67                 self.w(entity.printable_value(rtype, value))
       
    68                 return
       
    69         if etype in ('Time', 'Interval'):
       
    70             _ = self.req._
       
    71             # value is DateTimeDelta but we have no idea about what is the 
       
    72             # reference date here, so we can only approximate years and months
       
    73             if value.days > 730: # 2 years
       
    74                 self.w(_('%d years') % (value.days // 365))
       
    75             elif value.days > 60: # 2 months
       
    76                 self.w(_('%d months') % (value.days // 30))
       
    77             elif value.days > 14: # 2 weeks
       
    78                 self.w(_('%d weeks') % (value.days // 7))
       
    79             elif value.days > 2:
       
    80                 self.w(_('%s days') % int(value.days))
       
    81             elif value.hours > 2:
       
    82                 self.w(_('%s hours') % int(value.hours))
       
    83             elif value.minutes >= 2:
       
    84                 self.w(_('%s minutes') % int(value.minutes))
       
    85             else:
       
    86                 self.w(_('%s seconds') % int(value.seconds))
       
    87             return
       
    88         self.wdata(printable_value(self.req, etype, value, props, displaytime=displaytime))
       
    89 
       
    90 
       
    91 class EditableFinalView(FinalView):
       
    92     """same as FinalView but enables inplace-edition when possible"""
       
    93     id = 'editable-final'
       
    94                 
       
    95     def cell_call(self, row, col, props=None, displaytime=False):
       
    96         etype = self.rset.description[row][col]
       
    97         value = self.rset.rows[row][col]
       
    98         entity, rtype = self.rset.related_entity(row, col)
       
    99         if entity is not None:
       
   100             self.w(entity.view('reledit', rtype=rtype))
       
   101         else:
       
   102             super(EditableFinalView, self).cell_call(row, col, props, displaytime)
       
   103         
       
   104 PRIMARY_SKIP_RELS = set(['is', 'is_instance_of', 'identity',
       
   105                          'owned_by', 'created_by', 
       
   106                          'in_state', 'wf_info_for', 'require_permission',
       
   107                          'from_entity', 'to_entity',
       
   108                          'see_also'])
       
   109 
       
   110 class PrimaryView(EntityView):
       
   111     """the full view of an non final entity"""
       
   112     id = 'primary'
       
   113     title = _('primary')
       
   114     show_attr_label = True
       
   115     show_rel_label = True
       
   116     skip_none = True
       
   117     skip_attrs = ('eid', 'creation_date', 'modification_date')
       
   118     skip_rels = ()
       
   119     main_related_section = True
       
   120 
       
   121     def html_headers(self):
       
   122         """return a list of html headers (eg something to be inserted between
       
   123         <head> and </head> of the returned page
       
   124 
       
   125         by default primary views are indexed
       
   126         """
       
   127         return []
       
   128     
       
   129     def cell_call(self, row, col):        
       
   130         self.row = row
       
   131         self.render_entity(self.complete_entity(row, col))
       
   132     
       
   133     def render_entity(self, entity):
       
   134         """return html to display the given entity"""
       
   135         siderelations = []
       
   136         self.render_entity_title(entity)
       
   137         self.render_entity_metadata(entity)
       
   138         # entity's attributes and relations, excluding meta data
       
   139         # if the entity isn't meta itself
       
   140         self.w(u'<table border="0" width="100%">')
       
   141         self.w(u'<tr>')
       
   142         self.w(u'<td style="width:75%" valign="top">')
       
   143         self.w(u'<div class="mainInfo">')
       
   144         self.render_entity_attributes(entity, siderelations)
       
   145         self.w(u'</div>')
       
   146         self.w(u'<div class="navcontenttop">')
       
   147         for comp in self.vreg.possible_vobjects('contentnavigation',
       
   148                                                 self.req, self.rset,
       
   149                                                 view=self, context='navcontenttop'):
       
   150             comp.dispatch(w=self.w, view=self)
       
   151         self.w(u'</div>')
       
   152         if self.main_related_section:
       
   153             self.render_entity_relations(entity, siderelations)
       
   154         self.w(u'</td>')
       
   155         # side boxes
       
   156         self.w(u'<td valign="top">')
       
   157         self.render_side_related(entity, siderelations)
       
   158         self.w(u'</td>')
       
   159         self.w(u'<td valign="top">')
       
   160         self.w(u'</td>')        
       
   161         self.w(u'</tr>')
       
   162         self.w(u'</table>')        
       
   163         self.w(u'<div class="navcontentbottom">')
       
   164         for comp in self.vreg.possible_vobjects('contentnavigation',
       
   165                                                 self.req, self.rset,
       
   166                                                 view=self, context='navcontentbottom'):
       
   167             comp.dispatch(w=self.w, view=self)
       
   168         self.w(u'</div>')
       
   169 
       
   170     def iter_attributes(self, entity):
       
   171         for rschema, targetschema in entity.e_schema.attribute_definitions():
       
   172             attr = rschema.type
       
   173             if attr in self.skip_attrs:
       
   174                continue
       
   175             yield rschema, targetschema
       
   176             
       
   177     def iter_relations(self, entity):
       
   178         skip = set(self.skip_rels)
       
   179         skip.update(PRIMARY_SKIP_RELS)
       
   180         for rschema, targetschemas, x in entity.e_schema.relation_definitions():
       
   181             if rschema.type in skip:
       
   182                 continue
       
   183             yield rschema, targetschemas, x
       
   184 
       
   185     def render_entity_title(self, entity):
       
   186         title = self.content_title(entity) # deprecate content_title?
       
   187         if title:
       
   188             self.w(u'<h1><span class="etype">%s</span> %s</h1>'
       
   189                    % (entity.dc_type().capitalize(), title))
       
   190     
       
   191     def content_title(self, entity):
       
   192         """default implementation return an empty string"""
       
   193         return u''
       
   194             
       
   195     def render_entity_metadata(self, entity):
       
   196         entity.view('metadata', w=self.w)
       
   197         summary = self.summary(entity) # deprecate summary?
       
   198         if summary:
       
   199             self.w(u'<div class="summary">%s</div>' % summary)
       
   200     
       
   201     def summary(self, entity):
       
   202         """default implementation return an empty string"""
       
   203         return u''
       
   204     
       
   205                
       
   206     def render_entity_attributes(self, entity, siderelations):
       
   207         for rschema, targetschema in self.iter_attributes(entity):
       
   208             attr = rschema.type
       
   209             if targetschema.type in ('Password', 'Bytes'):
       
   210                 continue
       
   211             try:
       
   212                 wdg = entity.get_widget(attr)
       
   213             except Exception, ex:
       
   214                 value = entity.printable_value(attr, entity[attr], targetschema.type)
       
   215             else:
       
   216                 value = wdg.render(entity)
       
   217             if self.skip_none and (value is None or value == ''):
       
   218                 continue
       
   219             if rschema.meta:
       
   220                 continue
       
   221             self._render_related_entities(entity, rschema, value)
       
   222 
       
   223     def render_entity_relations(self, entity, siderelations):
       
   224         if hasattr(self, 'get_side_boxes_defs'):
       
   225             return
       
   226         eschema = entity.e_schema
       
   227         maxrelated = self.req.property_value('navigation.related-limit')
       
   228         for rschema, targetschemas, x in self.iter_relations(entity):
       
   229             try:
       
   230                 related = entity.related(rschema.type, x, limit=maxrelated+1)
       
   231             except Unauthorized:
       
   232                 continue
       
   233             if not related:
       
   234                 continue
       
   235             if self.is_side_related(rschema, eschema):
       
   236                 siderelations.append((rschema, related, x))
       
   237                 continue
       
   238             self._render_related_entities(entity, rschema, related, x)
       
   239 
       
   240     def render_side_related(self, entity, siderelations):
       
   241         """display side related relations:
       
   242         non-meta in a first step, meta in a second step
       
   243         """
       
   244         if hasattr(self, 'get_side_boxes_defs'):
       
   245             for label, rset in self.get_side_boxes_defs(entity):
       
   246                 if rset:
       
   247                     self.w(u'<div class="sideRelated">')
       
   248                     self.wview('sidebox', rset, title=label)
       
   249                     self.w(u'</div>')
       
   250         elif siderelations:
       
   251             self.w(u'<div class="sideRelated">')
       
   252             for relatedinfos in siderelations:
       
   253                 # if not relatedinfos[0].meta:
       
   254                 #    continue
       
   255                 self._render_related_entities(entity, *relatedinfos)
       
   256             self.w(u'</div>')
       
   257         for box in self.vreg.possible_vobjects('boxes', self.req, entity.rset,
       
   258                                                col=entity.col, row=entity.row,
       
   259                                                view=self, context='incontext'):
       
   260             try:
       
   261                 box.dispatch(w=self.w, col=entity.col, row=entity.row)
       
   262             except NotImplementedError:
       
   263                 # much probably a context insensitive box, which only implements
       
   264                 # .call() and not cell_call()
       
   265                 box.dispatch(w=self.w)
       
   266                 
       
   267     def is_side_related(self, rschema, eschema):
       
   268         return rschema.meta and \
       
   269                not rschema.schema_relation() == eschema.schema_entity()
       
   270 
       
   271     def _render_related_entities(self, entity, rschema, related,
       
   272                                  role='subject'):
       
   273         if rschema.is_final():
       
   274             value = related
       
   275             show_label = self.show_attr_label
       
   276         else:
       
   277             if not related:
       
   278                 return
       
   279             show_label = self.show_rel_label
       
   280             # if not too many entities, show them all in a list
       
   281             maxrelated = self.req.property_value('navigation.related-limit')
       
   282             if related.rowcount <= maxrelated:
       
   283                 if related.rowcount == 1:
       
   284                     value = self.view('incontext', related, row=0)
       
   285                 elif 1 < related.rowcount <= 5:
       
   286                     value = self.view('csv', related)
       
   287                 else:
       
   288                     value = '<div>' + self.view('simplelist', related) + '</div>'
       
   289             # else show links to display related entities
       
   290             else:
       
   291                 rql = related.printable_rql()
       
   292                 related.limit(maxrelated)
       
   293                 value = '<div>' + self.view('simplelist', related)
       
   294                 value += '[<a href="%s">%s</a>]' % (self.build_url(rql=rql),
       
   295                                                     self.req._('see them all'))
       
   296                 value +=  '</div>'
       
   297         label = display_name(self.req, rschema.type, role)
       
   298         self.field(label, value, show_label=show_label, w=self.w, tr=False)
       
   299 
       
   300 
       
   301 class SideBoxView(EntityView):
       
   302     """side box usually displaying some related entities in a primary view"""
       
   303     id = 'sidebox'
       
   304     
       
   305     def call(self, boxclass='sideBox', title=u''):
       
   306         """display a list of entities by calling their <item_vid> view
       
   307         """
       
   308         if title:
       
   309             self.w(u'<div class="sideBoxTitle"><span>%s</span></div>' % title)
       
   310         self.w(u'<div class="%s"><div class="sideBoxBody">' % boxclass)
       
   311         # if not too much entities, show them all in a list
       
   312         maxrelated = self.req.property_value('navigation.related-limit')
       
   313         if self.rset.rowcount <= maxrelated:
       
   314             if len(self.rset) == 1:
       
   315                 self.wview('incontext', self.rset, row=0)
       
   316             elif 1 < len(self.rset) < 5:
       
   317                 self.wview('csv', self.rset)
       
   318             else:
       
   319                 self.wview('simplelist', self.rset)
       
   320         # else show links to display related entities
       
   321         else:
       
   322             self.rset.limit(maxrelated)
       
   323             rql = self.rset.printable_rql(encoded=False)
       
   324             self.wview('simplelist', self.rset)
       
   325             self.w(u'[<a href="%s">%s</a>]' % (self.build_url(rql=rql),
       
   326                                                self.req._('see them all')))
       
   327         self.w(u'</div>\n</div>\n')
       
   328 
       
   329 
       
   330  
       
   331 class SecondaryView(EntityView):
       
   332     id = 'secondary'
       
   333     title = _('secondary')
       
   334     
       
   335     def cell_call(self, row, col):
       
   336         """the secondary view for an entity
       
   337         secondary = icon + view(oneline)
       
   338         """
       
   339         entity = self.entity(row, col)
       
   340         self.w(u'&nbsp;')
       
   341         self.wview('oneline', self.rset, row=row, col=col)
       
   342 
       
   343 class OneLineView(EntityView):
       
   344     id = 'oneline'
       
   345     title = _('oneline') 
       
   346 
       
   347     def cell_call(self, row, col):
       
   348         """the one line view for an entity: linked text view
       
   349         """
       
   350         entity = self.entity(row, col)
       
   351         self.w(u'<a href="%s">' % html_escape(entity.absolute_url()))
       
   352         self.w(html_escape(self.view('text', self.rset, row=row, col=col)))
       
   353         self.w(u'</a>')
       
   354 
       
   355 class TextView(EntityView):
       
   356     """the simplest text view for an entity
       
   357     """
       
   358     id = 'text'
       
   359     title = _('text')
       
   360     accepts = 'Any',
       
   361     def call(self, **kwargs):
       
   362         """the view is called for an entire result set, by default loop
       
   363         other rows of the result set and call the same view on the
       
   364         particular row
       
   365 
       
   366         Views applicable on None result sets have to override this method
       
   367         """
       
   368         rset = self.rset
       
   369         if rset is None:
       
   370             raise NotImplementedError, self
       
   371         for i in xrange(len(rset)):
       
   372             self.wview(self.id, rset, row=i, **kwargs)
       
   373             if len(rset) > 1:
       
   374                 self.w(u"\n")
       
   375     
       
   376     def cell_call(self, row, col=0, **kwargs):
       
   377         entity = self.entity(row, col)
       
   378         self.w(cut(entity.dc_title(),
       
   379                    self.req.property_value('navigation.short-line-size')))
       
   380 
       
   381 class MetaDataView(EntityView):
       
   382     """paragraph view of some metadata"""
       
   383     id = 'metadata'
       
   384     accepts = 'Any',
       
   385     show_eid = True
       
   386     
       
   387     def cell_call(self, row, col):
       
   388         _ = self.req._
       
   389         entity = self.entity(row, col)
       
   390         self.w(u'<div class="metadata">')
       
   391         if self.show_eid:
       
   392             self.w(u'#%s - ' % entity.eid)
       
   393         if entity.modification_date != entity.creation_date:
       
   394             self.w(u'<span>%s</span> ' % _('latest update on'))
       
   395             self.w(u'<span class="value">%s</span>,&nbsp;'
       
   396                    % self.format_date(entity.modification_date))
       
   397         # entities from external source may not have a creation date (eg ldap)
       
   398         if entity.creation_date: 
       
   399             self.w(u'<span>%s</span> ' % _('created on'))
       
   400             self.w(u'<span class="value">%s</span>'
       
   401                    % self.format_date(entity.creation_date))
       
   402         if entity.creator:
       
   403             creatoreid = entity.creator.eid
       
   404             self.w(u'&nbsp;<span>%s</span> ' % _('by'))
       
   405             self.w(u'<span class="value">%s</span>' % entity.creator.name())
       
   406         else:
       
   407             creatoreid = None            
       
   408         try:
       
   409             owners = ','.join(u.name() for u in entity.owned_by
       
   410                               if u.eid != creatoreid)
       
   411             if owners:
       
   412                 self.w(u',&nbsp;<span>%s</span> ' % _('owned by'))
       
   413                 self.w(u'<span class="value">%s</span>' % owners)
       
   414         except Unauthorized:
       
   415             pass
       
   416         self.w(u'</div>')
       
   417 
       
   418 
       
   419 # new default views for finner control in general views , to use instead of
       
   420 # oneline / secondary
       
   421 
       
   422 class InContextTextView(TextView):
       
   423     id = 'textincontext'
       
   424     title = None # not listed as a possible view
       
   425     def cell_call(self, row, col):
       
   426         entity = self.entity(row, col)
       
   427         self.w(entity.dc_title())
       
   428         
       
   429 class OutOfContextTextView(InContextTextView):
       
   430     id = 'textoutofcontext'
       
   431 
       
   432     def cell_call(self, row, col):
       
   433         entity = self.entity(row, col)
       
   434         self.w(entity.dc_long_title())
       
   435 
       
   436 
       
   437 class InContextView(EntityView):
       
   438     id = 'incontext'
       
   439 
       
   440     def cell_call(self, row, col):
       
   441         entity = self.entity(row, col)
       
   442         desc = cut(entity.dc_description(), 50)
       
   443         self.w(u'<a href="%s" title="%s">' % (html_escape(entity.absolute_url()),
       
   444                                               html_escape(desc)))
       
   445         self.w(html_escape(self.view('textincontext', self.rset, row=row, col=col)))
       
   446         self.w(u'</a>')
       
   447 
       
   448         
       
   449 class OutOfContextView(EntityView):
       
   450     id = 'outofcontext'
       
   451 
       
   452     def cell_call(self, row, col):
       
   453         self.w(u'<a href="%s">' % self.entity(row, col).absolute_url())
       
   454         self.w(html_escape(self.view('textoutofcontext', self.rset, row=row, col=col)))
       
   455         self.w(u'</a>')
       
   456 
       
   457 class NotClickableInContextView(EntityView):
       
   458     id = 'incontext'
       
   459     accepts = ('State',)
       
   460     def cell_call(self, row, col):
       
   461         self.w(html_escape(self.view('textincontext', self.rset, row=row, col=col)))
       
   462 
       
   463 ## class NotClickableOutOfContextView(EntityView):
       
   464 ##     id = 'outofcontext'
       
   465 ##     accepts = ('State',)
       
   466 ##     def cell_call(self, row, col):
       
   467 ##         self.w(html_escape(self.view('textoutofcontext', self.rset, row=row)))
       
   468 
       
   469             
       
   470 # list and table related views ################################################
       
   471     
       
   472 class ListView(EntityView):
       
   473     id = 'list'
       
   474     title = _('list')
       
   475     item_vid = 'listitem'
       
   476         
       
   477     def call(self, klass=None, title=None, subvid=None, listid=None, **kwargs):
       
   478         """display a list of entities by calling their <item_vid> view
       
   479         
       
   480         :param listid: the DOM id to use for the root element
       
   481         """
       
   482         if subvid is None and 'subvid' in self.req.form:
       
   483             subvid = self.req.form.pop('subvid') # consume it
       
   484         if listid:
       
   485             listid = u' id="%s"' % listid
       
   486         else:
       
   487             listid = u''
       
   488         if title:
       
   489             self.w(u'<div%s class="%s"><h4>%s</h4>\n' % (listid, klass or 'section', title))
       
   490             self.w(u'<ul>\n')
       
   491         else:
       
   492             self.w(u'<ul%s class="%s">\n' % (listid, klass or 'section'))
       
   493         for i in xrange(self.rset.rowcount):
       
   494             self.cell_call(row=i, col=0, vid=subvid, **kwargs)
       
   495         self.w(u'</ul>\n')
       
   496         if title:
       
   497             self.w(u'</div>\n')
       
   498 
       
   499     def cell_call(self, row, col=0, vid=None, **kwargs):
       
   500         self.w(u'<li>')
       
   501         self.wview(self.item_vid, self.rset, row=row, col=col, vid=vid, **kwargs)
       
   502         self.w(u'</li>\n')
       
   503 
       
   504     def url(self):
       
   505         """overrides url method so that by default, the view list is called
       
   506         with sorted entities
       
   507         """
       
   508         coltypes = self.rset.column_types(0)
       
   509         # don't want to generate the rql if there is some restriction on
       
   510         # something else than the entity type
       
   511         if len(coltypes) == 1:
       
   512             # XXX norestriction is not correct here. For instance, in cases like
       
   513             # Any P,N WHERE P is Project, P name N
       
   514             # norestriction should equal True
       
   515             restr = self.rset.syntax_tree().children[0].where
       
   516             norestriction = (isinstance(restr, nodes.Relation) and
       
   517                              restr.is_types_restriction())
       
   518             if norestriction:
       
   519                 etype = iter(coltypes).next()
       
   520                 return self.build_url(etype.lower(), vid=self.id)
       
   521         if len(self.rset) == 1:
       
   522             entity = self.rset.get_entity(0, 0)
       
   523             return self.build_url(entity.rest_path(), vid=self.id)
       
   524         return self.build_url(rql=self.rset.printable_rql(), vid=self.id)
       
   525 
       
   526  
       
   527 class ListItemView(EntityView):
       
   528     id = 'listitem'
       
   529     
       
   530     @property
       
   531     def redirect_vid(self):
       
   532         if self.req.search_state[0] == 'normal':
       
   533             return 'outofcontext'
       
   534         return 'outofcontext-search'
       
   535         
       
   536     def cell_call(self, row, col, vid=None, **kwargs):
       
   537         if not vid:
       
   538             vid = self.redirect_vid
       
   539         try:
       
   540             self.wview(vid, self.rset, row=row, col=col, **kwargs)
       
   541         except NoSelectableObject:
       
   542             if vid == self.redirect_vid:
       
   543                 raise
       
   544             kwargs.pop('done', None)
       
   545             self.wview(self.redirect_vid, self.rset, row=row, col=col, **kwargs)
       
   546 
       
   547 
       
   548 class SimpleListView(ListItemView):
       
   549     """list without bullets"""
       
   550     id = 'simplelist'
       
   551     redirect_vid = 'incontext'
       
   552 
       
   553 
       
   554 class CSVView(SimpleListView):
       
   555     id = 'csv'
       
   556     redirect_vid = 'incontext'
       
   557         
       
   558     def call(self, **kwargs):
       
   559         rset = self.rset
       
   560         for i in xrange(len(rset)):
       
   561             self.cell_call(i, 0, vid=kwargs.get('vid'))
       
   562             if i < rset.rowcount-1:
       
   563                 self.w(u", ")
       
   564 
       
   565 
       
   566 class TreeItemView(ListItemView):
       
   567     accepts = ('Any',)
       
   568     id = 'treeitem'
       
   569     
       
   570     def cell_call(self, row, col):
       
   571         self.wview('incontext', self.rset, row=row, col=col)
       
   572 
       
   573 
       
   574 # xml and xml/rss views #######################################################
       
   575     
       
   576 class XmlView(EntityView):
       
   577     id = 'xml'
       
   578     title = _('xml')
       
   579     templatable = False
       
   580     content_type = 'text/xml'
       
   581     xml_root = 'rset'
       
   582     item_vid = 'xmlitem'
       
   583     
       
   584     def cell_call(self, row, col):
       
   585         self.wview(self.item_vid, self.rset, row=row, col=col)
       
   586         
       
   587     def call(self):
       
   588         """display a list of entities by calling their <item_vid> view
       
   589         """
       
   590         self.w(u'<?xml version="1.0" encoding="%s"?>\n' % self.req.encoding)
       
   591         self.w(u'<%s size="%s">\n' % (self.xml_root, len(self.rset)))
       
   592         for i in xrange(self.rset.rowcount):
       
   593             self.cell_call(i, 0)
       
   594         self.w(u'</%s>\n' % self.xml_root)
       
   595 
       
   596 
       
   597 class XmlItemView(EntityView):
       
   598     id = 'xmlitem'
       
   599 
       
   600     def cell_call(self, row, col):
       
   601         """ element as an item for an xml feed """
       
   602         entity = self.complete_entity(row, col)
       
   603         self.w(u'<%s>\n' % (entity.e_schema))
       
   604         for rschema, attrschema in entity.e_schema.attribute_definitions():
       
   605             attr = rschema.type
       
   606             try:
       
   607                 value = entity[attr]
       
   608             except KeyError:
       
   609                 # Bytes
       
   610                 continue
       
   611             if value is not None:
       
   612                 if attrschema == 'Bytes':
       
   613                     from base64 import b64encode
       
   614                     value = '<![CDATA[%s]]>' % b64encode(value.getvalue())
       
   615                 elif isinstance(value, basestring):
       
   616                     value = value.replace('&', '&amp;').replace('<', '&lt;')
       
   617                 self.w(u'  <%s>%s</%s>\n' % (attr, value, attr))
       
   618         self.w(u'</%s>\n' % (entity.e_schema))
       
   619 
       
   620 
       
   621 class RssView(XmlView):
       
   622     id = 'rss'
       
   623     title = _('rss')
       
   624     templatable = False
       
   625     content_type = 'text/xml'
       
   626     http_cache_manager = MaxAgeHTTPCacheManager
       
   627     cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
       
   628     
       
   629     def cell_call(self, row, col):
       
   630         self.wview('rssitem', self.rset, row=row, col=col)
       
   631         
       
   632     def call(self):
       
   633         """display a list of entities by calling their <item_vid> view"""
       
   634         req = self.req
       
   635         self.w(u'<?xml version="1.0" encoding="%s"?>\n' % req.encoding)
       
   636         self.w(u'''<rdf:RDF
       
   637  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
       
   638  xmlns:dc="http://purl.org/dc/elements/1.1/"
       
   639  xmlns="http://purl.org/rss/1.0/"
       
   640 >''')
       
   641         self.w(u'  <channel rdf:about="%s">\n' % html_escape(req.url()))
       
   642         self.w(u'    <title>%s RSS Feed</title>\n' % html_escape(self.page_title()))
       
   643         self.w(u'    <description>%s</description>\n' % html_escape(req.form.get('vtitle', '')))
       
   644         params = req.form.copy()
       
   645         params.pop('vid', None)
       
   646         self.w(u'    <link>%s</link>\n' % html_escape(self.build_url(**params)))
       
   647         self.w(u'    <items>\n')
       
   648         self.w(u'      <rdf:Seq>\n')
       
   649         for entity in self.rset.entities():
       
   650             self.w(u'      <rdf:li resource="%s" />\n' % html_escape(entity.absolute_url()))
       
   651         self.w(u'      </rdf:Seq>\n')
       
   652         self.w(u'    </items>\n')
       
   653         self.w(u'  </channel>\n')
       
   654         for i in xrange(self.rset.rowcount):
       
   655             self.cell_call(i, 0)
       
   656         self.w(u'</rdf:RDF>')
       
   657 
       
   658 
       
   659 class RssItemView(EntityView):
       
   660     id = 'rssitem'
       
   661     date_format = '%%Y-%%m-%%dT%%H:%%M%+03i:00' % (timezone / 3600)
       
   662 
       
   663     def cell_call(self, row, col):
       
   664         entity = self.complete_entity(row, col)
       
   665         self.w(u'<item rdf:about="%s">\n' % html_escape(entity.absolute_url()))
       
   666         self._marker('title', entity.dc_long_title())
       
   667         self._marker('link', entity.absolute_url())
       
   668         self._marker('description', entity.dc_description())
       
   669         self._marker('dc:date', entity.dc_date(self.date_format))
       
   670         self._marker('author', entity.dc_authors())
       
   671         self.w(u'</item>\n')
       
   672         
       
   673     def _marker(self, marker, value):
       
   674         if value:
       
   675             self.w(u'  <%s>%s</%s>\n' % (marker, html_escape(value), marker))
       
   676 
       
   677 
       
   678 class CSVMixIn(object):
       
   679     """mixin class for CSV views"""
       
   680     templatable = False
       
   681     content_type = "text/comma-separated-values"    
       
   682     binary = True # avoid unicode assertion
       
   683     csv_params = {'dialect': 'excel',
       
   684                   'quotechar': '"',
       
   685                   'delimiter': ';',
       
   686                   'lineterminator': '\n'}
       
   687     
       
   688     def set_request_content_type(self):
       
   689         """overriden to set a .csv filename"""
       
   690         self.req.set_content_type(self.content_type, filename='cubicwebexport.csv')
       
   691             
       
   692     def csvwriter(self, **kwargs):
       
   693         params = self.csv_params.copy()
       
   694         params.update(kwargs)
       
   695         return UnicodeCSVWriter(self.w, self.req.encoding, **params)
       
   696 
       
   697     
       
   698 class CSVRsetView(CSVMixIn, AnyRsetView):
       
   699     """dumps rset in CSV"""
       
   700     id = 'csvexport'
       
   701     title = _('csv export')
       
   702         
       
   703     def call(self):
       
   704         writer = self.csvwriter()
       
   705         writer.writerow(self.get_headers_labels())
       
   706         descr = self.rset.description
       
   707         for rowindex, row in enumerate(self.rset):
       
   708             csvrow = []
       
   709             for colindex, val in enumerate(row):
       
   710                 etype = descr[rowindex][colindex]
       
   711                 if val is not None and not self.schema.eschema(etype).is_final():
       
   712                     # csvrow.append(val) # val is eid in that case
       
   713                     content = self.view('textincontext', self.rset, 
       
   714                                         row=rowindex, col=colindex)
       
   715                 else:
       
   716                     content = self.view('final', self.rset, displaytime=True,
       
   717                                         row=rowindex, col=colindex)
       
   718                 csvrow.append(content)                    
       
   719             writer.writerow(csvrow)
       
   720     
       
   721     def get_headers_labels(self):
       
   722         rqlstdescr = self.rset.syntax_tree().get_description()[0] # XXX missing Union support
       
   723         labels = []
       
   724         for colindex, attr in enumerate(rqlstdescr):
       
   725             # compute column header
       
   726             if colindex == 0 or attr == 'Any': # find a better label
       
   727                 label = ','.join(display_name(self.req, et)
       
   728                                  for et in self.rset.column_types(colindex))
       
   729             else:
       
   730                 label = display_name(self.req, attr)
       
   731             labels.append(label)
       
   732         return labels
       
   733 
       
   734     
       
   735 class CSVEntityView(CSVMixIn, EntityView):
       
   736     """dumps rset's entities (with full set of attributes) in CSV"""
       
   737     id = 'ecsvexport'
       
   738     title = _('csv entities export')
       
   739 
       
   740     def call(self):
       
   741         """
       
   742         the generated CSV file will have a table per entity type
       
   743         found in the resultset. ('table' here only means empty
       
   744         lines separation between table contents)
       
   745         """
       
   746         req = self.req
       
   747         rows_by_type = {}
       
   748         writer = self.csvwriter()
       
   749         rowdef_by_type = {}
       
   750         for index in xrange(len(self.rset)):
       
   751             entity = self.complete_entity(index)
       
   752             if entity.e_schema not in rows_by_type:
       
   753                 rowdef_by_type[entity.e_schema] = [rs for rs, as in entity.e_schema.attribute_definitions()
       
   754                                                    if as.type != 'Bytes']
       
   755                 rows_by_type[entity.e_schema] = [[display_name(req, rschema.type)
       
   756                                                   for rschema in rowdef_by_type[entity.e_schema]]]
       
   757             rows = rows_by_type[entity.e_schema]
       
   758             rows.append([entity.printable_value(rs.type, format='text/plain')
       
   759                          for rs in rowdef_by_type[entity.e_schema]])
       
   760         for etype, rows in rows_by_type.items():
       
   761             writer.writerows(rows)
       
   762             # use two empty lines as separator
       
   763             writer.writerows([[], []])        
       
   764     
       
   765 
       
   766 ## Work in progress ###########################################################
       
   767 
       
   768 class SearchForAssociationView(EntityView):
       
   769     """view called by the edition view when the user asks
       
   770     to search for something to link to the edited eid
       
   771     """
       
   772     id = 'search-associate'
       
   773     title = _('search for association')
       
   774     __selectors__ = (onelinerset_selector, searchstate_selector, accept_selector)
       
   775     accepts = ('Any',)
       
   776     search_states = ('linksearch',)
       
   777 
       
   778     def cell_call(self, row, col):
       
   779         rset, vid, divid, paginate = self.filter_box_context_info()
       
   780         self.w(u'<div id="%s">' % divid)
       
   781         self.pagination(self.req, rset, w=self.w)
       
   782         self.wview(vid, rset)
       
   783         self.w(u'</div>')
       
   784 
       
   785     @cached
       
   786     def filter_box_context_info(self):
       
   787         entity = self.entity(0, 0)
       
   788         role, eid, rtype, etype = self.req.search_state[1]
       
   789         assert entity.eid == typed_eid(eid)
       
   790         # the default behaviour is to fetch all unrelated entities and display
       
   791         # them. Use fetch_order and not fetch_unrelated_order as sort method
       
   792         # since the latter is mainly there to select relevant items in the combo
       
   793         # box, it doesn't give interesting result in this context
       
   794         rql = entity.unrelated_rql(rtype, etype, role,
       
   795                                    ordermethod='fetch_order',
       
   796                                    vocabconstraints=False)
       
   797         rset = self.req.execute(rql, {'x' : entity.eid}, 'x')
       
   798         #vid = vid_from_rset(self.req, rset, self.schema)
       
   799         return rset, 'list', "search-associate-content", True
       
   800 
       
   801 
       
   802 class OutOfContextSearch(EntityView):
       
   803     id = 'outofcontext-search'
       
   804     def cell_call(self, row, col):
       
   805         entity = self.entity(row, col)
       
   806         erset = entity.as_rset()
       
   807         if linksearch_match(self.req, erset):
       
   808             self.w(u'<a href="%s" title="%s">%s</a>&nbsp;<a href="%s" title="%s">[...]</a>' % (
       
   809                 html_escape(linksearch_select_url(self.req, erset)),
       
   810                 self.req._('select this entity'),
       
   811                 html_escape(entity.view('textoutofcontext')),
       
   812                 html_escape(entity.absolute_url(vid='primary')),
       
   813                 self.req._('view detail for this entity')))
       
   814         else:
       
   815             entity.view('outofcontext', w=self.w)
       
   816             
       
   817             
       
   818 class EditRelationView(EntityView):
       
   819     """Note: This is work in progress
       
   820 
       
   821     This view is part of the edition view refactoring.
       
   822     It is still too big and cluttered with strange logic, but it's a start
       
   823 
       
   824     The main idea is to be able to call an edition view for a specific
       
   825     relation. For example :
       
   826        self.wview('editrelation', person_rset, rtype='firstname')
       
   827        self.wview('editrelation', person_rset, rtype='works_for')
       
   828     """
       
   829     id = 'editrelation'
       
   830 
       
   831     __selectors__ = (req_form_params_selector,)
       
   832     form_params = ('rtype',)
       
   833     
       
   834     # TODO: inlineview, multiple edit, (widget view ?)
       
   835     def cell_call(self, row, col, rtype=None, role='subject', targettype=None,
       
   836                  showlabel=True):
       
   837         self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
       
   838         entity = self.entity(row, col)
       
   839         rtype = self.req.form.get('rtype', rtype)
       
   840         showlabel = self.req.form.get('showlabel', showlabel)
       
   841         assert rtype is not None, "rtype is mandatory for 'edirelation' view"
       
   842         targettype = self.req.form.get('targettype', targettype)
       
   843         role = self.req.form.get('role', role)
       
   844         mode = entity.rtags.get_mode(rtype, targettype, role)
       
   845         if mode == 'create':
       
   846             return
       
   847         category = entity.rtags.get_category(rtype, targettype, role)
       
   848         if category in ('generated', 'metadata'):
       
   849             return
       
   850         elif category in ('primary', 'secondary'):
       
   851             if hasattr(entity, '%s_format' % rtype):
       
   852                 formatwdg = entity.get_widget('%s_format' % rtype, role)
       
   853                 self.w(formatwdg.edit_render(entity))
       
   854                 self.w(u'<br/>')
       
   855             wdg = entity.get_widget(rtype, role)
       
   856             if showlabel:
       
   857                 self.w(u'%s' % wdg.render_label(entity))
       
   858             self.w(u'%s %s %s' %
       
   859                    (wdg.render_error(entity), wdg.edit_render(entity),
       
   860                     wdg.render_help(entity),))
       
   861         elif category == 'generic':
       
   862             self._render_generic_relation(entity, rtype, role)
       
   863         else:
       
   864             self.error("oops, wrong category %s", category)
       
   865 
       
   866     def _render_generic_relation(self, entity, relname, role):
       
   867         text = self.req.__('add %s %s %s' % (entity.e_schema, relname, role))
       
   868         # pending operations
       
   869         operations = self.req.get_pending_operations(entity, relname, role)
       
   870         if operations['insert'] or operations['delete'] or 'unfold' in self.req.form:
       
   871             self.w(u'<h3>%s</h3>' % text)
       
   872             self._render_generic_relation_form(operations, entity, relname, role)
       
   873         else:
       
   874             divid = "%s%sreledit" % (relname, role)
       
   875             url = ajax_replace_url(divid, rql_for_eid(entity.eid), 'editrelation',
       
   876                                    {'unfold' : 1, 'relname' : relname, 'role' : role})
       
   877             self.w(u'<a href="%s">%s</a>' % (url, text))
       
   878             self.w(u'<div id="%s"></div>' % divid)
       
   879         
       
   880 
       
   881     def _build_opvalue(self, entity, relname, target, role):
       
   882         if role == 'subject':
       
   883             return '%s:%s:%s' % (entity.eid, relname, target)
       
   884         else:
       
   885             return '%s:%s:%s' % (target, relname, entity.eid)
       
   886         
       
   887     
       
   888     def _render_generic_relation_form(self, operations, entity, relname, role):
       
   889         rqlexec = self.req.execute
       
   890         for optype, targets in operations.items():
       
   891             for target in targets:
       
   892                 self._render_pending(optype, entity, relname, target, role)
       
   893                 opvalue = self._build_opvalue(entity, relname, target, role)
       
   894                 self.w(u'<a href="javascript: addPendingDelete(\'%s\', %s);">-</a> '
       
   895                        % (opvalue, entity.eid))
       
   896                 rset = rqlexec('Any X WHERE X eid %(x)s', {'x': target}, 'x')
       
   897                 self.wview('oneline', rset)
       
   898         # now, unrelated ones
       
   899         self._render_unrelated_selection(entity, relname, role)
       
   900 
       
   901     def _render_pending(self, optype, entity, relname, target, role):
       
   902         opvalue = self._build_opvalue(entity, relname, target, role)
       
   903         self.w(u'<input type="hidden" name="__%s" value="%s" />'
       
   904                % (optype, opvalue))
       
   905         if optype == 'insert':
       
   906             checktext = '-'
       
   907         else:
       
   908             checktext = '+'
       
   909         rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': target}, 'x')
       
   910         self.w(u"""[<a href="javascript: cancelPending%s('%s:%s:%s')">%s</a>"""
       
   911                % (optype.capitalize(), relname, target, role,
       
   912                   self.view('oneline', rset)))
       
   913 
       
   914     def _render_unrelated_selection(self, entity, relname, role):
       
   915         rschema = self.schema.rschema(relname)
       
   916         if role == 'subject':
       
   917             targettypes = rschema.objects(entity.e_schema)
       
   918         else:
       
   919             targettypes = rschema.subjects(entity.e_schema)
       
   920         self.w(u'<select onselect="addPendingInsert(this.selected.value);">')
       
   921         for targettype in targettypes:
       
   922             unrelated = entity.unrelated(relname, targettype, role) # XXX limit
       
   923             for rowindex, row in enumerate(unrelated):
       
   924                 teid = row[0]
       
   925                 opvalue = self._build_opvalue(entity, relname, teid, role)
       
   926                 self.w(u'<option name="__insert" value="%s>%s</option>'
       
   927                        % (opvalue, self.view('text', unrelated, row=rowindex)))
       
   928         self.w(u'</select>')
       
   929 
       
   930 
       
   931 class TextSearchResultView(EntityView):
       
   932     """this view is used to display full-text search
       
   933 
       
   934     It tries to highlight part of data where the search word appears.
       
   935 
       
   936     XXX: finish me (fixed line width, fixed number of lines, CSS, etc.)
       
   937     """
       
   938     id = 'tsearch'
       
   939 
       
   940 
       
   941     def cell_call(self, row, col, **kwargs):
       
   942         entity = self.complete_entity(row, col)
       
   943         self.w(entity.view('incontext'))
       
   944         searched = self.rset.searched_text()
       
   945         if searched is None:
       
   946             return
       
   947         searched = searched.lower()
       
   948         highlighted = '<b>%s</b>' % searched
       
   949         for attr in entity.e_schema.indexable_attributes():
       
   950             try:
       
   951                 value = html_escape(entity.printable_value(attr, format='text/plain').lower())
       
   952             except TransformError, ex:
       
   953                 continue
       
   954             except:
       
   955                 continue
       
   956             if searched in value:
       
   957                 contexts = []
       
   958                 for ctx in value.split(searched):
       
   959                     if len(ctx) > 30:
       
   960                         contexts.append(u'...' + ctx[-30:])
       
   961                     else:
       
   962                         contexts.append(ctx)
       
   963                 value = u'\n' + highlighted.join(contexts)
       
   964                 self.w(value.replace('\n', '<br/>'))            
       
   965 
       
   966 
       
   967 class EntityRelationView(EntityView):
       
   968     accepts = ()
       
   969     vid = 'list'
       
   970     
       
   971     def cell_call(self, row, col):
       
   972         if self.target == 'object':
       
   973             role = 'subject'
       
   974         else:
       
   975             role = 'object'
       
   976         rset = self.rset.get_entity(row, col).related(self.rtype, role)
       
   977         self.w(u'<h1>%s</h1>' % self.req._(self.title).capitalize())
       
   978         self.w(u'<div class="mainInfo">')
       
   979         self.wview(self.vid, rset, 'noresult')
       
   980         self.w(u'</div>')
       
   981 
       
   982 
       
   983 class TooltipView(OneLineView):
       
   984     """A entity view used in a tooltip"""
       
   985     id = 'tooltip'
       
   986     title = None # don't display in possible views
       
   987     def cell_call(self, row, col):
       
   988         self.wview('oneline', self.rset, row=row, col=col)
       
   989 
       
   990 try:
       
   991     from cubicweb.web.views.tableview import TableView
       
   992     from logilab.common.deprecation import class_moved
       
   993     TableView = class_moved(TableView)
       
   994 except ImportError:
       
   995     pass # gae has no tableview module (yet)