--- a/entities/test/unittest_wfobjs.py Tue Sep 29 15:58:44 2009 +0200
+++ b/entities/test/unittest_wfobjs.py Wed Sep 30 18:57:42 2009 +0200
@@ -339,6 +339,46 @@
('asleep', 'activated', None, 'workflow changed to "default user workflow"'),])
+class AutoTransitionTC(CubicWebTC):
+
+ def setup_database(self):
+ self.wf = add_wf(self, 'CWUser')
+ asleep = self.wf.add_state('asleep', initial=True)
+ dead = self.wf.add_state('dead')
+ self.wf.add_transition('rest', asleep, asleep)
+ self.wf.add_transition('sick', asleep, dead, type=u'auto',
+ conditions=({'expr': u'U surname "toto"',
+ 'mainvars': u'U'},))
+
+ def test_auto_transition_fired(self):
+ user = self.create_user('member')
+ self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
+ {'wf': self.wf.eid, 'x': user.eid})
+ self.commit()
+ user.clear_all_caches()
+ self.assertEquals(user.state, 'asleep')
+ self.assertEquals([t.name for t in user.possible_transitions()],
+ ['rest'])
+ user.fire_transition('rest')
+ self.commit()
+ user.clear_all_caches()
+ self.assertEquals(user.state, 'asleep')
+ self.assertEquals([t.name for t in user.possible_transitions()],
+ ['rest'])
+ self.assertEquals(parse_hist(user.workflow_history),
+ [('asleep', 'asleep', 'rest', None)])
+ self.request().user.set_attributes(surname=u'toto') # fulfill condition
+ self.commit()
+ user.fire_transition('rest')
+ self.commit()
+ user.clear_all_caches()
+ self.assertEquals(user.state, 'dead')
+ self.assertEquals(parse_hist(user.workflow_history),
+ [('asleep', 'asleep', 'rest', None),
+ ('asleep', 'asleep', 'rest', None),
+ ('asleep', 'dead', 'sick', None),])
+
+
class WorkflowHooksTC(CubicWebTC):
def setUp(self):
--- a/entities/wfobjs.py Tue Sep 29 15:58:44 2009 +0200
+++ b/entities/wfobjs.py Wed Sep 30 18:57:42 2009 +0200
@@ -223,11 +223,15 @@
conditions = (conditions,)
for expr in conditions:
if isinstance(expr, str):
- expr = unicode(expr)
+ kwargs = {'expr': unicode(expr)}
+ elif isinstance(expr, dict):
+ kwargs = expr
+ kwargs['x'] = self.eid
+ kwargs.setdefault('mainvars', u'X')
self._cw.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
'X expression %(expr)s, T condition X '
'WHERE T eid %(x)s',
- {'x': self.eid, 'expr': expr}, 'x')
+ 'T condition X WHERE T eid %(x)s', kwargs, 'x')
# XXX clear caches?
@@ -415,16 +419,24 @@
self.warning("can't find any workflow for %s", self.__regid__)
return None
- def possible_transitions(self):
+ def possible_transitions(self, type='normal'):
"""generates transition that MAY be fired for the given entity,
expected to be in this state
"""
if self.current_state is None or self.current_workflow is None:
return
+<<<<<<< /home/syt/src/fcubicweb/cubicweb/entities/wfobjs.py
rset = self._cw.execute(
'Any T,N WHERE S allowed_transition T, S eid %(x)s, '
'T name N, T transition_of WF, WF eid %(wfeid)s',
{'x': self.current_state.eid,
+=======
+ rset = self.req.execute(
+ 'Any T,TT, TN WHERE S allowed_transition T, S eid %(x)s, '
+ 'T type TT, T type %(type)s, '
+ 'T name TN, T transition_of WF, WF eid %(wfeid)s',
+ {'x': self.current_state.eid, 'type': type,
+>>>>>>> /tmp/wfobjs.py~other.TyHPqT
'wfeid': self.current_workflow.eid}, 'x')
for tr in rset.entities():
if tr.may_be_fired(self.eid):
@@ -446,15 +458,21 @@
kwargs['S'] = tseid
return self._cw.create_entity('TrInfo', *args, **kwargs)
- def fire_transition(self, trname, comment=None, commentformat=None):
+ def fire_transition(self, tr, comment=None, commentformat=None):
"""change the entity's state by firing transition of the given name in
entity's workflow
"""
assert self.current_workflow
+<<<<<<< /home/syt/src/fcubicweb/cubicweb/entities/wfobjs.py
tr = self.current_workflow.transition_by_name(trname)
if tr is None:
raise WorkflowException('not a %s transition: %s' % (self.__regid__,
trname))
+=======
+ if isinstance(tr, basestring):
+ tr = self.current_workflow.transition_by_name(tr)
+ assert tr is not None, 'not a %s transition: %s' % (self.id, tr)
+>>>>>>> /tmp/wfobjs.py~other.TyHPqT
return self._add_trinfo(comment, commentformat, tr.eid)
def change_state(self, statename, comment=None, commentformat=None, tr=None):
--- a/hooks/notification.py Tue Sep 29 15:58:44 2009 +0200
+++ b/hooks/notification.py Wed Sep 30 18:57:42 2009 +0200
@@ -90,6 +90,52 @@
RenderAndSendNotificationView(self._cw, view=view)
+class EntityUpdatedNotificationOp(hook.SingleLastOperation):
+
+ def precommit_event(self):
+ session = self.session
+ for eid in session.transaction_data['changes']:
+ view = session.vreg['views'].select('notif_entity_updated', session,
+ rset=session.eid_rset(eid),
+ row=0)
+ RenderAndSendNotificationView(session, view=view)
+
+
+class EntityUpdateHook(NotificationHook):
+ __regid__ = 'notifentityupdated'
+ __abstract__ = True # do not register by default
+
+ events = ('before_update_entity',)
+ skip_attrs = set()
+
+ def __call__(self):
+ session = self._cw
+ if self.entity.eid in session.transaction_data.get('neweids', ()):
+ return # entity is being created
+ if session.is_super_session:
+ return # ignore changes triggered by hooks
+ # then compute changes
+ changes = session.transaction_data.setdefault('changes', {})
+ thisentitychanges = changes.setdefault(self.entity.eid, set())
+ attrs = [k for k in self.entity.edited_attributes if not k in self.skip_attrs]
+ if not attrs:
+ return
+ rqlsel, rqlrestr = [], ['X eid %(x)s']
+ for i, attr in enumerate(attrs):
+ var = chr(65+i)
+ rqlsel.append(var)
+ rqlrestr.append('X %s %s' % (attr, var))
+ rql = 'Any %s WHERE %s' % (','.join(rqlsel), ','.join(rqlrestr))
+ rset = session.execute(rql, {'x': self.entity.eid}, 'x')
+ for i, attr in enumerate(attrs):
+ oldvalue = rset[0][i]
+ newvalue = self.entity[attr]
+ if oldvalue != newvalue:
+ thisentitychanges.add((attr, oldvalue, newvalue))
+ if thisentitychanges:
+ EntityUpdatedNotificationOp(session)
+
+
# supervising ##################################################################
class SomethingChangedHook(NotificationHook):
--- a/hooks/syncschema.py Tue Sep 29 15:58:44 2009 +0200
+++ b/hooks/syncschema.py Wed Sep 30 18:57:42 2009 +0200
@@ -317,10 +317,11 @@
default = entity.defaultval
if default is not None:
default = TYPE_CONVERTER[entity.otype.name](default)
- rdef = self.init_rdef(default=default,
- indexed=entity.indexed,
- fulltextindexed=entity.fulltextindexed,
- internationalizable=entity.internationalizable)
+ props = {'default': default,
+ 'indexed': entity.indexed,
+ 'fulltextindexed': entity.fulltextindexed,
+ 'internationalizable': entity.internationalizable}
+ rdef = self.init_rdef(**props)
sysource = session.pool.source('system')
attrtype = type_from_constraints(sysource.dbhelper, rdef.object,
rdef.constraints)
@@ -353,6 +354,23 @@
except Exception, ex:
self.error('error while creating index for %s.%s: %s',
table, column, ex)
+ # final relations are not infered, propagate
+ try:
+ eschema = self.schema.eschema(rdef.subject)
+ except KeyError:
+ return # entity type currently being added
+ rschema = self.schema.rschema(rdef.name)
+ props.update({'constraints': rdef.constraints,
+ 'description': rdef.description,
+ 'cardinality': rdef.cardinality,
+ 'constraints': rdef.constraints,
+ 'order': rdef.order})
+ for specialization in eschema.specialized_by(False):
+ if rschema.has_rdef(specialization, rdef.object):
+ continue
+ for rql, args in ss.frdef2rql(rschema, str(specialization),
+ rdef.object, props):
+ session.execute(rql, args)
class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
--- a/hooks/test/unittest_hooks.py Tue Sep 29 15:58:44 2009 +0200
+++ b/hooks/test/unittest_hooks.py Wed Sep 30 18:57:42 2009 +0200
@@ -470,5 +470,15 @@
'RT name "surname", E name "CWUser"')
self.commit()
+
+ def test_add_attribute_to_base_class(self):
+ self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval "noname", X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F '
+ 'WHERE RT name "nom", E name "BaseTransition", F name "String"')
+ self.commit()
+ self.schema.rebuild_infered_relations()
+ self.failUnless('Transition' in self.schema['nom'].subjects())
+ self.failUnless('WorkflowTransition' in self.schema['nom'].subjects())
+ self.execute('Any X WHERE X is_instance_of BaseTransition, X nom "hop"')
+
if __name__ == '__main__':
unittest_main()
--- a/hooks/workflow.py Tue Sep 29 15:58:44 2009 +0200
+++ b/hooks/workflow.py Wed Sep 30 18:57:42 2009 +0200
@@ -47,6 +47,19 @@
session.super_session.add_relation(entity.eid, 'in_state',
state.eid)
+
+class _FireAutotransitionOp(PreCommitOperation):
+ """try to fire auto transition after state changes"""
+
+ def precommit_event(self):
+ session = self.session
+ entity = self.entity
+ autotrs = list(entity.possible_transitions('auto'))
+ if autotrs:
+ assert len(autotrs) == 1
+ entity.fire_transition(autotrs[0])
+
+
class _WorkflowChangedOp(hook.Operation):
"""fix entity current state when changing its workflow"""
@@ -206,6 +219,7 @@
nocheck = session.transaction_data.setdefault('skip-security', set())
nocheck.add((entity.eid, 'from_state', fromstate.eid))
nocheck.add((entity.eid, 'to_state', deststateeid))
+ FireAutotransitionOp(session, entity=forentity)
class FiredTransitionHook(WorkflowHook):
--- a/misc/migration/3.5.3_Any.py Tue Sep 29 15:58:44 2009 +0200
+++ b/misc/migration/3.5.3_Any.py Wed Sep 30 18:57:42 2009 +0200
@@ -1,2 +1,4 @@
sync_schema_props_perms('state_of')
sync_schema_props_perms('transition_of')
+
+add_attribute('BaseTransition', 'type')
--- a/schemas/workflow.py Tue Sep 29 15:58:44 2009 +0200
+++ b/schemas/workflow.py Wed Sep 30 18:57:42 2009 +0200
@@ -68,6 +68,7 @@
name = String(required=True, indexed=True, internationalizable=True,
maxsize=256)
+ type = String(vocabulary=(_('normal'), _('auto')), default='normal')
description = RichString(fulltextindexed=True,
description=_('semantic description of this transition'))
condition = SubjectRelation('RQLExpression', cardinality='*?', composite='subject',
@@ -116,15 +117,13 @@
description=_('destination state'))
-# XXX should we allow managers to delete TrInfo?
-
class TrInfo(EntityType):
"""workflow history item"""
# 'add' security actually done by hooks
permissions = {
'read': ('managers', 'users', 'guests',), # XXX U has_read_permission O ?
'add': ('managers', 'users', 'guests',),
- 'delete': (),
+ 'delete': (), # XXX should we allow managers to delete TrInfo?
'update': ('managers', 'owners',),
}
--- a/server/__init__.py Tue Sep 29 15:58:44 2009 +0200
+++ b/server/__init__.py Wed Sep 30 18:57:42 2009 +0200
@@ -147,7 +147,7 @@
for fpath in glob(join(CW_SOFTWARE_ROOT, 'schemas', '*.sql.%s' % driver)):
print '-> installing', fpath
sqlexec(open(fpath).read(), execute, False, delimiter=';;')
- for directory in config.cubes_path():
+ for directory in reversed(config.cubes_path()):
for fpath in glob(join(directory, 'schema', '*.sql.%s' % driver)):
print '-> installing', fpath
sqlexec(open(fpath).read(), execute, False, delimiter=';;')
--- a/server/sources/pyrorql.py Tue Sep 29 15:58:44 2009 +0200
+++ b/server/sources/pyrorql.py Wed Sep 30 18:57:42 2009 +0200
@@ -218,6 +218,8 @@
"""open and return a connection to the source"""
nshost = self.config.get('pyro-ns-host') or self.repo.config['pyro-ns-host']
nsgroup = self.config.get('pyro-ns-group') or self.repo.config['pyro-ns-group']
+ self.info('connecting to instance :%s.%s for user %s',
+ nsgroup, nshost, self.config['cubicweb-user'])
#cnxprops = ConnectionProperties(cnxtype=self.config['cnx-type'])
return dbapi.connect(database=self.config['pyro-ns-id'],
login=self.config['cubicweb-user'],
--- a/sobjects/notification.py Tue Sep 29 15:58:44 2009 +0200
+++ b/sobjects/notification.py Wed Sep 30 18:57:42 2009 +0200
@@ -19,6 +19,8 @@
from cubicweb.common.mail import NotificationView
from cubicweb.server.hook import SendMailOp
+parse_message_id = deprecated('parse_message_id is now defined in cubicweb.common.mail')(parse_message_id)
+
class RecipientsFinder(Component):
"""this component is responsible to find recipients of a notification
@@ -118,6 +120,70 @@
entity.eid, self.user_data['login'])
+def format_value(value):
+ if isinstance(value, unicode):
+ return u'"%s"' % value
+ return value
+
+
+class EntityUpdatedNotificationView(NotificationView):
+ """abstract class for notification on entity/relation
+
+ all you have to do by default is :
+ * set id and __select__ attributes to match desired events and entity types
+ * set a content attribute to define the content of the email (unless you
+ override call)
+ """
+ __abstract__ = True
+ id = 'notif_entity_updated'
+ msgid_timestamp = False
+ message = _('updated')
+ no_detailed_change_attrs = ()
+ content = """
+Properties have been updated by %(user)s:
+
+%(changes)s
+
+url: %(url)s
+"""
+
+ def context(self, **kwargs):
+ context = super(EntityUpdatedNotificationView, self).context(**kwargs)
+ changes = self.req.transaction_data['changes'][self.rset[0][0]]
+ _ = self.req._
+ formatted_changes = []
+ for attr, oldvalue, newvalue in sorted(changes):
+ # check current user has permission to see the attribute
+ rschema = self.vreg.schema[attr]
+ if rschema.is_final():
+ if not rschema.has_perm(self.req, 'read', eid=self.rset[0][0]):
+ continue
+ # XXX suppose it's a subject relation...
+ elif not rschema.has_perm(self.req, 'read', fromeid=self.rset[0][0]):
+ continue
+ if attr in self.no_detailed_change_attrs:
+ msg = _('%s updated') % _(attr)
+ elif oldvalue not in (None, ''):
+ msg = _('%(attr)s updated from %(oldvalue)s to %(newvalue)s') % {
+ 'attr': _(attr),
+ 'oldvalue': format_value(oldvalue),
+ 'newvalue': format_value(newvalue)}
+ else:
+ msg = _('%(attr)s set to %(newvalue)s') % {
+ 'attr': _(attr), 'newvalue': format_value(newvalue)}
+ formatted_changes.append('* ' + msg)
+ if not formatted_changes:
+ # current user isn't allowed to see changes, skip this notification
+ raise SkipEmail()
+ context['changes'] = '\n'.join(formatted_changes)
+ return context
+
+ def subject(self):
+ entity = self.entity(self.row or 0, self.col or 0)
+ return u'%s #%s (%s)' % (self.req.__('Updated %s' % entity.e_schema),
+ entity.eid, self.user_data['login'])
+
+
from logilab.common.deprecation import class_renamed, class_moved, deprecated
from cubicweb.hooks.notification import RenderAndSendNotificationView
from cubicweb.common.mail import parse_message_id
--- a/web/formfields.py Tue Sep 29 15:58:44 2009 +0200
+++ b/web/formfields.py Wed Sep 30 18:57:42 2009 +0200
@@ -181,8 +181,8 @@
try:
return widget.render(form, self, renderer)
except TypeError:
- warn('widget.render now take the renderer as third argument, please update %s implementation'
- % widget.__class__.__name__, DeprecationWarning)
+ warn('[3.3] %s: widget.render now take the renderer as third argument, '
+ 'please update implementation' % widget, DeprecationWarning)
return widget.render(form, self)
def vocabulary(self, form):
@@ -193,7 +193,7 @@
try:
vocab = self.choices(form=form)
except TypeError:
- warn('vocabulary method (eg field.choices) should now take '
+ warn('[3.3] vocabulary method (eg field.choices) should now take '
'the form instance as argument', DeprecationWarning)
vocab = self.choices(req=form._cw)
else:
--- a/web/test/unittest_formfields.py Tue Sep 29 15:58:44 2009 +0200
+++ b/web/test/unittest_formfields.py Wed Sep 30 18:57:42 2009 +0200
@@ -80,12 +80,15 @@
self.assertEquals(data_format_field, None)
data_encoding_field = guess_field(schema['File'], schema['data_encoding'])
self.assertEquals(data_encoding_field, None)
+ data_name_field = guess_field(schema['File'], schema['data_name'])
+ self.assertEquals(data_name_field, None)
data_field = guess_field(schema['File'], schema['data'])
self.assertIsInstance(data_field, FileField)
self.assertEquals(data_field.required, True)
self.assertIsInstance(data_field.format_field, StringField)
self.assertIsInstance(data_field.encoding_field, StringField)
+ self.assertIsInstance(data_field.name_field, StringField)
def test_constraints_priority(self):
salesterm_field = guess_field(schema['Salesterm'], schema['reason'])
--- a/web/views/editforms.py Tue Sep 29 15:58:44 2009 +0200
+++ b/web/views/editforms.py Wed Sep 30 18:57:42 2009 +0200
@@ -16,6 +16,7 @@
from logilab.mtconverter import xml_escape
from logilab.common.decorators import cached
+from cubicweb import neg_role
from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
specified_etype_implements, yes)
from cubicweb.utils import make_uid
@@ -498,15 +499,13 @@
counter=self.req.data[countkey], **kwargs))
def add_hiddens(self, form, entity):
- # to ease overriding (see cubes.vcsfile.views.forms for instance)
- if self.keep_entity(form, entity):
- if entity.has_eid():
- rval = entity.eid
- else:
- rval = INTERNAL_FIELD_VALUE
- form.form_add_hidden('edit%s-%s:%s' % (self.role[0], self.rtype, self.peid), rval)
- form.form_add_hidden(name='%s:%s' % (self.rtype, self.peid), value=entity.eid,
- id='rel-%s-%s-%s' % (self.peid, self.rtype, entity.eid))
+ """to ease overriding (see cubes.vcsfile.views.forms for instance)"""
+ iid = 'rel-%s-%s-%s' % (self.peid, self.rtype, entity.eid)
+ # * str(self.rtype) in case it's a schema object
+ # * neged_role() since role is the for parent entity, we want the role
+ # of the inlined entity
+ form.form_add_hidden(name=str(self.rtype), value=self.peid,
+ role=neg_role(self.role), eidparam=True, id=iid)
def keep_entity(self, form, entity):
if not entity.has_eid():