a bit of reorganisation inside web/views: tls-sprint
authorsylvain.thenault@logilab.fr
Thu, 12 Mar 2009 16:29:00 +0100
branchtls-sprint
changeset 1091 b5e253c0dd13
parent 1090 a99dc223c583
child 1092 b8fbb95dc0eb
a bit of reorganisation inside web/views: * move all workflow related stuff into views/workflow.py * move all schema related stuff into views/schema.py * move all RSS related stuff into views/xmlrss.py * start new editforms module, designed to contains new automatic forms code
web/views/__init__.py
web/views/actions.py
web/views/basecomponents.py
web/views/baseforms.py
web/views/baseviews.py
web/views/boxes.py
web/views/dynimages.py
web/views/editforms.py
web/views/massmailing.py
web/views/schema.py
web/views/schemaentities.py
web/views/wfentities.py
web/views/workflow.py
web/views/xmlrss.py
--- a/web/views/__init__.py	Thu Mar 12 16:23:31 2009 +0100
+++ b/web/views/__init__.py	Thu Mar 12 16:29:00 2009 +0100
@@ -1,13 +1,17 @@
-"""Views/forms and actions for the CubicWeb web client
+"""Views, forms, actions... for the CubicWeb web client
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
-    
+
+import os
+from tempfile import mktemp
+
 from rql import nodes
 
+
 def need_table_view(rset, schema):
     """return True if we think that a table view is more appropriate than a
     list or primary view to display the given result set
@@ -67,6 +71,7 @@
             return 'outofcontext-search'
         return 'list'
     return 'table'
+
     
 def linksearch_select_url(req, rset):
     """when searching an entity to create a relation, return an url to select
@@ -80,3 +85,21 @@
         id_fmt = '%%s:%s:%s' % (r_type, eid)
     triplets = '-'.join(id_fmt % row[0] for row in rset.rows)
     return "javascript: selectForAssociation('%s', '%s');" % (triplets, eid)
+                
+        
+class TmpFileViewMixin(object):
+    binary = True
+    content_type = 'application/octet-stream'
+    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
+    
+    def call(self):
+        self.cell_call()
+        
+    def cell_call(self, row=0, col=0):
+        self.row, self.col = row, col # in case one need it
+        tmpfile = mktemp('.png')
+        try:
+            self._generate(tmpfile)
+            self.w(open(tmpfile).read())
+        finally:
+            os.unlink(tmpfile)
--- a/web/views/actions.py	Thu Mar 12 16:23:31 2009 +0100
+++ b/web/views/actions.py	Thu Mar 12 16:29:00 2009 +0100
@@ -266,18 +266,6 @@
     order = 20
 
 
-class ViewSchemaAction(Action):
-    id = 'schema'
-    __select__ = yes()
-    
-    title = _("site schema")
-    category = 'siteactions'
-    order = 30
-    
-    def url(self):
-        return self.build_url(self.id)
-
-
 from logilab.common.deprecation import class_moved
 from cubicweb.web.views.bookmark import FollowAction
 FollowAction = class_moved(FollowAction)
--- a/web/views/basecomponents.py	Thu Mar 12 16:23:31 2009 +0100
+++ b/web/views/basecomponents.py	Thu Mar 12 16:29:00 2009 +0100
@@ -2,7 +2,6 @@
 
 * the rql input form
 * the logged user link
-* the workflow history section for workflowable objects
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
@@ -134,41 +133,6 @@
         self.w(u'</div>')
 
 
-class WFHistoryVComponent(EntityVComponent):
-    """display the workflow history for entities supporting it"""
-    id = 'wfhistory'
-    __select__ = (EntityVComponent.__select__
-                  & relation_possible('wf_info_for', role='object'))
-    context = 'navcontentbottom'
-    title = _('Workflow history')
-
-    def cell_call(self, row, col, view=None):
-        _ = self.req._
-        eid = self.rset[row][col]
-        sel = 'Any FS,TS,WF,D'
-        rql = ' ORDERBY D DESC WHERE WF wf_info_for X,'\
-              'WF from_state FS, WF to_state TS, WF comment C,'\
-              'WF creation_date D'
-        if self.vreg.schema.eschema('EUser').has_perm(self.req, 'read'):
-            sel += ',U,C'
-            rql += ', WF owned_by U?'
-            displaycols = range(5)
-            headers = (_('from_state'), _('to_state'), _('comment'), _('date'),
-                       _('EUser'))            
-        else:
-            sel += ',C'
-            displaycols = range(4)
-            headers = (_('from_state'), _('to_state'), _('comment'), _('date'))
-        rql = '%s %s, X eid %%(x)s' % (sel, rql)
-        try:
-            rset = self.req.execute(rql, {'x': eid}, 'x')
-        except Unauthorized:
-            return
-        if rset:
-            self.wview('table', rset, title=_(self.title), displayactions=False,
-                       displaycols=displaycols, headers=headers)
-
-
 class ApplicationName(Component):
     """display the application name"""
     id = 'appliname'
--- a/web/views/baseforms.py	Thu Mar 12 16:23:31 2009 +0100
+++ b/web/views/baseforms.py	Thu Mar 12 16:29:00 2009 +0100
@@ -14,188 +14,18 @@
 from logilab.mtconverter import html_escape
 from logilab.common.decorators import cached
 
-from cubicweb.interfaces import IWorkflowable
 from cubicweb.selectors import (specified_etype_implements, implements,
                                 match_kwargs, match_form_params, one_line_rset,
                                 non_final_entity, accepts_etype_compat)
 from cubicweb.utils import make_uid
 from cubicweb.view import View, EntityView
-from cubicweb.common import tags
 from cubicweb.common.uilib import cut
 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs, eid_param
 from cubicweb.web.controller import NAV_FORM_PARAMETERS
 from cubicweb.web.widgets import checkbox, InputWidget, ComboBoxWidget
-from cubicweb.web.form import MultipleFieldsForm, EntityFieldsForm, FormMixIn, relation_id
-from cubicweb.web.formfields import StringField,  RichTextField, find_field
-from cubicweb.web.formwidgets import HiddenInput
+from cubicweb.web.form import FormMixIn
 
 _ = unicode
-
-
-
-class DeleteConfForm(EntityView):
-    id = 'deleteconf'
-    title = _('delete')
-    domid = 'deleteconf'
-    onsubmit = None
-    # don't use navigation, all entities asked to be deleted should be displayed
-    # else we will only delete the displayed page
-    need_navigation = False
-    
-    def call(self):
-        """ask for confirmation before real deletion"""
-        req, w = self.req, self.w
-        _ = req._
-        req.add_css('cubicweb.form.css')
-        req.add_js('cubicweb.edition.js')
-        w(u'<script type="text/javascript">updateMessage(\'%s\');</script>\n'
-          % _('this action is not reversible!'))
-        # XXX above message should have style of a warning
-        w(u'<h4>%s</h4>\n' % _('Do you want to delete the following element(s) ?'))
-        form = MultipleFieldsForm(req, domid='deleteconf', action=self.build_url('edit'),
-                                  onsubmit=self.onsubmit, copy_nav_params=True)
-        form.buttons.append(form.button_delete(label=stdmsgs.YES))
-        form.buttons.append(form.button_cancel(label=stdmsgs.NO))
-        done = set()
-        w(u'<ul>\n')
-        for i in xrange(self.rset.rowcount):
-            if self.rset[i][0] in done:
-                continue
-            done.add(self.rset[i][0])
-            entity = self.rset.get_entity(i, 0)
-            subform = EntityFieldsForm(req, set_error_url=False,
-                                       entity=entity)
-            form.form_add_subform(subform)
-            # don't use outofcontext view or any other that may contain inline edition form
-            w(u'<li>%s</li>' % tags.a(entity.view('textoutofcontext'),
-                                      href=entity.absolute_url()))
-        w(u'</ul>\n')
-        w(form.form_render())
-
-
-class ChangeStateForm(EntityFieldsForm):
-    id = 'changestate'
-    
-    __method = StringField(name='__method', initial='set_state', widget=HiddenInput)
-    state = StringField(widget=HiddenInput, eidparam=True)
-    trcomment = RichTextField(eidparam=True)
-
-    def form_buttons(self):
-        return [self.button_ok(label=stdmsgs.YES,
-                               tabindex=self.req.next_tabindex()),
-                self.button_cancel(label=stdmsgs.NO,
-                                   tabindex=self.req.next_tabindex())]
-
-        
-class ChangeStateFormView(EntityView):
-    id = 'statuschange'
-    title = _('status change')
-    __select__ = implements(IWorkflowable) & match_form_params('treid')
-
-    def cell_call(self, row, col):
-        entity = self.entity(row, col)
-        eid = entity.eid
-        state = entity.in_state[0]
-        transition = self.req.eid_rset(self.req.form['treid']).get_entity(0, 0)
-        dest = transition.destination()
-        self.req.add_js('cubicweb.edition.js')
-        self.req.add_css('cubicweb.form.css')
-        _ = self.req._
-        form = self.vreg.select_object('forms', 'changestate', self.req, self.rset, row, col,
-                                  entity=entity, redirect_path=self.redirectpath(entity))
-        self.w(form.error_message())
-        self.w(u'<h4>%s %s</h4>\n' % (_(transition.name), entity.view('oneline')))
-        msg = _('status will change from %(st1)s to %(st2)s') % {
-            'st1': _(state.name),
-            'st2': _(dest.name)}
-        self.w(u'<p>%s</p>\n' % msg)
-        self.w(form.form_render(state=dest.eid, trcomment=u''))
-
-    def redirectpath(self, entity):
-        return entity.rest_path()
-
-
-class ClickAndEditForm(FormMixIn, EntityView):
-    id = 'reledit'
-    __select__ = match_kwargs('rtype')
-
-    # FIXME editableField class could be toggleable from userprefs
-      
-    def cell_call(self, row, col, rtype=None, role='subject', reload=False):
-        entity = self.entity(row, col)
-        if getattr(entity, rtype) is None:
-            value = self.req._('not specified')
-        else:
-            value = entity.printable_value(rtype)
-        if not entity.has_perm('update'):
-            self.w(value)
-            return
-        self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
-        eid = entity.eid
-        edit_key = make_uid('%s-%s' % (rtype, eid))
-        divid = 'd%s' % edit_key
-        reload = dumps(reload)
-        buttons = [tags.input(klass="validateButton", type="submit", name="__action_apply",
-                              value=self.req._(stdmsgs.BUTTON_OK), tabindex=self.req.next_tabindex()),
-                   tags.input(klass="validateButton", type="button",
-                              value=self.req._(stdmsgs.BUTTON_CANCEL),
-                              onclick="cancelInlineEdit(%s,\'%s\',\'%s\')" % (eid, rtype, divid),
-                              tabindex=self.req.next_tabindex())]
-        form = self.vreg.select_object('forms', 'edition', self.req, self.rset, row=row, col=col,
-                                       entity=entity, domid='%s-form' % divid, action='#',
-                                       cssstyle='display: none', buttons=buttons,
-                                       onsubmit="return inlineValidateForm('%(divid)s-form', '%(rtype)s', '%(eid)s', '%(divid)s', %(reload)s);" % locals())
-        renderer = FormRenderer(display_label=False, display_help=False,
-                                display_fields=(rtype,), button_bar_class='buttonbar')
-        self.w(tags.div(value, klass='editableField', id=divid,
-                        ondblclick="showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')" % locals()))
-        self.w(form.render(renderer=renderer)
-
-
-## work in progress, this class will replace the standard EditionView soon
-class _EditionForm(EntityView):
-    """primary entity edition form
-
-    When generating a new attribute_input, the editor will look for a method
-    named 'default_ATTRNAME' on the entity instance, where ATTRNAME is the
-    name of the attribute being edited. You may use this feature to compute
-    dynamic default values such as the 'tomorrow' date or the user's login
-    being connected
-    """    
-    id = '_edition'
-    __select__ = one_line_rset() & non_final_entity()
-
-    title = _('edition')
-    controller = 'edit'
-    skip_relations = FormMixIn.skip_relations.copy()
-
-    def cell_call(self, row, col, **kwargs):
-        self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
-        self.req.add_css('cubicweb.form.css')
-        self.initialize_varmaker()
-        entity = self.complete_entity(row, col)
-
-    def initialize_varmaker(self):
-        varmaker = self.req.get_page_data('rql_varmaker')
-        if varmaker is None:
-            varmaker = self.req.varmaker
-            self.req.set_page_data('rql_varmaker', varmaker)
-        self.varmaker = varmaker
-        
-    def edit_form(self, entity, kwargs):
-        form = EntityFieldsForm(self.req, entity=entity)
-        for rschema, target in self.editable_attributes(entity):
-            field = find_field(entity.__class__, entity.e_schema, rschema, target)
-            form.fields.append(field)
-        form.buttons.append(form.button_ok())
-        form.buttons.append(form.button_apply())
-        form.buttons.append(form.button_cancel())
-        self.w(form.form_render())
-
-    def editable_attributes(self, entity):
-        # XXX both (add, delete)
-        return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add')
-                if rschema != 'eid']
     
     
 class EditionForm(FormMixIn, EntityView):
@@ -242,8 +72,7 @@
 '''
 
     def cell_call(self, row, col, **kwargs):
-        self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
-        self.req.add_css('cubicweb.form.css')
+        self.req.add_js( ('cubicweb.ajax.js', ) )
         entity = self.complete_entity(row, col)
         self.edit_form(entity, kwargs)
 
@@ -512,7 +341,7 @@
 
 
     
-class CreationForm(_EditionForm):
+class CreationForm(EditionForm):
     __select__ = specified_etype_implements('Any')
     # XXX bw compat, use View.registered since we don't want accept_compat
     #    wrapper set in EntityView
@@ -522,8 +351,7 @@
     
     def call(self, **kwargs):
         """creation view for an entity"""
-        self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
-        self.req.add_css('cubicweb.form.css')
+        self.req.add_js( ('cubicweb.ajax.js',) )
         self.initialize_varmaker()
         etype = kwargs.pop('etype', self.req.form.get('etype'))
         try:
@@ -657,7 +485,6 @@
         :param rtype: the relation bridging `etype` and `parent`
         :param role: the role played by the `parent` in the relation
         """
-        self.req.add_css('cubicweb.form.css')
         try:
             entity = self.vreg.etype_class(etype)(self.req, None, None)
         except:
@@ -694,7 +521,6 @@
         insertions of <div class="section"> between each row of
         the resultset
         """
-        self.req.add_css('cubicweb.form.css')
         rset = self.rset
         for i in xrange(len(rset)):
             self.wview(self.id, rset, row=i, **kwargs)
@@ -726,8 +552,7 @@
     title = _('copy edition')
 
     def cell_call(self, row, col, **kwargs):
-        self.req.add_js(('cubicweb.ajax.js', 'cubicweb.edition.js'))
-        self.req.add_css('cubicweb.form.css')
+        self.req.add_js(('cubicweb.ajax.js',))
         entity = self.complete_entity(row, col, skip_bytes=True)
         # make a copy of entity to avoid altering the entity in the
         # request's cache. 
@@ -807,8 +632,6 @@
         """
         req = self.req
         form = req.form
-        req.add_js('cubicweb.edition.js')
-        req.add_css('cubicweb.form.css')
         _ = req._
         sampleentity = self.complete_entity(0)
         attrheaders = [u'<th>%s</th>' % rdef[0].display_name(req, rdef[-1])
--- a/web/views/baseviews.py	Thu Mar 12 16:23:31 2009 +0100
+++ b/web/views/baseviews.py	Thu Mar 12 16:29:00 2009 +0100
@@ -4,7 +4,6 @@
 * primary, sidebox
 * secondary, oneline, incontext, outofcontext, text
 * list
-* xml, rss
 
 
 :organization: Logilab
--- a/web/views/boxes.py	Thu Mar 12 16:23:31 2009 +0100
+++ b/web/views/boxes.py	Thu Mar 12 16:29:00 2009 +0100
@@ -3,7 +3,6 @@
 
 * actions box
 * possible views box
-* rss icon
 
 additional (disabled by default) boxes
 * schema box
@@ -186,26 +185,6 @@
         if not box.is_empty():
             box.render(self.w)
 
-        
-class RSSIconBox(BoxTemplate):
-    """just display the RSS icon on uniform result set"""
-    id = 'rss'
-    __select__ = (BoxTemplate.__select__
-                  & appobject_selectable('components', 'rss_feed_url'))
-    
-    visible = False
-    order = 999
-    
-    def call(self, **kwargs):
-        try:
-            rss = self.req.external_resource('RSS_LOGO')
-        except KeyError:
-            self.error('missing RSS_LOGO external resource')
-            return
-        urlgetter = self.vreg.select_component('rss_feed_url', self.req, self.rset)
-        url = urlgetter.feed_url()
-        self.w(u'<a href="%s"><img src="%s" alt="rss"/></a>\n' % (html_escape(url), rss))
-
 
 class StartupViewsBox(BoxTemplate):
     """display a box containing links to all startup views"""
--- a/web/views/dynimages.py	Thu Mar 12 16:23:31 2009 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,199 +0,0 @@
-"""dynamically generated image views
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-__docformat__ = "restructuredtext en"
-
-import os
-from tempfile import mktemp
-from itertools import cycle
-
-from logilab.common.graph import escape, GraphGenerator, DotBackend
-from yams import schema2dot as s2d
-
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView, StartupView
-
-
-class RestrictedSchemaDotPropsHandler(s2d.SchemaDotPropsHandler):
-    def __init__(self, req):
-        # FIXME: colors are arbitrary
-        self.nextcolor = cycle( ('#aa0000', '#00aa00', '#0000aa',
-                                 '#000000', '#888888') ).next
-        self.req = req
-        
-    def display_attr(self, rschema):
-        return not rschema.meta and (rschema.has_local_role('read')
-                                     or rschema.has_perm(self.req, 'read'))
-    
-    # XXX remove this method once yams > 0.20 is out
-    def node_properties(self, eschema):
-        """return default DOT drawing options for an entity schema"""
-        label = ['{',eschema.type,'|']
-        label.append(r'\l'.join(rel.type for rel in eschema.subject_relations()
-                                if rel.final and self.display_attr(rel)))
-        label.append(r'\l}') # trailing \l ensure alignement of the last one
-        return {'label' : ''.join(label), 'shape' : "record",
-                'fontname' : "Courier", 'style' : "filled"}
-
-    def edge_properties(self, rschema, subjnode, objnode):
-        kwargs = super(RestrictedSchemaDotPropsHandler, self).edge_properties(rschema, subjnode, objnode)
-        # symetric rels are handled differently, let yams decide what's best
-        if not rschema.symetric:
-            kwargs['color'] = self.nextcolor()
-        kwargs['fontcolor'] = kwargs['color']
-        # dot label decoration is just awful (1 line underlining the label
-        # + 1 line going to the closest edge spline point)
-        kwargs['decorate'] = 'false'
-        return kwargs
-    
-
-class RestrictedSchemaVisitorMiIn:
-    def __init__(self, req, *args, **kwargs):
-        # hack hack hack
-        assert len(self.__class__.__bases__) == 2
-        self.__parent = self.__class__.__bases__[1]
-        self.__parent.__init__(self, *args, **kwargs)
-        self.req = req
-        
-    def nodes(self):
-        for etype, eschema in self.__parent.nodes(self):
-            if eschema.has_local_role('read') or eschema.has_perm(self.req, 'read'):
-                yield eschema.type, eschema
-            
-    def edges(self):
-        for setype, oetype, rschema in self.__parent.edges(self):
-            if rschema.has_local_role('read') or rschema.has_perm(self.req, 'read'):
-                yield setype, oetype, rschema
-
-class FullSchemaVisitor(RestrictedSchemaVisitorMiIn, s2d.FullSchemaVisitor):
-    pass
-
-class OneHopESchemaVisitor(RestrictedSchemaVisitorMiIn, s2d.OneHopESchemaVisitor):
-    pass
-
-class OneHopRSchemaVisitor(RestrictedSchemaVisitorMiIn, s2d.OneHopRSchemaVisitor):
-    pass
-                
-        
-class TmpFileViewMixin(object):
-    binary = True
-    content_type = 'application/octet-stream'
-    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
-    
-    def call(self):
-        self.cell_call()
-        
-    def cell_call(self, row=0, col=0):
-        self.row, self.col = row, col # in case one need it
-        tmpfile = mktemp('.png')
-        try:
-            self._generate(tmpfile)
-            self.w(open(tmpfile).read())
-        finally:
-            os.unlink(tmpfile)
-    
-class SchemaImageView(TmpFileViewMixin, StartupView):
-    id = 'schemagraph'
-    content_type = 'image/png'
-    skip_rels = ('owned_by', 'created_by', 'identity', 'is', 'is_instance_of')
-    def _generate(self, tmpfile):
-        """display global schema information"""
-        skipmeta = not int(self.req.form.get('withmeta', 0))
-        visitor = FullSchemaVisitor(self.req, self.schema, skiprels=self.skip_rels, skipmeta=skipmeta)
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor,
-                       prophdlr=RestrictedSchemaDotPropsHandler(self.req))
-
-class EETypeSchemaImageView(TmpFileViewMixin, EntityView):
-    id = 'eschemagraph'
-    content_type = 'image/png'
-    __select__ = implements('EEType')
-    skip_rels = ('owned_by', 'created_by', 'identity', 'is', 'is_instance_of')
-    
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.entity(self.row, self.col)
-        eschema = self.vreg.schema.eschema(entity.name)
-        visitor = OneHopESchemaVisitor(self.req, eschema, skiprels=self.skip_rels)
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor,
-                       prophdlr=RestrictedSchemaDotPropsHandler(self.req))
-
-class ERTypeSchemaImageView(EETypeSchemaImageView):
-    __select__ = implements('ERType')
-    
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.entity(self.row, self.col)
-        rschema = self.vreg.schema.rschema(entity.name)
-        visitor = OneHopRSchemaVisitor(self.req, rschema)
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor,
-                       prophdlr=RestrictedSchemaDotPropsHandler(self.req))
-
-
-
-class WorkflowDotPropsHandler(object):
-    def __init__(self, req):
-        self._ = req._
-        
-    def node_properties(self, stateortransition):
-        """return default DOT drawing options for a state or transition"""
-        props = {'label': stateortransition.name, 
-                 'fontname': 'Courier'}
-        if hasattr(stateortransition, 'state_of'):
-            props['shape'] = 'box'
-            props['style'] = 'filled'
-            if stateortransition.reverse_initial_state:
-                props['color'] = '#88CC88'
-        else:
-            props['shape'] = 'ellipse'
-            descr = []
-            tr = stateortransition
-            if tr.require_group:
-                descr.append('%s %s'% (
-                    self._('groups:'),
-                    ','.join(g.name for g in tr.require_group)))
-            if tr.condition:
-                descr.append('%s %s'% (self._('condition:'), tr.condition))
-            if descr:
-                props['label'] += escape('\n'.join(descr))
-        return props
-    
-    def edge_properties(self, transition, fromstate, tostate):
-        return {'label': '', 'dir': 'forward',
-                'color': 'black', 'style': 'filled'}
-
-class WorkflowVisitor:
-    def __init__(self, entity):
-        self.entity = entity
-
-    def nodes(self):
-        for state in self.entity.reverse_state_of:
-            state.complete()
-            yield state.eid, state
-            
-        for transition in self.entity.reverse_transition_of:
-            transition.complete()
-            yield transition.eid, transition
-            
-    def edges(self):
-        for transition in self.entity.reverse_transition_of:
-            for incomingstate in transition.reverse_allowed_transition:
-                yield incomingstate.eid, transition.eid, transition
-            yield transition.eid, transition.destination().eid, transition
-
-
-class EETypeWorkflowImageView(TmpFileViewMixin, EntityView):
-    id = 'ewfgraph'
-    content_type = 'image/png'
-    __select__ = implements('EEType')
-    
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.entity(self.row, self.col)
-        visitor = WorkflowVisitor(entity)
-        prophdlr = WorkflowDotPropsHandler(self.req)
-        generator = GraphGenerator(DotBackend('workflow', 'LR',
-                                              ratio='compress', size='30,12'))
-        return generator.generate(visitor, prophdlr, tmpfile)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/editforms.py	Thu Mar 12 16:29:00 2009 +0100
@@ -0,0 +1,170 @@
+"""Set of HTML automatic forms to create, delete, copy or edit a single entity
+or a list of entities of the same type
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from copy import copy
+
+from simplejson import dumps
+
+from logilab.mtconverter import html_escape
+from logilab.common.decorators import cached
+
+from cubicweb.selectors import (specified_etype_implements, implements,
+                                match_kwargs, match_form_params, one_line_rset,
+                                non_final_entity, accepts_etype_compat)
+from cubicweb.utils import make_uid
+from cubicweb.view import View, EntityView
+from cubicweb.common import tags
+from cubicweb.common.uilib import cut
+from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs, eid_param
+from cubicweb.web.controller import NAV_FORM_PARAMETERS
+from cubicweb.web.form import MultipleFieldsForm, EntityFieldsForm, FormMixIn, FormRenderer
+from cubicweb.web.formfields import StringField,  RichTextField, guess_field
+from cubicweb.web.formwidgets import HiddenInput
+
+_ = unicode
+
+
+class DeleteConfForm(EntityView):
+    id = 'deleteconf'
+    title = _('delete')
+    domid = 'deleteconf'
+    onsubmit = None
+    # don't use navigation, all entities asked to be deleted should be displayed
+    # else we will only delete the displayed page
+    need_navigation = False
+    
+    def call(self):
+        """ask for confirmation before real deletion"""
+        req, w = self.req, self.w
+        _ = req._
+        w(u'<script type="text/javascript">updateMessage(\'%s\');</script>\n'
+          % _('this action is not reversible!'))
+        # XXX above message should have style of a warning
+        w(u'<h4>%s</h4>\n' % _('Do you want to delete the following element(s) ?'))
+        form = MultipleFieldsForm(req, domid='deleteconf', action=self.build_url('edit'),
+                                  onsubmit=self.onsubmit, copy_nav_params=True)
+        form.buttons.append(form.button_delete(label=stdmsgs.YES))
+        form.buttons.append(form.button_cancel(label=stdmsgs.NO))
+        done = set()
+        w(u'<ul>\n')
+        for i in xrange(self.rset.rowcount):
+            if self.rset[i][0] in done:
+                continue
+            done.add(self.rset[i][0])
+            entity = self.rset.get_entity(i, 0)
+            subform = EntityFieldsForm(req, set_error_url=False,
+                                       entity=entity)
+            form.form_add_subform(subform)
+            # don't use outofcontext view or any other that may contain inline edition form
+            w(u'<li>%s</li>' % tags.a(entity.view('textoutofcontext'),
+                                      href=entity.absolute_url()))
+        w(u'</ul>\n')
+        w(form.form_render())
+
+
+class ClickAndEditForm(FormMixIn, EntityView):
+    id = 'reledit'
+    __select__ = non_final_entity() & match_kwargs('rtype')
+
+    # FIXME editableField class could be toggleable from userprefs
+      
+    def cell_call(self, row, col, rtype=None, role='subject', reload=False):
+        entity = self.entity(row, col)
+        if getattr(entity, rtype) is None:
+            value = self.req._('not specified')
+        else:
+            value = entity.printable_value(rtype)
+        if not entity.has_perm('update'):
+            self.w(value)
+            return
+        self.req.add_js( ('cubicweb.ajax.js',) )
+        eid = entity.eid
+        edit_key = make_uid('%s-%s' % (rtype, eid))
+        divid = 'd%s' % edit_key
+        reload = dumps(reload)
+        buttons = [tags.input(klass="validateButton", type="submit", name="__action_apply",
+                              value=self.req._(stdmsgs.BUTTON_OK), tabindex=self.req.next_tabindex()),
+                   tags.input(klass="validateButton", type="button",
+                              value=self.req._(stdmsgs.BUTTON_CANCEL),
+                              onclick="cancelInlineEdit(%s,\'%s\',\'%s\')" % (eid, rtype, divid),
+                              tabindex=self.req.next_tabindex())]
+        form = self.vreg.select_object('forms', 'edition', self.req, self.rset, row=row, col=col,
+                                       entity=entity, domid='%s-form' % divid, action='#',
+                                       cssstyle='display: none', buttons=buttons,
+                                       onsubmit="return inlineValidateForm('%(divid)s-form', '%(rtype)s', '%(eid)s', '%(divid)s', %(reload)s);" % locals())
+        renderer = FormRenderer(display_label=False, display_help=False,
+                                display_fields=(rtype,), button_bar_class='buttonbar')
+        self.w(tags.div(value, klass='editableField', id=divid,
+                        ondblclick="showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')" % locals()))
+        self.w(form.render(renderer=renderer))
+
+
+class AutomaticEntityForm(EntityFieldsForm):
+    id = 'edition'
+    needs_js = EntityFieldsForm.needs_js + ('cubicweb.ajax.js',)
+    
+    def __init__(self, *args, **kwargs):
+        super(AutomaticEntityForm, self).__init__(*args, **kwargs)
+        self.entity.complete()
+        for rschema, target in self.editable_attributes(entity):
+            field = guess_field(entity.__class__, entity.e_schema, rschema, target)
+            self.fields.append(field)
+            
+    def form_buttons(self):
+        return [self.button_ok(tabindex=self.req.next_tabindex()),
+                self.button_apply(tabindex=self.req.next_tabindex()),
+                self.button_cancel(tabindex=self.req.next_tabindex())]
+
+    def editable_attributes(self, entity):
+        # XXX both (add, delete) required for non final relations
+        return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add')
+                if rschema != 'eid']
+    
+class _EditionForm(EntityView):
+    """primary entity edition form
+
+    When generating a new attribute_input, the editor will look for a method
+    named 'default_ATTRNAME' on the entity instance, where ATTRNAME is the
+    name of the attribute being edited. You may use this feature to compute
+    dynamic default values such as the 'tomorrow' date or the user's login
+    being connected
+    """    
+    id = 'edition'
+    __select__ = one_line_rset() & non_final_entity()
+
+    title = _('edition')
+    controller = 'edit'
+    skip_relations = FormMixIn.skip_relations.copy()
+
+    def cell_call(self, row, col, **kwargs):
+        self.req.add_js( ('cubicweb.ajax.js',) )
+        self.initialize_varmaker()
+        entity = self.complete_entity(row, col)
+
+    def initialize_varmaker(self):
+        varmaker = self.req.get_page_data('rql_varmaker')
+        if varmaker is None:
+            varmaker = self.req.varmaker
+            self.req.set_page_data('rql_varmaker', varmaker)
+        self.varmaker = varmaker
+        
+    def edit_form(self, entity, kwargs):
+        form = EntityFieldsForm(self.req, entity=entity)
+        for rschema, target in self.editable_attributes(entity):
+            field = guess_field(entity.__class__, entity.e_schema, rschema, target)
+            form.fields.append(field)
+        form.buttons.append(form.button_ok())
+        form.buttons.append(form.button_apply())
+        form.buttons.append(form.button_cancel())
+        self.w(form.form_render())
+
+    def editable_attributes(self, entity):
+        # XXX both (add, delete)
+        return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add')
+                if rschema != 'eid']
--- a/web/views/massmailing.py	Thu Mar 12 16:23:31 2009 +0100
+++ b/web/views/massmailing.py	Thu Mar 12 16:29:00 2009 +0100
@@ -61,7 +61,7 @@
     def form_buttons(self):
         context = {'domid': self.domid,
                    'cancel' : self.req._(stdmsgs.BUTTON_CANCEL),
-                   'cancelimgpath' : self.req.external_resource('CANCEL_EMAIL_ICON'),j
+                   'cancelimgpath' : self.req.external_resource('CANCEL_EMAIL_ICON'),
                    'send' : self.req._('send email'),
                    'sendimgpath' : self.req.external_resource('SEND_EMAIL_ICON'),
                    }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/schema.py	Thu Mar 12 16:29:00 2009 +0100
@@ -0,0 +1,220 @@
+"""Specific views for schema related entities
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from itertools import cycle
+
+from logilab.mtconverter import html_escape
+from logilab.common.graph import escape, GraphGenerator, DotBackend
+from yams import schema2dot as s2d
+
+from cubicweb.selectors import implements, rql_condition, yes
+from cubicweb.schemaviewer import SchemaViewer
+from cubicweb.view import EntityView, StartupView
+from cubicweb.common.uilib import ureport_as_html
+from cubicweb.web.action import Action
+from cubicweb.web.views import baseviews
+from cubicweb.web.views import TmpFileViewMixin
+
+
+class ViewSchemaAction(Action):
+    id = 'schema'
+    __select__ = yes()
+    
+    title = _("site schema")
+    category = 'siteactions'
+    order = 30
+    
+    def url(self):
+        return self.build_url(self.id)
+
+        
+# schema entity types views ###################################################
+
+class _SchemaEntityPrimaryView(baseviews.PrimaryView):
+    show_attr_label = False
+    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
+    
+    def content_title(self, entity):
+        return html_escape(entity.dc_long_title())
+    
+class EETypePrimaryView(_SchemaEntityPrimaryView):
+    __select__ = implements('EEType')
+    skip_attrs = _SchemaEntityPrimaryView.skip_attrs + ('name', 'meta', 'final')
+
+class ERTypePrimaryView(_SchemaEntityPrimaryView):
+    __select__ = implements('ERType')
+    skip_attrs = _SchemaEntityPrimaryView.skip_attrs + ('name', 'meta', 'final',
+                                                        'symetric', 'inlined')
+
+class ErdefPrimaryView(_SchemaEntityPrimaryView):
+    __select__ = implements('EFRDef', 'ENFRDef')
+    show_attr_label = True
+
+class EETypeOneLineView(baseviews.OneLineView):
+    __select__ = implements('EEType')
+    
+    def cell_call(self, row, col, **kwargs):
+        entity = self.entity(row, col)
+        final = entity.final
+        if final:
+            self.w(u'<em class="finalentity">')
+        super(EETypeOneLineView, self).cell_call(row, col, **kwargs)
+        if final:
+            self.w(u'</em>')
+
+
+# in memory schema views (yams class instances) ###############################
+
+class EETypeSchemaView(EETypePrimaryView):
+    id = 'eschema'
+    title = _('in memory entity schema')
+    main_related_section = False
+    skip_rels = ('is', 'is_instance_of', 'identity', 'created_by', 'owned_by',
+                 'has_text',)
+    
+    def render_entity_attributes(self, entity, siderelations):
+        super(EETypeSchemaView, self).render_entity_attributes(entity, siderelations)
+        eschema = self.vreg.schema.eschema(entity.name)
+        viewer = SchemaViewer(self.req)
+        layout = viewer.visit_entityschema(eschema, skiprels=self.skip_rels)
+        self.w(ureport_as_html(layout))
+        if not eschema.is_final():
+            self.w(u'<img src="%s" alt="%s"/>' % (
+                html_escape(entity.absolute_url(vid='eschemagraph')),
+                html_escape(self.req._('graphical schema for %s') % entity.name)))
+
+
+class ERTypeSchemaView(ERTypePrimaryView):
+    id = 'eschema'
+    title = _('in memory relation schema')
+    main_related_section = False
+
+    def render_entity_attributes(self, entity, siderelations):
+        super(ERTypeSchemaView, self).render_entity_attributes(entity, siderelations)
+        rschema = self.vreg.schema.rschema(entity.name)
+        viewer = SchemaViewer(self.req)
+        layout = viewer.visit_relationschema(rschema)
+        self.w(ureport_as_html(layout))
+        if not rschema.is_final():
+            self.w(u'<img src="%s" alt="%s"/>' % (
+                html_escape(entity.absolute_url(vid='eschemagraph')),
+                html_escape(self.req._('graphical schema for %s') % entity.name)))
+        
+
+# schema images ###############################################################
+
+class ImageView(EntityView):
+    __select__ = implements('EEType')
+    id = 'image'
+    title = _('image')
+
+    def cell_call(self, row, col):
+        entity = self.entity(row, col)
+        url = entity.absolute_url(vid='eschemagraph')
+        self.w(u'<img src="%s" alt="%s"/>' % (
+            html_escape(url),
+            html_escape(self.req._('graphical schema for %s') % entity.name)))
+
+
+class RestrictedSchemaDotPropsHandler(s2d.SchemaDotPropsHandler):
+    def __init__(self, req):
+        # FIXME: colors are arbitrary
+        self.nextcolor = cycle( ('#aa0000', '#00aa00', '#0000aa',
+                                 '#000000', '#888888') ).next
+        self.req = req
+        
+    def display_attr(self, rschema):
+        return not rschema.meta and (rschema.has_local_role('read')
+                                     or rschema.has_perm(self.req, 'read'))
+    
+    # XXX remove this method once yams > 0.20 is out
+    def node_properties(self, eschema):
+        """return default DOT drawing options for an entity schema"""
+        label = ['{',eschema.type,'|']
+        label.append(r'\l'.join(rel.type for rel in eschema.subject_relations()
+                                if rel.final and self.display_attr(rel)))
+        label.append(r'\l}') # trailing \l ensure alignement of the last one
+        return {'label' : ''.join(label), 'shape' : "record",
+                'fontname' : "Courier", 'style' : "filled"}
+
+    def edge_properties(self, rschema, subjnode, objnode):
+        kwargs = super(RestrictedSchemaDotPropsHandler, self).edge_properties(rschema, subjnode, objnode)
+        # symetric rels are handled differently, let yams decide what's best
+        if not rschema.symetric:
+            kwargs['color'] = self.nextcolor()
+        kwargs['fontcolor'] = kwargs['color']
+        # dot label decoration is just awful (1 line underlining the label
+        # + 1 line going to the closest edge spline point)
+        kwargs['decorate'] = 'false'
+        return kwargs
+    
+
+class RestrictedSchemaVisitorMiIn:
+    def __init__(self, req, *args, **kwargs):
+        # hack hack hack
+        assert len(self.__class__.__bases__) == 2
+        self.__parent = self.__class__.__bases__[1]
+        self.__parent.__init__(self, *args, **kwargs)
+        self.req = req
+        
+    def nodes(self):
+        for etype, eschema in self.__parent.nodes(self):
+            if eschema.has_local_role('read') or eschema.has_perm(self.req, 'read'):
+                yield eschema.type, eschema
+            
+    def edges(self):
+        for setype, oetype, rschema in self.__parent.edges(self):
+            if rschema.has_local_role('read') or rschema.has_perm(self.req, 'read'):
+                yield setype, oetype, rschema
+
+
+class FullSchemaVisitor(RestrictedSchemaVisitorMiIn, s2d.FullSchemaVisitor):
+    pass
+
+class OneHopESchemaVisitor(RestrictedSchemaVisitorMiIn, s2d.OneHopESchemaVisitor):
+    pass
+
+class OneHopRSchemaVisitor(RestrictedSchemaVisitorMiIn, s2d.OneHopRSchemaVisitor):
+    pass
+
+
+class SchemaImageView(TmpFileViewMixin, StartupView):
+    id = 'schemagraph'
+    content_type = 'image/png'
+    skip_rels = ('owned_by', 'created_by', 'identity', 'is', 'is_instance_of')
+    def _generate(self, tmpfile):
+        """display global schema information"""
+        skipmeta = not int(self.req.form.get('withmeta', 0))
+        visitor = FullSchemaVisitor(self.req, self.schema, skiprels=self.skip_rels, skipmeta=skipmeta)
+        s2d.schema2dot(outputfile=tmpfile, visitor=visitor,
+                       prophdlr=RestrictedSchemaDotPropsHandler(self.req))
+
+class EETypeSchemaImageView(TmpFileViewMixin, EntityView):
+    id = 'eschemagraph'
+    content_type = 'image/png'
+    __select__ = implements('EEType')
+    skip_rels = ('owned_by', 'created_by', 'identity', 'is', 'is_instance_of')
+    
+    def _generate(self, tmpfile):
+        """display schema information for an entity"""
+        entity = self.entity(self.row, self.col)
+        eschema = self.vreg.schema.eschema(entity.name)
+        visitor = OneHopESchemaVisitor(self.req, eschema, skiprels=self.skip_rels)
+        s2d.schema2dot(outputfile=tmpfile, visitor=visitor,
+                       prophdlr=RestrictedSchemaDotPropsHandler(self.req))
+
+class ERTypeSchemaImageView(EETypeSchemaImageView):
+    __select__ = implements('ERType')
+    
+    def _generate(self, tmpfile):
+        """display schema information for an entity"""
+        entity = self.entity(self.row, self.col)
+        rschema = self.vreg.schema.rschema(entity.name)
+        visitor = OneHopRSchemaVisitor(self.req, rschema)
+        s2d.schema2dot(outputfile=tmpfile, visitor=visitor,
+                       prophdlr=RestrictedSchemaDotPropsHandler(self.req))
--- a/web/views/schemaentities.py	Thu Mar 12 16:23:31 2009 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,124 +0,0 @@
-"""Specific views for schema related entities
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.mtconverter import html_escape
-
-from cubicweb.selectors import implements, rql_condition
-from cubicweb.schemaviewer import SchemaViewer
-from cubicweb.view import EntityView
-from cubicweb.common.uilib import ureport_as_html
-from cubicweb.web.views import baseviews
-
-
-class ImageView(EntityView):
-    __select__ = implements('EEType')
-    id = 'image'
-    title = _('image')
-
-    def cell_call(self, row, col):
-        entity = self.entity(row, col)
-        url = entity.absolute_url(vid='eschemagraph')
-        self.w(u'<img src="%s" alt="%s"/>' % (
-            html_escape(url),
-            html_escape(self.req._('graphical schema for %s') % entity.name)))
-
-
-class _SchemaEntityPrimaryView(baseviews.PrimaryView):
-    show_attr_label = False
-    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
-    
-    def content_title(self, entity):
-        return html_escape(entity.dc_long_title())
-    
-class EETypePrimaryView(_SchemaEntityPrimaryView):
-    __select__ = implements('EEType')
-    skip_attrs = _SchemaEntityPrimaryView.skip_attrs + ('name', 'meta', 'final')
-
-class ERTypePrimaryView(_SchemaEntityPrimaryView):
-    __select__ = implements('ERType')
-    skip_attrs = _SchemaEntityPrimaryView.skip_attrs + ('name', 'meta', 'final',
-                                                        'symetric', 'inlined')
-
-class ErdefPrimaryView(_SchemaEntityPrimaryView):
-    __select__ = implements('EFRDef', 'ENFRDef')
-    show_attr_label = True
-
-class EETypeSchemaView(EETypePrimaryView):
-    id = 'eschema'
-    title = _('in memory entity schema')
-    main_related_section = False
-    skip_rels = ('is', 'is_instance_of', 'identity', 'created_by', 'owned_by',
-                 'has_text',)
-    
-    def render_entity_attributes(self, entity, siderelations):
-        super(EETypeSchemaView, self).render_entity_attributes(entity, siderelations)
-        eschema = self.vreg.schema.eschema(entity.name)
-        viewer = SchemaViewer(self.req)
-        layout = viewer.visit_entityschema(eschema, skiprels=self.skip_rels)
-        self.w(ureport_as_html(layout))
-        if not eschema.is_final():
-            self.w(u'<img src="%s" alt="%s"/>' % (
-                html_escape(entity.absolute_url(vid='eschemagraph')),
-                html_escape(self.req._('graphical schema for %s') % entity.name)))
-
-class ERTypeSchemaView(ERTypePrimaryView):
-    id = 'eschema'
-    title = _('in memory relation schema')
-    main_related_section = False
-
-    def render_entity_attributes(self, entity, siderelations):
-        super(ERTypeSchemaView, self).render_entity_attributes(entity, siderelations)
-        rschema = self.vreg.schema.rschema(entity.name)
-        viewer = SchemaViewer(self.req)
-        layout = viewer.visit_relationschema(rschema)
-        self.w(ureport_as_html(layout))
-        if not rschema.is_final():
-            self.w(u'<img src="%s" alt="%s"/>' % (
-                html_escape(entity.absolute_url(vid='eschemagraph')),
-                html_escape(self.req._('graphical schema for %s') % entity.name)))
-
-        
-class EETypeWorkflowView(EntityView):
-    id = 'workflow'
-    __select__ = implements('EEType')
-    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
-    
-    def cell_call(self, row, col, **kwargs):
-        entity = self.entity(row, col)
-        self.w(u'<h1>%s</h1>' % (self.req._('workflow for %s')
-                                 % display_name(self.req, entity.name)))
-        self.w(u'<img src="%s" alt="%s"/>' % (
-            html_escape(entity.absolute_url(vid='ewfgraph')),
-            html_escape(self.req._('graphical workflow for %s') % entity.name)))
-
-
-class EETypeOneLineView(baseviews.OneLineView):
-    __select__ = implements('EEType')
-    
-    def cell_call(self, row, col, **kwargs):
-        entity = self.entity(row, col)
-        final = entity.final
-        if final:
-            self.w(u'<em class="finalentity">')
-        super(EETypeOneLineView, self).cell_call(row, col, **kwargs)
-        if final:
-            self.w(u'</em>')
-        
-
-from cubicweb.web.action import Action
-
-class ViewWorkflowAction(Action):
-    id = 'workflow'
-    __select__ = implements('EEType') & rql_condition('S state_of X')
-    
-    category = 'mainactions'
-    title = _('view workflow')
-    def url(self):
-        entity = self.rset.get_entity(self.row or 0, self.col or 0)
-        return entity.absolute_url(vid='workflow')
-        
--- a/web/views/wfentities.py	Thu Mar 12 16:23:31 2009 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-"""html view for workflow related entities
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.mtconverter import html_escape
-
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
-
-class CellView(EntityView):
-    id = 'cell'
-    __select__ = implements('TrInfo')
-    
-    def cell_call(self, row, col, cellvid=None):
-        entity = self.entity(row, col)
-        self.w(entity.printable_value('comment'))
-
-
-class StateInContextView(EntityView):
-    """convenience trick, State's incontext view should not be clickable"""
-    id = 'incontext'
-    __select__ = implements('State')
-    
-    def cell_call(self, row, col):
-        self.w(html_escape(self.view('textincontext', self.rset,
-                                     row=row, col=col)))
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/workflow.py	Thu Mar 12 16:29:00 2009 +0100
@@ -0,0 +1,214 @@
+"""workflow views:
+
+* IWorkflowable views and forms
+* workflow entities views (State, Transition, TrInfo)
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.mtconverter import html_escape
+from logilab.common.graph import escape, GraphGenerator, DotBackend
+
+from cubicweb import Unauthorized, view
+from cubicweb.selectors import (implements, has_related_entities,
+                                relation_possible, match_form_params)
+from cubicweb.interfaces import IWorkflowable
+from cubicweb.web import stdmsgs, action, component, form
+from cubicweb.web.formfields import StringField,  RichTextField
+from cubicweb.web.formwidgets import HiddenInput
+from cubicweb.web.views import TmpFileViewMixin
+
+
+# IWorkflowable views #########################################################
+
+class ChangeStateForm(form.EntityFieldsForm):
+    id = 'changestate'
+    
+    __method = StringField(name='__method', initial='set_state', widget=HiddenInput)
+    state = StringField(widget=HiddenInput, eidparam=True)
+    trcomment = RichTextField(eidparam=True)
+
+    def form_buttons(self):
+        return [self.button_ok(label=stdmsgs.YES,
+                               tabindex=self.req.next_tabindex()),
+                self.button_cancel(label=stdmsgs.NO,
+                                   tabindex=self.req.next_tabindex())]
+
+        
+class ChangeStateFormView(view.EntityView):
+    id = 'statuschange'
+    title = _('status change')
+    __select__ = implements(IWorkflowable) & match_form_params('treid')
+
+    def cell_call(self, row, col):
+        entity = self.entity(row, col)
+        eid = entity.eid
+        state = entity.in_state[0]
+        transition = self.req.eid_rset(self.req.form['treid']).get_entity(0, 0)
+        dest = transition.destination()
+        _ = self.req._
+        form = self.vreg.select_object('forms', 'changestate', self.req, self.rset, row=row, col=col,
+                                       entity=entity, redirect_path=self.redirectpath(entity))
+        self.w(form.error_message())
+        self.w(u'<h4>%s %s</h4>\n' % (_(transition.name), entity.view('oneline')))
+        msg = _('status will change from %(st1)s to %(st2)s') % {
+            'st1': _(state.name),
+            'st2': _(dest.name)}
+        self.w(u'<p>%s</p>\n' % msg)
+        self.w(form.form_render(state=dest.eid, trcomment=u''))
+
+    def redirectpath(self, entity):
+        return entity.rest_path()
+
+
+class WFHistoryVComponent(component.EntityVComponent):
+    """display the workflow history for entities supporting it"""
+    id = 'wfhistory'
+    __select__ = (component.EntityVComponent.__select__
+                  & relation_possible('wf_info_for', role='object'))
+    context = 'navcontentbottom'
+    title = _('Workflow history')
+
+    def cell_call(self, row, col, view=None):
+        _ = self.req._
+        eid = self.rset[row][col]
+        sel = 'Any FS,TS,WF,D'
+        rql = ' ORDERBY D DESC WHERE WF wf_info_for X,'\
+              'WF from_state FS, WF to_state TS, WF comment C,'\
+              'WF creation_date D'
+        if self.vreg.schema.eschema('EUser').has_perm(self.req, 'read'):
+            sel += ',U,C'
+            rql += ', WF owned_by U?'
+            displaycols = range(5)
+            headers = (_('from_state'), _('to_state'), _('comment'), _('date'),
+                       _('EUser'))            
+        else:
+            sel += ',C'
+            displaycols = range(4)
+            headers = (_('from_state'), _('to_state'), _('comment'), _('date'))
+        rql = '%s %s, X eid %%(x)s' % (sel, rql)
+        try:
+            rset = self.req.execute(rql, {'x': eid}, 'x')
+        except Unauthorized:
+            return
+        if rset:
+            self.wview('table', rset, title=_(self.title), displayactions=False,
+                       displaycols=displaycols, headers=headers)
+
+
+# workflow entity types views #################################################
+
+class CellView(view.EntityView):
+    id = 'cell'
+    __select__ = implements('TrInfo')
+    
+    def cell_call(self, row, col, cellvid=None):
+        self.w(self.entity(row, col).printable_value('comment'))
+
+
+class StateInContextView(view.EntityView):
+    """convenience trick, State's incontext view should not be clickable"""
+    id = 'incontext'
+    __select__ = implements('State')
+    
+    def cell_call(self, row, col):
+        self.w(html_escape(self.view('textincontext', self.rset,
+                                     row=row, col=col)))
+
+
+# workflow images #############################################################
+        
+class ViewWorkflowAction(action.Action):
+    id = 'workflow'
+    __select__ = implements('EEType') & has_related_entities('state_of', 'object')
+    
+    category = 'mainactions'
+    title = _('view workflow')
+    def url(self):
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
+        return entity.absolute_url(vid='workflow')
+
+        
+class EETypeWorkflowView(view.EntityView):
+    id = 'workflow'
+    __select__ = implements('EEType')
+    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
+    
+    def cell_call(self, row, col, **kwargs):
+        entity = self.entity(row, col)
+        self.w(u'<h1>%s</h1>' % (self.req._('workflow for %s')
+                                 % display_name(self.req, entity.name)))
+        self.w(u'<img src="%s" alt="%s"/>' % (
+            html_escape(entity.absolute_url(vid='ewfgraph')),
+            html_escape(self.req._('graphical workflow for %s') % entity.name)))
+
+
+class WorkflowDotPropsHandler(object):
+    def __init__(self, req):
+        self._ = req._
+        
+    def node_properties(self, stateortransition):
+        """return default DOT drawing options for a state or transition"""
+        props = {'label': stateortransition.name, 
+                 'fontname': 'Courier'}
+        if hasattr(stateortransition, 'state_of'):
+            props['shape'] = 'box'
+            props['style'] = 'filled'
+            if stateortransition.reverse_initial_state:
+                props['color'] = '#88CC88'
+        else:
+            props['shape'] = 'ellipse'
+            descr = []
+            tr = stateortransition
+            if tr.require_group:
+                descr.append('%s %s'% (
+                    self._('groups:'),
+                    ','.join(g.name for g in tr.require_group)))
+            if tr.condition:
+                descr.append('%s %s'% (self._('condition:'), tr.condition))
+            if descr:
+                props['label'] += escape('\n'.join(descr))
+        return props
+    
+    def edge_properties(self, transition, fromstate, tostate):
+        return {'label': '', 'dir': 'forward',
+                'color': 'black', 'style': 'filled'}
+
+
+class WorkflowVisitor:
+    def __init__(self, entity):
+        self.entity = entity
+
+    def nodes(self):
+        for state in self.entity.reverse_state_of:
+            state.complete()
+            yield state.eid, state
+            
+        for transition in self.entity.reverse_transition_of:
+            transition.complete()
+            yield transition.eid, transition
+            
+    def edges(self):
+        for transition in self.entity.reverse_transition_of:
+            for incomingstate in transition.reverse_allowed_transition:
+                yield incomingstate.eid, transition.eid, transition
+            yield transition.eid, transition.destination().eid, transition
+
+
+class EETypeWorkflowImageView(TmpFileViewMixin, view.EntityView):
+    id = 'ewfgraph'
+    content_type = 'image/png'
+    __select__ = implements('EEType')
+    
+    def _generate(self, tmpfile):
+        """display schema information for an entity"""
+        entity = self.entity(self.row, self.col)
+        visitor = WorkflowVisitor(entity)
+        prophdlr = WorkflowDotPropsHandler(self.req)
+        generator = GraphGenerator(DotBackend('workflow', 'LR',
+                                              ratio='compress', size='30,12'))
+        return generator.generate(visitor, prophdlr, tmpfile)
+
--- a/web/views/xmlrss.py	Thu Mar 12 16:23:31 2009 +0100
+++ b/web/views/xmlrss.py	Thu Mar 12 16:29:00 2009 +0100
@@ -1,6 +1,5 @@
 """base xml and rss views
 
-
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
@@ -11,10 +10,17 @@
 
 from logilab.mtconverter import xml_escape
 
+from cubicweb.selectors import non_final_entity, one_line_rset, appobject_selectable
 from cubicweb.view import EntityView, AnyRsetView
 from cubicweb.web.httpcache import MaxAgeHTTPCacheManager
+from cubicweb.web.component import Component
+from cubicweb.web.box import BoxTemplate
 from cubicweb.common.uilib import simple_sgml_tag
 
+_ = unicode
+
+
+# base xml views ##############################################################
 
 class XmlView(EntityView):
     """xml view for entities"""
@@ -60,7 +66,6 @@
                 self.w(u'  <%s>%s</%s>\n' % (attr, value, attr))
         self.w(u'</%s>\n' % (entity.e_schema))
 
-
     
 class XmlRsetView(AnyRsetView):
     """dumps raw rset as xml"""
@@ -97,7 +102,45 @@
                 w(simple_sgml_tag(tag, val, **attrs))
             w(u' </row>\n')
         w(u'</%s>\n' % self.xml_root)
+
     
+# RSS stuff ###################################################################
+
+class RSSFeedURL(Component):
+    id = 'rss_feed_url'
+    __select__ = non_final_entity()
+    
+    def feed_url(self):
+        return self.build_url(rql=self.limited_rql(), vid='rss')
+
+
+class RSSEntityFeedURL(Component):
+    id = 'rss_feed_url'
+    __select__ = non_final_entity() & one_line_rset()
+    
+    def feed_url(self):
+        return self.entity(0, 0).rss_feed_url()
+
+        
+class RSSIconBox(BoxTemplate):
+    """just display the RSS icon on uniform result set"""
+    id = 'rss'
+    __select__ = (BoxTemplate.__select__
+                  & appobject_selectable('components', 'rss_feed_url'))
+    
+    visible = False
+    order = 999
+    
+    def call(self, **kwargs):
+        try:
+            rss = self.req.external_resource('RSS_LOGO')
+        except KeyError:
+            self.error('missing RSS_LOGO external resource')
+            return
+        urlgetter = self.vreg.select_component('rss_feed_url', self.req, self.rset)
+        url = urlgetter.feed_url()
+        self.w(u'<a href="%s"><img src="%s" alt="rss"/></a>\n' % (html_escape(url), rss))
+
 
 class RssView(XmlView):
     id = 'rss'