# HG changeset patch # User Sylvain Thénault # Date 1251879818 -7200 # Node ID 096d680c9da2c43afb7c47940b6eef1f4aa17ded # Parent 32c2b6a34ab248cd2f279f3cd2852077cd10ce7d# Parent 99ab33abf414858baed321d2b101f7a8c6fd6d17 backport stable diff -r 99ab33abf414 -r 096d680c9da2 __init__.py --- a/__init__.py Wed Sep 02 10:20:12 2009 +0200 +++ b/__init__.py Wed Sep 02 10:23:38 2009 +0200 @@ -121,6 +121,27 @@ raise KeyError def set_entity_cache(self, entity): pass + + def create_entity(self, etype, *args, **kwargs): + """add a new entity of the given type""" + rql = 'INSERT %s X' % etype + relations = [] + restrictions = [] + cachekey = [] + for rtype, rvar in args: + relations.append('X %s %s' % (rtype, rvar)) + restrictions.append('%s eid %%(%s)s' % (rvar, rvar)) + cachekey.append(rvar) + for attr in kwargs: + if attr in cachekey: + continue + relations.append('X %s %%(%s)s' % (attr, attr)) + if relations: + rql = '%s: %s' % (rql, ', '.join(relations)) + if restrictions: + rql = '%s WHERE %s' % (rql, ', '.join(restrictions)) + return self.execute(rql, kwargs, cachekey).get_entity(0, 0) + # url generation methods ################################################## def build_url(self, *args, **kwargs): diff -r 99ab33abf414 -r 096d680c9da2 __pkginfo__.py --- a/__pkginfo__.py Wed Sep 02 10:20:12 2009 +0200 +++ b/__pkginfo__.py Wed Sep 02 10:23:38 2009 +0200 @@ -7,7 +7,7 @@ distname = "cubicweb" modname = "cubicweb" -numversion = (3, 4, 6) +numversion = (3, 5, 0) version = '.'.join(str(num) for num in numversion) license = 'LGPL v2' diff -r 99ab33abf414 -r 096d680c9da2 _exceptions.py --- a/_exceptions.py Wed Sep 02 10:20:12 2009 +0200 +++ b/_exceptions.py Wed Sep 02 10:23:38 2009 +0200 @@ -65,9 +65,8 @@ """no source support an entity type""" msg = 'No source supports %r entity\'s type' -class RTypeNotSupportedBySources(RepositoryError, InternalError): - """no source support a relation type""" - msg = 'No source supports %r relation\'s type' +class MultiSourcesError(RepositoryError, InternalError): + """usually due to bad multisources configuration or rql query""" # security exceptions ######################################################### diff -r 99ab33abf414 -r 096d680c9da2 common/mail.py --- a/common/mail.py Wed Sep 02 10:20:12 2009 +0200 +++ b/common/mail.py Wed Sep 02 10:23:38 2009 +0200 @@ -141,60 +141,28 @@ msgid_timestamp = True - def user_login(self): - try: - # if req is actually a session (we are on the server side), and we - # have to prevent nested internal session - return self.req.actual_session().user.login - except AttributeError: - return self.req.user.login - - def recipients(self): - finder = self.vreg['components'].select('recipients_finder', self.req, - rset=self.rset, - row=self.row or 0, - col=self.col or 0) - return finder.recipients() - - def subject(self): - entity = self.entity(self.row or 0, self.col or 0) - subject = self.req._(self.message) - etype = entity.dc_type() - eid = entity.eid - login = self.user_login() - return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals() - - def context(self, **kwargs): - entity = self.entity(self.row or 0, self.col or 0) - for key, val in kwargs.iteritems(): - if val and isinstance(val, unicode) and val.strip(): - kwargs[key] = self.req._(val) - kwargs.update({'user': self.user_login(), - 'eid': entity.eid, - 'etype': entity.dc_type(), - 'url': entity.absolute_url(), - 'title': entity.dc_long_title(),}) - return kwargs + # this is usually the method to call + def render_and_send(self, **kwargs): + """generate and send an email message for this view""" + delayed = kwargs.pop('delay_to_commit', None) + for recipients, msg in self.render_emails(**kwargs): + if delayed is None: + self.send(recipients, msg) + elif delayed: + self.send_on_commit(recipients, msg) + else: + self.send_now(recipients, msg) def cell_call(self, row, col=0, **kwargs): self.w(self.req._(self.content) % self.context(**kwargs)) - def construct_message_id(self, eid): - return construct_message_id(self.config.appid, eid, self.msgid_timestamp) - def render_emails(self, **kwargs): - """generate and send an email message for this view""" + """generate and send emails for this view (one per recipient)""" self._kwargs = kwargs recipients = self.recipients() if not recipients: self.info('skipping %s notification, no recipients', self.id) return - if not isinstance(recipients[0], tuple): - from warnings import warn - warn('recipients should now return a list of 2-uple (email, language)', - DeprecationWarning, stacklevel=1) - lang = self.vreg.property_value('ui.language') - recipients = zip(recipients, repeat(lang)) if self.rset is not None: entity = self.entity(self.row or 0, self.col or 0) # if the view is using timestamp in message ids, no way to reference @@ -225,16 +193,17 @@ # restore language self.req.set_language(origlang) - def render_and_send(self, **kwargs): - """generate and send an email message for this view""" - delayed = kwargs.pop('delay_to_commit', None) - for recipients, msg in self.render_emails(**kwargs): - if delayed is None: - self.send(recipients, msg) - elif delayed: - self.send_on_commit(recipients, msg) - else: - self.send_now(recipients, msg) + # recipients / email sending ############################################### + + def recipients(self): + """return a list of 2-uple (email, language) to who this email should be + sent + """ + finder = self.vreg['components'].select('recipients_finder', self.req, + rset=self.rset, + row=self.row or 0, + col=self.col or 0) + return finder.recipients() def send_now(self, recipients, msg): self.config.sendmails([(msg, recipients)]) @@ -243,3 +212,43 @@ raise NotImplementedError send = send_now + + # email generation helpers ################################################# + + def construct_message_id(self, eid): + return construct_message_id(self.config.appid, eid, self.msgid_timestamp) + + def format_field(self, attr, value): + return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value} + + def format_section(self, attr, value): + return '%(attr)s\n%(ul)s\n%(value)s\n' % { + 'attr': attr, 'ul': '-'*len(attr), 'value': value} + + def subject(self): + entity = self.entity(self.row or 0, self.col or 0) + subject = self.req._(self.message) + etype = entity.dc_type() + eid = entity.eid + login = self.user_login() + return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals() + + def context(self, **kwargs): + entity = self.entity(self.row or 0, self.col or 0) + for key, val in kwargs.iteritems(): + if val and isinstance(val, unicode) and val.strip(): + kwargs[key] = self.req._(val) + kwargs.update({'user': self.user_login(), + 'eid': entity.eid, + 'etype': entity.dc_type(), + 'url': entity.absolute_url(), + 'title': entity.dc_long_title(),}) + return kwargs + + def user_login(self): + try: + # if req is actually a session (we are on the server side), and we + # have to prevent nested internal session + return self.req.actual_session().user.login + except AttributeError: + return self.req.user.login diff -r 99ab33abf414 -r 096d680c9da2 common/mixins.py --- a/common/mixins.py Wed Sep 02 10:20:12 2009 +0200 +++ b/common/mixins.py Wed Sep 02 10:23:38 2009 +0200 @@ -13,7 +13,7 @@ from cubicweb import typed_eid from cubicweb.selectors import implements -from cubicweb.interfaces import IWorkflowable, IEmailable, ITree +from cubicweb.interfaces import IEmailable, ITree class TreeMixIn(object): @@ -158,97 +158,6 @@ return self.req.entity_from_eid(self.path()[0]) -class WorkflowableMixIn(object): - """base mixin providing workflow helper methods for workflowable entities. - This mixin will be automatically set on class supporting the 'in_state' - relation (which implies supporting 'wf_info_for' as well) - """ - __implements__ = (IWorkflowable,) - - @property - def state(self): - try: - return self.in_state[0].name - except IndexError: - self.warning('entity %s has no state', self) - return None - - @property - def displayable_state(self): - return self.req._(self.state) - - def wf_state(self, statename): - rset = self.req.execute('Any S, SN WHERE S name SN, S name %(n)s, S state_of E, E name %(e)s', - {'n': statename, 'e': str(self.e_schema)}) - if rset: - return rset.get_entity(0, 0) - return None - - def wf_transition(self, trname): - rset = self.req.execute('Any T, TN WHERE T name TN, T name %(n)s, T transition_of E, E name %(e)s', - {'n': trname, 'e': str(self.e_schema)}) - if rset: - return rset.get_entity(0, 0) - return None - - def change_state(self, state, trcomment=None, trcommentformat=None): - """change the entity's state according to a state defined in given - parameters - """ - if isinstance(state, basestring): - state = self.wf_state(state) - assert state is not None, 'not a %s state: %s' % (self.id, state) - if hasattr(state, 'eid'): - stateeid = state.eid - else: - stateeid = state - stateeid = typed_eid(stateeid) - if trcomment: - self.req.set_shared_data('trcomment', trcomment) - if trcommentformat: - self.req.set_shared_data('trcommentformat', trcommentformat) - self.req.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': self.eid, 's': stateeid}, 'x') - - def can_pass_transition(self, trname): - """return the Transition instance if the current user can pass the - transition with the given name, else None - """ - stateeid = self.in_state[0].eid - rset = self.req.execute('Any T,N,DS WHERE S allowed_transition T,' - 'S eid %(x)s,T name %(trname)s,ET name %(et)s,' - 'T name N,T destination_state DS,T transition_of ET', - {'x': stateeid, 'et': str(self.e_schema), - 'trname': trname}, 'x') - for tr in rset.entities(): - if tr.may_be_passed(self.eid, stateeid): - return tr - - def latest_trinfo(self): - """return the latest transition information for this entity""" - return self.reverse_wf_info_for[-1] - - # __method methods ######################################################## - - def set_state(self, params=None): - """change the entity's state according to a state defined in given - parameters, used to be called using __method controler facility - """ - params = params or self.req.form - self.change_state(typed_eid(params.pop('state')), - params.get('trcomment'), - params.get('trcomment_format')) - self.req.set_message(self.req._('__msg state changed')) - - # specific vocabulary methods ############################################# - - @deprecated('use EntityFieldsForm.subject_in_state_vocabulary') - def subject_in_state_vocabulary(self, rschema, limit=None): - form = self.vreg.select('forms', 'edition', self.req, entity=self) - return form.subject_in_state_vocabulary(rschema, limit) - - - class EmailableMixIn(object): """base mixin providing the default get_email() method used by the massmailing view @@ -288,7 +197,6 @@ MI_REL_TRIGGERS = { - ('in_state', 'subject'): WorkflowableMixIn, ('primary_email', 'subject'): EmailableMixIn, ('use_email', 'subject'): EmailableMixIn, } diff -r 99ab33abf414 -r 096d680c9da2 common/test/unittest_mixins.py --- a/common/test/unittest_mixins.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -from logilab.common.testlib import unittest_main -from cubicweb.devtools.apptest import EnvBasedTC - -class WorkfloableMixInTC(EnvBasedTC): - def test_wf_state(self): - s = self.add_entity('State', name=u'activated') - self.execute('SET X state_of ET WHERE ET name "Bookmark", X eid %(x)s', - {'x': s.eid}) - es = self.user().wf_state('activated') - self.assertEquals(es.state_of[0].name, 'CWUser') - - def test_wf_transition(self): - t = self.add_entity('Transition', name=u'deactivate') - self.execute('SET X transition_of ET WHERE ET name "Bookmark", X eid %(x)s', - {'x': t.eid}) - et = self.user().wf_transition('deactivate') - self.assertEquals(et.transition_of[0].name, 'CWUser') - - def test_change_state(self): - user = self.user() - user.change_state(user.wf_state('deactivated').eid) - self.assertEquals(user.state, 'deactivated') - -if __name__ == '__main__': - unittest_main() diff -r 99ab33abf414 -r 096d680c9da2 common/test/unittest_uilib.py --- a/common/test/unittest_uilib.py Wed Sep 02 10:20:12 2009 +0200 +++ b/common/test/unittest_uilib.py Wed Sep 02 10:23:38 2009 +0200 @@ -81,47 +81,6 @@ got = uilib.text_cut(text, 30) self.assertEquals(got, expected) -tree = ('root', ( - ('child_1_1', ( - ('child_2_1', ()), ('child_2_2', ( - ('child_3_1', ()), - ('child_3_2', ()), - ('child_3_3', ()), - )))), - ('child_1_2', (('child_2_3', ()),)))) - -generated_html = """\ - - - - - - - - - - - -
root
  
child_1_1
  
child_2_1
   
      
      
child_2_2
  
child_3_1
      
         
child_3_2
      
         
child_3_3
      
   
child_1_2
  
child_2_3
   
      
\ -""" - -def make_tree(tuple): - n = Node(tuple[0]) - for child in tuple[1]: - n.append(make_tree(child)) - return n - -class UIlibHTMLGenerationTC(TestCase): - """ a basic tree node, caracterised by an id""" - def setUp(self): - """ called before each test from this class """ - self.o = make_tree(tree) - - def test_generated_html(self): - s = uilib.render_HTML_tree(self.o, selected_node="child_2_2") - self.assertTextEqual(s, generated_html) - - if __name__ == '__main__': unittest_main() diff -r 99ab33abf414 -r 096d680c9da2 common/uilib.py --- a/common/uilib.py Wed Sep 02 10:20:12 2009 +0200 +++ b/common/uilib.py Wed Sep 02 10:23:38 2009 +0200 @@ -12,7 +12,6 @@ import csv import re -from urllib import quote as urlquote from StringIO import StringIO from logilab.mtconverter import xml_escape, html_unescape @@ -264,125 +263,6 @@ res = unicode(res, 'UTF8') return res -def render_HTML_tree(tree, selected_node=None, render_node=None, caption=None): - """ - Generate a pure HTML representation of a tree given as an instance - of a logilab.common.tree.Node - - selected_node is the currently selected node (if any) which will - have its surrounding
have id="selected" (which default - to a bold border libe with the default CSS). - - render_node is a function that should take a Node content (Node.id) - as parameter and should return a string (what will be displayed - in the cell). - - Warning: proper rendering of the generated html code depends on html_tree.css - """ - tree_depth = tree.depth_down() - if render_node is None: - render_node = str - - # helper function that build a matrix from the tree, like: - # +------+-----------+-----------+ - # | root | child_1_1 | child_2_1 | - # | root | child_1_1 | child_2_2 | - # | root | child_1_2 | | - # | root | child_1_3 | child_2_3 | - # | root | child_1_3 | child_2_4 | - # +------+-----------+-----------+ - # from: - # root -+- child_1_1 -+- child_2_1 - # | | - # | +- child_2_2 - # +- child_1_2 - # | - # +- child1_3 -+- child_2_3 - # | - # +- child_2_2 - def build_matrix(path, matrix): - if path[-1].is_leaf(): - matrix.append(path[:]) - else: - for child in path[-1].children: - build_matrix(path[:] + [child], matrix) - - matrix = [] - build_matrix([tree], matrix) - - # make all lines in the matrix have the same number of columns - for line in matrix: - line.extend([None]*(tree_depth-len(line))) - for i in range(len(matrix)-1, 0, -1): - prev_line, line = matrix[i-1:i+1] - for j in range(len(line)): - if line[j] == prev_line[j]: - line[j] = None - - # We build the matrix of link types (between 2 cells on a line of the matrix) - # link types are : - link_types = {(True, True, True ): 1, # T - (False, False, True ): 2, # | - (False, True, True ): 3, # + (actually, vert. bar with horiz. bar on the right) - (False, True, False): 4, # L - (True, True, False): 5, # - - } - links = [] - for i, line in enumerate(matrix): - links.append([]) - for j in range(tree_depth-1): - cell_11 = line[j] is not None - cell_12 = line[j+1] is not None - cell_21 = line[j+1] is not None and line[j+1].next_sibling() is not None - link_type = link_types.get((cell_11, cell_12, cell_21), 0) - if link_type == 0 and i > 0 and links[i-1][j] in (1, 2, 3): - link_type = 2 - links[-1].append(link_type) - - - # We can now generate the HTML code for the - s = u'
\n' - if caption: - s += '\n' % caption - - for i, link_line in enumerate(links): - line = matrix[i] - - s += '' - for j, link_cell in enumerate(link_line): - cell = line[j] - if cell: - if cell.id == selected_node: - s += '' % (render_node(cell.id)) - else: - s += '' % (render_node(cell.id)) - else: - s += '' - s += '' % link_cell - s += '' % link_cell - - cell = line[-1] - if cell: - if cell.id == selected_node: - s += '' % (render_node(cell.id)) - else: - s += '' % (render_node(cell.id)) - else: - s += '' - - s += '\n' - if link_line: - s += '' - for j, link_cell in enumerate(link_line): - s += '' % link_cell - s += '' % link_cell - s += '\n' - - s += '
%s
%s
%s
   
%s
%s
 
  
' - return s - - - # traceback formatting ######################################################## import traceback diff -r 99ab33abf414 -r 096d680c9da2 cwvreg.py --- a/cwvreg.py Wed Sep 02 10:20:12 2009 +0200 +++ b/cwvreg.py Wed Sep 02 10:23:38 2009 +0200 @@ -148,9 +148,9 @@ objects = self['Any'] assert len(objects) == 1, objects cls = objects[0] - if cls.id == etype: - cls.__initialize__() - return cls + # make a copy event if cls.id == etype, else we may have pb for client + # application using multiple connections to different repositories (eg + # shingouz) cls = dump_class(cls, etype) cls.id = etype cls.__initialize__() diff -r 99ab33abf414 -r 096d680c9da2 debian/control --- a/debian/control Wed Sep 02 10:20:12 2009 +0200 +++ b/debian/control Wed Sep 02 10:23:38 2009 +0200 @@ -62,7 +62,7 @@ Architecture: all XB-Python-Version: ${python:Versions} Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3), python-elementtree -Recommends: python-docutils, python-vobject, fckeditor, python-fyzz +Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-pysixt, fop Description: web interface library for the CubicWeb framework CubicWeb is a semantic web application framework. . diff -r 99ab33abf414 -r 096d680c9da2 devtools/_apptest.py --- a/devtools/_apptest.py Wed Sep 02 10:20:12 2009 +0200 +++ b/devtools/_apptest.py Wed Sep 02 10:23:38 2009 +0200 @@ -26,7 +26,7 @@ 'CWAttribute', 'CWRelation', 'CWConstraint', 'CWConstraintType', 'CWProperty', 'CWEType', 'CWRType', - 'State', 'Transition', 'TrInfo', + 'Workflow', 'State', 'BaseTransition', 'Transition', 'WorkflowTransition', 'TrInfo', 'SubWorkflowExitPoint', 'RQLExpression', ) SYSTEM_RELATIONS = ( @@ -35,9 +35,9 @@ # metadata 'is', 'is_instance_of', 'owned_by', 'created_by', 'specializes', # workflow related - 'state_of', 'transition_of', 'initial_state', 'allowed_transition', + 'workflow_of', 'state_of', 'transition_of', 'initial_state', 'allowed_transition', 'destination_state', 'in_state', 'wf_info_for', 'from_state', 'to_state', - 'condition', + 'condition', 'subworkflow', 'subworkflow_state', 'subworkflow_exit', # permission 'in_group', 'require_group', 'require_permission', 'read_permission', 'update_permission', 'delete_permission', 'add_permission', @@ -121,8 +121,7 @@ def create_user(self, login, groups=('users',), req=None): req = req or self.create_request() cursor = self._orig_cnx.cursor(req) - rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s,' - 'X in_state S WHERE S name "activated"', + rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s', {'login': unicode(login), 'passwd': login.encode('utf8')}) user = rset.get_entity(0, 0) cursor.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)' diff -r 99ab33abf414 -r 096d680c9da2 devtools/apptest.py --- a/devtools/apptest.py Wed Sep 02 10:20:12 2009 +0200 +++ b/devtools/apptest.py Wed Sep 02 10:23:38 2009 +0200 @@ -371,8 +371,7 @@ def create_user(self, user, groups=('users',), password=None, commit=True): if password is None: password = user - eid = self.execute('INSERT CWUser X: X login %(x)s, X upassword %(p)s,' - 'X in_state S WHERE S name "activated"', + eid = self.execute('INSERT CWUser X: X login %(x)s, X upassword %(p)s', {'x': unicode(user), 'p': password})[0][0] groups = ','.join(repr(group) for group in groups) self.execute('SET X in_group Y WHERE X eid %%(x)s, Y name IN (%s)' % groups, diff -r 99ab33abf414 -r 096d680c9da2 devtools/dataimport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/dataimport.py Wed Sep 02 10:23:38 2009 +0200 @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +"""This module provides tools to import tabular data. + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses + + +Example of use (run this with `cubicweb-ctl shell instance import-script.py`): + +.. sourcecode:: python + + # define data generators + GENERATORS = [] + + USERS = [('Prenom', 'firstname', ()), + ('Nom', 'surname', ()), + ('Identifiant', 'login', ()), + ] + + def gen_users(ctl): + for row in ctl.get_data('utilisateurs'): + entity = mk_entity(row, USERS) + entity['upassword'] = u'motdepasse' + ctl.check('login', entity['login'], None) + ctl.store.add('CWUser', entity) + email = {'address': row['email']} + ctl.store.add('EmailAddress', email) + ctl.store.relate(entity['eid'], 'use_email', email['eid']) + ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x':entity['eid']}) + + CHK = [('login', check_doubles, 'Utilisateurs Login', + 'Deux utilisateurs ne devraient pas avoir le même login.'), + ] + + GENERATORS.append( (gen_users, CHK) ) + + # create controller + ctl = CWImportController(RQLObjectStore()) + ctl.askerror = True + ctl.generators = GENERATORS + ctl.store._checkpoint = checkpoint + ctl.store._rql = rql + ctl.data['utilisateurs'] = lazytable(utf8csvreader(open('users.csv'))) + # run + ctl.run() + sys.exit(0) + +""" +__docformat__ = "restructuredtext en" + +import sys, csv, traceback + +from logilab.common import shellutils + +def utf8csvreader(file, encoding='utf-8', separator=',', quote='"'): + """A csv reader that accepts files with any encoding and outputs + unicode strings.""" + for row in csv.reader(file, delimiter=separator, quotechar=quote): + yield [item.decode(encoding) for item in row] + +def lazytable(reader): + """The first row is taken to be the header of the table and + used to output a dict for each row of data. + + >>> data = lazytable(utf8csvreader(open(filename))) + """ + header = reader.next() + for row in reader: + yield dict(zip(header, row)) + +def tell(msg): + print msg + +# base sanitizing functions ##### + +def capitalize_if_unicase(txt): + if txt.isupper() or txt.islower(): + return txt.capitalize() + return txt + +def no_space(txt): + return txt.replace(' ','') + +def no_uspace(txt): + return txt.replace(u'\xa0','') + +def no_dash(txt): + return txt.replace('-','') + +def alldigits(txt): + if txt.isdigit(): + return txt + else: + return u'' + +def strip(txt): + return txt.strip() + +# base checks ##### + +def check_doubles(buckets): + """Extract the keys that have more than one item in their bucket.""" + return [(key, len(value)) for key,value in buckets.items() if len(value) > 1] + +# make entity helper ##### + +def mk_entity(row, map): + """Return a dict made from sanitized mapped values. + + >>> row = {'myname': u'dupont'} + >>> map = [('myname', u'name', (capitalize_if_unicase,))] + >>> mk_entity(row, map) + {'name': u'Dupont'} + """ + res = {} + for src, dest, funcs in map: + res[dest] = row[src] + for func in funcs: + res[dest] = func(res[dest]) + return res + +# object stores + +class ObjectStore(object): + """Store objects in memory for faster testing. Will not + enforce the constraints of the schema and hence will miss + some problems. + + >>> store = ObjectStore() + >>> user = {'login': 'johndoe'} + >>> store.add('CWUser', user) + >>> group = {'name': 'unknown'} + >>> store.add('CWUser', group) + >>> store.relate(user['eid'], 'in_group', group['eid']) + """ + + def __init__(self): + self.items = [] + self.eids = {} + self.types = {} + self.relations = set() + self.indexes = {} + self._rql = None + self._checkpoint = None + + def _put(self, type, item): + self.items.append(item) + return len(self.items) - 1 + + def add(self, type, item): + assert isinstance(item, dict), item + eid = item['eid'] = self._put(type, item) + self.eids[eid] = item + self.types.setdefault(type, []).append(eid) + + def relate(self, eid_from, rtype, eid_to): + eids_valid = (eid_from < len(self.items) and eid_to <= len(self.items)) + assert eids_valid, 'eid error %s %s' % (eid_from, eid_to) + self.relations.add( (eid_from, rtype, eid_to) ) + + def build_index(self, name, type, func): + index = {} + for eid in self.types[type]: + index.setdefault(func(self.eids[eid]), []).append(eid) + self.indexes[name] = index + + def get_many(self, name, key): + return self.indexes[name].get(key, []) + + def get_one(self, name, key): + eids = self.indexes[name].get(key, []) + assert len(eids) == 1 + return eids[0] + + def find(self, type, key, value): + for idx in self.types[type]: + item = self.items[idx] + if item[key] == value: + yield item + + def rql(self, query, args): + if self._rql: + return self._rql(query, args) + + def checkpoint(self): + if self._checkpoint: + self._checkpoint() + +class RQLObjectStore(ObjectStore): + """ObjectStore that works with an actual RQL repository.""" + + def _put(self, type, item): + query = ('INSERT %s X: ' % type) + ', '.join(['X %s %%(%s)s' % (key,key) for key in item]) + return self.rql(query, item)[0][0] + + def relate(self, eid_from, rtype, eid_to): + query = 'SET X %s Y WHERE X eid %%(from)s, Y eid %%(to)s' % rtype + self.rql(query, {'from': int(eid_from), 'to': int(eid_to)}) + self.relations.add( (eid_from, rtype, eid_to) ) + +# import controller ##### + +class CWImportController(object): + """Controller of the data import process. + + >>> ctl = CWImportController(store) + >>> ctl.generators = list_of_data_generators + >>> ctl.data = dict_of_data_tables + >>> ctl.run() + """ + + def __init__(self, store): + self.store = store + self.generators = None + self.data = {} + self.errors = None + self.askerror = False + self._tell = tell + + def check(self, type, key, value): + self._checks.setdefault(type, {}).setdefault(key, []).append(value) + + def check_map(self, entity, key, map, default): + try: + entity[key] = map[entity[key]] + except KeyError: + self.check(key, entity[key], None) + entity[key] = default + + def run(self): + self.errors = {} + for func, checks in self.generators: + self._checks = {} + func_name = func.__name__[4:] + question = 'Importation de %s' % func_name + self.tell(question) + try: + func(self) + except: + import StringIO + tmp = StringIO.StringIO() + traceback.print_exc(file=tmp) + print tmp.getvalue() + self.errors[func_name] = ('Erreur lors de la transformation', + tmp.getvalue().splitlines()) + for key, func, title, help in checks: + buckets = self._checks.get(key) + if buckets: + err = func(buckets) + if err: + self.errors[title] = (help, err) + self.store.checkpoint() + errors = sum(len(err[1]) for err in self.errors.values()) + self.tell('Importation terminée. (%i objets, %i types, %i relations et %i erreurs).' + % (len(self.store.eids), len(self.store.types), + len(self.store.relations), errors)) + if self.errors and self.askerror and confirm('Afficher les erreurs ?'): + import pprint + pprint.pprint(self.errors) + + def get_data(self, key): + return self.data.get(key) + + def index(self, name, key, value): + self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value) + + def tell(self, msg): + self._tell(msg) + +def confirm(question): + """A confirm function that asks for yes/no/abort and exits on abort.""" + answer = shellutils.ASK.ask(question, ('Y','n','abort'), 'Y') + if answer == 'abort': + sys.exit(1) + return answer == 'Y' diff -r 99ab33abf414 -r 096d680c9da2 doc/book/en/development/testing/index.rst --- a/doc/book/en/development/testing/index.rst Wed Sep 02 10:20:12 2009 +0200 +++ b/doc/book/en/development/testing/index.rst Wed Sep 02 10:23:38 2009 +0200 @@ -16,14 +16,48 @@ * `EnvBasedTC`, to simulate a complete environment (web + repository) * `RepositoryBasedTC`, to simulate a repository environment only -Thos two classes almost have the same interface and offers numerous methods to -write tests rapidely and efficiently. +Those two classes almost have the same interface and offer numerous +methods to write tests rapidly and efficiently. XXX FILLME describe API In most of the cases, you will inherit `EnvBasedTC` to write Unittest or functional tests for your entities, views, hooks, etc... +Managing connections or users ++++++++++++++++++++++++++++++ + +Since unit tests are done with the SQLITE backend and this does not +support multiple connections at a time, you must be careful when +simulating security, changing users. + +By default, tests run with a user with admin privileges. This +user/connection must never be closed. +qwq +Before a self.login, one has to release the connection pool in use with a self.commit, self.rollback or self.close. + +When one is logged in as a normal user and wants to switch back to the admin user, one has to use self.restore_connection(). + +Usually it looks like this: + +.. sourcecode:: python + + # execute using default admin connection + self.execute(...) + # I want to login with another user, ensure to free admin connection pool + # (could have used rollback but not close here, we should never close defaut admin connection) + self.commit() + cnx = self.login('user') + # execute using user connection + self.execute(...) + # I want to login with another user or with admin user + self.commit(); cnx.close() + # restore admin connection, never use cnx = self.login('admin'), it will return + # the default admin connection and one may be tempted to close it + self.restore_connection() + +Take care of the references kept to the entities created with a connection or the other. + Email notifications tests ------------------------- diff -r 99ab33abf414 -r 096d680c9da2 entities/test/data/migration/postcreate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/test/data/migration/postcreate.py Wed Sep 02 10:23:38 2009 +0200 @@ -0,0 +1,2 @@ +wf = add_workflow(u'bmk wf', 'Bookmark') +wf.add_state(u'hop', initial=True) diff -r 99ab33abf414 -r 096d680c9da2 entities/test/data/schema.py --- a/entities/test/data/schema.py Wed Sep 02 10:20:12 2009 +0200 +++ b/entities/test/data/schema.py Wed Sep 02 10:23:38 2009 +0200 @@ -1,11 +1,13 @@ -""" +"""entities tests schema :organization: Logilab :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ + from yams.buildobjs import EntityType, String +from cubicweb.schema import make_workflowable class Company(EntityType): name = String() @@ -16,3 +18,7 @@ class SubDivision(Division): __specializes_schema__ = True + +from cubicweb.schemas import bootstrap, Bookmark +make_workflowable(bootstrap.CWGroup) +make_workflowable(Bookmark.Bookmark) diff -r 99ab33abf414 -r 096d680c9da2 entities/test/unittest_base.py --- a/entities/test/unittest_base.py Wed Sep 02 10:20:12 2009 +0200 +++ b/entities/test/unittest_base.py Wed Sep 02 10:23:38 2009 +0200 @@ -58,149 +58,6 @@ self.assertEquals(e.dc_title(), 'member') self.assertEquals(e.name(), u'bouah lôt') - -class StateAndTransitionsTC(BaseEntityTC): - - def test_transitions(self): - user = self.entity('CWUser X') - e = self.entity('State S WHERE S name "activated"') - trs = list(e.transitions(user)) - self.assertEquals(len(trs), 1) - self.assertEquals(trs[0].name, u'deactivate') - self.assertEquals(trs[0].destination().name, u'deactivated') - self.assert_(user.can_pass_transition('deactivate')) - self.assert_(not user.can_pass_transition('activate')) - # test a std user get no possible transition - self.login('member') - # fetch the entity using the new session - e = self.entity('State S WHERE S name "activated"') - trs = list(e.transitions(user)) - self.assertEquals(len(trs), 0) - user = self.entity('CWUser X') - self.assert_(not user.can_pass_transition('deactivate')) - self.assert_(not user.can_pass_transition('activate')) - - def test_transitions_with_dest_specfied(self): - user = self.entity('CWUser X') - e = self.entity('State S WHERE S name "activated"') - e2 = self.entity('State S WHERE S name "deactivated"') - trs = list(e.transitions(user, e2.eid)) - self.assertEquals(len(trs), 1) - self.assertEquals(trs[0].name, u'deactivate') - self.assertEquals(trs[0].destination().name, u'deactivated') - trs = list(e.transitions(user, e.eid)) - self.assertEquals(len(trs), 0) - - def test_transitions_maybe_passed(self): - self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' - 'X expression "X owned_by U", T condition X ' - 'WHERE T name "deactivate"') - self._test_deactivated() - - def test_transitions_maybe_passed_using_has_update_perm(self): - self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' - 'X expression "U has_update_permission X", T condition X ' - 'WHERE T name "deactivate"') - self._test_deactivated() - - - def _test_deactivated(self): - ueid = self.create_user('toto').eid - self.create_user('tutu') - cnx = self.login('tutu') - cu = cnx.cursor() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - cnx.close() - cnx = self.login('toto') - cu = cnx.cursor() - cu.execute('SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - cnx.commit() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S name "activated"', - {'x': ueid}, 'x') - - - def test_transitions_selection(self): - """ - ------------------------ tr1 ----------------- - | state1 (CWGroup, Bookmark) | ------> | state2 (CWGroup) | - ------------------------ ----------------- - | tr2 ------------------ - `------> | state3 (Bookmark) | - ------------------ - """ - state1 = self.add_entity('State', name=u'state1') - state2 = self.add_entity('State', name=u'state2') - state3 = self.add_entity('State', name=u'state3') - tr1 = self.add_entity('Transition', name=u'tr1') - tr2 = self.add_entity('Transition', name=u'tr2') - self.execute('SET X state_of Y WHERE X eid in (%s, %s), Y is CWEType, Y name "CWGroup"' % - (state1.eid, state2.eid)) - self.execute('SET X state_of Y WHERE X eid in (%s, %s), Y is CWEType, Y name "Bookmark"' % - (state1.eid, state3.eid)) - self.execute('SET X transition_of Y WHERE X eid %s, Y name "CWGroup"' % tr1.eid) - self.execute('SET X transition_of Y WHERE X eid %s, Y name "Bookmark"' % tr2.eid) - self.execute('SET X allowed_transition Y WHERE X eid %s, Y eid %s' % - (state1.eid, tr1.eid)) - self.execute('SET X allowed_transition Y WHERE X eid %s, Y eid %s' % - (state1.eid, tr2.eid)) - self.execute('SET X destination_state Y WHERE X eid %s, Y eid %s' % - (tr1.eid, state2.eid)) - self.execute('SET X destination_state Y WHERE X eid %s, Y eid %s' % - (tr2.eid, state3.eid)) - self.execute('SET X initial_state Y WHERE Y eid %s, X name "CWGroup"' % state1.eid) - self.execute('SET X initial_state Y WHERE Y eid %s, X name "Bookmark"' % state1.eid) - group = self.add_entity('CWGroup', name=u't1') - transitions = list(state1.transitions(group)) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr1') - bookmark = self.add_entity('Bookmark', title=u'111', path=u'/view') - transitions = list(state1.transitions(bookmark)) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr2') - - - def test_transitions_selection2(self): - """ - ------------------------ tr1 (Bookmark) ----------------------- - | state1 (CWGroup, Bookmark) | -------------> | state2 (CWGroup,Bookmark) | - ------------------------ ----------------------- - | tr2 (CWGroup) | - `---------------------------------/ - """ - state1 = self.add_entity('State', name=u'state1') - state2 = self.add_entity('State', name=u'state2') - tr1 = self.add_entity('Transition', name=u'tr1') - tr2 = self.add_entity('Transition', name=u'tr2') - self.execute('SET X state_of Y WHERE X eid in (%s, %s), Y is CWEType, Y name "CWGroup"' % - (state1.eid, state2.eid)) - self.execute('SET X state_of Y WHERE X eid in (%s, %s), Y is CWEType, Y name "Bookmark"' % - (state1.eid, state2.eid)) - self.execute('SET X transition_of Y WHERE X eid %s, Y name "CWGroup"' % tr1.eid) - self.execute('SET X transition_of Y WHERE X eid %s, Y name "Bookmark"' % tr2.eid) - self.execute('SET X allowed_transition Y WHERE X eid %s, Y eid %s' % - (state1.eid, tr1.eid)) - self.execute('SET X allowed_transition Y WHERE X eid %s, Y eid %s' % - (state1.eid, tr2.eid)) - self.execute('SET X destination_state Y WHERE X eid %s, Y eid %s' % - (tr1.eid, state2.eid)) - self.execute('SET X destination_state Y WHERE X eid %s, Y eid %s' % - (tr2.eid, state2.eid)) - self.execute('SET X initial_state Y WHERE Y eid %s, X name "CWGroup"' % state1.eid) - self.execute('SET X initial_state Y WHERE Y eid %s, X name "Bookmark"' % state1.eid) - group = self.add_entity('CWGroup', name=u't1') - transitions = list(state1.transitions(group)) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr1') - bookmark = self.add_entity('Bookmark', title=u'111', path=u'/view') - transitions = list(state1.transitions(bookmark)) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr2') - - class EmailAddressTC(BaseEntityTC): def test_canonical_form(self): eid1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"')[0][0] @@ -234,7 +91,6 @@ e = self.entity('CWUser X WHERE X login "admin"') e.complete() - def test_matching_groups(self): e = self.entity('CWUser X WHERE X login "admin"') self.failUnless(e.matching_groups('managers')) @@ -242,23 +98,6 @@ self.failUnless(e.matching_groups(('xyz', 'managers'))) self.failIf(e.matching_groups(('xyz', 'abcd'))) - def test_workflow_base(self): - e = self.create_user('toto') - self.assertEquals(e.state, 'activated') - activatedeid = self.execute('State X WHERE X name "activated"')[0][0] - deactivatedeid = self.execute('State X WHERE X name "deactivated"')[0][0] - e.change_state(deactivatedeid, u'deactivate 1') - self.commit() - e.change_state(activatedeid, u'activate 1') - self.commit() - e.change_state(deactivatedeid, u'deactivate 2') - self.commit() - # get a fresh user to avoid potential cache issues - e = self.entity('CWUser X WHERE X eid %s' % e.eid) - self.assertEquals([tr.comment for tr in e.reverse_wf_info_for], - [None, 'deactivate 1', 'activate 1', 'deactivate 2']) - self.assertEquals(e.latest_trinfo().comment, 'deactivate 2') - class InterfaceTC(EnvBasedTC): diff -r 99ab33abf414 -r 096d680c9da2 entities/test/unittest_wfobjs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/test/unittest_wfobjs.py Wed Sep 02 10:23:38 2009 +0200 @@ -0,0 +1,430 @@ +from cubicweb.devtools.apptest import EnvBasedTC +from cubicweb import ValidationError + +def add_wf(self, etype, name=None, default=False): + if name is None: + name = etype + wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': unicode(name)}).get_entity(0, 0) + self.execute('SET WF workflow_of ET WHERE WF eid %(wf)s, ET name %(et)s', + {'wf': wf.eid, 'et': etype}) + if default: + self.execute('SET ET default_workflow WF WHERE WF eid %(wf)s, ET name %(et)s', + {'wf': wf.eid, 'et': etype}) + return wf + +def parse_hist(wfhist): + return [(ti.previous_state.name, ti.new_state.name, + ti.transition and ti.transition.name, ti.comment) + for ti in wfhist] + + +class WorkflowBuildingTC(EnvBasedTC): + + def test_wf_construction(self): + wf = add_wf(self, 'Company') + foo = wf.add_state(u'foo', initial=True) + bar = wf.add_state(u'bar') + self.assertEquals(wf.state_by_name('bar').eid, bar.eid) + self.assertEquals(wf.state_by_name('barrr'), None) + baz = wf.add_transition(u'baz', (foo,), bar, ('managers',)) + self.assertEquals(wf.transition_by_name('baz').eid, baz.eid) + self.assertEquals(len(baz.require_group), 1) + self.assertEquals(baz.require_group[0].name, 'managers') + + def test_duplicated_state(self): + wf = add_wf(self, 'Company') + wf.add_state(u'foo', initial=True) + self.commit() + wf.add_state(u'foo') + ex = self.assertRaises(ValidationError, self.commit) + # XXX enhance message + self.assertEquals(ex.errors, {'state_of': 'unique constraint S name N, Y state_of O, Y name N failed'}) + # no pb if not in the same workflow + wf2 = add_wf(self, 'Company') + foo = wf2.add_state(u'foo', initial=True) + self.commit() + + def test_duplicated_transition(self): + wf = add_wf(self, 'Company') + foo = wf.add_state(u'foo', initial=True) + bar = wf.add_state(u'bar') + wf.add_transition(u'baz', (foo,), bar, ('managers',)) + wf.add_transition(u'baz', (bar,), foo) + ex = self.assertRaises(ValidationError, self.commit) + # XXX enhance message + self.assertEquals(ex.errors, {'transition_of': 'unique constraint S name N, Y transition_of O, Y name N failed'}) + + +class WorkflowTC(EnvBasedTC): + + def setup_database(self): + rschema = self.schema['in_state'] + for x, y in rschema.iter_rdefs(): + self.assertEquals(rschema.rproperty(x, y, 'cardinality'), '1*') + self.member = self.create_user('member') + + def test_workflow_base(self): + e = self.create_user('toto') + self.assertEquals(e.state, 'activated') + e.change_state('deactivated', u'deactivate 1') + self.commit() + e.change_state('activated', u'activate 1') + self.commit() + e.change_state('deactivated', u'deactivate 2') + self.commit() + e.clear_related_cache('wf_info_for', 'object') + self.assertEquals([tr.comment for tr in e.reverse_wf_info_for], + ['deactivate 1', 'activate 1', 'deactivate 2']) + self.assertEquals(e.latest_trinfo().comment, 'deactivate 2') + + def test_possible_transitions(self): + user = self.entity('CWUser X') + trs = list(user.possible_transitions()) + self.assertEquals(len(trs), 1) + self.assertEquals(trs[0].name, u'deactivate') + self.assertEquals(trs[0].destination().name, u'deactivated') + # test a std user get no possible transition + cnx = self.login('member') + # fetch the entity using the new session + trs = list(cnx.user().possible_transitions()) + self.assertEquals(len(trs), 0) + + def _test_manager_deactivate(self, user): + user.clear_related_cache('in_state', 'subject') + self.assertEquals(len(user.in_state), 1) + self.assertEquals(user.state, 'deactivated') + trinfo = user.latest_trinfo() + self.assertEquals(trinfo.previous_state.name, 'activated') + self.assertEquals(trinfo.new_state.name, 'deactivated') + self.assertEquals(trinfo.comment, 'deactivate user') + self.assertEquals(trinfo.comment_format, 'text/plain') + return trinfo + + def test_change_state(self): + user = self.user() + user.change_state('deactivated', comment=u'deactivate user') + trinfo = self._test_manager_deactivate(user) + self.assertEquals(trinfo.transition, None) + + def test_set_in_state_bad_wf(self): + wf = add_wf(self, 'CWUser') + s = wf.add_state(u'foo', initial=True) + self.commit() + ex = self.assertRaises(ValidationError, self.session().unsafe_execute, + 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', + {'x': self.user().eid, 's': s.eid}, 'x') + self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. " + "You may want to set a custom workflow for this entity first."}) + + def test_fire_transition(self): + user = self.user() + user.fire_transition('deactivate', comment=u'deactivate user') + user.clear_all_caches() + self.assertEquals(user.state, 'deactivated') + self._test_manager_deactivate(user) + trinfo = self._test_manager_deactivate(user) + self.assertEquals(trinfo.transition.name, 'deactivate') + + # XXX test managers can change state without matching transition + + def _test_stduser_deactivate(self): + ueid = self.member.eid + self.create_user('tutu') + cnx = self.login('tutu') + req = self.request() + member = req.entity_from_eid(self.member.eid) + ex = self.assertRaises(ValidationError, + member.fire_transition, 'deactivate') + self.assertEquals(ex.errors, {'by_transition': "transition may not be fired"}) + cnx.close() + cnx = self.login('member') + req = self.request() + member = req.entity_from_eid(self.member.eid) + member.fire_transition('deactivate') + cnx.commit() + ex = self.assertRaises(ValidationError, + member.fire_transition, 'activate') + self.assertEquals(ex.errors, {'by_transition': "transition may not be fired"}) + + def test_fire_transition_owned_by(self): + self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' + 'X expression "X owned_by U", T condition X ' + 'WHERE T name "deactivate"') + self._test_stduser_deactivate() + + def test_fire_transition_has_update_perm(self): + self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' + 'X expression "U has_update_permission X", T condition X ' + 'WHERE T name "deactivate"') + self._test_stduser_deactivate() + + def test_subworkflow_base(self): + """subworkflow + + +-----------+ tr1 +-----------+ + | swfstate1 | ------>| swfstate2 | + +-----------+ +-----------+ + | tr2 +-----------+ + `------>| swfstate3 | + +-----------+ + + main workflow + + +--------+ swftr1 +--------+ + | state1 | -------[swfstate2]->| state2 | + +--------+ | +--------+ + | +--------+ + `-[swfstate3]-->| state3 | + +--------+ + """ + # sub-workflow + swf = add_wf(self, 'CWGroup', name='subworkflow') + swfstate1 = swf.add_state(u'swfstate1', initial=True) + swfstate2 = swf.add_state(u'swfstate2') + swfstate3 = swf.add_state(u'swfstate3') + tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2) + tr2 = swf.add_transition(u'tr2', (swfstate1,), swfstate3) + # main workflow + mwf = add_wf(self, 'CWGroup', name='main workflow', default=True) + state1 = mwf.add_state(u'state1', initial=True) + state2 = mwf.add_state(u'state2') + state3 = mwf.add_state(u'state3') + swftr1 = mwf.add_wftransition(u'swftr1', swf, state1, + [(swfstate2, state2), (swfstate3, state3)]) + self.assertEquals(swftr1.destination().eid, swfstate1.eid) + # workflows built, begin test + self.group = self.add_entity('CWGroup', name=u'grp1') + self.commit() + self.assertEquals(self.group.current_state.eid, state1.eid) + self.assertEquals(self.group.current_workflow.eid, mwf.eid) + self.assertEquals(self.group.main_workflow.eid, mwf.eid) + self.assertEquals(self.group.subworkflow_input_transition(), None) + self.group.fire_transition('swftr1', u'go') + self.commit() + self.group.clear_all_caches() + self.assertEquals(self.group.current_state.eid, swfstate1.eid) + self.assertEquals(self.group.current_workflow.eid, swf.eid) + self.assertEquals(self.group.main_workflow.eid, mwf.eid) + self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid) + self.group.fire_transition('tr1', u'go') + self.commit() + self.group.clear_all_caches() + self.assertEquals(self.group.current_state.eid, state2.eid) + self.assertEquals(self.group.current_workflow.eid, mwf.eid) + self.assertEquals(self.group.main_workflow.eid, mwf.eid) + self.assertEquals(self.group.subworkflow_input_transition(), None) + # force back to swfstate1 is impossible since we can't any more find + # subworkflow input transition + ex = self.assertRaises(ValidationError, + self.group.change_state, swfstate1, u'gadget') + self.assertEquals(ex.errors, {'to_state': "state doesn't belong to entity's current workflow"}) + self.rollback() + # force back to state1 + self.group.change_state('state1', u'gadget') + self.group.fire_transition('swftr1', u'au') + self.group.clear_all_caches() + self.group.fire_transition('tr2', u'chapeau') + self.commit() + self.group.clear_all_caches() + self.assertEquals(self.group.current_state.eid, state3.eid) + self.assertEquals(self.group.current_workflow.eid, mwf.eid) + self.assertEquals(self.group.main_workflow.eid, mwf.eid) + self.assertListEquals(parse_hist(self.group.workflow_history), + [('state1', 'swfstate1', 'swftr1', 'go'), + ('swfstate1', 'swfstate2', 'tr1', 'go'), + ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'), + ('state2', 'state1', None, 'gadget'), + ('state1', 'swfstate1', 'swftr1', 'au'), + ('swfstate1', 'swfstate3', 'tr2', 'chapeau'), + ('swfstate3', 'state3', 'swftr1', 'exiting from subworkflow subworkflow'), + ]) + + def test_subworkflow_exit_consistency(self): + # sub-workflow + swf = add_wf(self, 'CWGroup', name='subworkflow') + swfstate1 = swf.add_state(u'swfstate1', initial=True) + swfstate2 = swf.add_state(u'swfstate2') + tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2) + # main workflow + mwf = add_wf(self, 'CWGroup', name='main workflow', default=True) + state1 = mwf.add_state(u'state1', initial=True) + state2 = mwf.add_state(u'state2') + state3 = mwf.add_state(u'state3') + mwf.add_wftransition(u'swftr1', swf, state1, + [(swfstate2, state2), (swfstate2, state3)]) + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'subworkflow_exit': u"can't have multiple exits on the same state"}) + + +class CustomWorkflowTC(EnvBasedTC): + + def setup_database(self): + self.member = self.create_user('member') + + def tearDown(self): + super(CustomWorkflowTC, self).tearDown() + self.execute('DELETE X custom_workflow WF') + + def test_custom_wf_replace_state_no_history(self): + """member in inital state with no previous history, state is simply + redirected when changing workflow + """ + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.member.clear_all_caches() + self.assertEquals(self.member.state, 'activated')# no change before commit + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.eid, wf.eid) + self.assertEquals(self.member.state, 'asleep') + self.assertEquals(self.member.workflow_history, []) + + def test_custom_wf_replace_state_keep_history(self): + """member in inital state with some history, state is redirected and + state change is recorded to history + """ + self.member.fire_transition('deactivate') + self.member.fire_transition('activate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.eid, wf.eid) + self.assertEquals(self.member.state, 'asleep') + self.assertEquals(parse_hist(self.member.workflow_history), + [('activated', 'deactivated', 'deactivate', None), + ('deactivated', 'activated', 'activate', None), + ('activated', 'asleep', None, 'workflow changed to "CWUser"')]) + + def test_custom_wf_no_initial_state(self): + """try to set a custom workflow which has no initial state""" + self.member.fire_transition('deactivate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep') + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'custom_workflow': u'workflow has no initial state'}) + + def test_custom_wf_bad_etype(self): + """try to set a custom workflow which doesn't apply to entity type""" + wf = add_wf(self, 'Company') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'custom_workflow': 'constraint S is ET, O workflow_of ET failed'}) + + def test_del_custom_wf(self): + """member in some state shared by the new workflow, nothing has to be + done + """ + self.member.fire_transition('deactivate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.commit() + self.execute('DELETE X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.member.clear_all_caches() + self.assertEquals(self.member.state, 'asleep')# no change before commit + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.name, "CWUser workflow") + self.assertEquals(self.member.state, 'activated') + self.assertEquals(parse_hist(self.member.workflow_history), + [('activated', 'deactivated', 'deactivate', None), + ('deactivated', 'asleep', None, 'workflow changed to "CWUser"'), + ('asleep', 'activated', None, 'workflow changed to "CWUser workflow"'),]) + + +from cubicweb.devtools.apptest import RepositoryBasedTC + +class WorkflowHooksTC(RepositoryBasedTC): + + def setUp(self): + RepositoryBasedTC.setUp(self) + self.wf = self.session.user.current_workflow + self.s_activated = self.wf.state_by_name('activated').eid + self.s_deactivated = self.wf.state_by_name('deactivated').eid + self.s_dummy = self.wf.add_state(u'dummy').eid + self.wf.add_transition(u'dummy', (self.s_deactivated,), self.s_dummy) + ueid = self.create_user('stduser', commit=False) + # test initial state is set + rset = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', + {'x' : ueid}) + self.failIf(rset, rset.rows) + self.commit() + initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', + {'x' : ueid})[0][0] + self.assertEquals(initialstate, u'activated') + # give access to users group on the user's wf transitions + # so we can test wf enforcing on euser (managers don't have anymore this + # enforcement + self.execute('SET X require_group G ' + 'WHERE G name "users", X transition_of WF, WF eid %(wf)s', + {'wf': self.wf.eid}) + self.commit() + + def tearDown(self): + self.execute('DELETE X require_group G ' + 'WHERE G name "users", X transition_of WF, WF eid %(wf)s', + {'wf': self.wf.eid}) + self.commit() + RepositoryBasedTC.tearDown(self) + + # XXX currently, we've to rely on hooks to set initial state, or to use unsafe_execute + # def test_initial_state(self): + # cnx = self.login('stduser') + # cu = cnx.cursor() + # self.assertRaises(ValidationError, cu.execute, + # 'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' + # 'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'}) + # cnx.close() + # # though managers can do whatever he want + # self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' + # 'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'}) + # self.commit() + + # test that the workflow is correctly enforced + def test_transition_checking1(self): + cnx = self.login('stduser') + user = cnx.user(self.current_session()) + ex = self.assertRaises(ValidationError, + user.fire_transition, 'activate') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + cnx.close() + + def test_transition_checking2(self): + cnx = self.login('stduser') + user = cnx.user(self.current_session()) + assert user.state == 'activated' + ex = self.assertRaises(ValidationError, + user.fire_transition, 'dummy') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + cnx.close() + + def test_transition_checking3(self): + cnx = self.login('stduser') + session = self.current_session() + user = cnx.user(session) + user.fire_transition('deactivate') + cnx.commit() + session.set_pool() + ex = self.assertRaises(ValidationError, + user.fire_transition, 'deactivate') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + # get back now + user.fire_transition('activate') + cnx.commit() + cnx.close() + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r 99ab33abf414 -r 096d680c9da2 entities/wfobjs.py --- a/entities/wfobjs.py Wed Sep 02 10:20:12 2009 +0200 +++ b/entities/wfobjs.py Wed Sep 02 10:23:38 2009 +0200 @@ -7,23 +7,176 @@ """ __docformat__ = "restructuredtext en" +from warnings import warn + +from logilab.common.decorators import cached, clear_cache +from logilab.common.deprecation import deprecated + from cubicweb.entities import AnyEntity, fetch_config +from cubicweb.interfaces import IWorkflowable +from cubicweb.common.mixins import MI_REL_TRIGGERS + +class WorkflowException(Exception): pass + +class Workflow(AnyEntity): + id = 'Workflow' + + @property + def initial(self): + """return the initial state for this workflow""" + return self.initial_state and self.initial_state[0] or None + + def is_default_workflow_of(self, etype): + """return True if this workflow is the default workflow for the given + entity type + """ + return any(et for et in self.reverse_default_workflow + if et.name == etype) + + def after_deletion_path(self): + """return (path, parameters) which should be used as redirect + information when this entity is being deleted + """ + if self.workflow_of: + return self.workflow_of[0].rest_path(), {'vid': 'workflow'} + return super(Workflow, self).after_deletion_path() + + def iter_workflows(self, _done=None): + """return an iterator on actual workflows, eg this workflow and its + subworkflows + """ + # infinite loop safety belt + if _done is None: + _done = set() + yield self + _done.add(self.eid) + for tr in self.req.execute('Any T WHERE T is WorkflowTransition, ' + 'T transition_of WF, WF eid %(wf)s', + {'wf': self.eid}).entities(): + if tr.subwf.eid in _done: + continue + for subwf in tr.subwf.iter_workflows(_done): + yield subwf + + # state / transitions accessors ############################################ + + def state_by_name(self, statename): + rset = self.req.execute('Any S, SN WHERE S name SN, S name %(n)s, ' + 'S state_of WF, WF eid %(wf)s', + {'n': statename, 'wf': self.eid}, 'wf') + if rset: + return rset.get_entity(0, 0) + return None + + def state_by_eid(self, eid): + rset = self.req.execute('Any S, SN WHERE S name SN, S eid %(s)s, ' + 'S state_of WF, WF eid %(wf)s', + {'s': eid, 'wf': self.eid}, ('wf', 's')) + if rset: + return rset.get_entity(0, 0) + return None + + def transition_by_name(self, trname): + rset = self.req.execute('Any T, TN WHERE T name TN, T name %(n)s, ' + 'T transition_of WF, WF eid %(wf)s', + {'n': trname, 'wf': self.eid}, 'wf') + if rset: + return rset.get_entity(0, 0) + return None + + def transition_by_eid(self, eid): + rset = self.req.execute('Any T, TN WHERE T name TN, T eid %(t)s, ' + 'T transition_of WF, WF eid %(wf)s', + {'t': eid, 'wf': self.eid}, ('wf', 't')) + if rset: + return rset.get_entity(0, 0) + return None + + # wf construction methods ################################################## + + def add_state(self, name, initial=False, **kwargs): + """add a state to this workflow""" + state = self.req.create_entity('State', name=unicode(name), **kwargs) + self.req.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s', + {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) + if initial: + assert not self.initial + self.req.execute('SET WF initial_state S ' + 'WHERE S eid %(s)s, WF eid %(wf)s', + {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) + return state + + def _add_transition(self, trtype, name, fromstates, + requiredgroups=(), conditions=(), **kwargs): + tr = self.req.create_entity(trtype, name=unicode(name), **kwargs) + self.req.execute('SET T transition_of WF ' + 'WHERE T eid %(t)s, WF eid %(wf)s', + {'t': tr.eid, 'wf': self.eid}, ('t', 'wf')) + assert fromstates, fromstates + if not isinstance(fromstates, (tuple, list)): + fromstates = (fromstates,) + for state in fromstates: + if hasattr(state, 'eid'): + state = state.eid + self.req.execute('SET S allowed_transition T ' + 'WHERE S eid %(s)s, T eid %(t)s', + {'s': state, 't': tr.eid}, ('s', 't')) + tr.set_transition_permissions(requiredgroups, conditions, reset=False) + return tr + + def add_transition(self, name, fromstates, tostate, + requiredgroups=(), conditions=(), **kwargs): + """add a transition to this workflow from some state(s) to another""" + tr = self._add_transition('Transition', name, fromstates, + requiredgroups, conditions, **kwargs) + if hasattr(tostate, 'eid'): + tostate = tostate.eid + self.req.execute('SET T destination_state S ' + 'WHERE S eid %(s)s, T eid %(t)s', + {'t': tr.eid, 's': tostate}, ('s', 't')) + return tr + + def add_wftransition(self, name, subworkflow, fromstates, exitpoints, + requiredgroups=(), conditions=(), **kwargs): + """add a workflow transition to this workflow""" + tr = self._add_transition('WorkflowTransition', name, fromstates, + requiredgroups, conditions, **kwargs) + if hasattr(subworkflow, 'eid'): + subworkflow = subworkflow.eid + self.req.execute('SET T subworkflow WF WHERE WF eid %(wf)s,T eid %(t)s', + {'t': tr.eid, 'wf': subworkflow}, ('wf', 't')) + for fromstate, tostate in exitpoints: + tr.add_exit_point(fromstate, tostate) + return tr -class Transition(AnyEntity): - """customized class for Transition entities +class BaseTransition(AnyEntity): + """customized class for abstract transition - provides a specific may_be_passed method to check if the relation may be - passed by the logged user + provides a specific may_be_fired method to check if the relation may be + fired by the logged user """ - id = 'Transition' + id = 'BaseTransition' fetch_attrs, fetch_order = fetch_config(['name']) - def may_be_passed(self, eid, stateeid): - """return true if the logged user may pass this transition + def __init__(self, *args, **kwargs): + if self.id == 'BaseTransition': + raise WorkflowException('should not be instantiated') + super(BaseTransition, self).__init__(*args, **kwargs) + + @property + def workflow(self): + return self.transition_of[0] - `eid` is the eid of the object on which we may pass the transition - `stateeid` is the eid of the current object'state XXX unused + def has_input_state(self, state): + if hasattr(state, 'eid'): + state = state.eid + return any(s for s in self.reverse_allowed_transition if s.eid == state) + + def may_be_fired(self, eid): + """return true if the logged user may fire this transition + + `eid` is the eid of the object on which we may fire the transition """ user = self.req.user # check user is at least in one of the required groups if any @@ -43,47 +196,122 @@ return False return True - def destination(self): - return self.destination_state[0] - def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted """ if self.transition_of: - return self.transition_of[0].rest_path(), {'vid': 'workflow'} + return self.transition_of[0].rest_path(), {} return super(Transition, self).after_deletion_path() + def set_transition_permissions(self, requiredgroups=(), conditions=(), + reset=True): + """set or add (if `reset` is False) groups and conditions for this + transition + """ + if reset: + self.req.execute('DELETE T require_group G WHERE T eid %(x)s', + {'x': self.eid}, 'x') + self.req.execute('DELETE T condition R WHERE T eid %(x)s', + {'x': self.eid}, 'x') + for gname in requiredgroups: + rset = self.req.execute('SET T require_group G ' + 'WHERE T eid %(x)s, G name %(gn)s', + {'x': self.eid, 'gn': gname}, 'x') + assert rset, '%s is not a known group' % gname + if isinstance(conditions, basestring): + conditions = (conditions,) + for expr in conditions: + if isinstance(expr, str): + expr = unicode(expr) + self.req.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') + # XXX clear caches? + + +class Transition(BaseTransition): + """customized class for Transition entities""" + id = 'Transition' + + def destination(self): + return self.destination_state[0] + + +class WorkflowTransition(BaseTransition): + """customized class for WorkflowTransition entities""" + id = 'WorkflowTransition' + + @property + def subwf(self): + return self.subworkflow[0] + + def destination(self): + return self.subwf.initial + + def add_exit_point(self, fromstate, tostate): + if hasattr(fromstate, 'eid'): + fromstate = fromstate.eid + if hasattr(tostate, 'eid'): + tostate = tostate.eid + self.req.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, ' + 'X subworkflow_state FS, X destination_state TS ' + 'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s', + {'t': self.eid, 'fs': fromstate, 'ts': tostate}, + ('t', 'fs', 'ts')) + + def get_exit_point(self, state): + """if state is an exit point, return its associated destination state""" + if hasattr(state, 'eid'): + state = state.eid + stateeid = self.exit_points().get(state) + if stateeid is not None: + return self.req.entity_from_eid(stateeid) + return None + + @cached + def exit_points(self): + result = {} + for ep in self.subworkflow_exit: + result[ep.subwf_state.eid] = ep.destination.eid + return result + + def clear_all_caches(self): + super(WorkflowableMixIn, self).clear_all_caches() + clear_cache(self, 'exit_points') + + +class SubWorkflowExitPoint(AnyEntity): + """customized class for SubWorkflowExitPoint entities""" + id = 'SubWorkflowExitPoint' + + @property + def subwf_state(self): + return self.subworkflow_state[0] + + @property + def destination(self): + return self.destination_state[0] + class State(AnyEntity): - """customized class for State entities - - provides a specific transitions method returning transitions that may be - passed by the current user for the given entity - """ + """customized class for State entities""" id = 'State' fetch_attrs, fetch_order = fetch_config(['name']) rest_attr = 'eid' - def transitions(self, entity, desteid=None): - """generates transition that MAY be passed""" - rql = ('Any T,N,DS where S allowed_transition T, S eid %(x)s, ' - 'T name N, T destination_state DS, ' - 'T transition_of ET, ET name %(et)s') - if desteid is not None: - rql += ', DS eid %(ds)s' - rset = self.req.execute(rql, {'x': self.eid, 'et': str(entity.e_schema), - 'ds': desteid}, 'x') - for tr in rset.entities(): - if tr.may_be_passed(entity.eid, self.eid): - yield tr + @property + def workflow(self): + # take care, may be missing in multi-sources configuration + return self.state_of and self.state_of[0] def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted """ if self.state_of: - return self.state_of[0].rest_path(), {'vid': 'workflow'} + return self.state_of[0].rest_path(), {} return super(State, self).after_deletion_path() @@ -95,15 +323,20 @@ pclass=None) # don't want modification_date @property def for_entity(self): - return self.wf_info_for and self.wf_info_for[0] + return self.wf_info_for[0] + @property def previous_state(self): - return self.from_state and self.from_state[0] + return self.from_state[0] @property def new_state(self): return self.to_state[0] + @property + def transition(self): + return self.by_transition and self.by_transition[0] or None + def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted @@ -111,3 +344,176 @@ if self.for_entity: return self.for_entity.rest_path(), {} return 'view', {} + + +class WorkflowableMixIn(object): + """base mixin providing workflow helper methods for workflowable entities. + This mixin will be automatically set on class supporting the 'in_state' + relation (which implies supporting 'wf_info_for' as well) + """ + __implements__ = (IWorkflowable,) + + @property + def main_workflow(self): + """return current workflow applied to this entity""" + if self.custom_workflow: + return self.custom_workflow[0] + return self.cwetype_workflow() + + @property + def current_workflow(self): + """return current workflow applied to this entity""" + return self.current_state and self.current_state.workflow or self.main_workflow + + @property + def current_state(self): + """return current state entity""" + return self.in_state and self.in_state[0] or None + + @property + def state(self): + """return current state name""" + try: + return self.in_state[0].name + except IndexError: + self.warning('entity %s has no state', self) + return None + + @property + def printable_state(self): + """return current state name translated to context's language""" + state = self.current_state + if state: + return self.req._(state.name) + return u'' + + @property + def workflow_history(self): + """return the workflow history for this entity (eg ordered list of + TrInfo entities) + """ + return self.reverse_wf_info_for + + def latest_trinfo(self): + """return the latest transition information for this entity""" + return self.reverse_wf_info_for[-1] + + @cached + def cwetype_workflow(self): + """return the default workflow for entities of this type""" + # XXX CWEType method + wfrset = self.req.execute('Any WF WHERE X is ET, X eid %(x)s, ' + 'WF workflow_of ET', {'x': self.eid}, 'x') + if len(wfrset) == 1: + return wfrset.get_entity(0, 0) + if len(wfrset) > 1: + for wf in wfrset.entities(): + if wf.is_default_workflow_of(self.id): + return wf + self.warning("can't find default workflow for %s", self.id) + else: + self.warning("can't find any workflow for %s", self.id) + return None + + def possible_transitions(self): + """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 + rset = self.req.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, + 'wfeid': self.current_workflow.eid}, 'x') + for tr in rset.entities(): + if tr.may_be_fired(self.eid): + yield tr + + def _add_trinfo(self, comment, commentformat, treid=None, tseid=None): + kwargs = {} + if comment is not None: + kwargs['comment'] = comment + if commentformat is not None: + kwargs['comment_format'] = commentformat + args = [('wf_info_for', 'E')] + kwargs['E'] = self.eid + if treid is not None: + args.append( ('by_transition', 'T') ) + kwargs['T'] = treid + if tseid is not None: + args.append( ('to_state', 'S') ) + kwargs['S'] = tseid + return self.req.create_entity('TrInfo', *args, **kwargs) + + def fire_transition(self, trname, comment=None, commentformat=None): + """change the entity's state by firing transition of the given name in + entity's workflow + """ + assert self.current_workflow + tr = self.current_workflow.transition_by_name(trname) + assert tr is not None, 'not a %s transition: %s' % (self.id, trname) + return self._add_trinfo(comment, commentformat, tr.eid) + + def change_state(self, statename, comment=None, commentformat=None, tr=None): + """change the entity's state to the given state (name or entity) in + entity's workflow. This method should only by used by manager to fix an + entity's state when their is no matching transition, otherwise + fire_transition should be used. + """ + assert self.current_workflow + if hasattr(statename, 'eid'): + stateeid = statename.eid + else: + if not isinstance(statename, basestring): + warn('give a state name') + state = self.current_workflow.state_by_eid(statename) + else: + state = self.current_workflow.state_by_name(statename) + if state is None: + raise WorkflowException('not a %s state: %s' % (self.id, + statename)) + stateeid = state.eid + # XXX try to find matching transition? + return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid) + + def subworkflow_input_transition(self): + """return the transition which has went through the current sub-workflow + """ + if self.main_workflow.eid == self.current_workflow.eid: + return # doesn't make sense + subwfentries = [] + for trinfo in reversed(self.workflow_history): + if (trinfo.transition and + trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid): + # entering or leaving a subworkflow + if (subwfentries and + subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid): + # leave + del subwfentries[-1] + else: + # enter + subwfentries.append(trinfo) + if not subwfentries: + return None + return subwfentries[-1].transition + + def clear_all_caches(self): + super(WorkflowableMixIn, self).clear_all_caches() + clear_cache(self, 'cwetype_workflow') + + @deprecated('get transition from current workflow and use its may_be_fired method') + def can_pass_transition(self, trname): + """return the Transition instance if the current user can fire the + transition with the given name, else None + """ + tr = self.current_workflow and self.current_workflow.transition_by_name(trname) + if tr and tr.may_be_fired(self.eid): + return tr + + @property + @deprecated('use printable_state') + def displayable_state(self): + return self.req._(self.state) + +MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn diff -r 99ab33abf414 -r 096d680c9da2 entity.py --- a/entity.py Wed Sep 02 10:20:12 2009 +0200 +++ b/entity.py Wed Sep 02 10:23:38 2009 +0200 @@ -163,7 +163,7 @@ id = None rest_attr = None fetch_attrs = None - skip_copy_for = () + skip_copy_for = ('in_state',) # class attributes set automatically at registration time e_schema = None @@ -182,15 +182,15 @@ continue setattr(cls, rschema.type, Attribute(rschema.type)) mixins = [] - for rschema, _, x in eschema.relation_definitions(): - if (rschema, x) in MI_REL_TRIGGERS: - mixin = MI_REL_TRIGGERS[(rschema, x)] + for rschema, _, role in eschema.relation_definitions(): + if (rschema, role) in MI_REL_TRIGGERS: + mixin = MI_REL_TRIGGERS[(rschema, role)] if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ? mixins.append(mixin) for iface in getattr(mixin, '__implements__', ()): if not interface.implements(cls, iface): interface.extend(cls, iface) - if x == 'subject': + if role == 'subject': setattr(cls, rschema.type, SubjectRelation(rschema)) else: attr = 'reverse_%s' % rschema.type @@ -488,13 +488,6 @@ continue if rschema.type in self.skip_copy_for: continue - if rschema.type == 'in_state': - # if the workflow is defining an initial state (XXX AND we are - # not in the managers group? not done to be more consistent) - # don't try to copy in_state - if execute('Any S WHERE S state_of ET, ET initial_state S,' - 'ET name %(etype)s', {'etype': str(self.e_schema)}): - continue # skip composite relation if self.e_schema.subjrproperty(rschema, 'composite'): continue @@ -625,14 +618,14 @@ self[str(selected[i-1][0])] = rset[i] # handle relations for i in xrange(lastattr, len(rset)): - rtype, x = selected[i-1][0] + rtype, role = selected[i-1][0] value = rset[i] if value is None: rrset = ResultSet([], rql, {'x': self.eid}) self.req.decorate_rset(rrset) else: rrset = self.req.eid_rset(value) - self.set_related_cache(rtype, x, rrset) + self.set_related_cache(rtype, role, rrset) def get_value(self, name): """get value for the attribute relation , query the repository @@ -835,6 +828,11 @@ assert role self._related_cache.pop('%s_%s' % (rtype, role), None) + def clear_all_caches(self): + self.clear() + for rschema, _, role in self.e_schema.relation_definitions(): + self.clear_related_cache(rschema.type, role) + # raw edition utilities ################################################### def set_attributes(self, _cw_unsafe=False, **kwargs): diff -r 99ab33abf414 -r 096d680c9da2 ext/xhtml2fo.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ext/xhtml2fo.py Wed Sep 02 10:23:38 2009 +0200 @@ -0,0 +1,142 @@ +from cubicweb.utils import can_do_pdf_conversion +assert can_do_pdf_conversion() + +from xml.etree.ElementTree import QName, fromstring +from pysixt.standard.xhtml_xslfo.transformer import XHTML2FOTransformer +from pysixt.utils.xslfo.standard import cm +from pysixt.utils.xslfo import SimplePageMaster +from pysixt.standard.xhtml_xslfo.default_styling import default_styles +from pysixt.standard.xhtml_xslfo import XHTML_NS + + +class ReportTransformer(XHTML2FOTransformer): + """ + Class transforming an XHTML input tree into a FO document + displaying reports (one report for each
+ element in the input tree. + """ + + def __init__(self, section, + page_width=21.0, page_height=29.7, + margin_top=1.0, margin_bottom=1.0, + margin_left=1.0, margin_right=1.0, + header_footer_height=0.75, + standard_font_size=11.0, default_lang=u"fr" ): + """ + Initializes a transformer turning an XHTML input tree + containing
elements representing + main content sections into a FO output tree displaying the + reports. + + page_width: float - width of the page (in cm) + page_height: float - height of the page (in cm) + margin_top: float - top margin of the page (in cm) + margin_bottom: float - bottom margin of the page (in cm) + margin_left: float - left margin of the page (in cm) + margin_right: float - right margin of the page (in cm) + header_footer_height: float - height of the header or the footer of the + page that the page number (if any) will be + inserted in. + standard_font_size: float - standard size of the font (in pt) + default_lang: u"" - default language (used for hyphenation) + """ + self.section = section + self.page_width = page_width + self.page_height = page_height + + self.page_tmargin = margin_top + self.page_bmargin = margin_bottom + self.page_lmargin = margin_left + self.page_rmargin = margin_right + + self.hf_height = header_footer_height + + self.font_size = standard_font_size + self.lang = default_lang + + XHTML2FOTransformer.__init__(self) + + + def define_pagemasters(self): + """ + Defines the page masters for the FO output document. + """ + pm = SimplePageMaster(u"page-report") + pm.set_page_dims( self.page_width*cm, self.page_height*cm ) + pm.set_page_margins({u'top' : self.page_tmargin*cm, + u'bottom': self.page_bmargin*cm, + u'left' : self.page_lmargin*cm, + u'right' : self.page_rmargin*cm }) + pm.add_peripheral_region(u"end",self.hf_height) + dims = {} + dims[u"bottom"] = self.hf_height + 0.25 + pm.set_main_region_margins(dims) + return [pm] + + def _visit_report(self, in_elt, _out_elt, params): + """ + Specific visit function for the input
elements whose class is + "report". The _root_visit method of this class selects these input + elements and asks the process of these elements with this specific + visit function. + """ + + ps = self.create_pagesequence(u"page-report") + props = { u"force-page-count": u"no-force", + u"initial-page-number": u"1", + u"format": u"1", } + self._output_properties(ps,props) + + sc = self.create_staticcontent(ps, u"end") + sc_bl = self.create_block(sc) + attrs = { u"hyphenate": u"false", } + attrs[u"font-size"] = u"%.1fpt" %(self.font_size*0.7) + attrs[u"language"] = self.lang + attrs[u"text-align"] = u"center" + self._output_properties(sc_bl,attrs) + sc_bl.text = u"Page" + u" " # ### Should be localised! + pn = self.create_pagenumber(sc_bl) + pn.tail = u"/" + lpn = self.create_pagenumbercitation( sc_bl, + u"last-block-of-report-%d" % params[u"context_pos"] + ) + + + fl = self.create_flow(ps,u"body") + bl = self.create_block(fl) + + # Sets on the highest block element the properties of the XHTML body + # element. These properties (at the least the inheritable ones) will + # be inherited by all the future FO elements. + bodies = list(self.in_tree.getiterator(QName(XHTML_NS,u"body"))) + if len(bodies) > 0: + attrs = self._extract_properties([bodies[0]]) + else: + attrs = default_styles[u"body"].copy() + attrs[u"font-size"] = u"%.1fpt" %self.font_size + attrs[u"language"] = self.lang + self._output_properties(bl,attrs) + + # Processes the report content + self._copy_text(in_elt,bl) + self._process_nodes(in_elt.getchildren(),bl) + + # Inserts an empty block at the end of the report in order to be able + # to compute the last page number of this report. + last_bl = self.create_block(bl) + props = { u"keep-with-previous": u"always", } + props[u"id"] = u"last-block-of-report-%d" % params[u"context_pos"] + self._output_properties(last_bl,props) + + + def _root_visit(self): + """ + Visit function called when starting the process of the input tree. + """ + content = [ d for d in self.in_tree.getiterator(QName(XHTML_NS,u"div")) + if d.get(u"id") == self.section ] + # Asks the process of the report elements with a specific visit + # function + self._process_nodes(content, self.fo_root, + with_function=self._visit_report) + diff -r 99ab33abf414 -r 096d680c9da2 interfaces.py --- a/interfaces.py Wed Sep 02 10:20:12 2009 +0200 +++ b/interfaces.py Wed Sep 02 10:23:38 2009 +0200 @@ -37,25 +37,22 @@ class IWorkflowable(Interface): """interface for entities dealing with a specific workflow""" + # XXX to be completed, see cw.entities.wfobjs.WorkflowableMixIn @property def state(self): - """return current state""" + """return current state name""" def change_state(self, stateeid, trcomment=None, trcommentformat=None): - """change the entity's state according to a state defined in given - parameters - """ - - def can_pass_transition(self, trname): - """return true if the current user can pass the transition with the - given name + """change the entity's state to the state of the given name in entity's + workflow """ def latest_trinfo(self): """return the latest transition information for this entity """ + class IProgress(Interface): """something that has a cost, a state and a progression diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.42.0_Any.py --- a/misc/migration/2.42.0_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -synchronize_rschema('created_by') -synchronize_rschema('owned_by') diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.42.1_Any.py --- a/misc/migration/2.42.1_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -if confirm('remove deprecated database constraints?'): - execute = session.system_sql - session.set_pool() - dbhelper = session.pool.source('system').dbhelper - cu = session.pool['system'] - for table in dbhelper.list_tables(cu): - if table.endswith('_relation'): - try: - execute('ALTER TABLE %s DROP CONSTRAINT %s_fkey1' % (table, table)) - execute('ALTER TABLE %s DROP CONSTRAINT %s_fkey2' % (table, table)) - except: - continue - checkpoint() - -if 'inline_view' in schema: - # inline_view attribute should have been deleted for a while now.... - drop_attribute('CWRelation', 'inline_view') - diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.43.0_Any.py --- a/misc/migration/2.43.0_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -synchronize_permissions('EmailAddress') diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.44.0_Any.py --- a/misc/migration/2.44.0_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -change_relation_props('CWAttribute', 'cardinality', 'String', internationalizable=True) -change_relation_props('CWRelation', 'cardinality', 'String', internationalizable=True) - -drop_relation_definition('CWPermission', 'require_state', 'State') - -if confirm('cleanup require_permission relation'): - try: - newrschema = fsschema.rschema('require_permission') - except KeyError: - newrschema = None - for rsubj, robj in schema.rschema('require_permission').rdefs(): - if newrschema is None or not newrschema.has_rdef(rsubj, robj): - print 'removing', rsubj, 'require_permission', robj - drop_relation_definition(rsubj, 'require_permission', robj, ask_confirm=False) diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.45.0_Any.py --- a/misc/migration/2.45.0_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -# following functions have been renamed, but keep old definition for bw compat -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -sql('''CREATE AGGREGATE group_concat ( - basetype = anyelement, - sfunc = array_append, - stype = anyarray, - finalfunc = comma_join, - initcond = '{}' -)''') - -sql('''CREATE FUNCTION text_limit_size (fulltext text, maxsize integer) RETURNS text AS $$ -BEGIN - RETURN limit_size(fulltext, 'text/plain', maxsize); -END -$$ LANGUAGE plpgsql; -''') - - -synchronize_rschema('bookmarked_by') diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.46.0_Any.py --- a/misc/migration/2.46.0_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" - - -rql('SET X value "navtop" WHERE X pkey ~= "contentnavigation.%.context", X value "header"') -rql('SET X value "navcontenttop" WHERE X pkey ~= "contentnavigation%.context", X value "incontext"') -rql('SET X value "navcontentbottom" WHERE X pkey ~= "contentnavigation%.context", X value "footer"') -checkpoint() - -if 'require_permission' in schema: - synchronize_rschema('require_permission') diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.47.0_Any.py --- a/misc/migration/2.47.0_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -synchronize_permissions('primary_email') -synchronize_rschema('wf_info_for') -synchronize_rschema('use_email') - diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.48.8_Any.py --- a/misc/migration/2.48.8_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -for etype in ('CWRType', 'CWAttribute', 'CWRelation', 'CWConstraint', 'CWConstraintType'): - synchronize_permissions(etype) diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.49.3_Any.py --- a/misc/migration/2.49.3_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -add_entity_type('Decimal') diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/2.50.0_Any.py --- a/misc/migration/2.50.0_Any.py Wed Sep 02 10:20:12 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -add_relation_type('specializes') diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Wed Sep 02 10:20:12 2009 +0200 +++ b/misc/migration/bootstrapmigration_repository.py Wed Sep 02 10:23:38 2009 +0200 @@ -30,6 +30,37 @@ repo.hm.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '') session.set_shared_data('do-not-insert-cwuri', False) +if applcubicwebversion < (3, 5, 0) and cubicwebversion >= (3, 5, 0): + add_entity_type('Workflow') + add_entity_type('BaseTransition') + add_entity_type('WorkflowTransition') + add_entity_type('SubWorkflowExitPoint') + drop_relation_definition('State', 'allowed_transition', 'Transition') # should be infered + schema.rebuild_infered_relations() # need to be explicitly called once everything is in place + + for et in rql('DISTINCT Any ET,ETN WHERE S state_of ET, ET name ETN', + ask_confirm=False).entities(): + wf = add_workflow(u'default %s workflow' % et.name, et.name, + ask_confirm=False) + rql('SET S state_of WF WHERE S state_of ET, ET eid %(et)s, WF eid %(wf)s', + {'et': et.eid, 'wf': wf.eid}, 'et', ask_confirm=False) + rql('SET T transition_of WF WHERE T transition_of ET, ET eid %(et)s, WF eid %(wf)s', + {'et': et.eid, 'wf': wf.eid}, 'et', ask_confirm=False) + rql('SET WF initial_state S WHERE ET initial_state S, S state_of ET, ET eid %(et)s, WF eid %(wf)s', + {'et': et.eid, 'wf': wf.eid}, 'et', ask_confirm=False) + + + rql('DELETE TrInfo TI WHERE NOT TI from_state S') + rql('SET TI by_transition T WHERE TI from_state FS, TI to_state TS, ' + 'FS allowed_transition T, T destination_state TS') + checkpoint() + + drop_relation_definition('State', 'state_of', 'CWEType') + drop_relation_definition('Transition', 'transition_of', 'CWEType') + drop_relation_definition('CWEType', 'initial_state', 'State') + + sync_schema_props_perms() + if applcubicwebversion < (3, 2, 2) and cubicwebversion >= (3, 2, 1): from base64 import b64encode for table in ('entities', 'deleted_entities'): @@ -41,37 +72,3 @@ if applcubicwebversion < (3, 2, 0) and cubicwebversion >= (3, 2, 0): add_cube('card', update_database=False) - -if applcubicwebversion < (2, 47, 0) and cubicwebversion >= (2, 47, 0): - from cubicweb.server import schemaserial - schemaserial.HAS_FULLTEXT_CONTAINER = False - session.set_shared_data('do-not-insert-is_instance_of', True) - add_attribute('CWRType', 'fulltext_container') - schemaserial.HAS_FULLTEXT_CONTAINER = True - - - -if applcubicwebversion < (2, 50, 0) and cubicwebversion >= (2, 50, 0): - session.set_shared_data('do-not-insert-is_instance_of', True) - add_relation_type('is_instance_of') - # fill the relation using an efficient sql query instead of using rql - sql('INSERT INTO is_instance_of_relation ' - ' SELECT * from is_relation') - checkpoint() - session.set_shared_data('do-not-insert-is_instance_of', False) - -if applcubicwebversion < (2, 42, 0) and cubicwebversion >= (2, 42, 0): - sql('ALTER TABLE entities ADD COLUMN mtime TIMESTAMP') - sql('UPDATE entities SET mtime=CURRENT_TIMESTAMP') - sql('CREATE INDEX entities_mtime_idx ON entities(mtime)') - sql('''CREATE TABLE deleted_entities ( - eid INTEGER PRIMARY KEY NOT NULL, - type VARCHAR(64) NOT NULL, - source VARCHAR(64) NOT NULL, - dtime TIMESTAMP NOT NULL, - extid VARCHAR(256) -)''') - sql('CREATE INDEX deleted_entities_type_idx ON deleted_entities(type)') - sql('CREATE INDEX deleted_entities_dtime_idx ON deleted_entities(dtime)') - sql('CREATE INDEX deleted_entities_extid_idx ON deleted_entities(extid)') - checkpoint() diff -r 99ab33abf414 -r 096d680c9da2 misc/migration/postcreate.py --- a/misc/migration/postcreate.py Wed Sep 02 10:20:12 2009 +0200 +++ b/misc/migration/postcreate.py Wed Sep 02 10:23:38 2009 +0200 @@ -6,41 +6,59 @@ :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ -activatedeid = add_state(_('activated'), 'CWUser', initial=True) -deactivatedeid = add_state(_('deactivated'), 'CWUser') -add_transition(_('deactivate'), 'CWUser', - (activatedeid,), deactivatedeid, - requiredgroups=('managers',)) -add_transition(_('activate'), 'CWUser', - (deactivatedeid,), activatedeid, - requiredgroups=('managers',)) +# insert versions +create_entity('CWProperty', pkey=u'system.version.cubicweb', + value=unicode(config.cubicweb_version())) +for cube in config.cubes(): + create_entity('CWProperty', pkey=u'system.version.%s' % cube.lower(), + value=unicode(config.cube_version(cube))) -# need this since we already have at least one user in the database (the default admin) -rql('SET X in_state S WHERE X is CWUser, S eid %s' % activatedeid) +# some entities have been added before schema entities, fix the 'is' and +# 'is_instance_of' relations +for rtype in ('is', 'is_instance_of'): + sql('INSERT INTO %s_relation ' + 'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET ' + 'WHERE X.type=ET.cw_name AND NOT EXISTS(' + ' SELECT 1 from is_relation ' + ' WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype) + +# user workflow +userwf = add_workflow(_('default user workflow'), 'CWUser') +activated = userwf.add_state(_('activated'), initial=True) +deactivated = userwf.add_state(_('deactivated')) +userwf.add_transition(_('deactivate'), (activated,), deactivated, + requiredgroups=('managers',)) +userwf.add_transition(_('activate'), (deactivated,), activated, + requiredgroups=('managers',)) # create anonymous user if all-in-one config and anonymous user has been specified if hasattr(config, 'anonymous_user'): anonlogin, anonpwd = config.anonymous_user() if anonlogin: rql('INSERT CWUser X: X login %(login)s, X upassword %(pwd)s,' - 'X in_state S, X in_group G WHERE G name "guests", S name "activated"', + 'X in_group G WHERE G name "guests"', {'login': unicode(anonlogin), 'pwd': anonpwd}) -cfg = config.persistent_options_configuration() -if interactive_mode: - cfg.input_config(inputlevel=0) +# need this since we already have at least one user in the database (the default admin) +for user in rql('Any X WHERE X is CWUser').entities(): + session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', + {'x': user.eid, 's': activated.eid}, 'x') -for section, options in cfg.options_by_section(): - for optname, optdict, value in options: - key = '%s.%s' % (section, optname) - default = cfg.option_default(optname, optdict) - # only record values differing from default - if value != default: - rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value}) +# on interactive mode, ask for level 0 persistent options +if interactive_mode: + cfg = config.persistent_options_configuration() + cfg.input_config(inputlevel=0) + for section, options in cfg.options_by_section(): + for optname, optdict, value in options: + key = '%s.%s' % (section, optname) + default = cfg.option_default(optname, optdict) + # only record values differing from default + if value != default: + rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value}) # add PERM_USE_TEMPLATE_FORMAT permission from cubicweb.schema import PERM_USE_TEMPLATE_FORMAT -eid = add_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT, - label=_('use template languages')) +usetmplperm = create_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT, + label=_('use template languages')) rql('SET X require_group G WHERE G name "managers", X eid %(x)s', - {'x': eid}, 'x') + {'x': usetmplperm.eid}, 'x') diff -r 99ab33abf414 -r 096d680c9da2 schema.py --- a/schema.py Wed Sep 02 10:20:12 2009 +0200 +++ b/schema.py Wed Sep 02 10:23:38 2009 +0200 @@ -847,23 +847,30 @@ This is the default metaclass for WorkflowableEntityType """ def __new__(mcs, name, bases, classdict): - abstract = classdict.pop('abstract', False) - defclass = super(workflowable_definition, mcs).__new__(mcs, name, bases, classdict) + abstract = classdict.pop('__abstract__', False) + cls = super(workflowable_definition, mcs).__new__(mcs, name, bases, + classdict) if not abstract: - existing_rels = set(rdef.name for rdef in defclass.__relations__) - if 'in_state' not in existing_rels and 'wf_info_for' not in existing_rels: - in_state = ybo.SubjectRelation('State', cardinality='1*', - # XXX automatize this - constraints=[RQLConstraint('S is ET, O state_of ET')], - description=_('account state')) - yams_add_relation(defclass.__relations__, in_state, 'in_state') - wf_info_for = ybo.ObjectRelation('TrInfo', cardinality='1*', composite='object') - yams_add_relation(defclass.__relations__, wf_info_for, 'wf_info_for') - return defclass + make_workflowable(cls) + return cls + +def make_workflowable(cls, in_state_descr=None): + existing_rels = set(rdef.name for rdef in cls.__relations__) + # let relation types defined in cw.schemas.workflow carrying + # cardinality, constraints and other relation definition properties + if 'custom_workflow' not in existing_rels: + rdef = ybo.SubjectRelation('Workflow') + yams_add_relation(cls.__relations__, rdef, 'custom_workflow') + if 'in_state' not in existing_rels: + rdef = ybo.SubjectRelation('State', description=in_state_descr) + yams_add_relation(cls.__relations__, rdef, 'in_state') + if 'wf_info_for' not in existing_rels: + rdef = ybo.ObjectRelation('TrInfo') + yams_add_relation(cls.__relations__, rdef, 'wf_info_for') class WorkflowableEntityType(ybo.EntityType): __metaclass__ = workflowable_definition - abstract = True + __abstract__ = True PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType diff -r 99ab33abf414 -r 096d680c9da2 schemas/workflow.py --- a/schemas/workflow.py Wed Sep 02 10:20:12 2009 +0200 +++ b/schemas/workflow.py Wed Sep 02 10:23:38 2009 +0200 @@ -10,8 +10,37 @@ from yams.buildobjs import (EntityType, RelationType, SubjectRelation, ObjectRelation, RichString, String) -from cubicweb.schema import RQLConstraint -from cubicweb.schemas import META_ETYPE_PERMS, META_RTYPE_PERMS, HOOKS_RTYPE_PERMS +from cubicweb.schema import RQLConstraint, RQLUniqueConstraint +from cubicweb.schemas import (META_ETYPE_PERMS, META_RTYPE_PERMS, + HOOKS_RTYPE_PERMS) + +class Workflow(EntityType): + permissions = META_ETYPE_PERMS + + name = String(required=True, indexed=True, internationalizable=True, + maxsize=256) + description = RichString(fulltextindexed=True, default_format='text/rest', + description=_('semantic description of this workflow')) + + workflow_of = SubjectRelation('CWEType', cardinality='+*', + description=_('entity types which may use this workflow'), + constraints=[RQLConstraint('O final FALSE')]) + + initial_state = SubjectRelation('State', cardinality='?*', + # S initial_state O, O state_of S + constraints=[RQLConstraint('O state_of S')], + description=_('initial state for this workflow')) + + +class default_workflow(RelationType): + """default workflow for this entity types""" + permissions = META_RTYPE_PERMS + + subject = 'CWEType' + object = 'Workflow' + cardinality = '?*' + constraints = [RQLConstraint('S final FALSE, O workflow_of S')] + class State(EntityType): """used to associate simple states to an entity type and/or to define @@ -24,23 +53,18 @@ description = RichString(fulltextindexed=True, default_format='text/rest', description=_('semantic description of this state')) - state_of = SubjectRelation('CWEType', cardinality='+*', - description=_('entity types which may use this state'), - constraints=[RQLConstraint('O final FALSE')]) - allowed_transition = SubjectRelation('Transition', cardinality='**', - constraints=[RQLConstraint('S state_of ET, O transition_of ET')], + # XXX should be on BaseTransition w/ AND/OR selectors when we will + # implements #345274 + allowed_transition = SubjectRelation('BaseTransition', cardinality='**', + constraints=[RQLConstraint('S state_of WF, O transition_of WF')], description=_('allowed transitions from this state')) - - initial_state = ObjectRelation('CWEType', cardinality='?*', - # S initial_state O, O state_of S - constraints=[RQLConstraint('O state_of S')], - description=_('initial state for entities of this type')) + state_of = SubjectRelation('Workflow', cardinality='1*', + description=_('workflow to which this state belongs'), + constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N')]) -class Transition(EntityType): - """use to define a transition from one or multiple states to a destination - states in workflow's definitions. - """ +class BaseTransition(EntityType): + """abstract base class for transitions""" permissions = META_ETYPE_PERMS name = String(required=True, indexed=True, internationalizable=True, @@ -57,47 +81,108 @@ require_group = SubjectRelation('CWGroup', cardinality='**', description=_('group in which a user should be to be ' 'allowed to pass this transition')) - transition_of = SubjectRelation('CWEType', cardinality='+*', - description=_('entity types which may use this transition'), - constraints=[RQLConstraint('O final FALSE')]) + transition_of = SubjectRelation('Workflow', cardinality='1*', + description=_('workflow to which this transition belongs'), + constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N')]) + + +class Transition(BaseTransition): + """use to define a transition from one or multiple states to a destination + states in workflow's definitions. + """ + __specializes_schema__ = True + destination_state = SubjectRelation('State', cardinality='1*', - constraints=[RQLConstraint('S transition_of ET, O state_of ET')], + constraints=[RQLConstraint('S transition_of WF, O state_of WF')], description=_('destination state for this transition')) -class TrInfo(EntityType): - permissions = META_ETYPE_PERMS +class WorkflowTransition(BaseTransition): + """special transition allowing to go through a sub-workflow""" + __specializes_schema__ = True + + subworkflow = SubjectRelation('Workflow', cardinality='1*', + constraints=[RQLConstraint('S transition_of WF, WF workflow_of ET, O workflow_of ET')]) + subworkflow_exit = SubjectRelation('SubWorkflowExitPoint', cardinality='+1', + composite='subject') + + +class SubWorkflowExitPoint(EntityType): + """define how we get out from a sub-workflow""" + subworkflow_state = SubjectRelation('State', cardinality='1*', + constraints=[RQLConstraint('T subworkflow_exit S, T subworkflow WF, O state_of WF')], + description=_('subworkflow state')) + destination_state = SubjectRelation('State', cardinality='1*', + constraints=[RQLConstraint('T subworkflow_exit S, T transition_of WF, O state_of WF')], + description=_('destination state')) - from_state = SubjectRelation('State', cardinality='?*') + +# 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': (), + 'update': ('managers', 'owners',), + } + + from_state = SubjectRelation('State', cardinality='1*') to_state = SubjectRelation('State', cardinality='1*') + # make by_transition optional because we want to allow managers to set + # entity into an arbitrary state without having to respect wf transition + by_transition = SubjectRelation('BaseTransition', cardinality='?*') comment = RichString(fulltextindexed=True) # get actor and date time using owned_by and creation_date +class from_state(RelationType): + permissions = HOOKS_RTYPE_PERMS.copy() + inlined = True -class from_state(RelationType): - permissions = HOOKS_RTYPE_PERMS +class to_state(RelationType): + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers',), + 'delete': (), + } inlined = True -class to_state(RelationType): - permissions = HOOKS_RTYPE_PERMS + +class by_transition(RelationType): + # 'add' security actually done by hooks + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers', 'users', 'guests',), + 'delete': (), + } inlined = True -class wf_info_for(RelationType): - """link a transition information to its object""" - permissions = { - 'read': ('managers', 'users', 'guests',),# RRQLExpression('U has_read_permission O')), - 'add': (), # handled automatically, no one should add one explicitly - 'delete': ('managers',), # RRQLExpression('U has_delete_permission O') - } - inlined = True - composite = 'object' - fulltext_container = composite +class workflow_of(RelationType): + """link a workflow to one or more entity type""" + permissions = META_RTYPE_PERMS class state_of(RelationType): - """link a state to one or more entity type""" + """link a state to one or more workflow""" permissions = META_RTYPE_PERMS + class transition_of(RelationType): - """link a transition to one or more entity type""" + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + +class subworkflow(RelationType): + """link a transition to one or more workflow""" permissions = META_RTYPE_PERMS + inlined = True + +class exit_point(RelationType): + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + +class subworkflow_state(RelationType): + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + inlined = True class initial_state(RelationType): """indicate which state should be used by default when an entity using @@ -115,16 +200,42 @@ """allowed transition from this state""" permissions = META_RTYPE_PERMS + +# "abstract" relations, set by WorkflowableEntityType ########################## + +class custom_workflow(RelationType): + """allow to set a specific workflow for an entity""" + permissions = META_RTYPE_PERMS + + cardinality = '?*' + constraints = [RQLConstraint('S is ET, O workflow_of ET')] + object = 'Workflow' + + +class wf_info_for(RelationType): + """link a transition information to its object""" + # 'add' security actually done by hooks + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers', 'users', 'guests',), + 'delete': (), + } + inlined = True + + cardinality='1*' + composite = 'object' + fulltext_container = composite + subject = 'TrInfo' + + class in_state(RelationType): """indicate the current state of an entity""" + permissions = HOOKS_RTYPE_PERMS + # not inlined intentionnaly since when using ldap sources, user'state # has to be stored outside the CWUser table inlined = False - # add/delete perms given to managers/users, after what most of the job - # is done by workflow enforcment - permissions = { - 'read': ('managers', 'users', 'guests',), - 'add': ('managers', 'users',), # XXX has_update_perm - 'delete': ('managers', 'users',), - } + cardinality = '1*' + constraints = [RQLConstraint('S is ET, O state_of WF, WF workflow_of ET')] + object = 'State' diff -r 99ab33abf414 -r 096d680c9da2 selectors.py diff -r 99ab33abf414 -r 096d680c9da2 server/__init__.py --- a/server/__init__.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/__init__.py Wed Sep 02 10:23:38 2009 +0200 @@ -180,22 +180,6 @@ handler = config.migration_handler(schema, interactive=False, cnx=cnx, repo=repo) initialize_schema(config, schema, handler) - # insert versions - handler.cmd_add_entity('CWProperty', pkey=u'system.version.cubicweb', - value=unicode(config.cubicweb_version())) - for cube in config.cubes(): - handler.cmd_add_entity('CWProperty', - pkey=u'system.version.%s' % cube.lower(), - value=unicode(config.cube_version(cube))) - # some entities have been added before schema entities, fix the 'is' and - # 'is_instance_of' relations - for rtype in ('is', 'is_instance_of'): - handler.sqlexec( - 'INSERT INTO %s_relation ' - 'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET ' - 'WHERE X.type=ET.cw_name AND NOT EXISTS(' - ' SELECT 1 from is_relation ' - ' WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype) # yoo ! cnx.commit() config.enabled_sources = None diff -r 99ab33abf414 -r 096d680c9da2 server/hooks.py --- a/server/hooks.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/hooks.py Wed Sep 02 10:23:38 2009 +0200 @@ -13,8 +13,8 @@ from cubicweb import UnknownProperty, ValidationError, BadConnectionId from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation -from cubicweb.server.hookhelper import (check_internal_entity, previous_state, - get_user_sessions, rproperty) +from cubicweb.server.hookhelper import (check_internal_entity, + get_user_sessions, rproperty) from cubicweb.server.repository import FTIndexEntityOp # special relations that don't have to be checked for integrity, usually @@ -37,7 +37,8 @@ # from the database (eg during tests) if eschema.eid is None: eschema.eid = session.unsafe_execute( - 'Any X WHERE X is CWEType, X name %(name)s', {'name': str(etype)})[0][0] + 'Any X WHERE X is CWEType, X name %(name)s', + {'name': str(etype)})[0][0] return eschema.eid @@ -417,51 +418,129 @@ # workflow handling ########################################################### -def before_add_in_state(session, fromeid, rtype, toeid): - """check the transition is allowed and record transition information +from cubicweb.entities.wfobjs import WorkflowTransition, WorkflowException + +def _change_state(session, x, oldstate, newstate): + nocheck = session.transaction_data.setdefault('skip-security', set()) + nocheck.add((x, 'in_state', oldstate)) + nocheck.add((x, 'in_state', newstate)) + # delete previous state first in case we're using a super session + fromsource = session.describe(x)[1] + # don't try to remove previous state if in_state isn't stored in the system + # source + if fromsource == 'system' or \ + not session.repo.sources_by_uri[fromsource].support_relation('in_state'): + session.delete_relation(x, 'in_state', oldstate) + session.add_relation(x, 'in_state', newstate) + + +def before_add_trinfo(session, entity): + """check the transition is allowed, add missing information. Expect that: + * wf_info_for inlined relation is set + * by_transition or to_state (managers only) inlined relation is set """ - assert rtype == 'in_state' - state = previous_state(session, fromeid) - etype = session.describe(fromeid)[0] - if not (session.is_super_session or 'managers' in session.user.groups): - if not state is None: - entity = session.entity_from_eid(fromeid) - # we should find at least one transition going to this state - try: - iter(state.transitions(entity, toeid)).next() - except StopIteration: - _ = session._ - msg = _('transition from %s to %s does not exist or is not allowed') % ( - _(state.name), _(session.entity_from_eid(toeid).name)) - raise ValidationError(fromeid, {'in_state': msg}) + # first retreive entity to which the state change apply + try: + foreid = entity['wf_info_for'] + except KeyError: + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'wf_info_for': msg}) + forentity = session.entity_from_eid(foreid) + # then check it has a workflow set, unless we're in the process of changing + # entity's workflow + if session.transaction_data.get((forentity.eid, 'customwf')): + wfeid = session.transaction_data[(forentity.eid, 'customwf')] + wf = session.entity_from_eid(wfeid) + else: + wf = forentity.current_workflow + if wf is None: + msg = session._('related entity has no workflow set') + raise ValidationError(entity.eid, {None: msg}) + # then check it has a state set + fromstate = forentity.current_state + if fromstate is None: + msg = session._('related entity has no state') + raise ValidationError(entity.eid, {None: msg}) + # True if we are coming back from subworkflow + swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) + cowpowers = session.is_super_session or 'managers' in session.user.groups + # no investigate the requested state change... + try: + treid = entity['by_transition'] + except KeyError: + # no transition set, check user is a manager and destination state is + # specified (and valid) + if not cowpowers: + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'by_transition': msg}) + deststateeid = entity.get('to_state') + if not deststateeid: + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'by_transition': msg}) + deststate = wf.state_by_eid(deststateeid) + if not cowpowers and deststate is None: + msg = entity.req._("state doesn't belong to entity's workflow") + raise ValidationError(entity.eid, {'to_state': msg}) + else: + # check transition is valid and allowed, unless we're coming back from + # subworkflow + tr = session.entity_from_eid(treid) + if swtr is None: + if tr is None: + msg = session._("transition doesn't belong to entity's workflow") + raise ValidationError(entity.eid, {'by_transition': msg}) + if not tr.has_input_state(fromstate): + msg = session._("transition isn't allowed") + raise ValidationError(entity.eid, {'by_transition': msg}) + if not tr.may_be_fired(foreid): + msg = session._("transition may not be fired") + raise ValidationError(entity.eid, {'by_transition': msg}) + if entity.get('to_state'): + deststateeid = entity['to_state'] + if not cowpowers and deststateeid != tr.destination().eid: + msg = session._("transition isn't allowed") + raise ValidationError(entity.eid, {'by_transition': msg}) + if swtr is None: + deststate = session.entity_from_eid(deststateeid) + if not cowpowers and deststate is None: + msg = entity.req._("state doesn't belong to entity's workflow") + raise ValidationError(entity.eid, {'to_state': msg}) else: - # not a transition - # check state is initial state if the workflow defines one - isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s', - {'etype': etype}) - if isrset and not toeid == isrset[0][0]: - _ = session._ - msg = _('%s is not the initial state (%s) for this entity') % ( - _(session.entity_from_eid(toeid).name), _(isrset.get_entity(0,0).name)) - raise ValidationError(fromeid, {'in_state': msg}) - eschema = session.repo.schema[etype] - if not 'wf_info_for' in eschema.object_relations(): - # workflow history not activated for this entity type - return - rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s' - args = {'comment': session.get_shared_data('trcomment', None, pop=True), - 'e': fromeid, 'ds': toeid} - cformat = session.get_shared_data('trcommentformat', None, pop=True) - if cformat is not None: - args['comment_format'] = cformat - rql += ', T comment_format %(comment_format)s' - restriction = ['DS eid %(ds)s, E eid %(e)s'] - if not state is None: # not a transition - rql += ', T from_state FS' - restriction.append('FS eid %(fs)s') - args['fs'] = state.eid - rql = '%s WHERE %s' % (rql, ', '.join(restriction)) - session.unsafe_execute(rql, args, 'e') + deststateeid = tr.destination().eid + # everything is ok, add missing information on the trinfo entity + entity['from_state'] = fromstate.eid + entity['to_state'] = deststateeid + nocheck = session.transaction_data.setdefault('skip-security', set()) + nocheck.add((entity.eid, 'from_state', fromstate.eid)) + nocheck.add((entity.eid, 'to_state', deststateeid)) + +def after_add_trinfo(session, entity): + """change related entity state""" + _change_state(session, entity['wf_info_for'], + entity['from_state'], entity['to_state']) + forentity = session.entity_from_eid(entity['wf_info_for']) + assert forentity.current_state.eid == entity['to_state'], forentity.current_state.name + if forentity.main_workflow.eid != forentity.current_workflow.eid: + # we're in a subworkflow, check if we've reached an exit point + wftr = forentity.subworkflow_input_transition() + if wftr is None: + # inconsistency detected + msg = entity.req._("state doesn't belong to entity's current workflow") + raise ValidationError(entity.eid, {'to_state': msg}) + tostate = wftr.get_exit_point(entity['to_state']) + if tostate is not None: + # reached an exit point + msg = session._('exiting from subworkflow %s') + msg %= session._(forentity.current_workflow.name) + session.transaction_data[(forentity.eid, 'subwfentrytr')] = True + # XXX iirk + req = forentity.req + forentity.req = session.super_session + try: + trinfo = forentity.change_state(tostate, msg, u'text/plain', + tr=wftr) + finally: + forentity.req = req class SetInitialStateOp(PreCommitOperation): @@ -473,26 +552,125 @@ # if there is an initial state and the entity's state is not set, # use the initial state as a default state pendingeids = session.transaction_data.get('pendingeids', ()) - if not entity.eid in pendingeids and not entity.in_state: - rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s', - {'name': entity.id}) - if rset: - session.add_relation(entity.eid, 'in_state', rset[0][0]) + if not entity.eid in pendingeids and not entity.in_state and \ + entity.main_workflow: + state = entity.main_workflow.initial + if state: + # use super session to by-pass security checks + session.super_session.add_relation(entity.eid, 'in_state', + state.eid) def set_initial_state_after_add(session, entity): SetInitialStateOp(session, entity=entity) +def before_add_in_state(session, eidfrom, rtype, eidto): + """check state apply, in case of direct in_state change using unsafe_execute + """ + nocheck = session.transaction_data.setdefault('skip-security', ()) + if (eidfrom, 'in_state', eidto) in nocheck: + # state changed through TrInfo insertion, so we already know it's ok + return + entity = session.entity_from_eid(eidfrom) + mainwf = entity.main_workflow + if mainwf is None: + msg = session._('entity has no workflow set') + raise ValidationError(entity.eid, {None: msg}) + for wf in mainwf.iter_workflows(): + if wf.state_by_eid(eidto): + break + else: + msg = session._("state doesn't belong to entity's workflow. You may " + "want to set a custom workflow for this entity first.") + raise ValidationError(eidfrom, {'in_state': msg}) + if entity.current_workflow and wf.eid != entity.current_workflow.eid: + msg = session._("state doesn't belong to entity's current workflow") + raise ValidationError(eidfrom, {'in_state': msg}) + + +class CheckTrExitPoint(PreCommitOperation): + + def precommit_event(self): + tr = self.session.entity_from_eid(self.treid) + outputs = set() + for ep in tr.subworkflow_exit: + if ep.subwf_state.eid in outputs: + msg = self.session._("can't have multiple exits on the same state") + raise ValidationError(self.treid, {'subworkflow_exit': msg}) + outputs.add(ep.subwf_state.eid) + + +def after_add_subworkflow_exit(session, eidfrom, rtype, eidto): + CheckTrExitPoint(session, treid=eidfrom) + + +class WorkflowChangedOp(PreCommitOperation): + """fix entity current state when changing its workflow""" + + def precommit_event(self): + # notice that enforcement that new workflow apply to the entity's type is + # done by schema rule, no need to check it here + session = self.session + pendingeids = session.transaction_data.get('pendingeids', ()) + if self.eid in pendingeids: + return + entity = session.entity_from_eid(self.eid) + # check custom workflow has not been rechanged to another one in the same + # transaction + mainwf = entity.main_workflow + if mainwf.eid == self.wfeid: + deststate = mainwf.initial + if not deststate: + msg = session._('workflow has no initial state') + raise ValidationError(entity.eid, {'custom_workflow': msg}) + if mainwf.state_by_eid(entity.current_state.eid): + # nothing to do + return + # if there are no history, simply go to new workflow's initial state + if not entity.workflow_history: + if entity.current_state.eid != deststate.eid: + _change_state(session, entity.eid, + entity.current_state.eid, deststate.eid) + return + msg = session._('workflow changed to "%s"') + msg %= session._(mainwf.name) + session.transaction_data[(entity.eid, 'customwf')] = self.wfeid + entity.change_state(deststate, msg, u'text/plain') + + +def set_custom_workflow(session, eidfrom, rtype, eidto): + WorkflowChangedOp(session, eid=eidfrom, wfeid=eidto) + + +def del_custom_workflow(session, eidfrom, rtype, eidto): + entity = session.entity_from_eid(eidfrom) + typewf = entity.cwetype_workflow() + if typewf is not None: + WorkflowChangedOp(session, eid=eidfrom, wfeid=typewf.eid) + + +def after_del_workflow(session, eid): + # workflow cleanup + session.execute('DELETE State X WHERE NOT X state_of Y') + session.execute('DELETE Transition X WHERE NOT X transition_of Y') + + def _register_wf_hooks(hm): """register workflow related hooks on the hooks manager""" if 'in_state' in hm.schema: - hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state') - hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') + hm.register_hook(before_add_trinfo, 'before_add_entity', 'TrInfo') + hm.register_hook(after_add_trinfo, 'after_add_entity', 'TrInfo') + #hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') for eschema in hm.schema.entities(): if 'in_state' in eschema.subject_relations(): hm.register_hook(set_initial_state_after_add, 'after_add_entity', str(eschema)) + hm.register_hook(set_custom_workflow, 'after_add_relation', 'custom_workflow') + hm.register_hook(del_custom_workflow, 'after_delete_relation', 'custom_workflow') + hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow') + hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state') + hm.register_hook(after_add_subworkflow_exit, 'after_add_relation', 'subworkflow_exit') # CWProperty hooks ############################################################# diff -r 99ab33abf414 -r 096d680c9da2 server/hooksmanager.py --- a/server/hooksmanager.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/hooksmanager.py Wed Sep 02 10:23:38 2009 +0200 @@ -268,3 +268,90 @@ from cubicweb import set_log_methods set_log_methods(HooksManager, getLogger('cubicweb.hooksmanager')) set_log_methods(Hook, getLogger('cubicweb.hooks')) + +# base classes for relation propagation ######################################## + +from cubicweb.server.pool import PreCommitOperation + +class RQLPrecommitOperation(PreCommitOperation): + def precommit_event(self): + execute = self.session.unsafe_execute + for rql in self.rqls: + execute(*rql) + + +class PropagateSubjectRelationHook(Hook): + """propagate permissions and nosy list when new entity are added""" + events = ('after_add_relation',) + # to set in concrete class + rtype = None + subject_relations = None + object_relations = None + accepts = None # subject_relations + object_relations + + def call(self, session, fromeid, rtype, toeid): + for eid in (fromeid, toeid): + etype = session.describe(eid)[0] + if not self.schema.eschema(etype).has_subject_relation(self.rtype): + return + if rtype in self.subject_relations: + meid, seid = fromeid, toeid + else: + assert rtype in self.object_relations + meid, seid = toeid, fromeid + rql = 'SET E %s P WHERE X %s P, X eid %%(x)s, E eid %%(e)s, NOT E %s P'\ + % (self.rtype, self.rtype, self.rtype) + rqls = [(rql, {'x': meid, 'e': seid}, ('x', 'e'))] + RQLPrecommitOperation(session, rqls=rqls) + + +class PropagateSubjectRelationAddHook(Hook): + """propagate on existing entities when a permission or nosy list is added""" + events = ('after_add_relation',) + # to set in concrete class + rtype = None + subject_relations = None + object_relations = None + accepts = None # (self.rtype,) + + def call(self, session, fromeid, rtype, toeid): + eschema = self.schema.eschema(session.describe(fromeid)[0]) + rqls = [] + for rel in self.subject_relations: + if eschema.has_subject_relation(rel): + rqls.append(('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'X %s R, NOT R %s P' % (rtype, rel, rtype), + {'x': fromeid, 'p': toeid}, 'x')) + for rel in self.object_relations: + if eschema.has_object_relation(rel): + rqls.append(('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'R %s X, NOT R %s P' % (rtype, rel, rtype), + {'x': fromeid, 'p': toeid}, 'x')) + if rqls: + RQLPrecommitOperation(session, rqls=rqls) + + +class PropagateSubjectRelationDelHook(Hook): + """propagate on existing entities when a permission is deleted""" + events = ('after_delete_relation',) + # to set in concrete class + rtype = None + subject_relations = None + object_relations = None + accepts = None # (self.rtype,) + + def call(self, session, fromeid, rtype, toeid): + eschema = self.schema.eschema(session.describe(fromeid)[0]) + rqls = [] + for rel in self.subject_relations: + if eschema.has_subject_relation(rel): + rqls.append(('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'X %s R' % (rtype, rel), + {'x': fromeid, 'p': toeid}, 'x')) + for rel in self.object_relations: + if eschema.has_object_relation(rel): + rqls.append(('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'R %s X' % (rtype, rel), + {'x': fromeid, 'p': toeid}, 'x')) + if rqls: + RQLPrecommitOperation(session, rqls=rqls) diff -r 99ab33abf414 -r 096d680c9da2 server/migractions.py --- a/server/migractions.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/migractions.py Wed Sep 02 10:23:38 2009 +0200 @@ -954,78 +954,78 @@ # Workflows handling ###################################################### + def cmd_add_workflow(self, name, wfof, default=True, commit=False, + **kwargs): + self.session.set_pool() # ensure pool is set + wf = self.cmd_create_entity('Workflow', name=unicode(name), + **kwargs) + if not isinstance(wfof, (list, tuple)): + wfof = (wfof,) + for etype in wfof: + rset = self.rqlexec( + 'SET X workflow_of ET WHERE X eid %(x)s, ET name %(et)s', + {'x': wf.eid, 'et': etype}, 'x', ask_confirm=False) + assert rset, 'unexistant entity type %s' % etype + if default: + self.rqlexec( + 'SET ET default_workflow X WHERE X eid %(x)s, ET name %(et)s', + {'x': wf.eid, 'et': etype}, 'x', ask_confirm=False) + if commit: + self.commit() + return wf + + # XXX remove once cmd_add_[state|transition] are removed + def _get_or_create_wf(self, etypes): + self.session.set_pool() # ensure pool is set + if not isinstance(etypes, (list, tuple)): + etypes = (etypes,) + rset = self.rqlexec('Workflow X WHERE X workflow_of ET, ET name %(et)s', + {'et': etypes[0]}) + if rset: + return rset.get_entity(0, 0) + return self.cmd_add_workflow('%s workflow' % ';'.join(etypes), etypes) + + @deprecated('use add_workflow and Workflow.add_state method') def cmd_add_state(self, name, stateof, initial=False, commit=False, **kwargs): """method to ease workflow definition: add a state for one or more entity type(s) """ - stateeid = self.cmd_add_entity('State', name=name, **kwargs) - if not isinstance(stateof, (list, tuple)): - stateof = (stateof,) - for etype in stateof: - # XXX ensure etype validity - self.rqlexec('SET X state_of Y WHERE X eid %(x)s, Y name %(et)s', - {'x': stateeid, 'et': etype}, 'x', ask_confirm=False) - if initial: - self.rqlexec('SET ET initial_state S WHERE ET name %(et)s, S eid %(x)s', - {'x': stateeid, 'et': etype}, 'x', ask_confirm=False) + wf = self._get_or_create_wf(stateof) + state = wf.add_state(name, initial, **kwargs) if commit: self.commit() - return stateeid + return state.eid + @deprecated('use add_workflow and Workflow.add_transition method') def cmd_add_transition(self, name, transitionof, fromstates, tostate, requiredgroups=(), conditions=(), commit=False, **kwargs): """method to ease workflow definition: add a transition for one or more entity type(s), from one or more state and to a single state """ - treid = self.cmd_add_entity('Transition', name=name, **kwargs) - if not isinstance(transitionof, (list, tuple)): - transitionof = (transitionof,) - for etype in transitionof: - # XXX ensure etype validity - self.rqlexec('SET X transition_of Y WHERE X eid %(x)s, Y name %(et)s', - {'x': treid, 'et': etype}, 'x', ask_confirm=False) - for stateeid in fromstates: - self.rqlexec('SET X allowed_transition Y WHERE X eid %(x)s, Y eid %(y)s', - {'x': stateeid, 'y': treid}, 'x', ask_confirm=False) - self.rqlexec('SET X destination_state Y WHERE X eid %(x)s, Y eid %(y)s', - {'x': treid, 'y': tostate}, 'x', ask_confirm=False) - self.cmd_set_transition_permissions(treid, requiredgroups, conditions, - reset=False) + wf = self._get_or_create_wf(transitionof) + tr = wf.add_transition(name, fromstates, tostate, requiredgroups, + conditions, **kwargs) if commit: self.commit() - return treid + return tr.eid + @deprecated('use Transition.set_transition_permissions method') def cmd_set_transition_permissions(self, treid, requiredgroups=(), conditions=(), reset=True, commit=False): """set or add (if `reset` is False) groups and conditions for a transition """ - if reset: - self.rqlexec('DELETE T require_group G WHERE T eid %(x)s', - {'x': treid}, 'x', ask_confirm=False) - self.rqlexec('DELETE T condition R WHERE T eid %(x)s', - {'x': treid}, 'x', ask_confirm=False) - for gname in requiredgroups: - ### XXX ensure gname validity - self.rqlexec('SET T require_group G WHERE T eid %(x)s, G name %(gn)s', - {'x': treid, 'gn': gname}, 'x', ask_confirm=False) - if isinstance(conditions, basestring): - conditions = (conditions,) - for expr in conditions: - if isinstance(expr, str): - expr = unicode(expr) - self.rqlexec('INSERT RQLExpression X: X exprtype "ERQLExpression", ' - 'X expression %(expr)s, T condition X ' - 'WHERE T eid %(x)s', - {'x': treid, 'expr': expr}, 'x', ask_confirm=False) + self.session.set_pool() # ensure pool is set + tr = self.session.entity_from_eid(treid) + tr.set_transition_permissions(requiredgroups, conditions, reset) if commit: self.commit() + @deprecated('use entity.fire_transition("transition") or entity.change_state("state")') def cmd_set_state(self, eid, statename, commit=False): self.session.set_pool() # ensure pool is set - entity = self.session.entity_from_eid(eid) - entity.change_state(entity.wf_state(statename).eid) + self.session.entity_from_eid(eid).change_state(statename) if commit: self.commit() @@ -1042,32 +1042,26 @@ prop = self.rqlexec('CWProperty X WHERE X pkey %(k)s', {'k': pkey}, ask_confirm=False).get_entity(0, 0) except: - self.cmd_add_entity('CWProperty', pkey=unicode(pkey), value=value) + self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value) else: self.rqlexec('SET X value %(v)s WHERE X pkey %(k)s', {'k': pkey, 'v': value}, ask_confirm=False) # other data migration commands ########################################### + def cmd_create_entity(self, etype, *args, **kwargs): + """add a new entity of the given type""" + commit = kwargs.pop('commit', False) + self.session.set_pool() + entity = self.session.create_entity(etype, *args, **kwargs) + if commit: + self.commit() + return entity + + @deprecated('use create_entity') def cmd_add_entity(self, etype, *args, **kwargs): """add a new entity of the given type""" - rql = 'INSERT %s X' % etype - relations = [] - restrictions = [] - for rtype, rvar in args: - relations.append('X %s %s' % (rtype, rvar)) - restrictions.append('%s eid %s' % (rvar, kwargs.pop(rvar))) - commit = kwargs.pop('commit', False) - for attr in kwargs: - relations.append('X %s %%(%s)s' % (attr, attr)) - if relations: - rql = '%s: %s' % (rql, ', '.join(relations)) - if restrictions: - rql = '%s WHERE %s' % (rql, ', '.join(restrictions)) - eid = self.rqlexec(rql, kwargs, ask_confirm=self.verbosity>=2).rows[0][0] - if commit: - self.commit() - return eid + return self.cmd_create_entity(etype, *args, **kwargs).eid def sqlexec(self, sql, args=None, ask_confirm=True): """execute the given sql if confirmed diff -r 99ab33abf414 -r 096d680c9da2 server/msplanner.py --- a/server/msplanner.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/msplanner.py Wed Sep 02 10:23:38 2009 +0200 @@ -97,9 +97,6 @@ # str() Constant.value to ensure generated table name won't be unicode Constant._ms_table_key = lambda x: str(x.value) -AbstractSource.dont_cross_relations = () -AbstractSource.cross_relations = () - def need_source_access_relation(vargraph): if not vargraph: return False diff -r 99ab33abf414 -r 096d680c9da2 server/repository.py --- a/server/repository.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/repository.py Wed Sep 02 10:23:38 2009 +0200 @@ -30,7 +30,7 @@ from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP, CW_EVENT_MANAGER, UnknownEid, AuthenticationError, ExecutionError, - ETypeNotSupportedBySources, RTypeNotSupportedBySources, + ETypeNotSupportedBySources, MultiSourcesError, BadConnectionId, Unauthorized, ValidationError, typed_eid) from cubicweb.cwvreg import CubicWebVRegistry @@ -199,6 +199,8 @@ 'cubicweb.entities.__init__') self.vreg.load_file(join(etdirectory, 'authobjs.py'), 'cubicweb.entities.authobjs') + self.vreg.load_file(join(etdirectory, 'wfobjs.py'), + 'cubicweb.entities.wfobjs') else: # test start: use the file system schema (quicker) self.warning("set fs instance'schema") @@ -970,12 +972,21 @@ def locate_relation_source(self, session, subject, rtype, object): subjsource = self.source_from_eid(subject, session) objsource = self.source_from_eid(object, session) - if not (subjsource is objsource and subjsource.support_relation(rtype, 1)): + if not subjsource is objsource: source = self.system_source - if not source.support_relation(rtype, 1): - raise RTypeNotSupportedBySources(rtype) + if not (subjsource.may_cross_relation(rtype) + and objsource.may_cross_relation(rtype)): + raise MultiSourcesError( + "relation %s can't be crossed among sources" + % rtype) + elif not subjsource.support_relation(rtype): + source = self.system_source else: source = subjsource + if not source.support_relation(rtype, True): + raise MultiSourcesError( + "source %s doesn't support write of %s relation" + % (source.uri, rtype)) return source def locate_etype_source(self, etype): diff -r 99ab33abf414 -r 096d680c9da2 server/schemahooks.py --- a/server/schemahooks.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/schemahooks.py Wed Sep 02 10:23:38 2009 +0200 @@ -759,8 +759,7 @@ def after_del_eetype(session, eid): # workflow cleanup - session.execute('DELETE State X WHERE NOT X state_of Y') - session.execute('DELETE Transition X WHERE NOT X transition_of Y') + session.execute('DELETE Workflow X WHERE NOT X workflow_of Y') def before_del_ertype(session, eid): diff -r 99ab33abf414 -r 096d680c9da2 server/schemaserial.py --- a/server/schemaserial.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/schemaserial.py Wed Sep 02 10:23:38 2009 +0200 @@ -110,7 +110,6 @@ print sql sqlcu.execute(sql) # other table renaming done once schema has been read - # print 'reading schema from the database...' index = {} permsdict = deserialize_ertype_permissions(session) schema.reading_from_database = True diff -r 99ab33abf414 -r 096d680c9da2 server/securityhooks.py --- a/server/securityhooks.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/securityhooks.py Wed Sep 02 10:23:38 2009 +0200 @@ -70,11 +70,17 @@ def before_add_relation(session, fromeid, rtype, toeid): if rtype in BEFORE_ADD_RELATIONS and not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return rschema = session.repo.schema[rtype] rschema.check_perm(session, 'add', fromeid, toeid) def after_add_relation(session, fromeid, rtype, toeid): if not rtype in BEFORE_ADD_RELATIONS and not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return rschema = session.repo.schema[rtype] if rtype in ON_COMMIT_ADD_RELATIONS: CheckRelationPermissionOp(session, action='add', rschema=rschema, @@ -84,6 +90,9 @@ def before_del_relation(session, fromeid, rtype, toeid): if not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return session.repo.schema[rtype].check_perm(session, 'delete', fromeid, toeid) def register_security_hooks(hm): diff -r 99ab33abf414 -r 096d680c9da2 server/session.py --- a/server/session.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/session.py Wed Sep 02 10:23:38 2009 +0200 @@ -78,16 +78,43 @@ def schema(self): return self.repo.schema - def add_relation(self, fromeid, rtype, toeid): + def _change_relation(self, cb, fromeid, rtype, toeid): if self.is_super_session: - self.repo.glob_add_relation(self, fromeid, rtype, toeid) + cb(self, fromeid, rtype, toeid) return self.is_super_session = True try: - self.repo.glob_add_relation(self, fromeid, rtype, toeid) + cb(self, fromeid, rtype, toeid) finally: self.is_super_session = False + def add_relation(self, fromeid, rtype, toeid): + """provide direct access to the repository method to add a relation. + + This is equivalent to the following rql query: + + SET X rtype Y WHERE X eid fromeid, T eid toeid + + without read security check but also all the burden of rql execution. + You may use this in hooks when you know both eids of the relation you + want to add. + """ + self._change_relation(self.repo.glob_add_relation, + fromeid, rtype, toeid) + def delete_relation(self, fromeid, rtype, toeid): + """provide direct access to the repository method to delete a relation. + + This is equivalent to the following rql query: + + DELETE X rtype Y WHERE X eid fromeid, T eid toeid + + without read security check but also all the burden of rql execution. + You may use this in hooks when you know both eids of the relation you + want to delete. + """ + self._change_relation(self.repo.glob_delete_relation, + fromeid, rtype, toeid) + def update_rel_cache_add(self, subject, rtype, object, symetric=False): self._update_entity_rel_cache_add(subject, rtype, 'subject', object) if symetric: diff -r 99ab33abf414 -r 096d680c9da2 server/sources/__init__.py --- a/server/sources/__init__.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/sources/__init__.py Wed Sep 02 10:23:38 2009 +0200 @@ -80,6 +80,11 @@ # a reference to the instance'schema (may differs from the source'schema) schema = None + # multi-sources planning control + dont_cross_relations = () + cross_relations = () + + def __init__(self, repo, appschema, source_config, *args, **kwargs): self.repo = repo self.uri = source_config['uri'] @@ -177,6 +182,19 @@ return wsupport return True + def may_cross_relation(self, rtype): + """return True if the relation may be crossed among sources. Rules are: + + * if this source support the relation, can't be crossed unless explicitly + specified in .cross_relations + + * if this source doesn't support the relation, can be crossed unless + explicitly specified in .dont_cross_relations + """ + if self.support_relation(rtype): + return rtype in self.cross_relations + return rtype not in self.dont_cross_relations + def eid2extid(self, eid, session=None): return self.repo.eid2extid(self, eid, session) diff -r 99ab33abf414 -r 096d680c9da2 server/sources/native.py --- a/server/sources/native.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/sources/native.py Wed Sep 02 10:23:38 2009 +0200 @@ -94,8 +94,6 @@ """adapter for source using the native cubicweb schema (see below) """ sqlgen_class = SQLGenerator - # need default value on class since migration doesn't call init method - has_deleted_entitites_table = True passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P" auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s" @@ -226,15 +224,6 @@ def init(self): self.init_creating() - pool = self.repo._get_pool() - pool.pool_set() - # XXX cubicweb < 2.42 compat - if 'deleted_entities' in self.dbhelper.list_tables(pool['system']): - self.has_deleted_entitites_table = True - else: - self.has_deleted_entitites_table = False - pool.pool_reset() - self.repo._free_pool(pool) def map_attribute(self, etype, attr, cb): self._rql_sqlgen.attr_map['%s.%s' % (etype, attr)] = cb @@ -279,6 +268,9 @@ # can't claim not supporting a relation return True #not rtype == 'content_for' + def may_cross_relation(self, rtype): + return True + def authenticate(self, session, login, password): """return CWUser eid for the given login/password if this account is defined in this source, else raise `AuthenticationError` @@ -549,13 +541,12 @@ """ attrs = {'eid': eid} session.system_sql(self.sqlgen.delete('entities', attrs), attrs) - if self.has_deleted_entitites_table: - if extid is not None: - assert isinstance(extid, str), type(extid) - extid = b64encode(extid) - attrs = {'type': etype, 'eid': eid, 'extid': extid, - 'source': uri, 'dtime': datetime.now()} - session.system_sql(self.sqlgen.insert('deleted_entities', attrs), attrs) + if extid is not None: + assert isinstance(extid, str), type(extid) + extid = b64encode(extid) + attrs = {'type': etype, 'eid': eid, 'extid': extid, + 'source': uri, 'dtime': datetime.now()} + session.system_sql(self.sqlgen.insert('deleted_entities', attrs), attrs) def fti_unindex_entity(self, session, eid): """remove text content for entity with the given eid from the full text diff -r 99ab33abf414 -r 096d680c9da2 server/sources/pyrorql.py --- a/server/sources/pyrorql.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/sources/pyrorql.py Wed Sep 02 10:23:38 2009 +0200 @@ -345,6 +345,7 @@ cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs, 'x') self._query_cache.clear() + entity.clear_all_caches() def delete_entity(self, session, etype, eid): """delete an entity from the source""" @@ -360,6 +361,8 @@ {'x': self.eid2extid(subject, session), 'y': self.eid2extid(object, session)}, ('x', 'y')) self._query_cache.clear() + session.entity_from_eid(subject).clear_all_caches() + session.entity_from_eid(object).clear_all_caches() def delete_relation(self, session, subject, rtype, object): """delete a relation from the source""" @@ -368,6 +371,8 @@ {'x': self.eid2extid(subject, session), 'y': self.eid2extid(object, session)}, ('x', 'y')) self._query_cache.clear() + session.entity_from_eid(subject).clear_all_caches() + session.entity_from_eid(object).clear_all_caches() class RQL2RQL(object): diff -r 99ab33abf414 -r 096d680c9da2 server/ssplanner.py --- a/server/ssplanner.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/ssplanner.py Wed Sep 02 10:23:38 2009 +0200 @@ -377,7 +377,7 @@ previous FetchStep relations values comes from the latest result, with one columns for - each relation defined in self.r_defs + each relation defined in self.rdefs for one entity definition, we'll construct N entity, where N is the number of the latest result @@ -387,33 +387,35 @@ RELATION = 1 REVERSE_RELATION = 2 - def __init__(self, plan, e_def, r_defs): + def __init__(self, plan, edef, rdefs): Step.__init__(self, plan) # partial entity definition to expand - self.e_def = e_def + self.edef = edef # definition of relations to complete - self.r_defs = r_defs + self.rdefs = rdefs def execute(self): """execute this step""" - base_e_def = self.e_def - result = [] - for row in self.execute_child(): + base_edef = self.edef + edefs = [] + result = self.execute_child() + for row in result: # get a new entity definition for this row - e_def = copy(base_e_def) + edef = copy(base_edef) # complete this entity def using row values - for i in range(len(self.r_defs)): - rtype, rorder = self.r_defs[i] + for i in range(len(self.rdefs)): + rtype, rorder = self.rdefs[i] if rorder == RelationsStep.FINAL: - e_def[rtype] = row[i] + edef[rtype] = row[i] elif rorder == RelationsStep.RELATION: - self.plan.add_relation_def( (e_def, rtype, row[i]) ) - e_def.querier_pending_relations[(rtype, 'subject')] = row[i] + self.plan.add_relation_def( (edef, rtype, row[i]) ) + edef.querier_pending_relations[(rtype, 'subject')] = row[i] else: - self.plan.add_relation_def( (row[i], rtype, e_def) ) - e_def.querier_pending_relations[(rtype, 'object')] = row[i] - result.append(e_def) - self.plan.substitute_entity_def(base_e_def, result) + self.plan.add_relation_def( (row[i], rtype, edef) ) + edef.querier_pending_relations[(rtype, 'object')] = row[i] + edefs.append(edef) + self.plan.substitute_entity_def(base_edef, edefs) + return result class InsertStep(Step): @@ -483,7 +485,8 @@ edefs = {} # insert relations attributes = set([relation.r_type for relation in self.attribute_relations]) - for row in self.execute_child(): + result = self.execute_child() + for row in result: for relation in self.attribute_relations: lhs, rhs = relation.get_variable_parts() eid = typed_eid(row[self.selected_index[str(lhs)]]) @@ -502,8 +505,6 @@ obj = row[self.selected_index[str(relation.children[1])]] repo.glob_add_relation(session, subj, relation.r_type, obj) # update entities - result = [] for eid, edef in edefs.iteritems(): repo.glob_update_entity(session, edef, attributes) - result.append( (eid,) ) return result diff -r 99ab33abf414 -r 096d680c9da2 server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/data/migratedapp/schema.py Wed Sep 02 10:23:38 2009 +0200 @@ -104,7 +104,7 @@ concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*') connait = SubjectRelation('Personne', symetric=True) -class Societe(EntityType): +class Societe(WorkflowableEntityType): permissions = { 'read': ('managers', 'users', 'guests'), 'update': ('managers', 'owners'), @@ -123,7 +123,6 @@ cp = String(maxsize=12) ville= String(maxsize=32) - in_state = SubjectRelation('State', cardinality='?*') class evaluee(RelationDefinition): subject = ('Personne', 'CWUser', 'Societe') diff -r 99ab33abf414 -r 096d680c9da2 server/test/data/schema.py --- a/server/test/data/schema.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/data/schema.py Wed Sep 02 10:23:38 2009 +0200 @@ -34,7 +34,7 @@ depends_on = SubjectRelation('Affaire') require_permission = SubjectRelation('CWPermission') concerne = SubjectRelation(('Societe', 'Note')) - todo_by = SubjectRelation('Personne') + todo_by = SubjectRelation('Personne', cardinality='?*') documented_by = SubjectRelation('Card') @@ -69,7 +69,7 @@ from cubicweb.schemas.base import CWUser CWUser.get_relations('login').next().fulltextindexed = True -class Note(EntityType): +class Note(WorkflowableEntityType): date = String(maxsize=10) type = String(maxsize=6) para = String(maxsize=512) @@ -146,18 +146,6 @@ 'delete': ('managers',), 'add': ('managers',)} - -class in_state(RelationDefinition): - subject = 'Note' - object = 'State' - cardinality = '1*' - constraints=[RQLConstraint('S is ET, O state_of ET')] - -class wf_info_for(RelationDefinition): - subject = 'TrInfo' - object = 'Note' - cardinality = '1*' - class multisource_rel(RelationDefinition): subject = ('Card', 'Note') object = 'Note' diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_hookhelper.py --- a/server/test/unittest_hookhelper.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_hookhelper.py Wed Sep 02 10:23:38 2009 +0200 @@ -49,41 +49,5 @@ op5 = hooks.CheckORelationOp(session) self.assertEquals(session.pending_operations, [op1, op2, op4, op5, op3]) - - def test_in_state_notification(self): - result = [] - # test both email notification and transition_information - # whatever if we can connect to the default stmp server, transaction - # should not fail - def in_state_changed(session, eidfrom, rtype, eidto): - tr = previous_state(session, eidfrom) - if tr is None: - result.append(tr) - return - content = u'trÀnsition from %s to %s' % (tr.name, entity_name(session, eidto)) - result.append(content) - SendMailOp(session, msg=content, recipients=['test@logilab.fr']) - self.hm.register_hook(in_state_changed, - 'before_add_relation', 'in_state') - self.execute('INSERT CWUser X: X login "paf", X upassword "wouf", X in_state S, X in_group G WHERE S name "activated", G name "users"') - self.assertEquals(result, [None]) - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 0, - self.session.pending_operations) - self.commit() - self.execute('SET X in_state S WHERE X login "paf", S name "deactivated"') - self.assertEquals(result, [None, u'trÀnsition from activated to deactivated']) - # one to send the mail, one to close the smtp connection - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 1, - self.session.pending_operations) - self.commit() - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 0, - self.session.pending_operations) - if __name__ == '__main__': unittest_main() diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_hooks.py --- a/server/test/unittest_hooks.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_hooks.py Wed Sep 02 10:23:38 2009 +0200 @@ -37,8 +37,8 @@ 'DELETE CWGroup X WHERE X name "owners"') def test_delete_required_relations_subject(self): - self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y, X in_state S ' - 'WHERE Y name "users", S name "activated"') + self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y ' + 'WHERE Y name "users"') self.commit() self.execute('DELETE X in_group Y WHERE X login "toto", Y name "users"') self.assertRaises(ValidationError, self.commit) @@ -60,18 +60,6 @@ self.assertRaises(ValidationError, self.commit) - def test_delete_if_singlecard1(self): - self.assertEquals(self.repo.schema['in_state'].inlined, False) - ueid = self.create_user('toto') - self.commit() - self.execute('SET X in_state S WHERE S name "deactivated", X eid %(x)s', {'x': ueid}) - rset = self.execute('Any S WHERE X in_state S, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) - self.commit() - self.assertRaises(Exception, self.execute, 'SET X in_state S WHERE S name "deactivated", X eid %s' % ueid) - rset2 = self.execute('Any S WHERE X in_state S, X eid %(x)s', {'x': ueid}) - self.assertEquals(rset.rows, rset2.rows) - def test_inlined(self): self.assertEquals(self.repo.schema['sender'].inlined, True) self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') @@ -155,6 +143,40 @@ self.assertEquals(entity.descr, u'R&D

yo

') + def test_metadata_cwuri(self): + eid = self.execute('INSERT Note X')[0][0] + cwuri = self.execute('Any U WHERE X eid %s, X cwuri U' % eid)[0][0] + self.assertEquals(cwuri, self.repo.config['base-url'] + 'eid/%s' % eid) + + def test_metadata_creation_modification_date(self): + _now = datetime.now() + eid = self.execute('INSERT Note X')[0][0] + creation_date, modification_date = self.execute('Any CD, MD WHERE X eid %s, ' + 'X creation_date CD, ' + 'X modification_date MD' % eid)[0] + self.assertEquals((creation_date - _now).seconds, 0) + self.assertEquals((modification_date - _now).seconds, 0) + + def test_metadata__date(self): + _now = datetime.now() + eid = self.execute('INSERT Note X')[0][0] + creation_date = self.execute('Any D WHERE X eid %s, X creation_date D' % eid)[0][0] + self.assertEquals((creation_date - _now).seconds, 0) + + def test_metadata_created_by(self): + eid = self.execute('INSERT Note X')[0][0] + self.commit() # fire operations + rset = self.execute('Any U WHERE X eid %s, X created_by U' % eid) + self.assertEquals(len(rset), 1) # make sure we have only one creator + self.assertEquals(rset[0][0], self.session.user.eid) + + def test_metadata_owned_by(self): + eid = self.execute('INSERT Note X')[0][0] + self.commit() # fire operations + rset = self.execute('Any U WHERE X eid %s, X owned_by U' % eid) + self.assertEquals(len(rset), 1) # make sure we have only one owner + self.assertEquals(rset[0][0], self.session.user.eid) + class UserGroupHooksTC(RepositoryBasedTC): @@ -480,177 +502,5 @@ 'RT name "prenom", E name "Personne"') self.commit() - -class WorkflowHooksTC(RepositoryBasedTC): - - def setUp(self): - RepositoryBasedTC.setUp(self) - self.s_activated = self.execute('State X WHERE X name "activated"')[0][0] - self.s_deactivated = self.execute('State X WHERE X name "deactivated"')[0][0] - self.s_dummy = self.execute('INSERT State X: X name "dummy", X state_of E WHERE E name "CWUser"')[0][0] - self.create_user('stduser') - # give access to users group on the user's wf transitions - # so we can test wf enforcing on euser (managers don't have anymore this - # enforcement - self.execute('SET X require_group G WHERE G name "users", X transition_of ET, ET name "CWUser"') - self.commit() - - def tearDown(self): - self.execute('DELETE X require_group G WHERE G name "users", X transition_of ET, ET name "CWUser"') - self.commit() - RepositoryBasedTC.tearDown(self) - - def test_set_initial_state(self): - ueid = self.execute('INSERT CWUser E: E login "x", E upassword "x", E in_group G ' - 'WHERE G name "users"')[0][0] - self.failIf(self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', - {'x' : ueid})) - self.commit() - initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', - {'x' : ueid})[0][0] - self.assertEquals(initialstate, u'activated') - - def test_initial_state(self): - cnx = self.login('stduser') - cu = cnx.cursor() - self.assertRaises(ValidationError, cu.execute, - 'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' - 'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'}) - cnx.close() - # though managers can do whatever he want - self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' - 'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'}) - self.commit() - - # test that the workflow is correctly enforced - def test_transition_checking1(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.close() - - def test_transition_checking2(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_dummy}, 'x') - cnx.close() - - def test_transition_checking3(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - cnx.commit() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - # get back now - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.commit() - cnx.close() - - def test_transition_checking4(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - cnx.commit() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_dummy}, 'x') - # get back now - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.commit() - cnx.close() - - def test_transition_information(self): - ueid = self.session.user.eid - self.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - self.commit() - rset = self.execute('TrInfo T ORDERBY T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 2) - tr = rset.get_entity(1, 0) - #tr.complete() - self.assertEquals(tr.comment, None) - self.assertEquals(tr.from_state[0].eid, self.s_activated) - self.assertEquals(tr.to_state[0].eid, self.s_deactivated) - - self.session.set_shared_data('trcomment', u'il est pas sage celui-la') - self.session.set_shared_data('trcommentformat', u'text/plain') - self.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - self.commit() - rset = self.execute('TrInfo T ORDERBY T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 3) - tr = rset.get_entity(2, 0) - #tr.complete() - self.assertEquals(tr.comment, u'il est pas sage celui-la') - self.assertEquals(tr.comment_format, u'text/plain') - self.assertEquals(tr.from_state[0].eid, self.s_deactivated) - self.assertEquals(tr.to_state[0].eid, self.s_activated) - self.assertEquals(tr.owned_by[0].login, 'admin') - - def test_transition_information_on_creation(self): - ueid = self.create_user('toto') - rset = self.execute('TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) - tr = rset.get_entity(0, 0) - #tr.complete() - self.assertEquals(tr.comment, None) - self.assertEquals(tr.from_state, []) - self.assertEquals(tr.to_state[0].eid, self.s_activated) - - def test_std_users_can_create_trinfo(self): - self.create_user('toto') - cnx = self.login('toto') - cu = cnx.cursor() - self.failUnless(cu.execute("INSERT Note X: X type 'a', X in_state S WHERE S name 'todo'")) - cnx.commit() - - def test_metadata_cwuri(self): - eid = self.execute('INSERT Note X')[0][0] - cwuri = self.execute('Any U WHERE X eid %s, X cwuri U' % eid)[0][0] - self.assertEquals(cwuri, self.repo.config['base-url'] + 'eid/%s' % eid) - - def test_metadata_creation_modification_date(self): - _now = datetime.now() - eid = self.execute('INSERT Note X')[0][0] - creation_date, modification_date = self.execute('Any CD, MD WHERE X eid %s, ' - 'X creation_date CD, ' - 'X modification_date MD' % eid)[0] - self.assertEquals((creation_date - _now).seconds, 0) - self.assertEquals((modification_date - _now).seconds, 0) - - def test_metadata__date(self): - _now = datetime.now() - eid = self.execute('INSERT Note X')[0][0] - creation_date = self.execute('Any D WHERE X eid %s, X creation_date D' % eid)[0][0] - self.assertEquals((creation_date - _now).seconds, 0) - - def test_metadata_created_by(self): - eid = self.execute('INSERT Note X')[0][0] - self.commit() # fire operations - rset = self.execute('Any U WHERE X eid %s, X created_by U' % eid) - self.assertEquals(len(rset), 1) # make sure we have only one creator - self.assertEquals(rset[0][0], self.session.user.eid) - - def test_metadata_owned_by(self): - eid = self.execute('INSERT Note X')[0][0] - self.commit() # fire operations - rset = self.execute('Any U WHERE X eid %s, X owned_by U' % eid) - self.assertEquals(len(rset), 1) # make sure we have only one owner - self.assertEquals(rset[0][0], self.session.user.eid) - if __name__ == '__main__': unittest_main() diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_ldapuser.py Wed Sep 02 10:23:38 2009 +0200 @@ -156,7 +156,8 @@ self.patch_authenticate() cnx = self.login('syt', 'dummypassword') cu = cnx.cursor() - cu.execute('SET X in_state S WHERE X login "alf", S name "deactivated"') + alf = cu.execute('Any X WHERE X login "alf"').get_entity(0, 0) + alf.fire_transition('deactivate') try: cnx.commit() alf = self.execute('CWUser X WHERE X login "alf"').get_entity(0, 0) @@ -172,7 +173,8 @@ finally: # restore db state self.restore_connection() - self.execute('SET X in_state S WHERE X login "alf", S name "activated"') + alf = self.execute('Any X WHERE X login "alf"').get_entity(0, 0) + alf.fire_transition('activate') self.execute('DELETE X in_group G WHERE X login "syt", G name "managers"') def test_same_column_names(self): diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_migractions.py Wed Sep 02 10:23:38 2009 +0200 @@ -106,23 +106,14 @@ def test_workflow_actions(self): - foo = self.mh.cmd_add_state(u'foo', ('Personne', 'Email'), initial=True) + wf = self.mh.cmd_add_workflow(u'foo', ('Personne', 'Email')) for etype in ('Personne', 'Email'): - s1 = self.mh.rqlexec('Any N WHERE S state_of ET, ET name "%s", S name N' % - etype)[0][0] - self.assertEquals(s1, "foo") - s1 = self.mh.rqlexec('Any N WHERE ET initial_state S, ET name "%s", S name N' % + s1 = self.mh.rqlexec('Any N WHERE WF workflow_of ET, ET name "%s", WF name N' % etype)[0][0] self.assertEquals(s1, "foo") - bar = self.mh.cmd_add_state(u'bar', ('Personne', 'Email'), initial=True) - baz = self.mh.cmd_add_transition(u'baz', ('Personne', 'Email'), - (foo,), bar, ('managers',)) - for etype in ('Personne', 'Email'): - t1 = self.mh.rqlexec('Any N WHERE T transition_of ET, ET name "%s", T name N' % + s1 = self.mh.rqlexec('Any N WHERE ET default_workflow WF, ET name "%s", WF name N' % etype)[0][0] - self.assertEquals(t1, "baz") - gn = self.mh.rqlexec('Any GN WHERE T require_group G, G name GN, T eid %s' % baz)[0][0] - self.assertEquals(gn, 'managers') + self.assertEquals(s1, "foo") def test_add_entity_type(self): self.failIf('Folder2' in self.schema) @@ -160,8 +151,9 @@ self.failIf('Folder2' in self.schema) self.failIf(self.execute('CWEType X WHERE X name "Folder2"')) # test automatic workflow deletion - self.failIf(self.execute('State X WHERE NOT X state_of ET')) - self.failIf(self.execute('Transition X WHERE NOT X transition_of ET')) + self.failIf(self.execute('Workflow X WHERE NOT X workflow_of ET')) + self.failIf(self.execute('State X WHERE NOT X state_of WF')) + self.failIf(self.execute('Transition X WHERE NOT X transition_of WF')) def test_add_drop_relation_type(self): self.mh.cmd_add_entity_type('Folder2', auto=False) diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_msplanner.py Wed Sep 02 10:23:38 2009 +0200 @@ -43,18 +43,19 @@ def syntax_tree_search(self, *args, **kwargs): return [] -X_ALL_SOLS = sorted([{'X': 'Affaire'}, {'X': 'Basket'}, {'X': 'Bookmark'}, +X_ALL_SOLS = sorted([{'X': 'Affaire'}, {'X': 'BaseTransition'}, {'X': 'Basket'}, + {'X': 'Bookmark'}, {'X': 'CWAttribute'}, {'X': 'CWCache'}, + {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, {'X': 'CWEType'}, + {'X': 'CWGroup'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, + {'X': 'CWRType'}, {'X': 'CWRelation'}, {'X': 'CWUser'}, {'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, - {'X': 'CWCache'}, {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, - {'X': 'CWEType'}, {'X': 'CWAttribute'}, {'X': 'CWGroup'}, - {'X': 'CWRelation'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, - {'X': 'CWRType'}, {'X': 'CWUser'}, {'X': 'Email'}, - {'X': 'EmailAddress'}, {'X': 'EmailPart'}, {'X': 'EmailThread'}, - {'X': 'ExternalUri'}, - {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'}, - {'X': 'Note'}, {'X': 'Personne'}, {'X': 'RQLExpression'}, - {'X': 'Societe'}, {'X': 'State'}, {'X': 'SubDivision'}, - {'X': 'Tag'}, {'X': 'TrInfo'}, {'X': 'Transition'}]) + {'X': 'Email'}, {'X': 'EmailAddress'}, {'X': 'EmailPart'}, + {'X': 'EmailThread'}, {'X': 'ExternalUri'}, {'X': 'File'}, + {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'}, + {'X': 'Personne'}, {'X': 'RQLExpression'}, {'X': 'Societe'}, + {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'SubWorkflowExitPoint'}, + {'X': 'Tag'}, {'X': 'TrInfo'}, {'X': 'Transition'}, + {'X': 'Workflow'}, {'X': 'WorkflowTransition'}]) # keep cnx so it's not garbage collected and the associated session is closed @@ -770,12 +771,13 @@ [{'X': 'Basket'}]), ('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser', [{'X': 'CWUser'}]), - ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, State, SubDivision, Tag, Transition)', - [{'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, - {'X': 'Email'}, {'X': 'EmailThread'}, {'X': 'File'}, - {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'}, - {'X': 'Personne'}, {'X': 'Societe'}, {'X': 'State'}, - {'X': 'SubDivision'}, {'X': 'Tag'}, {'X': 'Transition'}]),], + ('Any X WHERE X has_text "bla", X is IN(BaseTransition, Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, State, SubDivision, Tag, Transition, Workflow, WorkflowTransition)', + [{'X': 'BaseTransition'}, {'X': 'Card'}, {'X': 'Comment'}, + {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'}, + {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'}, + {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'}, + {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'Tag'}, + {'X': 'Transition'}, {'X': 'Workflow'}, {'X': 'WorkflowTransition'}]),], None, None, [self.system], {}, []), ]) ]) @@ -793,25 +795,27 @@ [self.system], {'E': 'table1.C0'}, {'X': 'table0.C0'}, []), ('FetchStep', [('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is Basket', - [{'X': 'Basket'}]), - ('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser', - [{'X': 'CWUser'}]), - ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, State, SubDivision, Tag, Transition)', - [{'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, - {'X': 'Email'}, {'X': 'EmailThread'}, {'X': 'File'}, - {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'}, - {'X': 'Personne'}, {'X': 'Societe'}, {'X': 'State'}, - {'X': 'SubDivision'}, {'X': 'Tag'}, {'X': 'Transition'}]),], + [{'X': 'Basket'}]), + ('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser', + [{'X': 'CWUser'}]), + ('Any X WHERE X has_text "bla", X is IN(BaseTransition, Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, State, SubDivision, Tag, Transition, Workflow, WorkflowTransition)', + [{'X': 'BaseTransition'}, {'X': 'Card'}, {'X': 'Comment'}, + {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'}, + {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'}, + {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'}, + {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'Tag'}, + {'X': 'Transition'}, {'X': 'Workflow'}, {'X': 'WorkflowTransition'}])], [self.system], {}, {'X': 'table0.C0'}, []), ]), ('OneFetchStep', [('Any X LIMIT 10 OFFSET 10', - [{'X': 'Affaire'}, {'X': 'Basket'}, {'X': 'Card'}, - {'X': 'Comment'}, {'X': 'Division'}, {'X': 'CWUser'}, - {'X': 'Email'}, {'X': 'EmailThread'}, {'X': 'File'}, - {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'}, - {'X': 'Personne'}, {'X': 'Societe'}, {'X': 'State'}, - {'X': 'SubDivision'}, {'X': 'Tag'}, {'X': 'Transition'}])], + [{'X': 'Affaire'}, {'X': 'BaseTransition'}, {'X': 'Basket'}, + {'X': 'CWUser'}, {'X': 'Card'}, {'X': 'Comment'}, + {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'}, + {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'}, + {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'}, + {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'Tag'}, + {'X': 'Transition'}, {'X': 'Workflow'}, {'X': 'WorkflowTransition'}])], 10, 10, [self.system], {'X': 'table0.C0'}, []) ]) @@ -874,16 +878,23 @@ [{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])], [self.cards, self.system], {}, {'X': 'table0.C0'}, []), ('FetchStep', - [('Any X WHERE X is IN(Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, Tag, TrInfo, Transition)', - sorted([{'X': 'Bookmark'}, {'X': 'Comment'}, {'X': 'Division'}, - {'X': 'CWCache'}, {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, - {'X': 'CWEType'}, {'X': 'CWAttribute'}, {'X': 'CWGroup'}, - {'X': 'CWRelation'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, - {'X': 'CWRType'}, {'X': 'Email'}, {'X': 'EmailAddress'}, - {'X': 'EmailPart'}, {'X': 'EmailThread'}, {'X': 'ExternalUri'}, {'X': 'File'}, - {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Personne'}, - {'X': 'RQLExpression'}, {'X': 'Societe'}, {'X': 'SubDivision'}, - {'X': 'Tag'}, {'X': 'TrInfo'}, {'X': 'Transition'}]))], + [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', + [{'X': 'BaseTransition'}, {'X': 'Bookmark'}, + {'X': 'CWAttribute'}, {'X': 'CWCache'}, + {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, + {'X': 'CWEType'}, {'X': 'CWGroup'}, + {'X': 'CWPermission'}, {'X': 'CWProperty'}, + {'X': 'CWRType'}, {'X': 'CWRelation'}, + {'X': 'Comment'}, {'X': 'Division'}, + {'X': 'Email'}, {'X': 'EmailAddress'}, + {'X': 'EmailPart'}, {'X': 'EmailThread'}, + {'X': 'ExternalUri'}, {'X': 'File'}, + {'X': 'Folder'}, {'X': 'Image'}, + {'X': 'Personne'}, {'X': 'RQLExpression'}, + {'X': 'Societe'}, {'X': 'SubDivision'}, + {'X': 'SubWorkflowExitPoint'}, {'X': 'Tag'}, + {'X': 'TrInfo'}, {'X': 'Transition'}, + {'X': 'Workflow'}, {'X': 'WorkflowTransition'}])], [self.system], {}, {'X': 'table0.C0'}, []), ]), ('FetchStep', [('Any X WHERE EXISTS(X owned_by 5), X is CWUser', [{'X': 'CWUser'}])], @@ -899,6 +910,11 @@ def test_security_complex_aggregat2(self): # use a guest user self.session = self._user_session()[1] + X_ET_ALL_SOLS = [] + for s in X_ALL_SOLS: + ets = {'ET': 'CWEType'} + ets.update(s) + X_ET_ALL_SOLS.append(ets) self._test('Any ET, COUNT(X) GROUPBY ET ORDERBY ET WHERE X is ET', [('FetchStep', [('Any X WHERE X is IN(Card, Note, State)', [{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])], @@ -923,23 +939,24 @@ [self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), # extra UnionFetchStep could be avoided but has no cost, so don't care ('UnionFetchStep', - [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, Tag, TrInfo, Transition)', - [{'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'Comment', 'ET': 'CWEType'}, - {'X': 'Division', 'ET': 'CWEType'}, {'X': 'CWCache', 'ET': 'CWEType'}, - {'X': 'CWConstraint', 'ET': 'CWEType'}, {'X': 'CWConstraintType', 'ET': 'CWEType'}, - {'X': 'CWEType', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'}, - {'X': 'CWGroup', 'ET': 'CWEType'}, {'X': 'CWRelation', 'ET': 'CWEType'}, - {'X': 'CWPermission', 'ET': 'CWEType'}, {'X': 'CWProperty', 'ET': 'CWEType'}, - {'X': 'CWRType', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'}, + [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', + [{'X': 'BaseTransition', 'ET': 'CWEType'}, + {'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'}, + {'X': 'CWCache', 'ET': 'CWEType'}, {'X': 'CWConstraint', 'ET': 'CWEType'}, + {'X': 'CWConstraintType', 'ET': 'CWEType'}, {'X': 'CWEType', 'ET': 'CWEType'}, + {'X': 'CWGroup', 'ET': 'CWEType'}, {'X': 'CWPermission', 'ET': 'CWEType'}, + {'X': 'CWProperty', 'ET': 'CWEType'}, {'X': 'CWRType', 'ET': 'CWEType'}, + {'X': 'CWRelation', 'ET': 'CWEType'}, {'X': 'Comment', 'ET': 'CWEType'}, + {'X': 'Division', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'}, {'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'}, - {'X': 'EmailThread', 'ET': 'CWEType'}, - {'ET': 'CWEType', 'X': 'ExternalUri'}, - {'X': 'File', 'ET': 'CWEType'}, - {'X': 'Folder', 'ET': 'CWEType'}, {'X': 'Image', 'ET': 'CWEType'}, - {'X': 'Personne', 'ET': 'CWEType'}, {'X': 'RQLExpression', 'ET': 'CWEType'}, - {'X': 'Societe', 'ET': 'CWEType'}, {'X': 'SubDivision', 'ET': 'CWEType'}, + {'X': 'EmailThread', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ET': 'CWEType'}, + {'X': 'File', 'ET': 'CWEType'}, {'X': 'Folder', 'ET': 'CWEType'}, + {'X': 'Image', 'ET': 'CWEType'}, {'X': 'Personne', 'ET': 'CWEType'}, + {'X': 'RQLExpression', 'ET': 'CWEType'}, {'X': 'Societe', 'ET': 'CWEType'}, + {'X': 'SubDivision', 'ET': 'CWEType'}, {'X': 'SubWorkflowExitPoint', 'ET': 'CWEType'}, {'X': 'Tag', 'ET': 'CWEType'}, {'X': 'TrInfo', 'ET': 'CWEType'}, - {'X': 'Transition', 'ET': 'CWEType'}])], + {'X': 'Transition', 'ET': 'CWEType'}, {'X': 'Workflow', 'ET': 'CWEType'}, + {'X': 'WorkflowTransition', 'ET': 'CWEType'}])], [self.system], {}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), ('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(Card, Note, State)', @@ -950,26 +967,7 @@ ]), ]), ('OneFetchStep', - [('Any ET,COUNT(X) GROUPBY ET ORDERBY ET', - sorted([{'ET': 'CWEType', 'X': 'Affaire'}, {'ET': 'CWEType', 'X': 'Basket'}, - {'ET': 'CWEType', 'X': 'Bookmark'}, {'ET': 'CWEType', 'X': 'Card'}, - {'ET': 'CWEType', 'X': 'Comment'}, {'ET': 'CWEType', 'X': 'Division'}, - {'ET': 'CWEType', 'X': 'CWCache'}, {'ET': 'CWEType', 'X': 'CWConstraint'}, - {'ET': 'CWEType', 'X': 'CWConstraintType'}, {'ET': 'CWEType', 'X': 'CWEType'}, - {'ET': 'CWEType', 'X': 'CWAttribute'}, {'ET': 'CWEType', 'X': 'CWGroup'}, - {'ET': 'CWEType', 'X': 'CWRelation'}, {'ET': 'CWEType', 'X': 'CWPermission'}, - {'ET': 'CWEType', 'X': 'CWProperty'}, {'ET': 'CWEType', 'X': 'CWRType'}, - {'ET': 'CWEType', 'X': 'CWUser'}, {'ET': 'CWEType', 'X': 'Email'}, - {'ET': 'CWEType', 'X': 'EmailAddress'}, {'ET': 'CWEType', 'X': 'EmailPart'}, - {'ET': 'CWEType', 'X': 'EmailThread'}, - {'ET': 'CWEType', 'X': 'ExternalUri'}, - {'ET': 'CWEType', 'X': 'File'}, - {'ET': 'CWEType', 'X': 'Folder'}, {'ET': 'CWEType', 'X': 'Image'}, - {'ET': 'CWEType', 'X': 'Note'}, {'ET': 'CWEType', 'X': 'Personne'}, - {'ET': 'CWEType', 'X': 'RQLExpression'}, {'ET': 'CWEType', 'X': 'Societe'}, - {'ET': 'CWEType', 'X': 'State'}, {'ET': 'CWEType', 'X': 'SubDivision'}, - {'ET': 'CWEType', 'X': 'Tag'}, {'ET': 'CWEType', 'X': 'TrInfo'}, - {'ET': 'CWEType', 'X': 'Transition'}]))], + [('Any ET,COUNT(X) GROUPBY ET ORDERBY ET', X_ET_ALL_SOLS)], None, None, [self.system], {'ET': 'table0.C0', 'X': 'table0.C1'}, []) ]) @@ -1707,6 +1705,7 @@ ]) def test_nonregr2(self): + self.session.user.fire_transition('deactivate') treid = self.session.user.latest_trinfo().eid self._test('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D', [('FetchStep', [('Any X,D WHERE X modification_date D, X is Note', diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_multisources.py Wed Sep 02 10:23:38 2009 +0200 @@ -30,7 +30,7 @@ cu = cnx2.cursor() ec1 = cu.execute('INSERT Card X: X title "C3: An external card", X wikiid "aaa"')[0][0] cu.execute('INSERT Card X: X title "C4: Ze external card", X wikiid "zzz"') -aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF", X in_state S WHERE S name "pitetre"')[0][0] +aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF"')[0][0] cnx2.commit() MTIME = datetime.now() - timedelta(0, 10) @@ -122,7 +122,7 @@ cu = cnx2.cursor() assert cu.execute('Any X WHERE X eid %(x)s', {'x': aff1}, 'x') cu.execute('SET X ref "BLAH" WHERE X eid %(x)s', {'x': aff1}, 'x') - aff2 = cu.execute('INSERT Affaire X: X ref "AFFREUX", X in_state S WHERE S name "pitetre"')[0][0] + aff2 = cu.execute('INSERT Affaire X: X ref "AFFREUX"')[0][0] cnx2.commit() try: # force sync @@ -267,6 +267,7 @@ {'x': affaire.eid, 'u': ueid}) def test_nonregr2(self): + self.session.user.fire_transition('deactivate') treid = self.session.user.latest_trinfo().eid rset = self.execute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D', {'x': treid}) diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_querier.py --- a/server/test/unittest_querier.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_querier.py Wed Sep 02 10:23:38 2009 +0200 @@ -109,10 +109,10 @@ 'X': 'Affaire', 'ET': 'CWEType', 'ETN': 'String'}]) rql, solutions = partrqls[1] - self.assertEquals(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, ' - 'X is IN(Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Note, Personne, RQLExpression, Societe, State, SubDivision, Tag, TrInfo, Transition)') + self.assertEquals(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)') self.assertListEquals(sorted(solutions), - sorted([{'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'}, + sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Card', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Comment', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Division', 'ETN': 'String', 'ET': 'CWEType'}, @@ -141,9 +141,12 @@ {'X': 'Societe', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'State', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'SubDivision', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'SubWorkflowExitPoint', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Tag', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Transition', 'ETN': 'String', 'ET': 'CWEType'}, - {'X': 'TrInfo', 'ETN': 'String', 'ET': 'CWEType'}])) + {'X': 'TrInfo', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'Workflow', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'WorkflowTransition', 'ETN': 'String', 'ET': 'CWEType'}])) rql, solutions = partrqls[2] self.assertEquals(rql, 'Any ETN,X WHERE X is ET, ET name ETN, EXISTS(X owned_by %(C)s), ' @@ -285,8 +288,8 @@ self.assert_(('Personne',) in rset.description) def test_select_not_attr(self): - self.execute("INSERT Personne X: X nom 'bidule'") - self.execute("INSERT Societe X: X nom 'chouette'") + peid = self.execute("INSERT Personne X: X nom 'bidule'")[0][0] + seid = self.execute("INSERT Societe X: X nom 'chouette'")[0][0] rset = self.execute('Personne X WHERE NOT X nom "bidule"') self.assertEquals(len(rset.rows), 0, rset.rows) rset = self.execute('Personne X WHERE NOT X nom "bid"') @@ -350,27 +353,11 @@ self.assertEquals(rset.rows, [[peid1]]) def test_select_left_outer_join(self): - ueid = self.execute("INSERT CWUser X: X login 'bob', X upassword 'toto', X in_group G " - "WHERE G name 'users'")[0][0] - self.commit() - try: - rset = self.execute('Any FS,TS,C,D,U ORDERBY D DESC ' - 'WHERE WF wf_info_for X,' - 'WF from_state FS?, WF to_state TS, WF comment C,' - 'WF creation_date D, WF owned_by U, X eid %(x)s', - {'x': ueid}, 'x') - self.assertEquals(len(rset), 1) - self.execute('SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - rset = self.execute('Any FS,TS,C,D,U ORDERBY D DESC ' - 'WHERE WF wf_info_for X,' - 'WF from_state FS?, WF to_state TS, WF comment C,' - 'WF creation_date D, WF owned_by U, X eid %(x)s', - {'x': ueid}, 'x') - self.assertEquals(len(rset), 2) - finally: - self.execute('DELETE CWUser X WHERE X eid %s' % ueid) - self.commit() + rset = self.execute('DISTINCT Any G WHERE U? in_group G') + self.assertEquals(len(rset), 4) + rset = self.execute('DISTINCT Any G WHERE U? in_group G, U eid %(x)s', + {'x': self.session.user.eid}, 'x') + self.assertEquals(len(rset), 4) def test_select_ambigous_outer_join(self): teid = self.execute("INSERT Tag X: X name 'tag'")[0][0] @@ -466,12 +453,17 @@ 'WHERE RT name N, RDEF relation_type RT ' 'HAVING COUNT(RDEF) > 10') self.assertListEquals(rset.rows, - [[u'description', 11], - [u'name', 13], [u'created_by', 34], - [u'creation_date', 34], [u'cwuri', 34], - ['in_basket', 34], - [u'is', 34], [u'is_instance_of', 34], - [u'modification_date', 34], [u'owned_by', 34]]) + [[u'description_format', 13], + [u'description', 14], + [u'name', 16], + [u'created_by', 38], + [u'creation_date', 38], + [u'cwuri', 38], + [u'in_basket', 38], + [u'is', 38], + [u'is_instance_of', 38], + [u'modification_date', 38], + [u'owned_by', 38]]) def test_select_aggregat_having_dumb(self): # dumb but should not raise an error @@ -721,9 +713,9 @@ def test_select_union(self): rset = self.execute('Any X,N ORDERBY N WITH X,N BEING ' - '((Any X,N WHERE X name N, X transition_of E, E name %(name)s)' + '((Any X,N WHERE X name N, X transition_of WF, WF workflow_of E, E name %(name)s)' ' UNION ' - '(Any X,N WHERE X name N, X state_of E, E name %(name)s))', + '(Any X,N WHERE X name N, X state_of WF, WF workflow_of E, E name %(name)s))', {'name': 'CWUser'}) self.assertEquals([x[1] for x in rset.rows], ['activate', 'activated', 'deactivate', 'deactivated']) @@ -995,20 +987,18 @@ # update queries tests #################################################### def test_update_1(self): - self.execute("INSERT Personne Y: Y nom 'toto'") + peid = self.execute("INSERT Personne Y: Y nom 'toto'")[0][0] rset = self.execute('Personne X WHERE X nom "toto"') self.assertEqual(len(rset.rows), 1) - self.execute("SET X nom 'tutu', X prenom 'original' WHERE X is Personne, X nom 'toto'") + rset = self.execute("SET X nom 'tutu', X prenom 'original' WHERE X is Personne, X nom 'toto'") + self.assertEqual(tuplify(rset.rows), [(peid, 'tutu', 'original')]) rset = self.execute('Any Y, Z WHERE X is Personne, X nom Y, X prenom Z') self.assertEqual(tuplify(rset.rows), [('tutu', 'original')]) def test_update_2(self): - self.execute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto'") - #rset = self.execute('Any X, Y WHERE X nom "bidule", Y nom "toto"') - #self.assertEqual(len(rset.rows), 1) - #rset = self.execute('Any X, Y WHERE X travaille Y') - #self.assertEqual(len(rset.rows), 0) - self.execute("SET X travaille Y WHERE X nom 'bidule', Y nom 'toto'") + peid, seid = self.execute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto'")[0] + rset = self.execute("SET X travaille Y WHERE X nom 'bidule', Y nom 'toto'") + self.assertEquals(tuplify(rset.rows), [(peid, seid)]) rset = self.execute('Any X, Y WHERE X travaille Y') self.assertEqual(len(rset.rows), 1) @@ -1028,9 +1018,6 @@ rset = self.execute('Any X, Y WHERE X travaille Y') self.assertEqual(len(rset.rows), 1) -## def test_update_4(self): -## self.execute("SET X know Y WHERE X ami Y") - def test_update_multiple1(self): peid1 = self.execute("INSERT Personne Y: Y nom 'tutu'")[0][0] peid2 = self.execute("INSERT Personne Y: Y nom 'toto'")[0][0] @@ -1130,7 +1117,7 @@ """bad sql generated on the second query (destination_state is not detected as an inlined relation) """ - rset = self.execute('Any S,ES,T WHERE S state_of ET, ET name "CWUser",' + rset = self.execute('Any S,ES,T WHERE S state_of WF, WF workflow_of ET, ET name "CWUser",' 'ES allowed_transition T, T destination_state S') self.assertEquals(len(rset.rows), 2) @@ -1260,9 +1247,8 @@ def test_nonregr_set_query(self): ueid = self.execute("INSERT CWUser X: X login 'bob', X upassword 'toto'")[0][0] - self.execute("SET E in_group G, E in_state S, " - "E firstname %(firstname)s, E surname %(surname)s " - "WHERE E eid %(x)s, G name 'users', S name 'activated'", + self.execute("SET E in_group G, E firstname %(firstname)s, E surname %(surname)s " + "WHERE E eid %(x)s, G name 'users'", {'x':ueid, 'firstname': u'jean', 'surname': u'paul'}, 'x') def test_nonregr_u_owned_by_u(self): diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_repository.py Wed Sep 02 10:23:38 2009 +0200 @@ -102,7 +102,7 @@ def test_login_upassword_accent(self): repo = self.repo cnxid = repo.connect(*self.default_user_password()) - repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S, X in_group G WHERE S name "activated", G name "users"', + repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_group G WHERE G name "users"', {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')}) repo.commit(cnxid) repo.close(cnxid) @@ -112,7 +112,7 @@ repo = self.repo cnxid = repo.connect(*self.default_user_password()) # no group - repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S WHERE S name "activated"', + repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s', {'login': u"tutetute", 'passwd': 'tutetute'}) self.assertRaises(ValidationError, repo.commit, cnxid) rset = repo.execute(cnxid, 'CWUser X WHERE X login "tutetute"') @@ -190,16 +190,13 @@ repo = self.repo cnxid = repo.connect(*self.default_user_password()) # rollback state change which trigger TrInfo insertion - ueid = repo._get_session(cnxid).user.eid - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) + user = repo._get_session(cnxid).user + user.fire_transition('deactivate') + rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid}) self.assertEquals(len(rset), 1) - repo.execute(cnxid, 'SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 2) repo.rollback(cnxid) - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) + rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid}) + self.assertEquals(len(rset), 0) def test_transaction_interleaved(self): self.skip('implement me') @@ -342,6 +339,22 @@ # self.set_debug(False) # print 'test time: %.3f (time) %.3f (cpu)' % ((time() - t), clock() - c) + def test_delete_if_singlecard1(self): + note = self.add_entity('Affaire') + p1 = self.add_entity('Personne', nom=u'toto') + self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s', + {'x': note.eid, 'p': p1.eid}) + rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s', + {'x': note.eid}) + self.assertEquals(len(rset), 1) + p2 = self.add_entity('Personne', nom=u'tutu') + self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s', + {'x': note.eid, 'p': p2.eid}) + rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s', + {'x': note.eid}) + self.assertEquals(len(rset), 1) + self.assertEquals(rset.rows[0][0], p2.eid) + class DataHelpersTC(RepositoryBasedTC): @@ -485,11 +498,11 @@ def test_after_add_inline(self): """make sure after__relation hooks are deferred""" + p1 = self.add_entity('Personne', nom=u'toto') self.hm.register_hook(self._after_relation_hook, - 'after_add_relation', 'in_state') - eidp = self.execute('INSERT CWUser X: X login "toto", X upassword "tutu", X in_state S WHERE S name "activated"')[0][0] - eids = self.execute('State X WHERE X name "activated"')[0][0] - self.assertEquals(self.called, [(eidp, 'in_state', eids,)]) + 'after_add_relation', 'ecrit_par') + eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0] + self.assertEquals(self.called, [(eidn, 'ecrit_par', p1.eid,)]) def test_before_delete_inline_relation(self): """make sure before__relation hooks are called directly""" diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_rql2sql.py Wed Sep 02 10:23:38 2009 +0200 @@ -339,6 +339,9 @@ ('Any XN ORDERBY XN WHERE X name XN', '''SELECT X.cw_name +FROM cw_BaseTransition AS X +UNION ALL +SELECT X.cw_name FROM cw_Basket AS X UNION ALL SELECT X.cw_name @@ -376,6 +379,12 @@ UNION ALL SELECT X.cw_name FROM cw_Transition AS X +UNION ALL +SELECT X.cw_name +FROM cw_Workflow AS X +UNION ALL +SELECT X.cw_name +FROM cw_WorkflowTransition AS X ORDER BY 1'''), # DISTINCT, can use relation under exists scope as principal @@ -462,6 +471,9 @@ ('Any MAX(X)+MIN(X), N GROUPBY N WHERE X name N;', '''SELECT (MAX(T1.C0) + MIN(T1.C0)), T1.C1 FROM (SELECT X.cw_eid AS C0, X.cw_name AS C1 +FROM cw_BaseTransition AS X +UNION ALL +SELECT X.cw_eid AS C0, X.cw_name AS C1 FROM cw_Basket AS X UNION ALL SELECT X.cw_eid AS C0, X.cw_name AS C1 @@ -498,7 +510,13 @@ FROM cw_Tag AS X UNION ALL SELECT X.cw_eid AS C0, X.cw_name AS C1 -FROM cw_Transition AS X) AS T1 +FROM cw_Transition AS X +UNION ALL +SELECT X.cw_eid AS C0, X.cw_name AS C1 +FROM cw_Workflow AS X +UNION ALL +SELECT X.cw_eid AS C0, X.cw_name AS C1 +FROM cw_WorkflowTransition AS X) AS T1 GROUP BY T1.C1'''), ('Any MAX(X)+MIN(LENGTH(D)), N GROUPBY N ORDERBY 1, N, DF WHERE X name N, X data D, X data_format DF;', @@ -1029,8 +1047,9 @@ ('Any S,ES,T WHERE S state_of ET, ET name "CWUser", ES allowed_transition T, T destination_state S', '''SELECT T.cw_destination_state, rel_allowed_transition1.eid_from, T.cw_eid -FROM allowed_transition_relation AS rel_allowed_transition1, cw_CWEType AS ET, cw_Transition AS T, state_of_relation AS rel_state_of0 +FROM allowed_transition_relation AS rel_allowed_transition1, cw_Transition AS T, cw_Workflow AS ET, state_of_relation AS rel_state_of0 WHERE T.cw_destination_state=rel_state_of0.eid_from AND rel_state_of0.eid_to=ET.cw_eid AND ET.cw_name=CWUser AND rel_allowed_transition1.eid_to=T.cw_eid'''), + ('Any O WHERE S eid 0, S in_state O', '''SELECT S.cw_in_state FROM cw_Affaire AS S @@ -1106,11 +1125,11 @@ delete = self.rqlhelper.parse( 'DELETE X read_permission READ_PERMISSIONSUBJECT,X add_permission ADD_PERMISSIONSUBJECT,' 'X in_basket IN_BASKETSUBJECT,X delete_permission DELETE_PERMISSIONSUBJECT,' - 'X initial_state INITIAL_STATESUBJECT,X update_permission UPDATE_PERMISSIONSUBJECT,' + 'X update_permission UPDATE_PERMISSIONSUBJECT,' 'X created_by CREATED_BYSUBJECT,X is ISSUBJECT,X is_instance_of IS_INSTANCE_OFSUBJECT,' 'X owned_by OWNED_BYSUBJECT,X specializes SPECIALIZESSUBJECT,ISOBJECT is X,' - 'SPECIALIZESOBJECT specializes X,STATE_OFOBJECT state_of X,IS_INSTANCE_OFOBJECT is_instance_of X,' - 'TO_ENTITYOBJECT to_entity X,TRANSITION_OFOBJECT transition_of X,FROM_ENTITYOBJECT from_entity X ' + 'SPECIALIZESOBJECT specializes X,IS_INSTANCE_OFOBJECT is_instance_of X,' + 'TO_ENTITYOBJECT to_entity X,FROM_ENTITYOBJECT from_entity X ' 'WHERE X is CWEType') self.rqlhelper.compute_solutions(delete) def var_sols(var): @@ -1379,7 +1398,7 @@ FROM appears AS appears0, entities AS X WHERE appears0.words @@ to_tsquery('default', 'hip&hop&momo') AND appears0.uid=X.eid AND X.type='Personne'"""), - ('Any X WHERE X has_text "toto tata", X name "tutu"', + ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)', """SELECT X.cw_eid FROM appears AS appears0, cw_Basket AS X WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu @@ -1391,22 +1410,7 @@ SELECT X.cw_eid FROM appears AS appears0, cw_Folder AS X WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Image AS X -WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_State AS X -WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Tag AS X -WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Transition AS X -WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu"""), +"""), ('Personne X where X has_text %(text)s, X travaille S, S has_text %(text)s', """SELECT X.eid @@ -1543,7 +1547,7 @@ FROM appears AS appears0, entities AS X WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.eid AND X.type='Personne'"""), - ('Any X WHERE X has_text "toto tata", X name "tutu"', + ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)', """SELECT X.cw_eid FROM appears AS appears0, cw_Basket AS X WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu @@ -1555,22 +1559,7 @@ SELECT X.cw_eid FROM appears AS appears0, cw_Folder AS X WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Image AS X -WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_State AS X -WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Tag AS X -WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Transition AS X -WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu"""), +"""), )): yield t @@ -1619,7 +1608,7 @@ """SELECT X.eid FROM appears AS appears0, entities AS X WHERE MATCH (appears0.words) AGAINST ('hip hop momo' IN BOOLEAN MODE) AND appears0.uid=X.eid AND X.type='Personne'"""), - ('Any X WHERE X has_text "toto tata", X name "tutu"', + ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)', """SELECT X.cw_eid FROM appears AS appears0, cw_Basket AS X WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu @@ -1631,22 +1620,7 @@ SELECT X.cw_eid FROM appears AS appears0, cw_Folder AS X WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Image AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_State AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Tag AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Transition AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu""") +""") ] for t in self._parse(queries): yield t diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_rqlrewrite.py --- a/server/test/unittest_rqlrewrite.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_rqlrewrite.py Wed Sep 02 10:23:38 2009 +0200 @@ -101,13 +101,12 @@ def test_or(self): constraint = '(X identity U) OR (X in_state ST, CL identity U, CL in_state ST, ST name "subscribed")' - rqlst = parse('Any S WHERE S owned_by C, C eid %(u)s') + rqlst = parse('Any S WHERE S owned_by C, C eid %(u)s, S is in (CWUser, CWGroup)') rewrite(rqlst, {'C': (constraint,)}, {'u':1}) self.failUnlessEqual(rqlst.as_string(), - "Any S WHERE S owned_by C, C eid %(u)s, A eid %(B)s, " + "Any S WHERE S owned_by C, C eid %(u)s, S is IN(CWUser, CWGroup), A eid %(B)s, " "EXISTS((C identity A) OR (C in_state D, E identity A, " - "E in_state D, D name 'subscribed'), D is State, E is CWUser), " - "S is IN(Affaire, Basket, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Note, Personne, RQLExpression, Societe, State, SubDivision, Tag, TrInfo, Transition)") + "E in_state D, D name 'subscribed'), D is State, E is CWUser)") def test_simplified_rqlst(self): card_constraint = ('X in_state S, U in_group G, P require_state S,' diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_schemaserial.py Wed Sep 02 10:23:38 2009 +0200 @@ -33,12 +33,17 @@ {'description': u'', 'final': True, 'name': u'String'})]) def test_eschema2rql_specialization(self): - self.assertListEquals(list(specialize2rql(schema)), - [ - ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', - {'x': 'Division', 'et': 'Societe'}), - ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', - {'x': 'SubDivision', 'et': 'Division'})]) + self.assertListEquals(sorted(specialize2rql(schema)), + [('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'BaseTransition', 'x': 'Transition'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'BaseTransition', 'x': 'WorkflowTransition'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'Division', 'x': 'SubDivision'}), + # ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + # {'et': 'File', 'x': 'Image'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'Societe', 'x': 'Division'})]) def test_rschema2rql1(self): self.assertListEquals(list(rschema2rql(schema.rschema('relation_type'))), diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_security.py --- a/server/test/unittest_security.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_security.py Wed Sep 02 10:23:38 2009 +0200 @@ -265,7 +265,7 @@ self.commit() cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() - aff2 = cu.execute("INSERT Affaire X: X sujet 'cool', X in_state S WHERE S name 'pitetre'")[0][0] + aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0] soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0] cu.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1}, ('a', 's')) @@ -347,25 +347,26 @@ def test_attribute_security_rqlexpr(self): # Note.para attribute editable by managers or if the note is in "todo" state - eid = self.execute("INSERT Note X: X para 'bidule', X in_state S WHERE S name 'done'")[0][0] + note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0) self.commit() - self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': eid}, 'x') + note.fire_transition('markasdone') + self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid}, 'x') self.commit() cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid}, 'x') self.assertRaises(Unauthorized, cnx.commit) - eid2 = cu.execute("INSERT Note X: X para 'bidule'")[0][0] + note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0) cnx.commit() - cu.execute("SET X in_state S WHERE X eid %(x)s, S name 'done'", {'x': eid2}, 'x') + note2.fire_transition('markasdone') cnx.commit() - self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': eid2}, 'x')), + self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid}, 'x')), 0) - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid2}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}, 'x') self.assertRaises(Unauthorized, cnx.commit) - cu.execute("SET X in_state S WHERE X eid %(x)s, S name 'todo'", {'x': eid2}, 'x') + note2.fire_transition('redoit') cnx.commit() - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid2}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}, 'x') cnx.commit() def test_attribute_read_security(self): @@ -398,16 +399,14 @@ cu.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"') cnx.commit() self.restore_connection() - self.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') + affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0) + affaire.fire_transition('abort') self.commit() self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')), - 2) + 1) self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",' 'X owned_by U, U login "admin"')), 1) # TrInfo at the above state change - self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",' - 'X owned_by U, U login "iaminusersgrouponly"')), - 1) # TrInfo created at creation time cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() cu.execute('DELETE Affaire X WHERE X ref "ARCT01"') @@ -499,29 +498,34 @@ self.assertRaises(Unauthorized, self.schema['Affaire'].check_perm, session, 'update', eid) cu = cnx.cursor() - cu.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') - cnx.commit() - # though changing a user state (even logged user) is reserved to managers - rql = u"SET X in_state S WHERE X eid %(x)s, S name 'deactivated'" - # XXX wether it should raise Unauthorized or ValidationError is not clear - # the best would probably ValidationError if the transition doesn't exist - # from the current state but Unauthorized if it exists but user can't pass it - self.assertRaises(ValidationError, cu.execute, rql, {'x': cnx.user(self.current_session()).eid}, 'x') + self.schema['Affaire'].set_groups('read', ('users',)) + try: + aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0) + aff.fire_transition('abort') + cnx.commit() + # though changing a user state (even logged user) is reserved to managers + user = cnx.user(self.current_session()) + # XXX wether it should raise Unauthorized or ValidationError is not clear + # the best would probably ValidationError if the transition doesn't exist + # from the current state but Unauthorized if it exists but user can't pass it + self.assertRaises(ValidationError, user.fire_transition, 'deactivate') + finally: + self.schema['Affaire'].set_groups('read', ('managers',)) def test_trinfo_security(self): aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0) self.commit() + aff.fire_transition('abort') + self.commit() # can change tr info comment self.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"', - {'c': u'creation'}) + {'c': u'bouh!'}) self.commit() aff.clear_related_cache('wf_info_for', 'object') - self.assertEquals(aff.latest_trinfo().comment, 'creation') + trinfo = aff.latest_trinfo() + self.assertEquals(trinfo.comment, 'bouh!') # but not from_state/to_state - self.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') - self.commit() aff.clear_related_cache('wf_info_for', role='object') - trinfo = aff.latest_trinfo() self.assertRaises(Unauthorized, self.execute, 'SET TI from_state S WHERE TI eid %(ti)s, S name "ben non"', {'ti': trinfo.eid}, 'ti') diff -r 99ab33abf414 -r 096d680c9da2 server/test/unittest_ssplanner.py --- a/server/test/unittest_ssplanner.py Wed Sep 02 10:20:12 2009 +0200 +++ b/server/test/unittest_ssplanner.py Wed Sep 02 10:23:38 2009 +0200 @@ -9,7 +9,7 @@ from cubicweb.devtools.repotest import BasePlannerTC, test_plan from cubicweb.server.ssplanner import SSPlanner -# keep cnx so it's not garbage collected and the associated session is closed +# keep cnx so it's not garbage collected and the associated session closed repo, cnx = init_test_database('sqlite') class SSPlannerTC(BasePlannerTC): @@ -25,40 +25,20 @@ BasePlannerTC.tearDown(self) def test_ordered_ambigous_sol(self): - self._test('Any XN ORDERBY XN WHERE X name XN', - [('OneFetchStep', [('Any XN ORDERBY XN WHERE X name XN', + self._test('Any XN ORDERBY XN WHERE X name XN, X is IN (Basket, File, Folder)', + [('OneFetchStep', [('Any XN ORDERBY XN WHERE X name XN, X is IN(Basket, File, Folder)', [{'X': 'Basket', 'XN': 'String'}, - {'X': 'CWCache', 'XN': 'String'}, - {'X': 'CWConstraintType', 'XN': 'String'}, - {'X': 'CWEType', 'XN': 'String'}, - {'X': 'CWGroup', 'XN': 'String'}, - {'X': 'CWPermission', 'XN': 'String'}, - {'X': 'CWRType', 'XN': 'String'}, {'X': 'File', 'XN': 'String'}, - {'X': 'Folder', 'XN': 'String'}, - {'X': 'Image', 'XN': 'String'}, - {'X': 'State', 'XN': 'String'}, - {'X': 'Tag', u'XN': 'String'}, - {'X': 'Transition', 'XN': 'String'}])], + {'X': 'Folder', 'XN': 'String'}])], None, None, [self.system], None, [])]) def test_groupeded_ambigous_sol(self): - self._test('Any XN,COUNT(X) GROUPBY XN WHERE X name XN', - [('OneFetchStep', [('Any XN,COUNT(X) GROUPBY XN WHERE X name XN', + self._test('Any XN,COUNT(X) GROUPBY XN WHERE X name XN, X is IN (Basket, File, Folder)', + [('OneFetchStep', [('Any XN,COUNT(X) GROUPBY XN WHERE X name XN, X is IN(Basket, File, Folder)', [{'X': 'Basket', 'XN': 'String'}, - {'X': 'CWCache', 'XN': 'String'}, - {'X': 'CWConstraintType', 'XN': 'String'}, - {'X': 'CWEType', 'XN': 'String'}, - {'X': 'CWGroup', 'XN': 'String'}, - {'X': 'CWPermission', 'XN': 'String'}, - {'X': 'CWRType', 'XN': 'String'}, {'X': 'File', 'XN': 'String'}, - {'X': 'Folder', 'XN': 'String'}, - {'X': 'Image', 'XN': 'String'}, - {'X': 'State', 'XN': 'String'}, - {'X': 'Tag', u'XN': 'String'}, - {'X': 'Transition', 'XN': 'String'}])], + {'X': 'Folder', 'XN': 'String'}])], None, None, [self.system], None, [])]) diff -r 99ab33abf414 -r 096d680c9da2 sobjects/supervising.py --- a/sobjects/supervising.py Wed Sep 02 10:20:12 2009 +0200 +++ b/sobjects/supervising.py Wed Sep 02 10:23:38 2009 +0200 @@ -83,27 +83,18 @@ added.add(entity.eid) if entity.e_schema == 'TrInfo': changes.remove(change) - if entity.from_state: - try: - changes.remove( ('delete_relation', - (entity.wf_info_for[0].eid, 'in_state', - entity.from_state[0].eid)) ) - except ValueError: - pass - try: - changes.remove( ('add_relation', - (entity.wf_info_for[0].eid, 'in_state', - entity.to_state[0].eid)) ) - except ValueError: - pass - event = 'change_state' - change = (event, - (entity.wf_info_for[0], - entity.from_state[0], entity.to_state[0])) - changes.append(change) + event = 'change_state' + change = (event, + (entity.wf_info_for[0], + entity.from_state[0], entity.to_state[0])) + changes.append(change) elif event == 'delete_entity': deleted.add(changedescr[0]) index.setdefault(event, set()).add(change) + for key in ('delete_relation', 'add_relation'): + for change in index.get(key, {}).copy(): + if change[1][1] == 'in_state': + index[key].remove(change) # filter changes for eid in added: try: @@ -112,14 +103,10 @@ # skip meta-relations which are set automatically # XXX generate list below using rtags (category = 'generated') if changedescr[1] in ('created_by', 'owned_by', 'is', 'is_instance_of', - 'from_state', 'to_state', 'wf_info_for',) \ + 'from_state', 'to_state', 'by_transition', + 'wf_info_for') \ and changedescr[0] == eid: index['add_relation'].remove(change) - # skip in_state relation if the entity is being created - # XXX this may be automatized by skipping all mandatory relation - # at entity creation time - elif changedescr[1] == 'in_state' and changedescr[0] in added: - index['add_relation'].remove(change) except KeyError: break diff -r 99ab33abf414 -r 096d680c9da2 sobjects/test/unittest_notification.py --- a/sobjects/test/unittest_notification.py Wed Sep 02 10:20:12 2009 +0200 +++ b/sobjects/test/unittest_notification.py Wed Sep 02 10:23:38 2009 +0200 @@ -9,9 +9,9 @@ from socket import gethostname from logilab.common.testlib import unittest_main, TestCase -from cubicweb.devtools.apptest import EnvBasedTC +from cubicweb.devtools.apptest import EnvBasedTC, MAILBOX -from cubicweb.sobjects.notification import construct_message_id, parse_message_id +from cubicweb.common.mail import construct_message_id, parse_message_id class MessageIdTC(TestCase): def test_base(self): @@ -71,16 +71,14 @@ def test_status_change_view(self): req = self.session() - u = self.create_user('toto', req=req) - assert u.req - assert u.rset - self.execute('SET X in_state S WHERE X eid %s, S name "deactivated"' % u.eid) - v = self.vreg['views'].select('notif_status_change', req, rset=u.rset, row=0) - content = v.render(row=0, comment='yeah', - previous_state='activated', - current_state='deactivated') - # remove date - self.assertEquals(content, + u = self.create_user('toto', req=req)#, commit=False) XXX in cw 3.6, and remove set_pool + req.set_pool() + u.fire_transition('deactivate', comment=u'yeah') + self.failIf(MAILBOX) + self.commit() + self.assertEquals(len(MAILBOX), 1) + email = MAILBOX[0] + self.assertEquals(email.content, ''' admin changed status from to for entity 'toto' @@ -89,7 +87,7 @@ url: http://testing.fr/cubicweb/cwuser/toto ''') - self.assertEquals(v.subject(), 'status changed cwuser #%s (admin)' % u.eid) + self.assertEquals(email.subject, 'status changed cwuser #%s (admin)' % u.eid) if __name__ == '__main__': unittest_main() diff -r 99ab33abf414 -r 096d680c9da2 sobjects/test/unittest_supervising.py --- a/sobjects/test/unittest_supervising.py Wed Sep 02 10:20:12 2009 +0200 +++ b/sobjects/test/unittest_supervising.py Wed Sep 02 10:23:38 2009 +0200 @@ -28,12 +28,11 @@ def test_supervision(self): session = self.session() # do some modification - ueid = self.execute('INSERT CWUser X: X login "toto", X upassword "sosafe", X in_group G, X in_state S ' - 'WHERE G name "users", S name "activated"')[0][0] - self.execute('SET X last_login_time NOW WHERE X eid %(x)s', {'x': ueid}, 'x') - self.execute('SET X in_state S WHERE X login "anon", S name "deactivated"') + user = self.execute('INSERT CWUser X: X login "toto", X upassword "sosafe", X in_group G ' + 'WHERE G name "users"').get_entity(0, 0) + self.execute('SET X last_login_time NOW WHERE X eid %(x)s', {'x': user.eid}, 'x') self.execute('DELETE Card B WHERE B title "une news !"') - self.execute('SET X bookmarked_by U WHERE X is Bookmark, U eid %(x)s', {'x': ueid}, 'x') + self.execute('SET X bookmarked_by U WHERE X is Bookmark, U eid %(x)s', {'x': user.eid}, 'x') self.execute('SET X content "duh?" WHERE X is Comment') self.execute('DELETE X comments Y WHERE Y is Card, Y title "une autre news !"') # check only one supervision email operation @@ -62,17 +61,31 @@ * updated comment #EID (#EID) http://testing.fr/cubicweb/comment/EID -* deleted relation comments from comment #EID to card #EID - -* changed state of cwuser #EID (anon) - from state activated to state deactivated - http://testing.fr/cubicweb/cwuser/anon''', +* deleted relation comments from comment #EID to card #EID''', data) # check prepared email op._prepare_email() self.assertEquals(len(op.to_send), 1) self.assert_(op.to_send[0][0]) self.assertEquals(op.to_send[0][1], ['test@logilab.fr']) + self.commit() + # some other changes ####### + user.fire_transition('deactivate') + sentops = [op for op in session.pending_operations + if isinstance(op, SupervisionMailOp)] + self.assertEquals(len(sentops), 1) + # check view content + op = sentops[0] + view = sentops[0]._get_view() + data = view.render(changes=session.transaction_data.get('pendingchanges')).strip() + data = re.sub('#\d+', '#EID', data) + data = re.sub('/\d+', '/EID', data) + self.assertTextEquals('''user admin has made the following change(s): + +* changed state of cwuser #EID (toto) + from state activated to state deactivated + http://testing.fr/cubicweb/cwuser/toto''', + data) def test_nonregr1(self): session = self.session() diff -r 99ab33abf414 -r 096d680c9da2 test/unittest_entity.py --- a/test/unittest_entity.py Wed Sep 02 10:20:12 2009 +0200 +++ b/test/unittest_entity.py Wed Sep 02 10:23:38 2009 +0200 @@ -76,8 +76,8 @@ e = self.entity('Any X WHERE X eid %(x)s', {'x':user.eid}, 'x') self.assertEquals(e.use_email[0].address, "toto@logilab.org") self.assertEquals(e.use_email[0].eid, adeleid) - usereid = self.execute('INSERT CWUser X: X login "toto", X upassword "toto", X in_group G, X in_state S ' - 'WHERE G name "users", S name "activated"')[0][0] + usereid = self.execute('INSERT CWUser X: X login "toto", X upassword "toto", X in_group G ' + 'WHERE G name "users"')[0][0] e = self.entity('Any X WHERE X eid %(x)s', {'x':usereid}, 'x') e.copy_relations(user.eid) self.failIf(e.use_email) @@ -85,14 +85,14 @@ def test_copy_with_non_initial_state(self): user = self.user() - eid = self.execute('INSERT CWUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"', - {'pwd': 'toto'})[0][0] + user = self.execute('INSERT CWUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"', + {'pwd': 'toto'}).get_entity(0, 0) self.commit() - self.execute('SET X in_state S WHERE X eid %(x)s, S name "deactivated"', {'x': eid}, 'x') + user.fire_transition('deactivate') self.commit() eid2 = self.execute('INSERT CWUser X: X login "tutu", X upassword %(pwd)s', {'pwd': 'toto'})[0][0] e = self.entity('Any X WHERE X eid %(x)s', {'x': eid2}, 'x') - e.copy_relations(eid) + e.copy_relations(user.eid) self.commit() e.clear_related_cache('in_state', 'subject') self.assertEquals(e.state, 'activated') @@ -132,7 +132,8 @@ seschema.subject_relation('evaluee').set_rproperty(seschema, Note.e_schema, 'cardinality', '1*') # testing basic fetch_attrs attribute self.assertEquals(Personne.fetch_rql(user), - 'Any X,AA,AB,AC ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC') + 'Any X,AA,AB,AC ORDERBY AA ASC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC') pfetch_attrs = Personne.fetch_attrs sfetch_attrs = Societe.fetch_attrs try: @@ -142,18 +143,21 @@ # testing one non final relation Personne.fetch_attrs = ('nom', 'prenom', 'travaille') self.assertEquals(Personne.fetch_rql(user), - 'Any X,AA,AB,AC,AD ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X travaille AC, AC nom AD') + 'Any X,AA,AB,AC,AD ORDERBY AA ASC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') # testing two non final relations Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee') self.assertEquals(Personne.fetch_rql(user), - 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC WHERE X is Personne, X nom AA, ' - 'X prenom AB, X travaille AC, AC nom AD, X evaluee AE, AE modification_date AF') + 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' + 'X evaluee AE?, AE modification_date AF') # testing one non final relation with recursion Personne.fetch_attrs = ('nom', 'prenom', 'travaille') Societe.fetch_attrs = ('nom', 'evaluee') self.assertEquals(Personne.fetch_rql(user), - 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC WHERE X is Personne, X nom AA, X prenom AB, ' - 'X travaille AC, AC nom AD, AC evaluee AE, AE modification_date AF' + 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' + 'AC evaluee AE?, AE modification_date AF' ) # testing symetric relation Personne.fetch_attrs = ('nom', 'connait') @@ -323,33 +327,17 @@ self.failUnless(not p1.reverse_evaluee) def test_complete_relation(self): - self.execute('SET RT add_permission G WHERE RT name "wf_info_for", G name "managers"') - self.commit() session = self.session() - try: - eid = session.unsafe_execute( - 'INSERT TrInfo X: X comment "zou", X wf_info_for U, X from_state S1, X to_state S2 ' - 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] - trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') - trinfo.complete() - self.failUnless(trinfo.relation_cached('from_state', 'subject')) - self.failUnless(trinfo.relation_cached('to_state', 'subject')) - self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) - # check with a missing relation - eid = session.unsafe_execute( - 'INSERT TrInfo X: X comment "zou", X wf_info_for U,X to_state S2 ' - 'WHERE U login "admin", S2 name "activated"')[0][0] - trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') - trinfo.complete() - self.failUnless(isinstance(trinfo.creation_date, datetime)) - self.failUnless(trinfo.relation_cached('from_state', 'subject')) - self.failUnless(trinfo.relation_cached('to_state', 'subject')) - self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) - self.assertEquals(trinfo.from_state, []) - finally: - self.rollback() - self.execute('DELETE RT add_permission G WHERE RT name "wf_info_for", G name "managers"') - self.commit() + eid = session.unsafe_execute( + 'INSERT TrInfo X: X comment "zou", X wf_info_for U, X from_state S1, X to_state S2 ' + 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] + trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') + trinfo.complete() + self.failUnless(isinstance(trinfo['creation_date'], datetime)) + self.failUnless(trinfo.relation_cached('from_state', 'subject')) + self.failUnless(trinfo.relation_cached('to_state', 'subject')) + self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) + self.assertEquals(trinfo.by_transition, []) def test_request_cache(self): req = self.request() diff -r 99ab33abf414 -r 096d680c9da2 test/unittest_schema.py --- a/test/unittest_schema.py Wed Sep 02 10:20:12 2009 +0200 +++ b/test/unittest_schema.py Wed Sep 02 10:23:38 2009 +0200 @@ -145,7 +145,7 @@ self.assertEquals(schema.name, 'data') entities = [str(e) for e in schema.entities()] entities.sort() - expected_entities = ['Bookmark', 'Boolean', 'Bytes', 'Card', + expected_entities = ['BaseTransition', 'Bookmark', 'Boolean', 'Bytes', 'Card', 'Date', 'Datetime', 'Decimal', 'CWCache', 'CWConstraint', 'CWConstraintType', 'CWEType', 'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation', @@ -153,19 +153,20 @@ 'ExternalUri', 'File', 'Float', 'Image', 'Int', 'Interval', 'Note', 'Password', 'Personne', 'RQLExpression', - 'Societe', 'State', 'String', 'SubNote', 'Tag', 'Time', - 'Transition', 'TrInfo'] + 'Societe', 'State', 'String', 'SubNote', 'SubWorkflowExitPoint', + 'Tag', 'Time', 'Transition', 'TrInfo', + 'Workflow', 'WorkflowTransition'] self.assertListEquals(entities, sorted(expected_entities)) relations = [str(r) for r in schema.relations()] relations.sort() - expected_relations = ['add_permission', 'address', 'alias', - 'allowed_transition', 'bookmarked_by', 'canonical', + expected_relations = ['add_permission', 'address', 'alias', 'allowed_transition', + 'bookmarked_by', 'by_transition', - 'cardinality', 'comment', 'comment_format', + 'canonical', 'cardinality', 'comment', 'comment_format', 'composite', 'condition', 'connait', 'constrained_by', 'content', - 'content_format', 'created_by', 'creation_date', 'cstrtype', 'cwuri', + 'content_format', 'created_by', 'creation_date', 'cstrtype', 'custom_workflow', 'cwuri', - 'data', 'data_encoding', 'data_format', 'defaultval', 'delete_permission', + 'data', 'data_encoding', 'data_format', 'default_workflow', 'defaultval', 'delete_permission', 'description', 'description_format', 'destination_state', 'ecrit_par', 'eid', 'evaluee', 'expression', 'exprtype', @@ -189,7 +190,7 @@ 'read_permission', 'relation_type', 'require_group', - 'specializes', 'state_of', 'surname', 'symetric', 'synopsis', + 'specializes', 'state_of', 'subworkflow', 'subworkflow_exit', 'subworkflow_state', 'surname', 'symetric', 'synopsis', 'tags', 'timestamp', 'title', 'to_entity', 'to_state', 'transition_of', 'travaille', 'type', @@ -197,13 +198,13 @@ 'value', - 'wf_info_for', 'wikiid'] + 'wf_info_for', 'wikiid', 'workflow_of'] self.assertListEquals(relations, expected_relations) eschema = schema.eschema('CWUser') rels = sorted(str(r) for r in eschema.subject_relations()) - self.assertListEquals(rels, ['created_by', 'creation_date', 'cwuri', 'eid', + self.assertListEquals(rels, ['created_by', 'creation_date', 'custom_workflow', 'cwuri', 'eid', 'evaluee', 'firstname', 'has_text', 'identity', 'in_group', 'in_state', 'is', 'is_instance_of', 'last_login_time', diff -r 99ab33abf414 -r 096d680c9da2 utils.py --- a/utils.py Wed Sep 02 10:20:12 2009 +0200 +++ b/utils.py Wed Sep 02 10:23:38 2009 +0200 @@ -320,9 +320,27 @@ self.body.getvalue()) -class AcceptMixIn(object): - """Mixin class for appobjects defining the 'accepts' attribute describing - a set of supported entity type (Any by default). +def can_do_pdf_conversion(__answer=[None]): + """pdf conversion depends on + * pyxmltrf (python package) + * fop 0.9x """ - # XXX deprecated, no more necessary - + if __answer[0] is not None: + return __answer[0] + try: + import pysixt + except ImportError: + __answer[0] = False + return False + from subprocess import Popen, STDOUT + import os + try: + Popen(['/usr/bin/fop', '-q'], + stdout=open(os.devnull, 'w'), + stderr=STDOUT) + except OSError, e: + print e + __answer[0] = False + return False + __answer[0] = True + return True diff -r 99ab33abf414 -r 096d680c9da2 vregistry.py --- a/vregistry.py Wed Sep 02 10:20:12 2009 +0200 +++ b/vregistry.py Wed Sep 02 10:23:38 2009 +0200 @@ -171,7 +171,7 @@ raise `NoSelectableObject` if not object apply """ if len(args) > 1: - warn('only the request param can not be named when calling select', + warn('[3.5] only the request param can not be named when calling select*', DeprecationWarning, stacklevel=3) score, winners = 0, [] for appobject in appobjects: diff -r 99ab33abf414 -r 096d680c9da2 web/__init__.py --- a/web/__init__.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/__init__.py Wed Sep 02 10:23:38 2009 +0200 @@ -16,7 +16,7 @@ from logilab.common.deprecation import deprecated -from cubicweb.common.uilib import urlquote +from urllib import quote as urlquote from cubicweb.web._exceptions import * diff -r 99ab33abf414 -r 096d680c9da2 web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Wed Sep 02 10:20:12 2009 +0200 +++ b/web/data/cubicweb.ajax.js Wed Sep 02 10:23:38 2009 +0200 @@ -63,7 +63,12 @@ roundedCorners(node); } loadDynamicFragments(node); - jQuery(CubicWeb).trigger('ajax-loaded'); + // XXX simulates document.ready, but the former + // only runs once, this one potentially many times + // we probably need to unbind the fired events + // When this is done, jquery.treeview.js (for instance) + // can be unpatched. + jQuery(CubicWeb).trigger('ajax-loaded'); } /* cubicweb loadxhtml plugin to make jquery handle xhtml response diff -r 99ab33abf414 -r 096d680c9da2 web/data/cubicweb.css --- a/web/data/cubicweb.css Wed Sep 02 10:20:12 2009 +0200 +++ b/web/data/cubicweb.css Wed Sep 02 10:23:38 2009 +0200 @@ -843,3 +843,11 @@ border-color:#edecd2 #cfceb7 #cfceb7 #edecd2; background: #fffff8 url("button.png") bottom left repeat-x; } + +/********************************/ +/* placement of alt. view icons */ +/********************************/ + +.otherView { + float: right; +} diff -r 99ab33abf414 -r 096d680c9da2 web/data/cubicweb.widgets.js --- a/web/data/cubicweb.widgets.js Wed Sep 02 10:20:12 2009 +0200 +++ b/web/data/cubicweb.widgets.js Wed Sep 02 10:23:38 2009 +0200 @@ -181,13 +181,6 @@ } } -Widgets.TreeView = defclass("TreeView", null, { - __init__: function(wdgnode) { - jQuery(wdgnode).treeview({toggle: toggleTree, - prerendered: true}); - } -}); - /* widget based on SIMILE's timeline widget * http://code.google.com/p/simile-widgets/ diff -r 99ab33abf414 -r 096d680c9da2 web/data/jquery.treeview.js --- a/web/data/jquery.treeview.js Wed Sep 02 10:20:12 2009 +0200 +++ b/web/data/jquery.treeview.js Wed Sep 02 10:23:38 2009 +0200 @@ -1,6 +1,6 @@ /* * Treeview 1.4 - jQuery plugin to hide and show branches of a tree - * + * * http://bassistance.de/jquery-plugins/jquery-plugin-treeview/ * http://docs.jquery.com/Plugins/Treeview * @@ -11,5 +11,6 @@ * http://www.gnu.org/licenses/gpl.html * * Revision: $Id: jquery.treeview.js 4684 2008-02-07 19:08:06Z joern.zaefferer $ + * updated by Aurelien Campeas, 2009-09-01, to handle top-level ajax loads * - */;(function($){$.extend($.fn,{swapClass:function(c1,c2){var c1Elements=this.filter('.'+c1);this.filter('.'+c2).removeClass(c2).addClass(c1);c1Elements.removeClass(c1).addClass(c2);return this;},replaceClass:function(c1,c2){return this.filter('.'+c1).removeClass(c1).addClass(c2).end();},hoverClass:function(className){className=className||"hover";return this.hover(function(){$(this).addClass(className);},function(){$(this).removeClass(className);});},heightToggle:function(animated,callback){animated?this.animate({height:"toggle"},animated,callback):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();if(callback)callback.apply(this,arguments);});},heightHide:function(animated,callback){if(animated){this.animate({height:"hide"},animated,callback);}else{this.hide();if(callback)this.each(callback);}},prepareBranches:function(settings){if(!settings.prerendered){this.filter(":last-child:not(ul)").addClass(CLASSES.last);this.filter((settings.collapsed?"":"."+CLASSES.closed)+":not(."+CLASSES.open+")").find(">ul").hide();}return this.filter(":has(>ul)");},applyClasses:function(settings,toggler){this.filter(":has(>ul):not(:has(>a))").find(">span").click(function(event){toggler.apply($(this).next());}).add($("a",this)).hoverClass();if(!settings.prerendered){this.filter(":has(>ul:hidden)").addClass(CLASSES.expandable).replaceClass(CLASSES.last,CLASSES.lastExpandable);this.not(":has(>ul:hidden)").addClass(CLASSES.collapsable).replaceClass(CLASSES.last,CLASSES.lastCollapsable);this.prepend("
").find("div."+CLASSES.hitarea).each(function(){var classes="";$.each($(this).parent().attr("class").split(" "),function(){classes+=this+"-hitarea ";});$(this).addClass(classes);});}this.find("div."+CLASSES.hitarea).click(toggler);},treeview:function(settings){settings=$.extend({cookieId:"treeview"},settings);if(settings.add){return this.trigger("add",[settings.add]);}if(settings.toggle){var callback=settings.toggle;settings.toggle=function(){return callback.apply($(this).parent()[0],arguments);};}function treeController(tree,control){function handler(filter){return function(){toggler.apply($("div."+CLASSES.hitarea,tree).filter(function(){return filter?$(this).parent("."+filter).length:true;}));return false;};}$("a:eq(0)",control).click(handler(CLASSES.collapsable));$("a:eq(1)",control).click(handler(CLASSES.expandable));$("a:eq(2)",control).click(handler());}function toggler(){$(this).parent().find(">.hitarea").swapClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).swapClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().swapClass(CLASSES.collapsable,CLASSES.expandable).swapClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightToggle(settings.animated,settings.toggle);if(settings.unique){$(this).parent().siblings().find(">.hitarea").replaceClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).replaceClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().replaceClass(CLASSES.collapsable,CLASSES.expandable).replaceClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightHide(settings.animated,settings.toggle);}}function serialize(){function binary(arg){return arg?1:0;}var data=[];branches.each(function(i,e){data[i]=$(e).is(":has(>ul:visible)")?1:0;});$.cookie(settings.cookieId,data.join(""));}function deserialize(){var stored=$.cookie(settings.cookieId);if(stored){var data=stored.split("");branches.each(function(i,e){$(e).find(">ul")[parseInt(data[i])?"show":"hide"]();});}}this.addClass("treeview");var branches=this.find("li").prepareBranches(settings);switch(settings.persist){case"cookie":var toggleCallback=settings.toggle;settings.toggle=function(){serialize();if(toggleCallback){toggleCallback.apply(this,arguments);}};deserialize();break;case"location":var current=this.find("a").filter(function(){return this.href.toLowerCase()==location.href.toLowerCase();});if(current.length){current.addClass("selected").parents("ul, li").add(current.next()).show();}break;}branches.applyClasses(settings,toggler);if(settings.control){treeController(this,settings.control);$(settings.control).show();}return this.bind("add",function(event,branches){$(branches).prev().removeClass(CLASSES.last).removeClass(CLASSES.lastCollapsable).removeClass(CLASSES.lastExpandable).find(">.hitarea").removeClass(CLASSES.lastCollapsableHitarea).removeClass(CLASSES.lastExpandableHitarea);$(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings,toggler);});}});var CLASSES=$.fn.treeview.classes={open:"open",closed:"closed",expandable:"expandable",expandableHitarea:"expandable-hitarea",lastExpandableHitarea:"lastExpandable-hitarea",collapsable:"collapsable",collapsableHitarea:"collapsable-hitarea",lastCollapsableHitarea:"lastCollapsable-hitarea",lastCollapsable:"lastCollapsable",lastExpandable:"lastExpandable",last:"last",hitarea:"hitarea"};$.fn.Treeview=$.fn.treeview;})(jQuery); \ No newline at end of file + */;(function($){$.extend($.fn,{swapClass:function(c1,c2){var c1Elements=this.filter('.'+c1);this.filter('.'+c2).removeClass(c2).addClass(c1);c1Elements.removeClass(c1).addClass(c2);return this;},replaceClass:function(c1,c2){return this.filter('.'+c1).removeClass(c1).addClass(c2).end();},hoverClass:function(className){className=className||"hover";return this.hover(function(){$(this).addClass(className);},function(){$(this).removeClass(className);});},heightToggle:function(animated,callback){animated?this.animate({height:"toggle"},animated,callback):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();if(callback)callback.apply(this,arguments);});},heightHide:function(animated,callback){if(animated){this.animate({height:"hide"},animated,callback);}else{this.hide();if(callback)this.each(callback);}},prepareBranches:function(settings){if(!settings.prerendered){this.filter(":last-child:not(ul)").addClass(CLASSES.last);this.filter((settings.collapsed?"":"."+CLASSES.closed)+":not(."+CLASSES.open+")").find(">ul").hide();}return this.filter(":has(>ul)");},applyClasses:function(settings,toggler){this.filter(":has(>ul):not(:has(>a))").find(">span").click(function(event){toggler.apply($(this).next());}).add($("a",this)).hoverClass();if(!settings.prerendered){this.filter(":has(>ul:hidden)").addClass(CLASSES.expandable).replaceClass(CLASSES.last,CLASSES.lastExpandable);this.not(":has(>ul:hidden)").addClass(CLASSES.collapsable).replaceClass(CLASSES.last,CLASSES.lastCollapsable);this.prepend("
").find("div."+CLASSES.hitarea).each(function(){var classes="";$.each($(this).parent().attr("class").split(" "),function(){classes+=this+"-hitarea ";});$(this).addClass(classes);});}this.find("div."+CLASSES.hitarea).click(toggler);},treeview:function(settings){if(this.attr('cubicweb:type')=='prepared-treeview'){return this;}this.attr('cubicweb:type','prepared-treeview');settings=$.extend({cookieId:"treeview"},settings);if(settings.add){return this.trigger("add",[settings.add]);}if(settings.toggle){var callback=settings.toggle;settings.toggle=function(){return callback.apply($(this).parent()[0],arguments);};}function treeController(tree,control){function handler(filter){return function(){toggler.apply($("div."+CLASSES.hitarea,tree).filter(function(){return filter?$(this).parent("."+filter).length:true;}));return false;};}$("a:eq(0)",control).click(handler(CLASSES.collapsable));$("a:eq(1)",control).click(handler(CLASSES.expandable));$("a:eq(2)",control).click(handler());}function toggler(){$(this).parent().find(">.hitarea").swapClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).swapClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().swapClass(CLASSES.collapsable,CLASSES.expandable).swapClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightToggle(settings.animated,settings.toggle);if(settings.unique){$(this).parent().siblings().find(">.hitarea").replaceClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).replaceClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().replaceClass(CLASSES.collapsable,CLASSES.expandable).replaceClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightHide(settings.animated,settings.toggle);}}function serialize(){function binary(arg){return arg?1:0;}var data=[];branches.each(function(i,e){data[i]=$(e).is(":has(>ul:visible)")?1:0;});$.cookie(settings.cookieId,data.join(""));}function deserialize(){var stored=$.cookie(settings.cookieId);if(stored){var data=stored.split("");branches.each(function(i,e){$(e).find(">ul")[parseInt(data[i])?"show":"hide"]();});}}this.addClass("treeview");var branches=this.find("li").prepareBranches(settings);switch(settings.persist){case"cookie":var toggleCallback=settings.toggle;settings.toggle=function(){serialize();if(toggleCallback){toggleCallback.apply(this,arguments);}};deserialize();break;case"location":var current=this.find("a").filter(function(){return this.href.toLowerCase()==location.href.toLowerCase();});if(current.length){current.addClass("selected").parents("ul, li").add(current.next()).show();}break;}branches.applyClasses(settings,toggler);if(settings.control){treeController(this,settings.control);$(settings.control).show();}return this.bind("add",function(event,branches){$(branches).prev().removeClass(CLASSES.last).removeClass(CLASSES.lastCollapsable).removeClass(CLASSES.lastExpandable).find(">.hitarea").removeClass(CLASSES.lastCollapsableHitarea).removeClass(CLASSES.lastExpandableHitarea);$(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings,toggler);});}});var CLASSES=$.fn.treeview.classes={open:"open",closed:"closed",expandable:"expandable",expandableHitarea:"expandable-hitarea",lastExpandableHitarea:"lastExpandable-hitarea",collapsable:"collapsable",collapsableHitarea:"collapsable-hitarea",lastCollapsableHitarea:"lastCollapsable-hitarea",lastCollapsable:"lastCollapsable",lastExpandable:"lastExpandable",last:"last",hitarea:"hitarea"};$.fn.Treeview=$.fn.treeview;})(jQuery); \ No newline at end of file diff -r 99ab33abf414 -r 096d680c9da2 web/data/pdf_icon.gif Binary file web/data/pdf_icon.gif has changed diff -r 99ab33abf414 -r 096d680c9da2 web/test/data/sample1.pdf Binary file web/test/data/sample1.pdf has changed diff -r 99ab33abf414 -r 096d680c9da2 web/test/data/sample1.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/data/sample1.xml Wed Sep 02 10:23:38 2009 +0200 @@ -0,0 +1,138 @@ + + + + + ] > + + + + + + + +Comet 0.2.0 (unset title) + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + \ No newline at end of file diff -r 99ab33abf414 -r 096d680c9da2 web/test/unittest_form.py --- a/web/test/unittest_form.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/test/unittest_form.py Wed Sep 02 10:23:38 2009 +0200 @@ -61,19 +61,19 @@ # should be default groups but owners, i.e. managers, users, guests self.assertEquals(unrelated, [u'guests', u'managers', u'users']) - def test_subject_in_state_vocabulary(self): - # on a new entity - e = self.etype_instance('CWUser') - form = EntityFieldsForm(self.request(), None, entity=e) - states = list(form.subject_in_state_vocabulary('in_state')) - self.assertEquals(len(states), 1) - self.assertEquals(states[0][0], u'activated') # list of (combobox view, state eid) - # on an existant entity - e = self.user() - form = EntityFieldsForm(self.request(), None, entity=e) - states = list(form.subject_in_state_vocabulary('in_state')) - self.assertEquals(len(states), 1) - self.assertEquals(states[0][0], u'deactivated') # list of (combobox view, state eid) + # def test_subject_in_state_vocabulary(self): + # # on a new entity + # e = self.etype_instance('CWUser') + # form = EntityFieldsForm(self.request(), None, entity=e) + # states = list(form.subject_in_state_vocabulary('in_state')) + # self.assertEquals(len(states), 1) + # self.assertEquals(states[0][0], u'activated') # list of (combobox view, state eid) + # # on an existant entity + # e = self.user() + # form = EntityFieldsForm(self.request(), None, entity=e) + # states = list(form.subject_in_state_vocabulary('in_state')) + # self.assertEquals(len(states), 1) + # self.assertEquals(states[0][0], u'deactivated') # list of (combobox view, state eid) def test_consider_req_form_params(self): e = self.etype_instance('CWUser') @@ -143,7 +143,7 @@ def _test_richtextfield(self, expected): class RTFForm(EntityFieldsForm): description = RichTextField() - state = self.execute('State X WHERE X name "activated", X state_of ET, ET name "CWUser"').get_entity(0, 0) + state = self.execute('State X WHERE X name "activated", X state_of WF, WF workflow_of ET, ET name "CWUser"').get_entity(0, 0) form = RTFForm(self.req, redirect_path='perdu.com', entity=state) # make it think it can use fck editor anyway form.form_field_format = lambda x: 'text/html' diff -r 99ab33abf414 -r 096d680c9da2 web/test/unittest_pdf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/unittest_pdf.py Wed Sep 02 10:23:38 2009 +0200 @@ -0,0 +1,34 @@ +from unittest import TestCase +import os.path as osp +from xml.etree.cElementTree import ElementTree, fromstring, tostring, dump + +from tempfile import NamedTemporaryFile +from subprocess import Popen as sub + +from cubicweb.utils import can_do_pdf_conversion + +from cubicweb.ext.xhtml2fo import ReportTransformer + +DATADIR = osp.join(osp.dirname(__file__), 'data') + +class PDFTC(TestCase): + + def test_xhtml_to_fop_to_pdf(self): + if not can_do_pdf_conversion(): + self.skip('dependencies not available : check pysixt and fop') + xmltree = ElementTree() + xmltree.parse(osp.join(DATADIR, 'sample1.xml')) + foptree = ReportTransformer(u'contentmain').transform(xmltree) + # next + foptmp = NamedTemporaryFile() + foptree.write(foptmp) + foptmp.flush() + pdftmp = NamedTemporaryFile() + fopproc = sub(['/usr/bin/fop', foptmp.name, pdftmp.name]) + fopproc.wait() + del foptmp + pdftmp.seek(0) # a bit superstitious + reference = open(osp.join(DATADIR, 'sample1.pdf'), 'r').read() + output = pdftmp.read() + # XXX almost equals due to ID, creation date, so it seems to fail + self.assertTextEquals(output, reference) diff -r 99ab33abf414 -r 096d680c9da2 web/test/unittest_views_editforms.py --- a/web/test/unittest_views_editforms.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/test/unittest_views_editforms.py Wed Sep 02 10:23:38 2009 +0200 @@ -53,6 +53,7 @@ ]) self.assertListEquals(rbc(e, 'generic'), [('primary_email', 'subject'), + ('custom_workflow', 'subject'), ('connait', 'subject'), ('checked_by', 'object'), ]) diff -r 99ab33abf414 -r 096d680c9da2 web/uicfg.py --- a/web/uicfg.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/uicfg.py Wed Sep 02 10:23:38 2009 +0200 @@ -149,12 +149,14 @@ # * 'application' # * 'system' # * 'schema' +# * 'hidden' # * 'subobject' (not displayed by default) indexview_etype_section = {'EmailAddress': 'subobject', 'CWUser': 'system', 'CWGroup': 'system', 'CWPermission': 'system', + 'BaseTransition': 'hidden', } diff -r 99ab33abf414 -r 096d680c9da2 web/views/autoform.py --- a/web/views/autoform.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/views/autoform.py Wed Sep 02 10:23:38 2009 +0200 @@ -12,9 +12,8 @@ from cubicweb import typed_eid from cubicweb.web import stdmsgs, uicfg -from cubicweb.web.form import FieldNotFound +from cubicweb.web import form, formwidgets as fwdgs from cubicweb.web.formfields import guess_field -from cubicweb.web import formwidgets from cubicweb.web.views import forms, editforms @@ -35,9 +34,9 @@ cwtarget = 'eformframe' cssclass = 'entityForm' copy_nav_params = True - form_buttons = [formwidgets.SubmitButton(), - formwidgets.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'), - formwidgets.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')] + form_buttons = [fwdgs.SubmitButton(), + fwdgs.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'), + fwdgs.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')] attrcategories = ('primary', 'secondary') # class attributes below are actually stored in the uicfg module since we # don't want them to be reloaded @@ -133,7 +132,7 @@ """ try: return super(AutomaticEntityForm, cls_or_self).field_by_name(name, role) - except FieldNotFound: # XXX should raise more explicit exception + except form.FieldNotFound: if eschema is None or not name in cls_or_self.schema: raise rschema = cls_or_self.schema.rschema(name) @@ -163,13 +162,13 @@ try: self.field_by_name(rschema.type, role) continue # explicitly specified - except FieldNotFound: + except form.FieldNotFound: # has to be guessed try: field = self.field_by_name(rschema.type, role, eschema=entity.e_schema) self.fields.append(field) - except FieldNotFound: + except form.FieldNotFound: # meta attribute such as _format continue self.maxrelitems = self.req.property_value('navigation.related-limit') @@ -330,7 +329,11 @@ uicfg.autoform_section.tag_subject_of(('*', 'identity', '*'), 'generated') uicfg.autoform_section.tag_object_of(('*', 'identity', '*'), 'generated') uicfg.autoform_section.tag_subject_of(('*', 'require_permission', '*'), 'generated') -uicfg.autoform_section.tag_subject_of(('*', 'wf_info_for', '*'), 'generated') +uicfg.autoform_section.tag_subject_of(('*', 'by_transition', '*'), 'primary') +uicfg.autoform_section.tag_object_of(('*', 'by_transition', '*'), 'generated') +uicfg.autoform_section.tag_object_of(('*', 'from_state', '*'), 'generated') +uicfg.autoform_section.tag_object_of(('*', 'to_state', '*'), 'generated') +uicfg.autoform_section.tag_subject_of(('*', 'wf_info_for', '*'), 'primary') uicfg.autoform_section.tag_object_of(('*', 'wf_info_for', '*'), 'generated') uicfg.autoform_section.tag_subject_of(('*', 'for_user', '*'), 'generated') uicfg.autoform_section.tag_object_of(('*', 'for_user', '*'), 'generated') @@ -349,9 +352,11 @@ uicfg.autoform_section.tag_subject_of(('*', 'primary_email', '*'), 'generic') uicfg.autoform_field_kwargs.tag_attribute(('RQLExpression', 'expression'), - {'widget': formwidgets.TextInput}) + {'widget': fwdgs.TextInput}) uicfg.autoform_field_kwargs.tag_attribute(('Bookmark', 'path'), - {'widget': formwidgets.TextInput}) + {'widget': fwdgs.TextInput}) +uicfg.autoform_field_kwargs.tag_subject_of(('TrInfo', 'wf_info_for', '*'), + {'widget': fwdgs.HiddenInput}) uicfg.autoform_is_inlined.tag_subject_of(('*', 'use_email', '*'), True) uicfg.autoform_is_inlined.tag_subject_of(('CWRelation', 'relation_type', '*'), True) diff -r 99ab33abf414 -r 096d680c9da2 web/views/basecomponents.py --- a/web/views/basecomponents.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/views/basecomponents.py Wed Sep 02 10:23:38 2009 +0200 @@ -2,6 +2,7 @@ * the rql input form * the logged user link +* pdf view link :organization: Logilab :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. @@ -214,6 +215,23 @@ self.w(u' | '.join(html)) self.w(u'
') +class PdfViewComponent(component.Component): + id = 'pdfview' + __select__ = yes() + + context = 'header' + property_defs = { + _('visible'): dict(type='Boolean', default=True, + help=_('display the pdf icon or not')), + } + + def call(self, vid): + entity = self.entity(0,0) + url = entity.absolute_url(vid=vid, __template='pdf-main-template') + self.w(u'%s' % + (xml_escape(url), self.req._('download page as pdf'))) + + def registration_callback(vreg): vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,)) diff -r 99ab33abf414 -r 096d680c9da2 web/views/basetemplates.py --- a/web/views/basetemplates.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/views/basetemplates.py Wed Sep 02 10:23:38 2009 +0200 @@ -13,7 +13,7 @@ from cubicweb.appobject import objectify_selector from cubicweb.selectors import match_kwargs from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW -from cubicweb.utils import make_uid, UStringIO +from cubicweb.utils import make_uid, UStringIO, can_do_pdf_conversion # main templates ############################################################## @@ -266,6 +266,45 @@ self.w(u'\n') self.w(u'\n') +if can_do_pdf_conversion(): + from xml.etree.cElementTree import ElementTree + from subprocess import Popen as sub + from StringIO import StringIO + from tempfile import NamedTemporaryFile + from cubicweb.ext.xhtml2fo import ReportTransformer + + class PdfMainTemplate(TheMainTemplate): + id = 'pdf-main-template' + + def call(self, view): + """build the standard view, then when it's all done, convert xhtml to pdf + """ + super(PdfMainTemplate, self).call(view) + section = self.req.form.pop('section', 'contentmain') + pdf = self.to_pdf(self._stream, section) + self.req.set_content_type('application/pdf', filename='report.pdf') + self.binary = True + self.w = None + self.set_stream() + # pylint needs help + self.w(pdf) + + def to_pdf(self, stream, section): + # XXX see ticket/345282 + stream = stream.getvalue().replace(' ', ' ').encode('utf-8') + xmltree = ElementTree() + xmltree.parse(StringIO(stream)) + foptree = ReportTransformer(section).transform(xmltree) + foptmp = NamedTemporaryFile() + pdftmp = NamedTemporaryFile() + foptree.write(foptmp) + foptmp.flush() + fopproc = sub(['/usr/bin/fop', foptmp.name, pdftmp.name]) + fopproc.wait() + pdftmp.seek(0) + pdf = pdftmp.read() + return pdf + # page parts templates ######################################################## class HTMLHeader(View): @@ -490,4 +529,4 @@ ## vregistry registration callback ############################################ def registration_callback(vreg): - vreg.register_all(globals().values(), modname=__name__) + vreg.register_all(globals().values(), __name__) diff -r 99ab33abf414 -r 096d680c9da2 web/views/boxes.py --- a/web/views/boxes.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/views/boxes.py Wed Sep 02 10:23:38 2009 +0200 @@ -65,17 +65,8 @@ not self.schema[self.rset.description[0][0]].is_final() and \ searchstate == 'normal': entity = self.rset.get_entity(0, 0) - #entity.complete() - if add_menu.items: - self.info('explicit actions defined, ignoring potential rtags for %s', - entity.e_schema) - else: - # some addrelated actions may be specified but no one is selectable - # in which case we should not fallback to schema_actions. The proper - # way to avoid this is to override add_related_schemas() on the - # entity class to return an empty list - for action in self.schema_actions(entity): - add_menu.append(action) + for action in self.schema_actions(entity): + add_menu.append(action) self.workflow_actions(entity, box) if box.is_empty() and not other_menu.is_empty(): box.items = other_menu.items @@ -113,9 +104,9 @@ """this is actually used ui method to generate 'addrelated' actions from the schema. - If you're using explicit 'addrelated' actions for an entity types, you - should probably overrides this method to return an empty list else you - may get some unexpected actions. + If you don't want any auto-generated actions, you should overrides this + method to return an empty list. If you only want some, you can configure + them by using uicfg.actionbox_appearsin_addmenu """ req = self.req eschema = entity.e_schema @@ -142,19 +133,18 @@ def workflow_actions(self, entity, box): - if 'in_state' in entity.e_schema.subject_relations() and entity.in_state: + if entity.e_schema.has_subject_relation('in_state') and entity.in_state: _ = self.req._ - state = entity.in_state[0] - menu_title = u'%s: %s' % (_('state'), state.view('text')) + menu_title = u'%s: %s' % (_('state'), entity.printable_state) menu_items = [] - for tr in state.transitions(entity): + for tr in entity.possible_transitions(): url = entity.absolute_url(vid='statuschange', treid=tr.eid) menu_items.append(self.mk_action(_(tr.name), url)) # don't propose to see wf if user can't pass any transition if menu_items: wfurl = self.build_url('cwetype/%s'%entity.e_schema, vid='workflow') menu_items.append(self.mk_action(_('view workflow'), wfurl)) - if len(entity.reverse_wf_info_for) > 1: # first item is not a transition + if entity.workflow_history: wfurl = entity.absolute_url(vid='wfhistory') menu_items.append(self.mk_action(_('view history'), wfurl)) box.append(BoxMenu(menu_title, menu_items)) diff -r 99ab33abf414 -r 096d680c9da2 web/views/forms.py --- a/web/views/forms.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/views/forms.py Wed Sep 02 10:23:38 2009 +0200 @@ -529,24 +529,24 @@ break return result - def subject_in_state_vocabulary(self, rtype, limit=None): - """vocabulary method for the in_state relation, looking for relation's - object entities (i.e. self is the subject) according to initial_state, - state_of and next_state relation - """ - entity = self.edited_entity - if not entity.has_eid() or not entity.in_state: - # get the initial state - rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S' - rset = self.req.execute(rql, {'etype': str(entity.e_schema)}) - if rset: - return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])] - return [] - results = [] - for tr in entity.in_state[0].transitions(entity): - state = tr.destination_state[0] - results.append((state.view('combobox'), state.eid)) - return sorted(results) + # def subject_in_state_vocabulary(self, rtype, limit=None): + # """vocabulary method for the in_state relation, looking for relation's + # object entities (i.e. self is the subject) according to initial_state, + # state_of and next_state relation + # """ + # entity = self.edited_entity + # if not entity.has_eid() or not entity.in_state: + # # get the initial state + # rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S' + # rset = self.req.execute(rql, {'etype': str(entity.e_schema)}) + # if rset: + # return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])] + # return [] + # results = [] + # for tr in entity.in_state[0].transitions(entity): + # state = tr.destination_state[0] + # results.append((state.view('combobox'), state.eid)) + # return sorted(results) def srelations_by_category(self, categories=None, permission=None): return () @@ -556,7 +556,7 @@ class CompositeForm(FieldsForm): - """form composed for sub-forms""" + """form composed of sub-forms""" id = 'composite' form_renderer_id = id @@ -568,3 +568,18 @@ """mark given form as a subform and append it""" subform.is_subform = True self.forms.append(subform) + + +class CompositeEntityForm(EntityFieldsForm): + """form composed of sub-forms""" + id = 'composite' + form_renderer_id = id + + def __init__(self, *args, **kwargs): + super(CompositeEntityForm, self).__init__(*args, **kwargs) + self.forms = [] + + def form_add_subform(self, subform): + """mark given form as a subform and append it""" + subform.is_subform = True + self.forms.append(subform) diff -r 99ab33abf414 -r 096d680c9da2 web/views/primary.py --- a/web/views/primary.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/views/primary.py Wed Sep 02 10:23:38 2009 +0200 @@ -232,9 +232,9 @@ for rtype in ('eid', 'creation_date', 'modification_date', 'cwuri', 'is', 'is_instance_of', 'identity', - 'owned_by', 'created_by', - 'in_state', 'wf_info_for', 'require_permission', - 'from_entity', 'to_entity', + 'owned_by', 'created_by', 'in_state', + 'wf_info_for', 'by_transition', 'from_state', 'to_state', + 'require_permission', 'from_entity', 'to_entity', 'see_also'): uicfg.primaryview_section.tag_subject_of(('*', rtype, '*'), 'hidden') uicfg.primaryview_section.tag_object_of(('*', rtype, '*'), 'hidden') diff -r 99ab33abf414 -r 096d680c9da2 web/views/treeview.py --- a/web/views/treeview.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/views/treeview.py Wed Sep 02 10:23:38 2009 +0200 @@ -32,17 +32,22 @@ if treeid is None: self.warning('Tree state won\'t be properly restored after next reload') treeid = make_uid('throw away uid') - self.w(u'
    ' % (treeid, self.css_classes)) + toplevel_thru_ajax = self.req.form.pop('treeview_top', False) + toplevel = toplevel_thru_ajax or (initial_load and not self.req.form.get('fname')) + ulid = ' ' + if toplevel: + ulid = ' id="tree-%s"' % treeid + self.w(u'' % (ulid, self.css_classes)) for rowidx in xrange(len(self.rset)): self.wview(self.itemvid, self.rset, row=rowidx, col=0, vid=subvid, parentvid=self.id, treeid=treeid) self.w(u'
') - if initial_load and not self.req.form.get('fname'): + if toplevel: self.req.add_css('jquery.treeview.css') self.req.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery.treeview.js')) self.req.html_headers.add_onload(u""" -jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid) - +jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid, + jsoncall=toplevel_thru_ajax) class FileTreeView(TreeView): """specific version of the treeview to display file trees @@ -91,14 +96,15 @@ (each item should be expandable if it's not a tree leaf) """ id = 'treeitemview' - __select__ = EntityView.__select__ & implements(ITree) # XXX + default_branch_state_is_open = False + __select__ = EntityView.__select__ & implements(ITree) def open_state(self, eeid, treeid): cookies = self.req.get_cookie() treestate = cookies.get(treecookiename(treeid)) if treestate: return str(eeid) in treestate.value.split(';') - return False + return self.default_branch_state_is_open def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview'): w = self.w diff -r 99ab33abf414 -r 096d680c9da2 web/views/workflow.py --- a/web/views/workflow.py Wed Sep 02 10:20:12 2009 +0200 +++ b/web/views/workflow.py Wed Sep 02 10:23:38 2009 +0200 @@ -20,50 +20,48 @@ from cubicweb.interfaces import IWorkflowable from cubicweb.view import EntityView from cubicweb.web import stdmsgs, action, component, form -from cubicweb.web.form import FormViewMixIn -from cubicweb.web.formfields import StringField, RichTextField -from cubicweb.web.formwidgets import HiddenInput, SubmitButton, Button +from cubicweb.web import formfields as ff, formwidgets as fwdgs from cubicweb.web.views import TmpFileViewMixin, forms # IWorkflowable views ######################################################### -class ChangeStateForm(forms.EntityFieldsForm): +class ChangeStateForm(forms.CompositeEntityForm): id = 'changestate' form_renderer_id = 'base' # don't want EntityFormRenderer - form_buttons = [SubmitButton(stdmsgs.YES), - Button(stdmsgs.NO, cwaction='cancel')] - - __method = StringField(name='__method', initial='set_state', - widget=HiddenInput) - state = StringField(eidparam=True, widget=HiddenInput) - trcomment = RichTextField(label=_('comment:'), eidparam=True) + form_buttons = [fwdgs.SubmitButton(stdmsgs.YES), + fwdgs.Button(stdmsgs.NO, cwaction='cancel')] -class ChangeStateFormView(FormViewMixIn, view.EntityView): +class ChangeStateFormView(form.FormViewMixIn, 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) - state = entity.in_state[0] transition = self.req.entity_from_eid(self.req.form['treid']) dest = transition.destination() _ = self.req._ - form = self.vreg.select('forms', 'changestate', self.req, rset=self.rset, - row=row, col=col, entity=entity, - redirect_path=self.redirectpath(entity)) + form = self.vreg['forms'].select('changestate', self.req, entity=entity, + redirect_path=self.redirectpath(entity)) self.w(form.error_message()) self.w(u'

%s %s

\n' % (_(transition.name), entity.view('oneline'))) msg = _('status will change from %(st1)s to %(st2)s') % { - 'st1': _(state.name), + 'st1': _(entity.current_state.name), 'st2': _(dest.name)} self.w(u'

%s

\n' % msg) - self.w(form.form_render(state=dest.eid, trcomment=u'', - trcomment_format=self.req.property_value('ui.default-text-format'))) + trinfo = self.vreg['etypes'].etype_class('TrInfo')(self.req) + self.initialize_varmaker() + trinfo.eid = self.varmaker.next() + subform = self.vreg['forms'].select('edition', self.req, entity=trinfo, + mainform=False) + subform.field_by_name('by_transition').widget = fwdgs.HiddenInput() + form.form_add_subform(subform) + self.w(form.form_render(wf_info_for=entity.eid, + by_transition=transition.eid)) def redirectpath(self, entity): return entity.rest_path() @@ -135,7 +133,7 @@ class ViewWorkflowAction(action.Action): id = 'workflow' - __select__ = implements('CWEType') & has_related_entities('state_of', 'object') + __select__ = implements('CWEType') & has_related_entities('workflow_of', 'object') category = 'mainactions' title = _('view workflow')