# HG changeset patch # User Sylvain Thénault # Date 1254329862 -7200 # Node ID f6c9a5df80fbd24c4d53c4506832e3c76d98bc3a # Parent a3431f4e2f40d42564a240de4407504cfb0f7ea3# Parent 106b95ec114460d330516041f17105377bbdcaa8 backport stable branch diff -r a3431f4e2f40 -r f6c9a5df80fb entities/test/unittest_wfobjs.py --- 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): diff -r a3431f4e2f40 -r f6c9a5df80fb entities/wfobjs.py --- 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): diff -r a3431f4e2f40 -r f6c9a5df80fb hooks/notification.py --- 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): diff -r a3431f4e2f40 -r f6c9a5df80fb hooks/syncschema.py --- 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): diff -r a3431f4e2f40 -r f6c9a5df80fb hooks/test/unittest_hooks.py --- 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() diff -r a3431f4e2f40 -r f6c9a5df80fb hooks/workflow.py --- 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): diff -r a3431f4e2f40 -r f6c9a5df80fb misc/migration/3.5.3_Any.py --- 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') diff -r a3431f4e2f40 -r f6c9a5df80fb schemas/workflow.py --- 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',), } diff -r a3431f4e2f40 -r f6c9a5df80fb server/__init__.py --- 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=';;') diff -r a3431f4e2f40 -r f6c9a5df80fb server/sources/pyrorql.py --- 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'], diff -r a3431f4e2f40 -r f6c9a5df80fb sobjects/notification.py --- 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 diff -r a3431f4e2f40 -r f6c9a5df80fb web/formfields.py --- 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: diff -r a3431f4e2f40 -r f6c9a5df80fb web/test/unittest_formfields.py --- 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']) diff -r a3431f4e2f40 -r f6c9a5df80fb web/views/editforms.py --- 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():