--- a/__init__.py Wed Aug 26 11:16:10 2009 +0200
+++ b/__init__.py Wed Aug 26 13:09:19 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):
--- a/__pkginfo__.py Wed Aug 26 11:16:10 2009 +0200
+++ b/__pkginfo__.py Wed Aug 26 13:09:19 2009 +0200
@@ -7,7 +7,7 @@
distname = "cubicweb"
modname = "cubicweb"
-numversion = (3, 4, 4)
+numversion = (3, 5, 0)
version = '.'.join(str(num) for num in numversion)
license = 'LGPL v2'
--- a/common/mixins.py Wed Aug 26 11:16:10 2009 +0200
+++ b/common/mixins.py Wed Aug 26 13:09:19 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,
}
--- a/common/test/unittest_mixins.py Wed Aug 26 11:16:10 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()
--- a/common/test/unittest_uilib.py Wed Aug 26 11:16:10 2009 +0200
+++ b/common/test/unittest_uilib.py Wed Aug 26 13:09:19 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 = """\
-<table class="tree">
-<tr><td class="tree_cell" rowspan="2"><div class="tree_cell">root</div></td><td class="tree_cell_1_1"> </td><td class="tree_cell_1_2"> </td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_1_1</div></td><td class="tree_cell_1_1"> </td><td class="tree_cell_1_2"> </td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_2_1</div></td><td class="tree_cell_0_1"> </td><td class="tree_cell_0_2"> </td><td rowspan="2"> </td></tr>
-<tr><td class="tree_cell_1_3"> </td><td class="tree_cell_1_4"> </td><td class="tree_cell_1_3"> </td><td class="tree_cell_1_4"> </td><td class="tree_cell_0_3"> </td><td class="tree_cell_0_4"> </td></tr>
-<tr><td rowspan="2"> </td><td class="tree_cell_2_1"> </td><td class="tree_cell_2_2"> </td><td rowspan="2"> </td><td class="tree_cell_4_1"> </td><td class="tree_cell_4_2"> </td><td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">child_2_2</div></td><td class="tree_cell_1_1"> </td><td class="tree_cell_1_2"> </td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_1</div></td></tr>
-<tr><td class="tree_cell_2_3"> </td><td class="tree_cell_2_4"> </td><td class="tree_cell_4_3"> </td><td class="tree_cell_4_4"> </td><td class="tree_cell_1_3"> </td><td class="tree_cell_1_4"> </td></tr>
-<tr><td rowspan="2"> </td><td class="tree_cell_2_1"> </td><td class="tree_cell_2_2"> </td><td rowspan="2"> </td><td class="tree_cell_0_1"> </td><td class="tree_cell_0_2"> </td><td rowspan="2"> </td><td class="tree_cell_3_1"> </td><td class="tree_cell_3_2"> </td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_2</div></td></tr>
-<tr><td class="tree_cell_2_3"> </td><td class="tree_cell_2_4"> </td><td class="tree_cell_0_3"> </td><td class="tree_cell_0_4"> </td><td class="tree_cell_3_3"> </td><td class="tree_cell_3_4"> </td></tr>
-<tr><td rowspan="2"> </td><td class="tree_cell_2_1"> </td><td class="tree_cell_2_2"> </td><td rowspan="2"> </td><td class="tree_cell_0_1"> </td><td class="tree_cell_0_2"> </td><td rowspan="2"> </td><td class="tree_cell_4_1"> </td><td class="tree_cell_4_2"> </td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_3</div></td></tr>
-<tr><td class="tree_cell_2_3"> </td><td class="tree_cell_2_4"> </td><td class="tree_cell_0_3"> </td><td class="tree_cell_0_4"> </td><td class="tree_cell_4_3"> </td><td class="tree_cell_4_4"> </td></tr>
-<tr><td rowspan="2"> </td><td class="tree_cell_4_1"> </td><td class="tree_cell_4_2"> </td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_1_2</div></td><td class="tree_cell_5_1"> </td><td class="tree_cell_5_2"> </td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_2_3</div></td><td class="tree_cell_0_1"> </td><td class="tree_cell_0_2"> </td><td rowspan="2"> </td></tr>
-<tr><td class="tree_cell_4_3"> </td><td class="tree_cell_4_4"> </td><td class="tree_cell_5_3"> </td><td class="tree_cell_5_4"> </td><td class="tree_cell_0_3"> </td><td class="tree_cell_0_4"> </td></tr>
-</table>\
-"""
-
-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()
--- a/common/uilib.py Wed Aug 26 11:16:10 2009 +0200
+++ b/common/uilib.py Wed Aug 26 13:09:19 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 <div> 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 <table>
- s = u'<table class="tree">\n'
- if caption:
- s += '<caption>%s</caption>\n' % caption
-
- for i, link_line in enumerate(links):
- line = matrix[i]
-
- s += '<tr>'
- for j, link_cell in enumerate(link_line):
- cell = line[j]
- if cell:
- if cell.id == selected_node:
- s += '<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>' % (render_node(cell.id))
- else:
- s += '<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>' % (render_node(cell.id))
- else:
- s += '<td rowspan="2"> </td>'
- s += '<td class="tree_cell_%d_1"> </td>' % link_cell
- s += '<td class="tree_cell_%d_2"> </td>' % link_cell
-
- cell = line[-1]
- if cell:
- if cell.id == selected_node:
- s += '<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>' % (render_node(cell.id))
- else:
- s += '<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>' % (render_node(cell.id))
- else:
- s += '<td rowspan="2"> </td>'
-
- s += '</tr>\n'
- if link_line:
- s += '<tr>'
- for j, link_cell in enumerate(link_line):
- s += '<td class="tree_cell_%d_3"> </td>' % link_cell
- s += '<td class="tree_cell_%d_4"> </td>' % link_cell
- s += '</tr>\n'
-
- s += '</table>'
- return s
-
-
-
# traceback formatting ########################################################
import traceback
--- a/debian/control Wed Aug 26 11:16:10 2009 +0200
+++ b/debian/control Wed Aug 26 13:09:19 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.
.
--- a/devtools/_apptest.py Wed Aug 26 11:16:10 2009 +0200
+++ b/devtools/_apptest.py Wed Aug 26 13:09:19 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)'
--- a/devtools/apptest.py Wed Aug 26 11:16:10 2009 +0200
+++ b/devtools/apptest.py Wed Aug 26 13:09:19 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,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/dataimport.py Wed Aug 26 13:09:19 2009 +0200
@@ -0,0 +1,278 @@
+# -*- 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) )
+
+ # progress callback
+ def tell(msg):
+ print msg
+
+ # create controller
+ ctl = CWImportController(RQLObjectStore())
+ ctl.askerror = True
+ ctl._tell = tell
+ 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))
+
+# 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
+
+ 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'
--- a/doc/book/en/development/testing/index.rst Wed Aug 26 11:16:10 2009 +0200
+++ b/doc/book/en/development/testing/index.rst Wed Aug 26 13:09:19 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
-------------------------
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/data/migration/postcreate.py Wed Aug 26 13:09:19 2009 +0200
@@ -0,0 +1,2 @@
+wf = add_workflow(u'bmk wf', 'Bookmark')
+wf.add_state(u'hop', initial=True)
--- a/entities/test/data/schema.py Wed Aug 26 11:16:10 2009 +0200
+++ b/entities/test/data/schema.py Wed Aug 26 13:09:19 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)
--- a/entities/test/unittest_base.py Wed Aug 26 11:16:10 2009 +0200
+++ b/entities/test/unittest_base.py Wed Aug 26 13:09:19 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):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/unittest_wfobjs.py Wed Aug 26 13:09:19 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()
--- a/entities/wfobjs.py Wed Aug 26 11:16:10 2009 +0200
+++ b/entities/wfobjs.py Wed Aug 26 13:09:19 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,121 @@
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):
+ return 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 +322,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 +343,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
--- a/entity.py Wed Aug 26 11:16:10 2009 +0200
+++ b/entity.py Wed Aug 26 13:09:19 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 <name>, 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):
--- a/interfaces.py Wed Aug 26 11:16:10 2009 +0200
+++ b/interfaces.py Wed Aug 26 13:09:19 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
--- a/misc/migration/2.42.0_Any.py Wed Aug 26 11:16:10 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')
--- a/misc/migration/2.42.1_Any.py Wed Aug 26 11:16:10 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')
-
--- a/misc/migration/2.43.0_Any.py Wed Aug 26 11:16:10 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')
--- a/misc/migration/2.44.0_Any.py Wed Aug 26 11:16:10 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)
--- a/misc/migration/2.45.0_Any.py Wed Aug 26 11:16:10 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')
--- a/misc/migration/2.46.0_Any.py Wed Aug 26 11:16:10 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')
--- a/misc/migration/2.47.0_Any.py Wed Aug 26 11:16:10 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')
-
--- a/misc/migration/2.48.8_Any.py Wed Aug 26 11:16:10 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)
--- a/misc/migration/2.49.3_Any.py Wed Aug 26 11:16:10 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')
--- a/misc/migration/2.50.0_Any.py Wed Aug 26 11:16:10 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')
--- a/misc/migration/bootstrapmigration_repository.py Wed Aug 26 11:16:10 2009 +0200
+++ b/misc/migration/bootstrapmigration_repository.py Wed Aug 26 13:09:19 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()
--- a/misc/migration/postcreate.py Wed Aug 26 11:16:10 2009 +0200
+++ b/misc/migration/postcreate.py Wed Aug 26 13:09:19 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')
--- a/schema.py Wed Aug 26 11:16:10 2009 +0200
+++ b/schema.py Wed Aug 26 13:09:19 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
--- a/schemas/workflow.py Wed Aug 26 11:16:10 2009 +0200
+++ b/schemas/workflow.py Wed Aug 26 13:09:19 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'
--- a/server/__init__.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/__init__.py Wed Aug 26 13:09:19 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
--- a/server/hooks.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/hooks.py Wed Aug 26 13:09:19 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': etype})[0][0]
+ 'Any X WHERE X is CWEType, X name %(name)s',
+ {'name': str(etype)})[0][0]
return eschema.eid
@@ -417,51 +418,124 @@
# 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
+ 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']
+ 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 +547,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 #############################################################
--- a/server/hooksmanager.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/hooksmanager.py Wed Aug 26 13:09:19 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)
--- a/server/migractions.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/migractions.py Wed Aug 26 13:09:19 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
--- a/server/repository.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/repository.py Wed Aug 26 13:09:19 2009 +0200
@@ -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")
--- a/server/schemahooks.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/schemahooks.py Wed Aug 26 13:09:19 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):
--- a/server/schemaserial.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/schemaserial.py Wed Aug 26 13:09:19 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
--- a/server/securityhooks.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/securityhooks.py Wed Aug 26 13:09:19 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):
--- a/server/session.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/session.py Wed Aug 26 13:09:19 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:
--- a/server/sources/native.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/sources/native.py Wed Aug 26 13:09:19 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
@@ -549,13 +538,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
--- a/server/ssplanner.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/ssplanner.py Wed Aug 26 13:09:19 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
--- a/server/test/data/migratedapp/schema.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/data/migratedapp/schema.py Wed Aug 26 13:09:19 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')
--- a/server/test/data/schema.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/data/schema.py Wed Aug 26 13:09:19 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'
--- a/server/test/unittest_hookhelper.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_hookhelper.py Wed Aug 26 13:09:19 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()
--- a/server/test/unittest_hooks.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_hooks.py Wed Aug 26 13:09:19 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<p>yo</p>')
+ 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()
--- a/server/test/unittest_ldapuser.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_ldapuser.py Wed Aug 26 13:09:19 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):
--- a/server/test/unittest_migractions.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_migractions.py Wed Aug 26 13:09:19 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)
--- a/server/test/unittest_msplanner.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_msplanner.py Wed Aug 26 13:09:19 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',
--- a/server/test/unittest_multisources.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_multisources.py Wed Aug 26 13:09:19 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})
--- a/server/test/unittest_querier.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_querier.py Wed Aug 26 13:09:19 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):
--- a/server/test/unittest_repository.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_repository.py Wed Aug 26 13:09:19 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_<event>_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_<event>_relation hooks are called directly"""
--- a/server/test/unittest_rql2sql.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_rql2sql.py Wed Aug 26 13:09:19 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
--- a/server/test/unittest_rqlrewrite.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_rqlrewrite.py Wed Aug 26 13:09:19 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,'
--- a/server/test/unittest_schemaserial.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_schemaserial.py Wed Aug 26 13:09:19 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'))),
--- a/server/test/unittest_security.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_security.py Wed Aug 26 13:09:19 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')
--- a/server/test/unittest_ssplanner.py Wed Aug 26 11:16:10 2009 +0200
+++ b/server/test/unittest_ssplanner.py Wed Aug 26 13:09:19 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, [])])
--- a/sobjects/supervising.py Wed Aug 26 11:16:10 2009 +0200
+++ b/sobjects/supervising.py Wed Aug 26 13:09:19 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
--- a/sobjects/test/unittest_notification.py Wed Aug 26 11:16:10 2009 +0200
+++ b/sobjects/test/unittest_notification.py Wed Aug 26 13:09:19 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 <activated> to <deactivated> 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()
--- a/sobjects/test/unittest_supervising.py Wed Aug 26 11:16:10 2009 +0200
+++ b/sobjects/test/unittest_supervising.py Wed Aug 26 13:09:19 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()
--- a/test/unittest_entity.py Wed Aug 26 11:16:10 2009 +0200
+++ b/test/unittest_entity.py Wed Aug 26 13:09:19 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()
--- a/test/unittest_schema.py Wed Aug 26 11:16:10 2009 +0200
+++ b/test/unittest_schema.py Wed Aug 26 13:09:19 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',
--- a/utils.py Wed Aug 26 11:16:10 2009 +0200
+++ b/utils.py Wed Aug 26 13:09:19 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
--- a/web/__init__.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/__init__.py Wed Aug 26 13:09:19 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 *
--- a/web/data/cubicweb.css Wed Aug 26 11:16:10 2009 +0200
+++ b/web/data/cubicweb.css Wed Aug 26 13:09:19 2009 +0200
@@ -838,4 +838,13 @@
border: 1px solid #edecd2;
border-color:#edecd2 #cfceb7 #cfceb7 #edecd2;
background: #fffff8 url("button.png") bottom left repeat-x;
-}
\ No newline at end of file
+}
+
+
+/********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+ float: right;
+}
Binary file web/data/pdf_icon.gif has changed
Binary file web/test/data/sample1.pdf has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sample1.xml Wed Aug 26 13:09:19 2009 +0200
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" [
+ <!ATTLIST html xmlns:cubicweb CDATA #FIXED 'http://www.logilab.org/2008/cubicweb' >
+
+<!ENTITY % coreattrs
+ "id ID #IMPLIED
+ class CDATA #IMPLIED
+ style CDATA #IMPLIED
+ title CDATA #IMPLIED
+
+ cubicweb:sortvalue CDATA #IMPLIED
+ cubicweb:target CDATA #IMPLIED
+ cubicweb:limit CDATA #IMPLIED
+ cubicweb:type CDATA #IMPLIED
+ cubicweb:loadtype CDATA #IMPLIED
+ cubicweb:wdgtype CDATA #IMPLIED
+ cubicweb:initfunc CDATA #IMPLIED
+ cubicweb:inputid CDATA #IMPLIED
+ cubicweb:tindex CDATA #IMPLIED
+ cubicweb:inputname CDATA #IMPLIED
+ cubicweb:value CDATA #IMPLIED
+ cubicweb:required CDATA #IMPLIED
+ cubicweb:accesskey CDATA #IMPLIED
+ cubicweb:maxlength CDATA #IMPLIED
+ cubicweb:variables CDATA #IMPLIED
+ cubicweb:displayactions CDATA #IMPLIED
+ cubicweb:fallbackvid CDATA #IMPLIED
+ cubicweb:fname CDATA #IMPLIED
+ cubicweb:vid CDATA #IMPLIED
+ cubicweb:rql CDATA #IMPLIED
+ cubicweb:actualrql CDATA #IMPLIED
+ cubicweb:rooteid CDATA #IMPLIED
+ cubicweb:dataurl CDATA #IMPLIED
+ cubicweb:size CDATA #IMPLIED
+ cubicweb:tlunit CDATA #IMPLIED
+ cubicweb:loadurl CDATA #IMPLIED
+ cubicweb:uselabel CDATA #IMPLIED
+ cubicweb:facetargs CDATA #IMPLIED
+ cubicweb:facetName CDATA #IMPLIED
+ "> ] >
+
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb" xml:lang="fr" lang="fr">
+<head>
+<base href="http://crater:8888/"></base><meta http-equiv="content-type" content="application/xhtml+xml; charset=UTF-8"/>
+<meta name="ROBOTS" content="NOINDEX" />
+<link rel="shortcut icon" href="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/favicon.ico"/>
+<link rel="alternate" type="application/rss+xml" title="RSS feed" href="http://crater:8888/project/Comet/0.2.0?vid=rss"/>
+<title>Comet 0.2.0 (unset title)</title>
+<script type="text/javascript"><!--//--><![CDATA[//><!--
+pageid = "0499a5d7add13919a458db30006d9832";
+//--><!]]></script>
+<link rel="stylesheet" type="text/css" media="all" href="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubes.tracker.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.print.css"/>
+<link rel="stylesheet" type="text/css" media="all" href="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.login.css"/>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/jquery.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/jquery.corner.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/jquery.json.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.compat.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.python.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.htmlhelpers.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.ajax.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.bookmarks.js"></script>
+<script type="text/javascript">
+jQuery(document).ready(function () {
+ jQuery("#__login:visible").focus()
+ });
+</script>
+</head>
+
+<body>
+<table id="header"><tr>
+<td id="firstcolumn"><a href="http://crater:8888/"><img class="logo" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/logo.png" alt="logo"/></a></td>
+<td id="headtext"><span id="appliName"><a href="http://crater:8888/">unset title</a></span><span class="pathbar"> > <a href="http://crater:8888/Project">projets</a> > <a href="http://crater:8888/project/Comet" title="">Comet</a> > 
+0.2.0</span></td><td>
+anonyme [<a class="logout" href="javascript: popupLoginBox();">s'authentifier</a>]</td><td><a href="http://crater:8888/doc/main" class="help" title="aide"> </a></td><td id="lastcolumn"></td>
+</tr></table>
+<div id="popupLoginBox" class="hidden"><div id="loginContent">
+<form method="post" action="http://crater:8888/project/Comet/0.2.0?vid=statussheet" id="login_form">
+<table>
+<tr>
+<td><label for="__login">identifiant</label></td><td><input name="__login" id="__login" class="data" type="text" /></td></tr><tr>
+<td><label for="__password" >mot de passe</label></td><td><input name="__password" id="__password" class="data" type="password" /></td>
+</tr><tr>
+<td> </td><td><input type="submit" class="loginButton right" value="s'identifier" />
+</td></tr>
+</table>
+</form>
+</div></div>
+
+ <div id="stateheader">
+ </div>
+ <div id="page"><table width="100%" border="0" id="mainLayout"><tr>
+<td class="navcol"><div class="navboxes">
+<div class="searchBoxFrame" id="search_box"><div class="boxTitle"><span><span onclick="javascript: toggleVisibility('rqlinput')">rechercher</span></span></div><div class="boxContent">
+<form action="http://crater:8888/view">
+<table id="tsearch"><tr><td>
+<input id="norql" type="text" accesskey="q" tabindex="1" title="search text" value="" name="rql" />
+<input type="hidden" name="__fromsearchbox" value="1" />
+<input type="hidden" name="subvid" value="tsearch" />
+</td><td>
+<input tabindex="2" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
+</td></tr></table>
+</form></div>
+<div class="shadow"> </div></div><div class="greyBoxFrame" id="edit_box"><div class="boxTitle"><span>actions - version</span></div><div class="boxContent">
+<ul class="boxListing"><li class="boxMainactions"><a href="http://crater:8888/project/Comet/0.2.0" title="keyword: view">voir</a></li>
+<li class="boxMainactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=edition" title="keyword: edit">modifier</a></li>
+<li class="boxMainactions"><a title="aucune transition possible">état: <i>en cours</i></a></li><li><a href="javascript: toggleVisibility('boxmenu_ajouter')" class="boxMenu">ajouter</a><ul id="boxmenu_ajouter" class="hidden"><li class="boxItem"><a href="http://crater:8888/project/Comet/0.2.0?etype=Ticket&__linkto=done_in%3A789%3Asubject&__redirectvid=statussheet&__redirectpath=project%2FComet%2F0.2.0&vid=creation" title="">ticket</a></li>
+<li class="boxItem"><a href="http://crater:8888/project/Comet/0.2.0?etype=Ticket&__linkto=appeared_in%3A789%3Asubject&__redirectvid=statussheet&__redirectpath=project%2FComet%2F0.2.0&vid=creation" title="">signaler une anomalie</a></li>
+</ul></li><li><a href="javascript: toggleVisibility('boxmenu_plus_dactions')" class="boxMenu">plus d'actions</a><ul id="boxmenu_plus_dactions" class="hidden"><li class="boxMoreactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=security" title="keyword: managepermission">gestion des permissions</a></li>
+<li class="boxMoreactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=deleteconf" title="keyword: delete">supprimer</a></li>
+<li class="boxMoreactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=copy" title="keyword: copy">copier</a></li>
+<li class="boxMoreactions"><a href="http://crater:8888/view?rql=Any%20X%20WHERE%20X%20version_of%20P%2C%20P%20name%20%22Comet%22%2C%20X%20num%20%220.2.0%22%2C%20X%20is%20Version&template=pdf-main-template" title="keyword: pdfexport">export pdf</a></li>
+<li class="boxMoreactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=document" title="keyword: pvrestexport">export ReST</a></li>
+</ul></li></ul>
+</div>
+<div class="shadow"> </div></div><div class="boxFrame" id="bookmarks_box"><div class="boxTitle"><span>signets</span></div><div class="boxContent">
+<ul class="sideBox"><li><a href="javascript: toggleVisibility('boxmenu_gérer_les_signets')" class="boxMenu">gérer les signets</a><ul id="boxmenu_gérer_les_signets" class="hidden"><li class="boxManage"><a href="http://crater:8888/add/Bookmark?__linkto=bookmarked_by%3A5%3Asubject&path=project%2FComet%2F0.2.0%3Fvid%3Dstatussheet" title="keyword: bookmark">poser un signet ici</a></li>
+<li class="boxManage"><a href="http://crater:8888/cwuser/admin?target=subject&vid=xaddrelation&rtype=bookmarked_by" title="">récupérer des signets existants</a></li>
+</ul></li></ul>
+</div>
+<div class="shadow"> </div></div></div></td>
+<td id="contentcol">
+<div id="rqlinput" class="hidden">
+ <form action="http://crater:8888/view">
+<fieldset>
+<input type="text" id="rql" name="rql" value="Any X WHERE X version_of P, P name "Comet", X num "0.2.0", X is Version" title="texte à rechercher ou requête RQL" tabindex="3" accesskey="q" class="searchField" />
+<input type="submit" value="" class="rqlsubmit" tabindex="4" />
+</fieldset>
+</form></div><div id="appMsg" onclick="javascript: toggleVisibility('appMsg')" class="hidden">
+</div><div id="pageContent">
+<div id="contentmain">
+<h2>Fiche de statut</h2><table class="listing"><tr><th rowspan="2">Projets</th><th colspan="2">Version</th><th rowspan="2">Parent</th><th rowspan="2">Tickets ouverts</th><th rowspan="2">Tickets implémentés</th><th rowspan="2">Statut</th></tr><tr><th>actuelle</th><th>ciblée</th></tr><tr><td title=""><a href="http://crater:8888/project/Developper%20manual" title="">Developper manual</a></td><td><a href="http://crater:8888/project/Developper%20manual/0.1.0">0.1.0</a></td><td><a href="http://crater:8888/project/Developper%20manual/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Comet%20documentation" title="">Comet documentation</a></td><td><div title="detail a bit configuration steps"><a href="http://crater:8888/ticket/803">T 803</a></div></td><td></td><td>en cours</td></tr><tr><td title=""><a href="http://crater:8888/project/User%20manual" title="">User manual</a></td><td><a href="http://crater:8888/project/User%20manual/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/User%20manual/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Comet%20documentation" title="">Comet documentation</a></td><td></td><td><div title="write a tutorial"><a href="http://crater:8888/ticket/801">T 801</a></div></td><td>livrée</td></tr><tr><td title=""><a href="http://crater:8888/project/Comet%20documentation" title="">Comet documentation</a></td><td><a href="http://crater:8888/project/Comet%20documentation/0.1.0">0.1.0</a></td><td><a href="http://crater:8888/project/Comet%20documentation/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Comet" title="">Comet</a></td><td></td><td></td><td>en cours</td></tr><tr><td title=""><a href="http://crater:8888/project/Lgc" title="">Lgc</a></td><td><a href="http://crater:8888/project/Lgc/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Lgc/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Tracker" title="">Tracker</a></td><td></td><td><div title="add support for xhtml -> pdf conversion"><a href="http://crater:8888/ticket/793">T 793</a></div></td><td>livrée</td></tr><tr><td title=""><a href="http://crater:8888/project/Tracker" title="">Tracker</a></td><td><a href="http://crater:8888/project/Tracker/0.1.0">0.1.0</a></td><td><a href="http://crater:8888/project/Tracker/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Confman" title="">Confman</a></td><td><div title="extract core from forge cube"><a href="http://crater:8888/ticket/795">T 795</a></div></td><td></td><td>en cours</td></tr><tr><td title=""><a href="http://crater:8888/project/Confman" title="">Confman</a></td><td><a href="http://crater:8888/project/Confman/0.1.0">0.1.0</a></td><td><a href="http://crater:8888/project/Confman/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Comet" title="">Comet</a></td><td><div title="have a version status sheet"><a href="http://crater:8888/ticket/797">T 797</a></div></td><td></td><td>en cours</td></tr></table></div>
+</div>
+</td>
+</tr></table></div>
+<div class="footer"><a href="http://crater:8888/changelog">nouveautés</a> | <a href="http://crater:8888/doc/about">à propos de ce site</a> | © 2001-2009 <a href="http://www.logilab.fr">Logilab S.A.</a></div></body>
+</html>
\ No newline at end of file
--- a/web/test/unittest_form.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/test/unittest_form.py Wed Aug 26 13:09:19 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'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_pdf.py Wed Aug 26 13:09:19 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.web.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)
--- a/web/test/unittest_views_editforms.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/test/unittest_views_editforms.py Wed Aug 26 13:09:19 2009 +0200
@@ -53,6 +53,7 @@
])
self.assertListEquals(rbc(e, 'generic'),
[('primary_email', 'subject'),
+ ('custom_workflow', 'subject'),
('connait', 'subject'),
('checked_by', 'object'),
])
--- a/web/uicfg.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/uicfg.py Wed Aug 26 13:09:19 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',
}
--- a/web/views/autoform.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/views/autoform.py Wed Aug 26 13:09:19 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 <attr>_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)
--- a/web/views/basecomponents.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/views/basecomponents.py Wed Aug 26 13:09:19 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'</div>')
+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):
+ self.req.add_css('cubes.confman.css')
+ entity = self.entity(0,0)
+ self.w(u'<a href="%s" class="otherView"><img src="data/pdf_icon.gif"/></a>' %
+ (xml_escape(entity.absolute_url() + '?vid=%s&__template=pdf-main-template' % vid)))
+
+
def registration_callback(vreg):
vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,))
--- a/web/views/basetemplates.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/views/basetemplates.py Wed Aug 26 13:09:19 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,44 @@
self.w(u'</td>\n')
self.w(u'</tr></table>\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.web.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)
+ pdf = self.to_pdf(self._stream)
+ 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='contentmain'):
+ # 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):
@@ -489,4 +527,4 @@
## vregistry registration callback ############################################
def registration_callback(vreg):
- vreg.register_all(globals().values(), modname=__name__)
+ vreg.register_all(globals().values(), __name__)
--- a/web/views/boxes.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/views/boxes.py Wed Aug 26 13:09:19 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,12 +133,11 @@
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))
wfurl = self.build_url('cwetype/%s'%entity.e_schema, vid='workflow')
--- a/web/views/forms.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/views/forms.py Wed Aug 26 13:09:19 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)
--- a/web/views/primary.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/views/primary.py Wed Aug 26 13:09:19 2009 +0200
@@ -229,9 +229,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')
--- a/web/views/workflow.py Wed Aug 26 11:16:10 2009 +0200
+++ b/web/views/workflow.py Wed Aug 26 13:09:19 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'<h4>%s %s</h4>\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'<p>%s</p>\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')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/xhtml2fo.py Wed Aug 26 13:09:19 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 <div class="contentmain">
+ 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 <div class="contentmain"> 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 <div> 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)
+