# HG changeset patch # User Sylvain Thénault # Date 1253191998 -7200 # Node ID 70c0dd1c3b7d747c3a268396a7f79d9a7a3340e6 # Parent 266d31e7afa09e1fcab003923ac926af9c6f3cf3# Parent 24489cbbd697235c19c38dcc1358b28aa4572282 backport stable diff -r 24489cbbd697 -r 70c0dd1c3b7d .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,10 @@ +\.svn +^build$ +^dist$ +\.pyc$ +\.pyo$ +\.bak$ +\.old$ +\~$ +\#.*?\#$ +\.swp$ diff -r 24489cbbd697 -r 70c0dd1c3b7d __init__.py --- a/__init__.py Thu Sep 17 14:03:21 2009 +0200 +++ b/__init__.py Thu Sep 17 14:53:18 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): @@ -191,18 +212,9 @@ userinfo['email'] = "" return userinfo user = self.actual_session().user - rql = "Any F,S,A where U eid %(x)s, U firstname F, U surname S, U primary_email E, E address A" - try: - firstname, lastname, email = self.execute(rql, {'x': user.eid}, 'x')[0] - if firstname is None and lastname is None: - userinfo['name'] = '' - else: - userinfo['name'] = ("%s %s" % (firstname, lastname)) - userinfo['email'] = email - except IndexError: - userinfo['name'] = None - userinfo['email'] = None userinfo['login'] = user.login + userinfo['name'] = user.name() + userinfo['email'] = user.get_email() return userinfo def is_internal_session(self): diff -r 24489cbbd697 -r 70c0dd1c3b7d __pkginfo__.py --- a/__pkginfo__.py Thu Sep 17 14:03:21 2009 +0200 +++ b/__pkginfo__.py Thu Sep 17 14:53:18 2009 +0200 @@ -7,7 +7,7 @@ distname = "cubicweb" modname = "cubicweb" -numversion = (3, 4, 11) +numversion = (3, 5, 0) version = '.'.join(str(num) for num in numversion) license = 'LGPL v2' diff -r 24489cbbd697 -r 70c0dd1c3b7d _exceptions.py --- a/_exceptions.py Thu Sep 17 14:03:21 2009 +0200 +++ b/_exceptions.py Thu Sep 17 14:53:18 2009 +0200 @@ -65,9 +65,8 @@ """no source support an entity type""" msg = 'No source supports %r entity\'s type' -class RTypeNotSupportedBySources(RepositoryError, InternalError): - """no source support a relation type""" - msg = 'No source supports %r relation\'s type' +class MultiSourcesError(RepositoryError, InternalError): + """usually due to bad multisources configuration or rql query""" # security exceptions ######################################################### diff -r 24489cbbd697 -r 70c0dd1c3b7d bin/cubicweb-ctl.bat --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/cubicweb-ctl.bat Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,18 @@ +@echo off +rem = """-*-Python-*- script +rem -------------------- DOS section -------------------- +rem You could set PYTHONPATH or TK environment variables here +python -x "%~f0" %* +goto exit + +""" +# -------------------- Python section -------------------- +from cubicweb.cwctl import run +import sys +run(sys.argv[1:]) + +DosExitLabel = """ +:exit +rem """ + + diff -r 24489cbbd697 -r 70c0dd1c3b7d common/i18n.py --- a/common/i18n.py Thu Sep 17 14:03:21 2009 +0200 +++ b/common/i18n.py Thu Sep 17 14:53:18 2009 +0200 @@ -25,10 +25,14 @@ output.close() -def add_msg(w, msgid): +def add_msg(w, msgid, msgctx=None): """write an empty pot msgid definition""" if isinstance(msgid, unicode): msgid = msgid.encode('utf-8') + if msgctx: + if isinstance(msgctx, unicode): + msgctx = msgctx.encode('utf-8') + w('msgctxt "%s"\n' % msgctx) msgid = msgid.replace('"', r'\"').splitlines() if len(msgid) > 1: w('msgid ""\n') @@ -44,9 +48,10 @@ status != 0 """ print cmd.replace(os.getcwd() + os.sep, '') - status = os.system(cmd) + from subprocess import call + status = call(cmd, shell=True) if status != 0: - raise Exception() + raise Exception('status = %s' % status) def available_catalogs(i18ndir=None): @@ -74,15 +79,15 @@ mergedpo = join(destdir, '%s_merged.po' % lang) try: # merge instance/cubes messages catalogs with the stdlib's one - execute('msgcat --use-first --sort-output --strict %s > %s' - % (' '.join(pofiles), mergedpo)) - # make sure the .mo file is writeable and compile with *msgfmt* + execute('msgcat --use-first --sort-output --strict -o "%s" %s' + % (mergedpo, ' '.join('"%s"' % f for f in pofiles))) + # make sure the .mo file is writeable and compiles with *msgfmt* applmo = join(destdir, lang, 'LC_MESSAGES', 'cubicweb.mo') try: ensure_fs_mode(applmo) except OSError: pass # suppose not exists - execute('msgfmt %s -o %s' % (mergedpo, applmo)) + execute('msgfmt "%s" -o "%s"' % (mergedpo, applmo)) except Exception, ex: errors.append('while handling language %s: %s' % (lang, ex)) try: diff -r 24489cbbd697 -r 70c0dd1c3b7d common/mail.py --- a/common/mail.py Thu Sep 17 14:03:21 2009 +0200 +++ b/common/mail.py Thu Sep 17 14:53:18 2009 +0200 @@ -142,39 +142,21 @@ msgid_timestamp = True - def recipients(self): - finder = self.vreg['components'].select('recipients_finder', self.req, - rset=self.rset, - row=self.row or 0, - col=self.col or 0) - return finder.recipients() - - def subject(self): - entity = self.entity(self.row or 0, self.col or 0) - subject = self.req._(self.message) - etype = entity.dc_type() - eid = entity.eid - login = self.user_data['login'] - return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals() - - def context(self, **kwargs): - entity = self.entity(self.row or 0, self.col or 0) - for key, val in kwargs.iteritems(): - if val and isinstance(val, unicode) and val.strip(): - kwargs[key] = self.req._(val) - kwargs.update({'user': self.user_data['login'], - 'eid': entity.eid, - 'etype': entity.dc_type(), - 'url': entity.absolute_url(), - 'title': entity.dc_long_title(),}) - return kwargs + # this is usually the method to call + def render_and_send(self, **kwargs): + """generate and send an email message for this view""" + delayed = kwargs.pop('delay_to_commit', None) + for recipients, msg in self.render_emails(**kwargs): + if delayed is None: + self.send(recipients, msg) + elif delayed: + self.send_on_commit(recipients, msg) + else: + self.send_now(recipients, msg) def cell_call(self, row, col=0, **kwargs): self.w(self.req._(self.content) % self.context(**kwargs)) - def construct_message_id(self, eid): - return construct_message_id(self.config.appid, eid, self.msgid_timestamp) - def render_emails(self, **kwargs): """generate and send emails for this view (one per recipient)""" self._kwargs = kwargs @@ -222,16 +204,17 @@ # restore language req.set_language(origlang) - def render_and_send(self, **kwargs): - """generate and send an email message for this view""" - delayed = kwargs.pop('delay_to_commit', None) - for recipients, msg in self.render_emails(**kwargs): - if delayed is None: - self.send(recipients, msg) - elif delayed: - self.send_on_commit(recipients, msg) - else: - self.send_now(recipients, msg) + # recipients / email sending ############################################### + + def recipients(self): + """return a list of either 2-uple (email, language) or user entity to + who this email should be sent + """ + finder = self.vreg['components'].select('recipients_finder', self.req, + rset=self.rset, + row=self.row or 0, + col=self.col or 0) + return finder.recipients() def send_now(self, recipients, msg): self.config.sendmails([(msg, recipients)]) @@ -241,6 +224,38 @@ send = send_now + # email generation helpers ################################################# + + def construct_message_id(self, eid): + return construct_message_id(self.config.appid, eid, self.msgid_timestamp) + + def format_field(self, attr, value): + return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value} + + def format_section(self, attr, value): + return '%(attr)s\n%(ul)s\n%(value)s\n' % { + 'attr': attr, 'ul': '-'*len(attr), 'value': value} + + def subject(self): + entity = self.entity(self.row or 0, self.col or 0) + subject = self.req._(self.message) + etype = entity.dc_type() + eid = entity.eid + login = self.user_data['login'] + return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals() + + def context(self, **kwargs): + entity = self.entity(self.row or 0, self.col or 0) + for key, val in kwargs.iteritems(): + if val and isinstance(val, unicode) and val.strip(): + kwargs[key] = self.req._(val) + kwargs.update({'user': self.user_data['login'], + 'eid': entity.eid, + 'etype': entity.dc_type(), + 'url': entity.absolute_url(), + 'title': entity.dc_long_title(),}) + return kwargs + class SkipEmail(Exception): """raise this if you decide to skip an email during its generation""" diff -r 24489cbbd697 -r 70c0dd1c3b7d common/migration.py --- a/common/migration.py Thu Sep 17 14:03:21 2009 +0200 +++ b/common/migration.py Thu Sep 17 14:53:18 2009 +0200 @@ -228,7 +228,10 @@ else: readline.set_completer(Completer(local_ctx).complete) readline.parse_and_bind('tab: complete') - histfile = os.path.join(os.environ["HOME"], ".eshellhist") + home_key = 'HOME' + if sys.platform == 'win32': + home_key = 'USERPROFILE' + histfile = os.path.join(os.environ[home_key], ".eshellhist") try: readline.read_history_file(histfile) except IOError: diff -r 24489cbbd697 -r 70c0dd1c3b7d common/mixins.py --- a/common/mixins.py Thu Sep 17 14:03:21 2009 +0200 +++ b/common/mixins.py Thu Sep 17 14:53:18 2009 +0200 @@ -13,7 +13,7 @@ from cubicweb import typed_eid from cubicweb.selectors import implements -from cubicweb.interfaces import IWorkflowable, IEmailable, ITree +from cubicweb.interfaces import IEmailable, ITree class TreeMixIn(object): @@ -158,97 +158,6 @@ return self.req.entity_from_eid(self.path()[0]) -class WorkflowableMixIn(object): - """base mixin providing workflow helper methods for workflowable entities. - This mixin will be automatically set on class supporting the 'in_state' - relation (which implies supporting 'wf_info_for' as well) - """ - __implements__ = (IWorkflowable,) - - @property - def state(self): - try: - return self.in_state[0].name - except IndexError: - self.warning('entity %s has no state', self) - return None - - @property - def displayable_state(self): - return self.req._(self.state) - - def wf_state(self, statename): - rset = self.req.execute('Any S, SN WHERE S name SN, S name %(n)s, S state_of E, E name %(e)s', - {'n': statename, 'e': str(self.e_schema)}) - if rset: - return rset.get_entity(0, 0) - return None - - def wf_transition(self, trname): - rset = self.req.execute('Any T, TN WHERE T name TN, T name %(n)s, T transition_of E, E name %(e)s', - {'n': trname, 'e': str(self.e_schema)}) - if rset: - return rset.get_entity(0, 0) - return None - - def change_state(self, state, trcomment=None, trcommentformat=None): - """change the entity's state according to a state defined in given - parameters - """ - if isinstance(state, basestring): - state = self.wf_state(state) - assert state is not None, 'not a %s state: %s' % (self.id, state) - if hasattr(state, 'eid'): - stateeid = state.eid - else: - stateeid = state - stateeid = typed_eid(stateeid) - if trcomment: - self.req.set_shared_data('trcomment', trcomment) - if trcommentformat: - self.req.set_shared_data('trcommentformat', trcommentformat) - self.req.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': self.eid, 's': stateeid}, 'x') - - def can_pass_transition(self, trname): - """return the Transition instance if the current user can pass the - transition with the given name, else None - """ - stateeid = self.in_state[0].eid - rset = self.req.execute('Any T,N,DS WHERE S allowed_transition T,' - 'S eid %(x)s,T name %(trname)s,ET name %(et)s,' - 'T name N,T destination_state DS,T transition_of ET', - {'x': stateeid, 'et': str(self.e_schema), - 'trname': trname}, 'x') - for tr in rset.entities(): - if tr.may_be_passed(self.eid, stateeid): - return tr - - def latest_trinfo(self): - """return the latest transition information for this entity""" - return self.reverse_wf_info_for[-1] - - # __method methods ######################################################## - - def set_state(self, params=None): - """change the entity's state according to a state defined in given - parameters, used to be called using __method controler facility - """ - params = params or self.req.form - self.change_state(typed_eid(params.pop('state')), - params.get('trcomment'), - params.get('trcomment_format')) - self.req.set_message(self.req._('__msg state changed')) - - # specific vocabulary methods ############################################# - - @deprecated('use EntityFieldsForm.subject_in_state_vocabulary') - def subject_in_state_vocabulary(self, rschema, limit=None): - form = self.vreg.select('forms', 'edition', self.req, entity=self) - return form.subject_in_state_vocabulary(rschema, limit) - - - class EmailableMixIn(object): """base mixin providing the default get_email() method used by the massmailing view @@ -288,7 +197,6 @@ MI_REL_TRIGGERS = { - ('in_state', 'subject'): WorkflowableMixIn, ('primary_email', 'subject'): EmailableMixIn, ('use_email', 'subject'): EmailableMixIn, } diff -r 24489cbbd697 -r 70c0dd1c3b7d common/test/unittest_mixins.py --- a/common/test/unittest_mixins.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -from logilab.common.testlib import unittest_main -from cubicweb.devtools.apptest import EnvBasedTC - -class WorkfloableMixInTC(EnvBasedTC): - def test_wf_state(self): - s = self.add_entity('State', name=u'activated') - self.execute('SET X state_of ET WHERE ET name "Bookmark", X eid %(x)s', - {'x': s.eid}) - es = self.user().wf_state('activated') - self.assertEquals(es.state_of[0].name, 'CWUser') - - def test_wf_transition(self): - t = self.add_entity('Transition', name=u'deactivate') - self.execute('SET X transition_of ET WHERE ET name "Bookmark", X eid %(x)s', - {'x': t.eid}) - et = self.user().wf_transition('deactivate') - self.assertEquals(et.transition_of[0].name, 'CWUser') - - def test_change_state(self): - user = self.user() - user.change_state(user.wf_state('deactivated').eid) - self.assertEquals(user.state, 'deactivated') - -if __name__ == '__main__': - unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d common/test/unittest_uilib.py --- a/common/test/unittest_uilib.py Thu Sep 17 14:03:21 2009 +0200 +++ b/common/test/unittest_uilib.py Thu Sep 17 14:53:18 2009 +0200 @@ -81,47 +81,6 @@ got = uilib.text_cut(text, 30) self.assertEquals(got, expected) -tree = ('root', ( - ('child_1_1', ( - ('child_2_1', ()), ('child_2_2', ( - ('child_3_1', ()), - ('child_3_2', ()), - ('child_3_3', ()), - )))), - ('child_1_2', (('child_2_3', ()),)))) - -generated_html = """\ - - - - - - - - - - - -
root
  
child_1_1
  
child_2_1
   
      
      
child_2_2
  
child_3_1
      
         
child_3_2
      
         
child_3_3
      
   
child_1_2
  
child_2_3
   
      
\ -""" - -def make_tree(tuple): - n = Node(tuple[0]) - for child in tuple[1]: - n.append(make_tree(child)) - return n - -class UIlibHTMLGenerationTC(TestCase): - """ a basic tree node, caracterised by an id""" - def setUp(self): - """ called before each test from this class """ - self.o = make_tree(tree) - - def test_generated_html(self): - s = uilib.render_HTML_tree(self.o, selected_node="child_2_2") - self.assertTextEqual(s, generated_html) - - if __name__ == '__main__': unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d common/uilib.py --- a/common/uilib.py Thu Sep 17 14:03:21 2009 +0200 +++ b/common/uilib.py Thu Sep 17 14:53:18 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 @@ -47,9 +46,9 @@ if attrtype == 'Time': return ustrftime(value, req.property_value('ui.time-format')) if attrtype == 'Datetime': - if not displaytime: - return ustrftime(value, req.property_value('ui.date-format')) - return ustrftime(value, req.property_value('ui.datetime-format')) + if displaytime: + return ustrftime(value, req.property_value('ui.datetime-format')) + return ustrftime(value, req.property_value('ui.date-format')) if attrtype == 'Boolean': if value: return req._('yes') @@ -264,125 +263,6 @@ res = unicode(res, 'UTF8') return res -def render_HTML_tree(tree, selected_node=None, render_node=None, caption=None): - """ - Generate a pure HTML representation of a tree given as an instance - of a logilab.common.tree.Node - - selected_node is the currently selected node (if any) which will - have its surrounding
have id="selected" (which default - to a bold border libe with the default CSS). - - render_node is a function that should take a Node content (Node.id) - as parameter and should return a string (what will be displayed - in the cell). - - Warning: proper rendering of the generated html code depends on html_tree.css - """ - tree_depth = tree.depth_down() - if render_node is None: - render_node = str - - # helper function that build a matrix from the tree, like: - # +------+-----------+-----------+ - # | root | child_1_1 | child_2_1 | - # | root | child_1_1 | child_2_2 | - # | root | child_1_2 | | - # | root | child_1_3 | child_2_3 | - # | root | child_1_3 | child_2_4 | - # +------+-----------+-----------+ - # from: - # root -+- child_1_1 -+- child_2_1 - # | | - # | +- child_2_2 - # +- child_1_2 - # | - # +- child1_3 -+- child_2_3 - # | - # +- child_2_2 - def build_matrix(path, matrix): - if path[-1].is_leaf(): - matrix.append(path[:]) - else: - for child in path[-1].children: - build_matrix(path[:] + [child], matrix) - - matrix = [] - build_matrix([tree], matrix) - - # make all lines in the matrix have the same number of columns - for line in matrix: - line.extend([None]*(tree_depth-len(line))) - for i in range(len(matrix)-1, 0, -1): - prev_line, line = matrix[i-1:i+1] - for j in range(len(line)): - if line[j] == prev_line[j]: - line[j] = None - - # We build the matrix of link types (between 2 cells on a line of the matrix) - # link types are : - link_types = {(True, True, True ): 1, # T - (False, False, True ): 2, # | - (False, True, True ): 3, # + (actually, vert. bar with horiz. bar on the right) - (False, True, False): 4, # L - (True, True, False): 5, # - - } - links = [] - for i, line in enumerate(matrix): - links.append([]) - for j in range(tree_depth-1): - cell_11 = line[j] is not None - cell_12 = line[j+1] is not None - cell_21 = line[j+1] is not None and line[j+1].next_sibling() is not None - link_type = link_types.get((cell_11, cell_12, cell_21), 0) - if link_type == 0 and i > 0 and links[i-1][j] in (1, 2, 3): - link_type = 2 - links[-1].append(link_type) - - - # We can now generate the HTML code for the - s = u'
\n' - if caption: - s += '\n' % caption - - for i, link_line in enumerate(links): - line = matrix[i] - - s += '' - for j, link_cell in enumerate(link_line): - cell = line[j] - if cell: - if cell.id == selected_node: - s += '' % (render_node(cell.id)) - else: - s += '' % (render_node(cell.id)) - else: - s += '' - s += '' % link_cell - s += '' % link_cell - - cell = line[-1] - if cell: - if cell.id == selected_node: - s += '' % (render_node(cell.id)) - else: - s += '' % (render_node(cell.id)) - else: - s += '' - - s += '\n' - if link_line: - s += '' - for j, link_cell in enumerate(link_line): - s += '' % link_cell - s += '' % link_cell - s += '\n' - - s += '
%s
%s
%s
   
%s
%s
 
  
' - return s - - - # traceback formatting ######################################################## import traceback diff -r 24489cbbd697 -r 70c0dd1c3b7d cwconfig.py --- a/cwconfig.py Thu Sep 17 14:03:21 2009 +0200 +++ b/cwconfig.py Thu Sep 17 14:53:18 2009 +0200 @@ -20,6 +20,7 @@ from smtplib import SMTP from threading import Lock from os.path import exists, join, expanduser, abspath, normpath, basename, isdir +import tempfile from logilab.common.decorators import cached from logilab.common.deprecation import deprecated @@ -391,7 +392,11 @@ 'server/serverctl.py', 'hercule.py', 'devtools/devctl.py', 'goa/goactl.py'): if exists(join(CW_SOFTWARE_ROOT, ctlfile)): - load_module_from_file(join(CW_SOFTWARE_ROOT, ctlfile)) + try: + load_module_from_file(join(CW_SOFTWARE_ROOT, ctlfile)) + except ImportError, err: + cls.critical('could not import the command provider %s (cause : %s)' % + (ctlfile, err)) cls.info('loaded cubicweb-ctl plugin %s', ctlfile) for cube in cls.available_cubes(): pluginfile = join(cls.cube_dir(cube), 'ecplugin.py') @@ -526,13 +531,13 @@ if CubicWebNoAppConfiguration.mode == 'test': root = os.environ['APYCOT_ROOT'] REGISTRY_DIR = '%s/etc/cubicweb.d/' % root - RUNTIME_DIR = '/tmp/' + RUNTIME_DIR = tempfile.gettempdir() MIGRATION_DIR = '%s/local/share/cubicweb/migration/' % root if not exists(REGISTRY_DIR): os.makedirs(REGISTRY_DIR) elif CubicWebNoAppConfiguration.mode == 'dev': REGISTRY_DIR = expanduser('~/etc/cubicweb.d/') - RUNTIME_DIR = '/tmp/' + RUNTIME_DIR = tempfile.gettempdir() MIGRATION_DIR = join(CW_SOFTWARE_ROOT, 'misc', 'migration') else: #mode = 'installed' REGISTRY_DIR = '/etc/cubicweb.d/' @@ -651,7 +656,7 @@ def default_log_file(self): """return default path to the log file of the instance'server""" if self.mode == 'dev': - basepath = '/tmp/%s-%s' % (basename(self.appid), self.name) + basepath = join(tempfile.gettempdir(), '%s-%s' % (basename(self.appid), self.name)) path = basepath + '.log' i = 1 while exists(path) and i < 100: # arbitrary limit to avoid infinite loop @@ -794,8 +799,8 @@ from glob import glob yield 'en' # ensure 'en' is yielded even if no .mo found for path in glob(join(self.apphome, 'i18n', - '*', 'LC_MESSAGES', 'cubicweb.mo')): - lang = path.split(os.sep)[-3] + '*', 'LC_MESSAGES')): + lang = path.split(os.sep)[-2] if lang != 'en': yield lang @@ -807,7 +812,7 @@ self.info("loading language %s", language) try: tr = translation('cubicweb', path, languages=[language]) - self.translations[language] = tr.ugettext + self.translations[language] = (tr.ugettext, tr.upgettext) except (ImportError, AttributeError, IOError): self.exception('localisation support error for language %s', language) diff -r 24489cbbd697 -r 70c0dd1c3b7d cwctl.py --- a/cwctl.py Thu Sep 17 14:03:21 2009 +0200 +++ b/cwctl.py Thu Sep 17 14:53:18 2009 +0200 @@ -5,7 +5,13 @@ %s""" import sys -from os import remove, listdir, system, kill, getpgid, pathsep +from os import remove, listdir, system, pathsep +try: + from os import kill, getpgid +except ImportError: + def kill(*args): pass + def getpgid(): pass + from os.path import exists, join, isfile, isdir from logilab.common.clcommands import register_commands, pop_arg @@ -23,7 +29,7 @@ while nbtry < maxtry: try: kill(pid, signal.SIGUSR1) - except OSError: + except (OSError, AttributeError): # XXX win32 break nbtry += 1 sleep(waittime) diff -r 24489cbbd697 -r 70c0dd1c3b7d cwvreg.py --- a/cwvreg.py Thu Sep 17 14:03:21 2009 +0200 +++ b/cwvreg.py Thu Sep 17 14:53:18 2009 +0200 @@ -140,9 +140,12 @@ try: objects = self[btype] assert len(objects) == 1, objects - cls = objects[0] + if btype == etype: + cls = objects[0] + else: + cls = self.etype_class(btype) except ObjectNotFound: - pass + continue else: # ensure parent classes are built first self.etype_class(btype) @@ -152,9 +155,9 @@ objects = self['Any'] assert len(objects) == 1, objects cls = objects[0] - if cls.id == etype: - cls.__initialize__() - return cls + # make a copy event if cls.id == etype, else we may have pb for client + # application using multiple connections to different repositories (eg + # shingouz) cls = dump_class(cls, etype) cls.id = etype cls.__initialize__() @@ -288,8 +291,7 @@ def set_schema(self, schema): """set instance'schema and load application objects""" - self.schema = schema - clear_cache(self, 'rqlhelper') + self._set_schema(schema) # now we can load application's web objects searchpath = self.config.vregistry_path() self.reset(searchpath, force_reload=False) @@ -300,6 +302,11 @@ etype = str(etype) self.case_insensitive_etypes[etype.lower()] = etype + def _set_schema(self, schema): + """set instance'schema""" + self.schema = schema + clear_cache(self, 'rqlhelper') + def update_schema(self, schema): """update .schema attribute on registered objects, necessary for some tests @@ -383,16 +390,7 @@ # objects on automatic reloading self._needs_iface.clear() - def parse(self, session, rql, args=None): - rqlst = self.rqlhelper.parse(rql) - def type_from_eid(eid, session=session): - return session.describe(eid)[0] - try: - self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args) - except UnknownEid: - for select in rqlst.children: - select.solutions = [] - return rqlst + # rql parsing utilities #################################################### @property @cached @@ -400,38 +398,19 @@ return RQLHelper(self.schema, special_relations={'eid': 'uid', 'has_text': 'fti'}) - - @deprecated('use vreg["etypes"].etype_class(etype)') - def etype_class(self, etype): - return self["etypes"].etype_class(etype) - - @deprecated('use vreg["views"].main_template(*args, **kwargs)') - def main_template(self, req, oid='main-template', **context): - return self["views"].main_template(req, oid, **context) - - @deprecated('use vreg[registry].possible_vobjects(*args, **kwargs)') - def possible_vobjects(self, registry, *args, **kwargs): - return self[registry].possible_vobjects(*args, **kwargs) + def solutions(self, req, rqlst, args): + def type_from_eid(eid, req=req): + return req.describe(eid)[0] + self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args) - @deprecated('use vreg["actions"].possible_actions(*args, **kwargs)') - def possible_actions(self, req, rset=None, **kwargs): - return self["actions"].possible_actions(req, rest=rset, **kwargs) - - @deprecated("use vreg['boxes'].select_object(...)") - def select_box(self, oid, *args, **kwargs): - return self['boxes'].select_object(oid, *args, **kwargs) - - @deprecated("use vreg['components'].select_object(...)") - def select_component(self, cid, *args, **kwargs): - return self['components'].select_object(cid, *args, **kwargs) - - @deprecated("use vreg['actions'].select_object(...)") - def select_action(self, oid, *args, **kwargs): - return self['actions'].select_object(oid, *args, **kwargs) - - @deprecated("use vreg['views'].select(...)") - def select_view(self, __vid, req, rset=None, **kwargs): - return self['views'].select(__vid, req, rset=rset, **kwargs) + def parse(self, req, rql, args=None): + rqlst = self.rqlhelper.parse(rql) + try: + self.solutions(req, rqlst, args) + except UnknownEid: + for select in rqlst.children: + select.solutions = [] + return rqlst # properties handling ##################################################### @@ -504,6 +483,40 @@ self.warning('%s (you should probably delete that property ' 'from the database)', ex) + # deprecated code #################################################### + + @deprecated('[3.4] use vreg["etypes"].etype_class(etype)') + def etype_class(self, etype): + return self["etypes"].etype_class(etype) + + @deprecated('[3.4] use vreg["views"].main_template(*args, **kwargs)') + def main_template(self, req, oid='main-template', **context): + return self["views"].main_template(req, oid, **context) + + @deprecated('[3.4] use vreg[registry].possible_vobjects(*args, **kwargs)') + def possible_vobjects(self, registry, *args, **kwargs): + return self[registry].possible_vobjects(*args, **kwargs) + + @deprecated('[3.4] use vreg["actions"].possible_actions(*args, **kwargs)') + def possible_actions(self, req, rset=None, **kwargs): + return self["actions"].possible_actions(req, rest=rset, **kwargs) + + @deprecated('[3.4] use vreg["boxes"].select_object(...)') + def select_box(self, oid, *args, **kwargs): + return self['boxes'].select_object(oid, *args, **kwargs) + + @deprecated('[3.4] use vreg["components"].select_object(...)') + def select_component(self, cid, *args, **kwargs): + return self['components'].select_object(cid, *args, **kwargs) + + @deprecated('[3.4] use vreg["actions"].select_object(...)') + def select_action(self, oid, *args, **kwargs): + return self['actions'].select_object(oid, *args, **kwargs) + + @deprecated('[3.4] use vreg["views"].select(...)') + def select_view(self, __vid, req, rset=None, **kwargs): + return self['views'].select(__vid, req, rset=rset, **kwargs) + from datetime import datetime, date, time, timedelta diff -r 24489cbbd697 -r 70c0dd1c3b7d dbapi.py --- a/dbapi.py Thu Sep 17 14:03:21 2009 +0200 +++ b/dbapi.py Thu Sep 17 14:53:18 2009 +0200 @@ -214,10 +214,13 @@ self.lang = 'en' # use req.__ to translate a message without registering it to the catalog try: - self._ = self.__ = self.translations[self.lang] + gettext, pgettext = self.translations[self.lang] + self._ = self.__ = gettext + self.pgettext = pgettext except KeyError: # this occurs usually during test execution self._ = self.__ = unicode + self.pgettext = lambda x,y: y self.debug('request default language: %s', self.lang) def decorate_rset(self, rset): diff -r 24489cbbd697 -r 70c0dd1c3b7d debian/changelog --- a/debian/changelog Thu Sep 17 14:03:21 2009 +0200 +++ b/debian/changelog Thu Sep 17 14:53:18 2009 +0200 @@ -1,3 +1,9 @@ +cubicweb (3.5.0-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Wed, 16 Sep 2009 17:51:13 +0200 + cubicweb (3.4.11-1) unstable; urgency=low * new upstream release diff -r 24489cbbd697 -r 70c0dd1c3b7d debian/control --- a/debian/control Thu Sep 17 14:03:21 2009 +0200 +++ b/debian/control Thu Sep 17 14:53:18 2009 +0200 @@ -62,7 +62,7 @@ Architecture: all XB-Python-Version: ${python:Versions} Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3), python-elementtree -Recommends: python-docutils, python-vobject, fckeditor, python-fyzz +Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-pysixt, fop Description: web interface library for the CubicWeb framework CubicWeb is a semantic web application framework. . diff -r 24489cbbd697 -r 70c0dd1c3b7d devtools/_apptest.py --- a/devtools/_apptest.py Thu Sep 17 14:03:21 2009 +0200 +++ b/devtools/_apptest.py Thu Sep 17 14:53:18 2009 +0200 @@ -26,7 +26,7 @@ 'CWAttribute', 'CWRelation', 'CWConstraint', 'CWConstraintType', 'CWProperty', 'CWEType', 'CWRType', - 'State', 'Transition', 'TrInfo', + 'Workflow', 'State', 'BaseTransition', 'Transition', 'WorkflowTransition', 'TrInfo', 'SubWorkflowExitPoint', 'RQLExpression', ) SYSTEM_RELATIONS = ( @@ -35,9 +35,9 @@ # metadata 'is', 'is_instance_of', 'owned_by', 'created_by', 'specializes', # workflow related - 'state_of', 'transition_of', 'initial_state', 'allowed_transition', + 'workflow_of', 'state_of', 'transition_of', 'initial_state', 'allowed_transition', 'destination_state', 'in_state', 'wf_info_for', 'from_state', 'to_state', - 'condition', + 'condition', 'subworkflow', 'subworkflow_state', 'subworkflow_exit', # permission 'in_group', 'require_group', 'require_permission', 'read_permission', 'update_permission', 'delete_permission', 'add_permission', @@ -121,8 +121,7 @@ def create_user(self, login, groups=('users',), req=None): req = req or self.create_request() cursor = self._orig_cnx.cursor(req) - rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s,' - 'X in_state S WHERE S name "activated"', + rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s', {'login': unicode(login), 'passwd': login.encode('utf8')}) user = rset.get_entity(0, 0) cursor.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)' diff -r 24489cbbd697 -r 70c0dd1c3b7d devtools/apptest.py --- a/devtools/apptest.py Thu Sep 17 14:03:21 2009 +0200 +++ b/devtools/apptest.py Thu Sep 17 14:53:18 2009 +0200 @@ -230,6 +230,23 @@ return [(a.id, a.__class__) for a in self.vreg['actions'].possible_vobjects(req, rset=rset) if a.category not in skipcategories] + def action_submenu(self, req, rset, id): + return self._test_action(self.vreg['actions'].select(id, req, rset=rset)) + + def _test_action(self, action): + class fake_menu(list): + @property + def items(self): + return self + class fake_box(object): + def mk_action(self, label, url, **kwargs): + return (label, url) + def box_action(self, action, **kwargs): + return (action.title, action.url()) + submenu = fake_menu() + action.fill_menu(fake_box(), submenu) + return submenu + def pactions_by_cats(self, req, rset, categories=('addrelated',)): return [(a.id, a.__class__) for a in self.vreg['actions'].possible_vobjects(req, rset=rset) if a.category in categories] @@ -371,8 +388,7 @@ def create_user(self, user, groups=('users',), password=None, commit=True): if password is None: password = user - eid = self.execute('INSERT CWUser X: X login %(x)s, X upassword %(p)s,' - 'X in_state S WHERE S name "activated"', + eid = self.execute('INSERT CWUser X: X login %(x)s, X upassword %(p)s', {'x': unicode(user), 'p': password})[0][0] groups = ','.join(repr(group) for group in groups) self.execute('SET X in_group Y WHERE X eid %%(x)s, Y name IN (%s)' % groups, diff -r 24489cbbd697 -r 70c0dd1c3b7d devtools/dataimport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/dataimport.py Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +"""This module provides tools to import tabular data. + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses + + +Example of use (run this with `cubicweb-ctl shell instance import-script.py`): + +.. sourcecode:: python + + # define data generators + GENERATORS = [] + + USERS = [('Prenom', 'firstname', ()), + ('Nom', 'surname', ()), + ('Identifiant', 'login', ()), + ] + + def gen_users(ctl): + for row in ctl.get_data('utilisateurs'): + entity = mk_entity(row, USERS) + entity['upassword'] = u'motdepasse' + ctl.check('login', entity['login'], None) + ctl.store.add('CWUser', entity) + email = {'address': row['email']} + ctl.store.add('EmailAddress', email) + ctl.store.relate(entity['eid'], 'use_email', email['eid']) + ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x':entity['eid']}) + + CHK = [('login', check_doubles, 'Utilisateurs Login', + 'Deux utilisateurs ne devraient pas avoir le même login.'), + ] + + GENERATORS.append( (gen_users, CHK) ) + + # create controller + ctl = CWImportController(RQLObjectStore()) + ctl.askerror = True + ctl.generators = GENERATORS + ctl.store._checkpoint = checkpoint + ctl.store._rql = rql + ctl.data['utilisateurs'] = lazytable(utf8csvreader(open('users.csv'))) + # run + ctl.run() + sys.exit(0) + +""" +__docformat__ = "restructuredtext en" + +import sys, csv, traceback + +from logilab.common import shellutils + +def utf8csvreader(file, encoding='utf-8', separator=',', quote='"'): + """A csv reader that accepts files with any encoding and outputs + unicode strings.""" + for row in csv.reader(file, delimiter=separator, quotechar=quote): + yield [item.decode(encoding) for item in row] + +def lazytable(reader): + """The first row is taken to be the header of the table and + used to output a dict for each row of data. + + >>> data = lazytable(utf8csvreader(open(filename))) + """ + header = reader.next() + for row in reader: + yield dict(zip(header, row)) + +def tell(msg): + print msg + +# base sanitizing functions ##### + +def capitalize_if_unicase(txt): + if txt.isupper() or txt.islower(): + return txt.capitalize() + return txt + +def no_space(txt): + return txt.replace(' ','') + +def no_uspace(txt): + return txt.replace(u'\xa0','') + +def no_dash(txt): + return txt.replace('-','') + +def alldigits(txt): + if txt.isdigit(): + return txt + else: + return u'' + +def strip(txt): + return txt.strip() + +# base checks ##### + +def check_doubles(buckets): + """Extract the keys that have more than one item in their bucket.""" + return [(key, len(value)) for key,value in buckets.items() if len(value) > 1] + +# make entity helper ##### + +def mk_entity(row, map): + """Return a dict made from sanitized mapped values. + + >>> row = {'myname': u'dupont'} + >>> map = [('myname', u'name', (capitalize_if_unicase,))] + >>> mk_entity(row, map) + {'name': u'Dupont'} + """ + res = {} + for src, dest, funcs in map: + res[dest] = row[src] + for func in funcs: + res[dest] = func(res[dest]) + return res + +# object stores + +class ObjectStore(object): + """Store objects in memory for faster testing. Will not + enforce the constraints of the schema and hence will miss + some problems. + + >>> store = ObjectStore() + >>> user = {'login': 'johndoe'} + >>> store.add('CWUser', user) + >>> group = {'name': 'unknown'} + >>> store.add('CWUser', group) + >>> store.relate(user['eid'], 'in_group', group['eid']) + """ + + def __init__(self): + self.items = [] + self.eids = {} + self.types = {} + self.relations = set() + self.indexes = {} + self._rql = None + self._checkpoint = None + + def _put(self, type, item): + self.items.append(item) + return len(self.items) - 1 + + def add(self, type, item): + assert isinstance(item, dict), item + eid = item['eid'] = self._put(type, item) + self.eids[eid] = item + self.types.setdefault(type, []).append(eid) + + def relate(self, eid_from, rtype, eid_to): + eids_valid = (eid_from < len(self.items) and eid_to <= len(self.items)) + assert eids_valid, 'eid error %s %s' % (eid_from, eid_to) + self.relations.add( (eid_from, rtype, eid_to) ) + + def build_index(self, name, type, func): + index = {} + for eid in self.types[type]: + index.setdefault(func(self.eids[eid]), []).append(eid) + self.indexes[name] = index + + def get_many(self, name, key): + return self.indexes[name].get(key, []) + + def get_one(self, name, key): + eids = self.indexes[name].get(key, []) + assert len(eids) == 1 + return eids[0] + + def find(self, type, key, value): + for idx in self.types[type]: + item = self.items[idx] + if item[key] == value: + yield item + + def rql(self, query, args): + if self._rql: + return self._rql(query, args) + + def checkpoint(self): + if self._checkpoint: + self._checkpoint() + +class RQLObjectStore(ObjectStore): + """ObjectStore that works with an actual RQL repository.""" + + def _put(self, type, item): + query = ('INSERT %s X: ' % type) + ', '.join(['X %s %%(%s)s' % (key,key) for key in item]) + return self.rql(query, item)[0][0] + + def relate(self, eid_from, rtype, eid_to): + query = 'SET X %s Y WHERE X eid %%(from)s, Y eid %%(to)s' % rtype + self.rql(query, {'from': int(eid_from), 'to': int(eid_to)}) + self.relations.add( (eid_from, rtype, eid_to) ) + +# import controller ##### + +class CWImportController(object): + """Controller of the data import process. + + >>> ctl = CWImportController(store) + >>> ctl.generators = list_of_data_generators + >>> ctl.data = dict_of_data_tables + >>> ctl.run() + """ + + def __init__(self, store): + self.store = store + self.generators = None + self.data = {} + self.errors = None + self.askerror = False + self._tell = tell + + def check(self, type, key, value): + self._checks.setdefault(type, {}).setdefault(key, []).append(value) + + def check_map(self, entity, key, map, default): + try: + entity[key] = map[entity[key]] + except KeyError: + self.check(key, entity[key], None) + entity[key] = default + + def run(self): + self.errors = {} + for func, checks in self.generators: + self._checks = {} + func_name = func.__name__[4:] + question = 'Importation de %s' % func_name + self.tell(question) + try: + func(self) + except: + import StringIO + tmp = StringIO.StringIO() + traceback.print_exc(file=tmp) + print tmp.getvalue() + self.errors[func_name] = ('Erreur lors de la transformation', + tmp.getvalue().splitlines()) + for key, func, title, help in checks: + buckets = self._checks.get(key) + if buckets: + err = func(buckets) + if err: + self.errors[title] = (help, err) + self.store.checkpoint() + errors = sum(len(err[1]) for err in self.errors.values()) + self.tell('Importation terminée. (%i objets, %i types, %i relations et %i erreurs).' + % (len(self.store.eids), len(self.store.types), + len(self.store.relations), errors)) + if self.errors and self.askerror and confirm('Afficher les erreurs ?'): + import pprint + pprint.pprint(self.errors) + + def get_data(self, key): + return self.data.get(key) + + def index(self, name, key, value): + self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value) + + def tell(self, msg): + self._tell(msg) + +def confirm(question): + """A confirm function that asks for yes/no/abort and exits on abort.""" + answer = shellutils.ASK.ask(question, ('Y','n','abort'), 'Y') + if answer == 'abort': + sys.exit(1) + return answer == 'Y' diff -r 24489cbbd697 -r 70c0dd1c3b7d devtools/devctl.py --- a/devtools/devctl.py Thu Sep 17 14:03:21 2009 +0200 +++ b/devtools/devctl.py Thu Sep 17 14:53:18 2009 +0200 @@ -10,8 +10,9 @@ import sys from datetime import datetime -from os import mkdir, chdir +from os import mkdir, chdir, getcwd from os.path import join, exists, abspath, basename, normpath, split, isdir +from copy import deepcopy from warnings import warn from logilab.common import STD_BLACKLIST @@ -20,7 +21,8 @@ from logilab.common.shellutils import ASK from logilab.common.clcommands import register_commands -from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, underline_title +from cubicweb import (CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, + underline_title) from cubicweb.__pkginfo__ import version as cubicwebversion from cubicweb.toolsutils import Command, copy_skeleton from cubicweb.web.webconfig import WebConfiguration @@ -111,88 +113,100 @@ def _generate_schema_pot(w, vreg, schema, libconfig=None, cube=None): from cubicweb.common.i18n import add_msg + from cubicweb.web import uicfg + from cubicweb.schema import META_RTYPES, SYSTEM_RTYPES + no_context_rtypes = META_RTYPES | SYSTEM_RTYPES w('# schema pot file, generated on %s\n' % datetime.now().strftime('%Y-%m-%d %H:%M:%S')) w('# \n') w('# singular and plural forms for each entity type\n') w('\n') + vregdone = set() if libconfig is not None: + from cubicweb.cwvreg import CubicWebVRegistry, clear_rtag_objects libschema = libconfig.load_schema(remove_unused_rtypes=False) - entities = [e for e in schema.entities() if not e in libschema] + rinlined = deepcopy(uicfg.autoform_is_inlined) + appearsin_addmenu = deepcopy(uicfg.actionbox_appearsin_addmenu) + clear_rtag_objects() + cleanup_sys_modules(libconfig) + libvreg = CubicWebVRegistry(libconfig) + libvreg.set_schema(libschema) # trigger objects registration + librinlined = uicfg.autoform_is_inlined + libappearsin_addmenu = uicfg.actionbox_appearsin_addmenu + # prefill vregdone set + list(_iter_vreg_objids(libvreg, vregdone)) else: - entities = schema.entities() + libschema = {} + rinlined = uicfg.autoform_is_inlined + appearsin_addmenu = uicfg.actionbox_appearsin_addmenu done = set() - for eschema in sorted(entities): + for eschema in sorted(schema.entities()): etype = eschema.type - add_msg(w, etype) - add_msg(w, '%s_plural' % etype) - if not eschema.is_final(): - add_msg(w, 'This %s' % etype) - add_msg(w, 'New %s' % etype) - add_msg(w, 'add a %s' % etype) - add_msg(w, 'remove this %s' % etype) - if eschema.description and not eschema.description in done: - done.add(eschema.description) - add_msg(w, eschema.description) - w('# subject and object forms for each relation type\n') - w('# (no object form for final relation types)\n') - w('\n') - if libconfig is not None: - relations = [r for r in schema.relations() if not r in libschema] - else: - relations = schema.relations() - for rschema in sorted(set(relations)): - rtype = rschema.type - add_msg(w, rtype) - done.add(rtype) - if not (schema.rschema(rtype).is_final() or rschema.symetric): - add_msg(w, '%s_object' % rtype) - if rschema.description and rschema.description not in done: - done.add(rschema.description) - add_msg(w, rschema.description) - w('# add related box generated message\n') - w('\n') - actionbox = vreg['boxes']['edit_box'][0] - for eschema in schema.entities(): + if etype not in libschema: + add_msg(w, etype) + add_msg(w, '%s_plural' % etype) + if not eschema.is_final(): + add_msg(w, 'This %s' % etype) + add_msg(w, 'New %s' % etype) + if eschema.description and not eschema.description in done: + done.add(eschema.description) + add_msg(w, eschema.description) if eschema.is_final(): continue - for role, rschemas in (('subject', eschema.subject_relations()), - ('object', eschema.object_relations())): - for rschema in rschemas: - if rschema.is_final(): - continue - if libconfig is not None: - librschema = libschema.get(rschema) - for teschema in rschema.targets(eschema, role): - if libconfig is not None and librschema is not None: - if role == 'subject': - subjtype, objtype = eschema, teschema - else: - subjtype, objtype = teschema, eschema - if librschema.has_rdef(subjtype, objtype): - continue - if actionbox.appearsin_addmenu.etype_get(eschema, rschema, - role, teschema): - if role == 'subject': - label = 'add %s %s %s %s' % (eschema, rschema, - teschema, role) - label2 = "creating %s (%s %%(linkto)s %s %s)" % ( - teschema, eschema, rschema, teschema) - else: - label = 'add %s %s %s %s' % (teschema, rschema, - eschema, role) - label2 = "creating %s (%s %s %s %%(linkto)s)" % ( - teschema, teschema, rschema, eschema) - add_msg(w, label) - add_msg(w, label2) - #cube = (cube and 'cubes.%s.' % cube or 'cubicweb.') - done = set() - if libconfig is not None: - from cubicweb.cwvreg import CubicWebVRegistry - libvreg = CubicWebVRegistry(libconfig) - libvreg.set_schema(libschema) # trigger objects registration - # prefill done set - list(_iter_vreg_objids(libvreg, done)) - for objid in _iter_vreg_objids(vreg, done): + for rschema, targetschemas, role in eschema.relation_definitions(True): + for tschema in targetschemas: + if rinlined.etype_get(eschema, rschema, role, tschema) and \ + (libconfig is None or not + librinlined.etype_get(eschema, rschema, role, tschema)): + add_msg(w, 'add a %s' % tschema, + 'inlined:%s.%s.%s' % (etype, rschema, role)) + add_msg(w, 'remove this %s' % tschema, + 'inlined:%s:%s:%s' % (etype, rschema, role)) + if appearsin_addmenu.etype_get(eschema, rschema, role, tschema) and \ + (libconfig is None or not + libappearsin_addmenu.etype_get(eschema, rschema, role, tschema)): + if role == 'subject': + label = 'add %s %s %s %s' % (eschema, rschema, + tschema, role) + label2 = "creating %s (%s %%(linkto)s %s %s)" % ( + tschema, eschema, rschema, tschema) + else: + label = 'add %s %s %s %s' % (tschema, rschema, + eschema, role) + label2 = "creating %s (%s %s %s %%(linkto)s)" % ( + tschema, tschema, rschema, eschema) + add_msg(w, label) + add_msg(w, label2) + w('# subject and object forms for each relation type\n') + w('# (no object form for final or symetric relation types)\n') + w('\n') + for rschema in sorted(schema.relations()): + rtype = rschema.type + if rtype not in libschema: + # bw compat, necessary until all translation of relation are done properly... + add_msg(w, rtype) + if rschema.description and rschema.description not in done: + done.add(rschema.description) + add_msg(w, rschema.description) + done.add(rtype) + librschema = None + else: + librschema = libschema.rschema(rtype) + # add context information only for non-metadata rtypes + if rschema not in no_context_rtypes: + libsubjects = librschema and librschema.subjects() or () + for subjschema in rschema.subjects(): + if not subjschema in libsubjects: + add_msg(w, rtype, subjschema.type) + if not (schema.rschema(rtype).is_final() or rschema.symetric): + if rschema not in no_context_rtypes: + libobjects = librschema and librschema.objects() or () + for objschema in rschema.objects(): + if not objschema in libobjects: + add_msg(w, '%s_object' % rtype, objschema.type) + if rtype not in libschema: + # bw compat, necessary until all translation of relation are done properly... + add_msg(w, '%s_object' % rtype) + for objid in _iter_vreg_objids(vreg, vregdone): add_msg(w, '%s_description' % objid) add_msg(w, objid) @@ -283,20 +297,20 @@ if lang is not None: cmd += ' -L %s' % lang potfile = join(tempdir, '%s.pot' % id) - execute(cmd % (potfile, ' '.join(files))) + execute(cmd % (potfile, ' '.join('"%s"' % f for f in files))) if exists(potfile): potfiles.append(potfile) else: print '-> WARNING: %s file was not generated' % potfile print '-> merging %i .pot files' % len(potfiles) cubicwebpot = join(tempdir, 'cubicweb.pot') - execute('msgcat %s > %s' % (' '.join(potfiles), cubicwebpot)) + execute('msgcat -o %s %s' % (cubicwebpot, ' '.join('"%s"' % f for f in potfiles))) print '-> merging main pot file with existing translations.' chdir(I18NDIR) toedit = [] for lang in LANGS: target = '%s.po' % lang - execute('msgmerge -N --sort-output %s %s > %snew' % (target, cubicwebpot, target)) + execute('msgmerge -N --sort-output -o "%snew" "%s" "%s"' % (target, target, cubicwebpot)) ensure_fs_mode(target) shutil.move('%snew' % target, target) toedit.append(abspath(target)) @@ -390,12 +404,13 @@ cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',)) cubefiles.append(tali18nfile) execute('xgettext --no-location --omit-header -k_ -o %s %s' - % (tmppotfile, ' '.join(cubefiles))) + % (tmppotfile, ' '.join('"%s"' % f for f in cubefiles))) if exists(tmppotfile): # doesn't exists of no translation string found potfiles.append(tmppotfile) potfile = join(tempdir, 'cube.pot') print '-> merging %i .pot files:' % len(potfiles) - execute('msgcat %s > %s' % (' '.join(potfiles), potfile)) + execute('msgcat -o %s %s' % (potfile, + ' '.join('"%s"' % f for f in potfiles))) print '-> merging main pot file with existing translations:' chdir('i18n') for lang in LANGS: @@ -404,7 +419,7 @@ if not exists(cubepo): shutil.copy(potfile, cubepo) else: - execute('msgmerge -N -s %s %s > %snew' % (cubepo, potfile, cubepo)) + execute('msgmerge -N -s -o %snew %s %s' % (cubepo, cubepo, potfile)) ensure_fs_mode(cubepo) shutil.move('%snew' % cubepo, cubepo) toedit.append(abspath(cubepo)) diff -r 24489cbbd697 -r 70c0dd1c3b7d devtools/fake.py --- a/devtools/fake.py Thu Sep 17 14:03:21 2009 +0200 +++ b/devtools/fake.py Thu Sep 17 14:53:18 2009 +0200 @@ -13,6 +13,7 @@ from indexer import get_indexer from cubicweb import RequestSessionMixIn +from cubicweb.cwvreg import CubicWebVRegistry from cubicweb.web.request import CubicWebRequestBase from cubicweb.devtools import BASE_URL, BaseApptestConfiguration @@ -35,39 +36,13 @@ def sources(self): return {} -class FakeVReg(dict): - def __init__(self, schema=None, config=None): - self.schema = schema - self.config = config or FakeConfig() - self.properties = {'ui.encoding': 'UTF8', - 'ui.language': 'en', - } - self.update({ - 'controllers' : {'login': []}, - 'views' : {}, - }) - - def property_value(self, key): - return self.properties[key] - - def etype_class(self, etype): - class Entity(dict): - e_schema = self.schema[etype] - def __init__(self, session, eid, row=0, col=0): - self.req = session - self.eid = eid - self.row, self.col = row, col - def set_eid(self, eid): - self.eid = self['eid'] = eid - return Entity - class FakeRequest(CubicWebRequestBase): """test implementation of an cubicweb request object""" def __init__(self, *args, **kwargs): if not (args or 'vreg' in kwargs): - kwargs['vreg'] = FakeVReg() + kwargs['vreg'] = CubicWebVRegistry(FakeConfig(), initlog=False) kwargs['https'] = False self._url = kwargs.pop('url', 'view?rql=Blop&vid=blop') super(FakeRequest, self).__init__(*args, **kwargs) @@ -148,25 +123,6 @@ return self.execute(*args, **kwargs) -# class FakeRequestNoCnx(FakeRequest): -# def get_session_data(self, key, default=None, pop=False): -# """return value associated to `key` in session data""" -# if pop: -# return self._session_data.pop(key, default) -# else: -# return self._session_data.get(key, default) - -# def set_session_data(self, key, value): -# """set value associated to `key` in session data""" -# self._session_data[key] = value - -# def del_session_data(self, key): -# try: -# del self._session_data[key] -# except KeyError: -# pass - - class FakeUser(object): login = 'toto' eid = 0 @@ -177,7 +133,7 @@ class FakeSession(RequestSessionMixIn): def __init__(self, repo=None, user=None): self.repo = repo - self.vreg = getattr(self.repo, 'vreg', FakeVReg()) + self.vreg = getattr(self.repo, 'vreg', CubicWebVRegistry(FakeConfig(), initlog=False)) self.pool = FakePool() self.user = user or FakeUser() self.is_internal_session = False @@ -210,8 +166,9 @@ self.eids = {} self._count = 0 self.schema = schema - self.vreg = vreg or FakeVReg() self.config = config or FakeConfig() + self.vreg = vreg or CubicWebVRegistry(self.config, initlog=False) + self.vreg.schema = schema def internal_session(self): return FakeSession(self) @@ -249,10 +206,3 @@ class FakePool(object): def source(self, uri): return FakeSource(uri) - -# commented until proven to be useful -## from logging import getLogger -## from cubicweb import set_log_methods -## for cls in (FakeConfig, FakeVReg, FakeRequest, FakeSession, FakeRepo, -## FakeSource, FakePool): -## set_log_methods(cls, getLogger('fake')) diff -r 24489cbbd697 -r 70c0dd1c3b7d devtools/repotest.py --- a/devtools/repotest.py Thu Sep 17 14:03:21 2009 +0200 +++ b/devtools/repotest.py Thu Sep 17 14:53:18 2009 +0200 @@ -108,9 +108,10 @@ schema = None # set this in concret test def setUp(self): + self.repo = FakeRepo(self.schema) self.rqlhelper = RQLHelper(self.schema, special_relations={'eid': 'uid', 'has_text': 'fti'}) - self.qhelper = QuerierHelper(FakeRepo(self.schema), self.schema) + self.qhelper = QuerierHelper(self.repo, self.schema) ExecutionPlan._check_permissions = _dummy_check_permissions rqlannotation._select_principal = _select_principal @@ -129,7 +130,7 @@ #print '********* solutions', solutions self.rqlhelper.simplify(union) #print '********* simplified', union.as_string() - plan = self.qhelper.plan_factory(union, {}, FakeSession()) + plan = self.qhelper.plan_factory(union, {}, FakeSession(self.repo)) plan.preprocess(union) for select in union.children: select.solutions.sort() @@ -167,7 +168,7 @@ set_debug(debug) def _rqlhelper(self): - rqlhelper = self.o._rqlhelper + rqlhelper = self.repo.vreg.rqlhelper # reset uid_func so it don't try to get type from eids rqlhelper._analyser.uid_func = None rqlhelper._analyser.uid_func_mapping = {} @@ -241,7 +242,7 @@ rqlst = self.o.parse(rql, annotate=True) self.o.solutions(self.session, rqlst, kwargs) if rqlst.TYPE == 'select': - self.o._rqlhelper.annotate(rqlst) + self.repo.vreg.rqlhelper.annotate(rqlst) for select in rqlst.children: select.solutions.sort() else: @@ -251,7 +252,7 @@ # monkey patch some methods to get predicatable results ####################### -from cubicweb.server.rqlrewrite import RQLRewriter +from cubicweb.rqlrewrite import RQLRewriter _orig_insert_snippets = RQLRewriter.insert_snippets _orig_build_variantes = RQLRewriter.build_variantes diff -r 24489cbbd697 -r 70c0dd1c3b7d devtools/testlib.py --- a/devtools/testlib.py Thu Sep 17 14:03:21 2009 +0200 +++ b/devtools/testlib.py Thu Sep 17 14:53:18 2009 +0200 @@ -333,7 +333,7 @@ # resultset's syntax tree rset = backup_rset for action in self.list_actions_for(rset): - yield InnerTest(self._testname(rset, action.id, 'action'), action.url) + yield InnerTest(self._testname(rset, action.id, 'action'), self._test_action, action) for box in self.list_boxes_for(rset): yield InnerTest(self._testname(rset, box.id, 'box'), box.render) diff -r 24489cbbd697 -r 70c0dd1c3b7d doc/book/en/admin/setup.rst --- a/doc/book/en/admin/setup.rst Thu Sep 17 14:03:21 2009 +0200 +++ b/doc/book/en/admin/setup.rst Thu Sep 17 14:53:18 2009 +0200 @@ -77,6 +77,133 @@ In both cases, make sure you have installed the dependencies (see appendixes for the list). +Windows installation +```````````````````` + +Base elements +_____________ + +Setting up a windows development environment is not too complicated +but requires a series of small steps. What is proposed there is only +an example of what can be done. We assume everything goes into C:\ in +this document. Adjusting the installation drive should be +straightforward. + +You should start by downloading and installing the Python(x,y) +distribution. It contains python 2.5 plus numerous useful third-party +modules and applications:: + + http://www.pythonxy.com/download_fr.php + +At the time of this writting, one gets version 2.1.15. Among the many +things provided, one finds Eclipse + pydev (an arguably good IDE for +python under windows). + +Then you must grab Twisted. There is a windows installer directly +available from this page:: + + http://twistedmatrix.com/trac/ + +A windows installer for lxml will be found there:: + + http://pypi.python.org/pypi/lxml/2.2.1 + +Check out the lxml-2.2.1-win32-py2.5.exe file. More recent bugfix +releases should probably work, too. + +You should find postgresql 8.4 there:: + + http://www.enterprisedb.com/products/pgdownload.do#windows + +The python drivers for posgtresql are to be found there:: + + http://www.stickpeople.com/projects/python/win-psycopg/#Version2 + +Please be careful to select the right python (2.5) and postgres (8.4) +versions. + +Having graphviz will allow schema drawings, which is quite recommended +(albeit not mandatory). You should get an msi installer there:: + + http://www.graphviz.org/Download_windows.php + +Tools +_____ + +Get mercurial + its standard windows GUI (TortoiseHG) there (the +latest is the greatest):: + + http://bitbucket.org/tortoisehg/stable/wiki/download + +If you need to peruse mercurial over ssh, it can be helpful to get an +ssh client like Putty:: + + http://www.putty.org/ + +Integration of mercurial and Eclipse is convenient enough that we want +it. Instructions are set there, in the `Download & Install` section:: + + http://www.vectrace.com/mercurialeclipse/ + +Setting up the sources +______________________ + +You need to enable the mercurial forest extension. To do this, edit +the file:: + + C:\Program Files\TortoiseHg\Mercurial.ini + +In the [extensions] section, add the following line:: + + forest=C:\Program Files\TortoiseHg\ext\forest\forest.py + +Now, you need to clone the cubicweb repository. We assume that you use +Eclipse. From the IDE, choose File -> Import. In the box, select +`Mercurial/Clone repository using MercurialEclipse`. + +In the import main panel you just have to: + +* fill the URL field with http://www.logilab.org/hg/forests/cubicwin32 + +* check the 'Repository is a forest' box. + +Then, click on 'Finish'. It might take some time to get it all. Note +that the `cubicwin32` forest contains additional python packages such +as yapps, vobject, simplejson and twisted-web2 which are not provided +with Python(x,y). This is provided for convenience, as we do not +ensure the up-to-dateness of these packages, especially with respect +to security fixes. + +Environment variables +_____________________ + +You will need some convenience environment variables once all is set +up. These variables are settable through the GUI by getting at the +'System properties' window (by righ-clicking on 'My Computer' -> +properties). + +In the 'advanced' tab, there is an 'Environment variables' +button. Click on it. That opens a small window allowing edition of +user-related and system-wide variables. + +We will consider only user variables. First, the PATH variable. You +should ensure it contains, separated by semi-colons, and assuming you +are logged in as user Jane:: + + C:\Documents and Settings\Jane\My Documents\Python\cubicweb\cubicweb\bin + C:\Program Files\Graphviz2.24\bin + +The PYTHONPATH variable should also contain:: + + C:\Documents and Settings\Jane\My Documents\Python\cubicweb\ + +From now, on a fresh `cmd` shell, you should be able to type:: + + cubicweb-ctl list + +... and get a meaningful output. + + PostgreSQL installation ``````````````````````` @@ -135,8 +262,6 @@ Databases configuration ----------------------- - - .. _ConfigurationPostgresql: PostgreSQL configuration diff -r 24489cbbd697 -r 70c0dd1c3b7d doc/book/en/development/entityclasses/load-sort.rst --- a/doc/book/en/development/entityclasses/load-sort.rst Thu Sep 17 14:03:21 2009 +0200 +++ b/doc/book/en/development/entityclasses/load-sort.rst Thu Sep 17 14:53:18 2009 +0200 @@ -16,18 +16,30 @@ `None` if we do not want to sort on the attribute given in the parameter. By default, the entities are sorted according to their creation date. -* The class method `fetch_unrelated_order(attr, var)` is similar to the - method `fetch_order` except that it is essentially used to control - the sorting of drop-down lists enabling relations creation in - the editing view of an entity. +* The class method `fetch_unrelated_order(attr, var)` is similar to + the method `fetch_order` except that it is essentially used to + control the sorting of drop-down lists enabling relations creation + in the editing view of an entity. The default implementation uses + the modification date. Here's how to adapt it for one entity (sort + on the name attribute): :: + + class MyEntity(AnyEntity): + fetch_attrs = ('modification_date', 'name') + + @classmethod + def fetch_unrelated_order(cls, attr, var): + if attr == 'name': + return '%s ASC' % var + return None + The function `fetch_config(fetchattrs, mainattr=None)` simplifies the definition of the attributes to load and the sorting by returning a -list of attributes to pre-load (considering automatically the attributes -of `AnyEntity`) and a sorting function based on the main attribute -(the second parameter if specified otherwisethe first attribute from -the list `fetchattrs`). -This function is defined in `cubicweb.entities`. +list of attributes to pre-load (considering automatically the +attributes of `AnyEntity`) and a sorting function based on the main +attribute (the second parameter if specified, otherwise the first +attribute from the list `fetchattrs`). This function is defined in +`cubicweb.entities`. For example: :: diff -r 24489cbbd697 -r 70c0dd1c3b7d doc/book/en/development/testing/index.rst --- a/doc/book/en/development/testing/index.rst Thu Sep 17 14:03:21 2009 +0200 +++ b/doc/book/en/development/testing/index.rst Thu Sep 17 14:53:18 2009 +0200 @@ -16,14 +16,48 @@ * `EnvBasedTC`, to simulate a complete environment (web + repository) * `RepositoryBasedTC`, to simulate a repository environment only -Thos two classes almost have the same interface and offers numerous methods to -write tests rapidely and efficiently. +Those two classes almost have the same interface and offer numerous +methods to write tests rapidly and efficiently. XXX FILLME describe API In most of the cases, you will inherit `EnvBasedTC` to write Unittest or functional tests for your entities, views, hooks, etc... +Managing connections or users ++++++++++++++++++++++++++++++ + +Since unit tests are done with the SQLITE backend and this does not +support multiple connections at a time, you must be careful when +simulating security, changing users. + +By default, tests run with a user with admin privileges. This +user/connection must never be closed. +qwq +Before a self.login, one has to release the connection pool in use with a self.commit, self.rollback or self.close. + +When one is logged in as a normal user and wants to switch back to the admin user, one has to use self.restore_connection(). + +Usually it looks like this: + +.. sourcecode:: python + + # execute using default admin connection + self.execute(...) + # I want to login with another user, ensure to free admin connection pool + # (could have used rollback but not close here, we should never close defaut admin connection) + self.commit() + cnx = self.login('user') + # execute using user connection + self.execute(...) + # I want to login with another user or with admin user + self.commit(); cnx.close() + # restore admin connection, never use cnx = self.login('admin'), it will return + # the default admin connection and one may be tempted to close it + self.restore_connection() + +Take care of the references kept to the entities created with a connection or the other. + Email notifications tests ------------------------- diff -r 24489cbbd697 -r 70c0dd1c3b7d entities/lib.py --- a/entities/lib.py Thu Sep 17 14:03:21 2009 +0200 +++ b/entities/lib.py Thu Sep 17 14:53:18 2009 +0200 @@ -10,7 +10,7 @@ from urlparse import urlsplit, urlunsplit from datetime import datetime -from logilab.common.decorators import cached +from logilab.common.deprecation import deprecated from cubicweb import UnknownProperty from cubicweb.entity import _marker @@ -25,7 +25,7 @@ class EmailAddress(AnyEntity): id = 'EmailAddress' - fetch_attrs, fetch_order = fetch_config(['address', 'alias', 'canonical']) + fetch_attrs, fetch_order = fetch_config(['address', 'alias']) def dc_title(self): if self.alias: @@ -36,15 +36,13 @@ def email_of(self): return self.reverse_use_email and self.reverse_use_email[0] - @cached + @property + def prefered(self): + return self.prefered_form and self.prefered_form[0] or self + + @deprecated('use .prefered') def canonical_form(self): - if self.canonical: - return self - rql = 'EmailAddress X WHERE X identical_to Y, X canonical TRUE, Y eid %(y)s' - cnrset = self.req.execute(rql, {'y': self.eid}, 'y') - if cnrset: - return cnrset.get_entity(0, 0) - return None + return self.prefered_form and self.prefered_form[0] or self def related_emails(self, skipeids=None): # XXX move to eemail diff -r 24489cbbd697 -r 70c0dd1c3b7d entities/test/data/migration/postcreate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/test/data/migration/postcreate.py Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,2 @@ +wf = add_workflow(u'bmk wf', 'Bookmark') +wf.add_state(u'hop', initial=True) diff -r 24489cbbd697 -r 70c0dd1c3b7d entities/test/data/schema.py --- a/entities/test/data/schema.py Thu Sep 17 14:03:21 2009 +0200 +++ b/entities/test/data/schema.py Thu Sep 17 14:53:18 2009 +0200 @@ -1,11 +1,13 @@ -""" +"""entities tests schema :organization: Logilab :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ + from yams.buildobjs import EntityType, String +from cubicweb.schema import make_workflowable class Company(EntityType): name = String() @@ -16,3 +18,7 @@ class SubDivision(Division): __specializes_schema__ = True + +from cubicweb.schemas import bootstrap, Bookmark +make_workflowable(bootstrap.CWGroup) +make_workflowable(Bookmark.Bookmark) diff -r 24489cbbd697 -r 70c0dd1c3b7d entities/test/unittest_base.py --- a/entities/test/unittest_base.py Thu Sep 17 14:03:21 2009 +0200 +++ b/entities/test/unittest_base.py Thu Sep 17 14:53:18 2009 +0200 @@ -16,7 +16,6 @@ from cubicweb import ValidationError from cubicweb.interfaces import IMileStone, IWorkflowable from cubicweb.entities import AnyEntity -from cubicweb.entities.authobjs import CWUser from cubicweb.web.widgets import AutoCompletionWidget @@ -58,161 +57,15 @@ 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] - eid2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com", X canonical TRUE')[0][0] - self.execute('SET X identical_to Y WHERE X eid %s, Y eid %s' % (eid1, eid2)) - email1 = self.entity('Any X WHERE X eid %(x)s', {'x':eid1}, 'x') - email2 = self.entity('Any X WHERE X eid %(x)s', {'x':eid2}, 'x') - self.assertEquals(email1.canonical_form().eid, eid2) - self.assertEquals(email2.canonical_form(), email2) - eid3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"')[0][0] - email3 = self.entity('Any X WHERE X eid %s'%eid3) - self.assertEquals(email3.canonical_form(), None) + email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0) + email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0) + email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0) + self.execute('SET X prefered_form Y WHERE X eid %s, Y eid %s' % (email1.eid, email2.eid)) + self.assertEquals(email1.prefered.eid, email2.eid) + self.assertEquals(email2.prefered.eid, email2.eid) + self.assertEquals(email3.prefered.eid, email3.eid) def test_mangling(self): eid = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"')[0][0] @@ -234,7 +87,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,27 +94,11 @@ 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): def test_nonregr_subclasses_and_mixins_interfaces(self): + CWUser = self.vreg['etypes'].etype_class('CWUser') self.failUnless(implements(CWUser, IWorkflowable)) class MyUser(CWUser): __implements__ = (IMileStone,) @@ -270,9 +106,13 @@ self.vreg.register_appobject_class(MyUser) self.vreg['etypes'].initialization_completed() MyUser_ = self.vreg['etypes'].etype_class('CWUser') - self.failUnless(MyUser is MyUser_) + # a copy is done systematically + self.failUnless(issubclass(MyUser_, MyUser)) self.failUnless(implements(MyUser_, IMileStone)) self.failUnless(implements(MyUser_, IWorkflowable)) + # original class should not have beed modified, only the copy + self.failUnless(implements(MyUser, IMileStone)) + self.failIf(implements(MyUser, IWorkflowable)) class SpecializedEntityClassesTC(EnvBasedTC): @@ -295,11 +135,11 @@ id = etype self.vreg.register_appobject_class(Foo) eclass = self.select_eclass('SubDivision') + self.failUnless(eclass.__autogenerated__) if etype == 'SubDivision': - self.failUnless(eclass is Foo) + self.assertEquals(eclass.__bases__, (Foo,)) else: - self.failUnless(eclass.__autogenerated__) - self.assertEquals(eclass.__bases__, (Foo,)) + self.assertEquals(eclass.__bases__[0].__bases__, (Foo,)) # check Division eclass is still selected for plain Division entities eclass = self.select_eclass('Division') self.assertEquals(eclass.id, 'Division') diff -r 24489cbbd697 -r 70c0dd1c3b7d entities/test/unittest_wfobjs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/test/unittest_wfobjs.py Thu Sep 17 14:53:18 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, "default user 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 "default user workflow"'),]) + + +from cubicweb.devtools.apptest import RepositoryBasedTC + +class WorkflowHooksTC(RepositoryBasedTC): + + def setUp(self): + RepositoryBasedTC.setUp(self) + self.wf = self.session.user.current_workflow + self.s_activated = self.wf.state_by_name('activated').eid + self.s_deactivated = self.wf.state_by_name('deactivated').eid + self.s_dummy = self.wf.add_state(u'dummy').eid + self.wf.add_transition(u'dummy', (self.s_deactivated,), self.s_dummy) + ueid = self.create_user('stduser', commit=False) + # test initial state is set + rset = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', + {'x' : ueid}) + self.failIf(rset, rset.rows) + self.commit() + initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', + {'x' : ueid})[0][0] + self.assertEquals(initialstate, u'activated') + # give access to users group on the user's wf transitions + # so we can test wf enforcing on euser (managers don't have anymore this + # enforcement + self.execute('SET X require_group G ' + 'WHERE G name "users", X transition_of WF, WF eid %(wf)s', + {'wf': self.wf.eid}) + self.commit() + + def tearDown(self): + self.execute('DELETE X require_group G ' + 'WHERE G name "users", X transition_of WF, WF eid %(wf)s', + {'wf': self.wf.eid}) + self.commit() + RepositoryBasedTC.tearDown(self) + + # XXX currently, we've to rely on hooks to set initial state, or to use unsafe_execute + # def test_initial_state(self): + # cnx = self.login('stduser') + # cu = cnx.cursor() + # self.assertRaises(ValidationError, cu.execute, + # 'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' + # 'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'}) + # cnx.close() + # # though managers can do whatever he want + # self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' + # 'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'}) + # self.commit() + + # test that the workflow is correctly enforced + def test_transition_checking1(self): + cnx = self.login('stduser') + user = cnx.user(self.current_session()) + ex = self.assertRaises(ValidationError, + user.fire_transition, 'activate') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + cnx.close() + + def test_transition_checking2(self): + cnx = self.login('stduser') + user = cnx.user(self.current_session()) + assert user.state == 'activated' + ex = self.assertRaises(ValidationError, + user.fire_transition, 'dummy') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + cnx.close() + + def test_transition_checking3(self): + cnx = self.login('stduser') + session = self.current_session() + user = cnx.user(session) + user.fire_transition('deactivate') + cnx.commit() + session.set_pool() + ex = self.assertRaises(ValidationError, + user.fire_transition, 'deactivate') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + # get back now + user.fire_transition('activate') + cnx.commit() + cnx.close() + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d entities/wfobjs.py --- a/entities/wfobjs.py Thu Sep 17 14:03:21 2009 +0200 +++ b/entities/wfobjs.py Thu Sep 17 14:53:18 2009 +0200 @@ -7,23 +7,176 @@ """ __docformat__ = "restructuredtext en" +from warnings import warn + +from logilab.common.decorators import cached, clear_cache +from logilab.common.deprecation import deprecated + from cubicweb.entities import AnyEntity, fetch_config +from cubicweb.interfaces import IWorkflowable +from cubicweb.common.mixins import MI_REL_TRIGGERS + +class WorkflowException(Exception): pass + +class Workflow(AnyEntity): + id = 'Workflow' + + @property + def initial(self): + """return the initial state for this workflow""" + return self.initial_state and self.initial_state[0] or None + + def is_default_workflow_of(self, etype): + """return True if this workflow is the default workflow for the given + entity type + """ + return any(et for et in self.reverse_default_workflow + if et.name == etype) + + def after_deletion_path(self): + """return (path, parameters) which should be used as redirect + information when this entity is being deleted + """ + if self.workflow_of: + return self.workflow_of[0].rest_path(), {'vid': 'workflow'} + return super(Workflow, self).after_deletion_path() + + def iter_workflows(self, _done=None): + """return an iterator on actual workflows, eg this workflow and its + subworkflows + """ + # infinite loop safety belt + if _done is None: + _done = set() + yield self + _done.add(self.eid) + for tr in self.req.execute('Any T WHERE T is WorkflowTransition, ' + 'T transition_of WF, WF eid %(wf)s', + {'wf': self.eid}).entities(): + if tr.subwf.eid in _done: + continue + for subwf in tr.subwf.iter_workflows(_done): + yield subwf + + # state / transitions accessors ############################################ + + def state_by_name(self, statename): + rset = self.req.execute('Any S, SN WHERE S name SN, S name %(n)s, ' + 'S state_of WF, WF eid %(wf)s', + {'n': statename, 'wf': self.eid}, 'wf') + if rset: + return rset.get_entity(0, 0) + return None + + def state_by_eid(self, eid): + rset = self.req.execute('Any S, SN WHERE S name SN, S eid %(s)s, ' + 'S state_of WF, WF eid %(wf)s', + {'s': eid, 'wf': self.eid}, ('wf', 's')) + if rset: + return rset.get_entity(0, 0) + return None + + def transition_by_name(self, trname): + rset = self.req.execute('Any T, TN WHERE T name TN, T name %(n)s, ' + 'T transition_of WF, WF eid %(wf)s', + {'n': trname, 'wf': self.eid}, 'wf') + if rset: + return rset.get_entity(0, 0) + return None + + def transition_by_eid(self, eid): + rset = self.req.execute('Any T, TN WHERE T name TN, T eid %(t)s, ' + 'T transition_of WF, WF eid %(wf)s', + {'t': eid, 'wf': self.eid}, ('wf', 't')) + if rset: + return rset.get_entity(0, 0) + return None + + # wf construction methods ################################################## + + def add_state(self, name, initial=False, **kwargs): + """add a state to this workflow""" + state = self.req.create_entity('State', name=unicode(name), **kwargs) + self.req.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s', + {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) + if initial: + assert not self.initial + self.req.execute('SET WF initial_state S ' + 'WHERE S eid %(s)s, WF eid %(wf)s', + {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) + return state + + def _add_transition(self, trtype, name, fromstates, + requiredgroups=(), conditions=(), **kwargs): + tr = self.req.create_entity(trtype, name=unicode(name), **kwargs) + self.req.execute('SET T transition_of WF ' + 'WHERE T eid %(t)s, WF eid %(wf)s', + {'t': tr.eid, 'wf': self.eid}, ('t', 'wf')) + assert fromstates, fromstates + if not isinstance(fromstates, (tuple, list)): + fromstates = (fromstates,) + for state in fromstates: + if hasattr(state, 'eid'): + state = state.eid + self.req.execute('SET S allowed_transition T ' + 'WHERE S eid %(s)s, T eid %(t)s', + {'s': state, 't': tr.eid}, ('s', 't')) + tr.set_transition_permissions(requiredgroups, conditions, reset=False) + return tr + + def add_transition(self, name, fromstates, tostate, + requiredgroups=(), conditions=(), **kwargs): + """add a transition to this workflow from some state(s) to another""" + tr = self._add_transition('Transition', name, fromstates, + requiredgroups, conditions, **kwargs) + if hasattr(tostate, 'eid'): + tostate = tostate.eid + self.req.execute('SET T destination_state S ' + 'WHERE S eid %(s)s, T eid %(t)s', + {'t': tr.eid, 's': tostate}, ('s', 't')) + return tr + + def add_wftransition(self, name, subworkflow, fromstates, exitpoints, + requiredgroups=(), conditions=(), **kwargs): + """add a workflow transition to this workflow""" + tr = self._add_transition('WorkflowTransition', name, fromstates, + requiredgroups, conditions, **kwargs) + if hasattr(subworkflow, 'eid'): + subworkflow = subworkflow.eid + self.req.execute('SET T subworkflow WF WHERE WF eid %(wf)s,T eid %(t)s', + {'t': tr.eid, 'wf': subworkflow}, ('wf', 't')) + for fromstate, tostate in exitpoints: + tr.add_exit_point(fromstate, tostate) + return tr -class Transition(AnyEntity): - """customized class for Transition entities +class BaseTransition(AnyEntity): + """customized class for abstract transition - provides a specific may_be_passed method to check if the relation may be - passed by the logged user + provides a specific may_be_fired method to check if the relation may be + fired by the logged user """ - id = 'Transition' + id = 'BaseTransition' fetch_attrs, fetch_order = fetch_config(['name']) - def may_be_passed(self, eid, stateeid): - """return true if the logged user may pass this transition + def __init__(self, *args, **kwargs): + if self.id == 'BaseTransition': + raise WorkflowException('should not be instantiated') + super(BaseTransition, self).__init__(*args, **kwargs) + + @property + def workflow(self): + return self.transition_of[0] - `eid` is the eid of the object on which we may pass the transition - `stateeid` is the eid of the current object'state XXX unused + def has_input_state(self, state): + if hasattr(state, 'eid'): + state = state.eid + return any(s for s in self.reverse_allowed_transition if s.eid == state) + + def may_be_fired(self, eid): + """return true if the logged user may fire this transition + + `eid` is the eid of the object on which we may fire the transition """ user = self.req.user # check user is at least in one of the required groups if any @@ -43,47 +196,122 @@ return False return True - def destination(self): - return self.destination_state[0] - def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted """ if self.transition_of: - return self.transition_of[0].rest_path(), {'vid': 'workflow'} + return self.transition_of[0].rest_path(), {} return super(Transition, self).after_deletion_path() + def set_transition_permissions(self, requiredgroups=(), conditions=(), + reset=True): + """set or add (if `reset` is False) groups and conditions for this + transition + """ + if reset: + self.req.execute('DELETE T require_group G WHERE T eid %(x)s', + {'x': self.eid}, 'x') + self.req.execute('DELETE T condition R WHERE T eid %(x)s', + {'x': self.eid}, 'x') + for gname in requiredgroups: + rset = self.req.execute('SET T require_group G ' + 'WHERE T eid %(x)s, G name %(gn)s', + {'x': self.eid, 'gn': gname}, 'x') + assert rset, '%s is not a known group' % gname + if isinstance(conditions, basestring): + conditions = (conditions,) + for expr in conditions: + if isinstance(expr, str): + expr = unicode(expr) + self.req.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' + 'X expression %(expr)s, T condition X ' + 'WHERE T eid %(x)s', + {'x': self.eid, 'expr': expr}, 'x') + # XXX clear caches? + + +class Transition(BaseTransition): + """customized class for Transition entities""" + id = 'Transition' + + def destination(self): + return self.destination_state[0] + + +class WorkflowTransition(BaseTransition): + """customized class for WorkflowTransition entities""" + id = 'WorkflowTransition' + + @property + def subwf(self): + return self.subworkflow[0] + + def destination(self): + return self.subwf.initial + + def add_exit_point(self, fromstate, tostate): + if hasattr(fromstate, 'eid'): + fromstate = fromstate.eid + if hasattr(tostate, 'eid'): + tostate = tostate.eid + self.req.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, ' + 'X subworkflow_state FS, X destination_state TS ' + 'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s', + {'t': self.eid, 'fs': fromstate, 'ts': tostate}, + ('t', 'fs', 'ts')) + + def get_exit_point(self, state): + """if state is an exit point, return its associated destination state""" + if hasattr(state, 'eid'): + state = state.eid + stateeid = self.exit_points().get(state) + if stateeid is not None: + return self.req.entity_from_eid(stateeid) + return None + + @cached + def exit_points(self): + result = {} + for ep in self.subworkflow_exit: + result[ep.subwf_state.eid] = ep.destination.eid + return result + + def clear_all_caches(self): + super(WorkflowableMixIn, self).clear_all_caches() + clear_cache(self, 'exit_points') + + +class SubWorkflowExitPoint(AnyEntity): + """customized class for SubWorkflowExitPoint entities""" + id = 'SubWorkflowExitPoint' + + @property + def subwf_state(self): + return self.subworkflow_state[0] + + @property + def destination(self): + return self.destination_state[0] + class State(AnyEntity): - """customized class for State entities - - provides a specific transitions method returning transitions that may be - passed by the current user for the given entity - """ + """customized class for State entities""" id = 'State' fetch_attrs, fetch_order = fetch_config(['name']) rest_attr = 'eid' - def transitions(self, entity, desteid=None): - """generates transition that MAY be passed""" - rql = ('Any T,N,DS where S allowed_transition T, S eid %(x)s, ' - 'T name N, T destination_state DS, ' - 'T transition_of ET, ET name %(et)s') - if desteid is not None: - rql += ', DS eid %(ds)s' - rset = self.req.execute(rql, {'x': self.eid, 'et': str(entity.e_schema), - 'ds': desteid}, 'x') - for tr in rset.entities(): - if tr.may_be_passed(entity.eid, self.eid): - yield tr + @property + def workflow(self): + # take care, may be missing in multi-sources configuration + return self.state_of and self.state_of[0] def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted """ if self.state_of: - return self.state_of[0].rest_path(), {'vid': 'workflow'} + return self.state_of[0].rest_path(), {} return super(State, self).after_deletion_path() @@ -95,15 +323,20 @@ pclass=None) # don't want modification_date @property def for_entity(self): - return self.wf_info_for and self.wf_info_for[0] + return self.wf_info_for[0] + @property def previous_state(self): - return self.from_state and self.from_state[0] + return self.from_state[0] @property def new_state(self): return self.to_state[0] + @property + def transition(self): + return self.by_transition and self.by_transition[0] or None + def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted @@ -111,3 +344,176 @@ if self.for_entity: return self.for_entity.rest_path(), {} return 'view', {} + + +class WorkflowableMixIn(object): + """base mixin providing workflow helper methods for workflowable entities. + This mixin will be automatically set on class supporting the 'in_state' + relation (which implies supporting 'wf_info_for' as well) + """ + __implements__ = (IWorkflowable,) + + @property + def main_workflow(self): + """return current workflow applied to this entity""" + if self.custom_workflow: + return self.custom_workflow[0] + return self.cwetype_workflow() + + @property + def current_workflow(self): + """return current workflow applied to this entity""" + return self.current_state and self.current_state.workflow or self.main_workflow + + @property + def current_state(self): + """return current state entity""" + return self.in_state and self.in_state[0] or None + + @property + def state(self): + """return current state name""" + try: + return self.in_state[0].name + except IndexError: + self.warning('entity %s has no state', self) + return None + + @property + def printable_state(self): + """return current state name translated to context's language""" + state = self.current_state + if state: + return self.req._(state.name) + return u'' + + @property + def workflow_history(self): + """return the workflow history for this entity (eg ordered list of + TrInfo entities) + """ + return self.reverse_wf_info_for + + def latest_trinfo(self): + """return the latest transition information for this entity""" + return self.reverse_wf_info_for[-1] + + @cached + def cwetype_workflow(self): + """return the default workflow for entities of this type""" + # XXX CWEType method + wfrset = self.req.execute('Any WF WHERE X is ET, X eid %(x)s, ' + 'WF workflow_of ET', {'x': self.eid}, 'x') + if len(wfrset) == 1: + return wfrset.get_entity(0, 0) + if len(wfrset) > 1: + for wf in wfrset.entities(): + if wf.is_default_workflow_of(self.id): + return wf + self.warning("can't find default workflow for %s", self.id) + else: + self.warning("can't find any workflow for %s", self.id) + return None + + def possible_transitions(self): + """generates transition that MAY be fired for the given entity, + expected to be in this state + """ + if self.current_state is None or self.current_workflow is None: + return + rset = self.req.execute( + 'Any T,N WHERE S allowed_transition T, S eid %(x)s, ' + 'T name N, T transition_of WF, WF eid %(wfeid)s', + {'x': self.current_state.eid, + 'wfeid': self.current_workflow.eid}, 'x') + for tr in rset.entities(): + if tr.may_be_fired(self.eid): + yield tr + + def _add_trinfo(self, comment, commentformat, treid=None, tseid=None): + kwargs = {} + if comment is not None: + kwargs['comment'] = comment + if commentformat is not None: + kwargs['comment_format'] = commentformat + args = [('wf_info_for', 'E')] + kwargs['E'] = self.eid + if treid is not None: + args.append( ('by_transition', 'T') ) + kwargs['T'] = treid + if tseid is not None: + args.append( ('to_state', 'S') ) + kwargs['S'] = tseid + return self.req.create_entity('TrInfo', *args, **kwargs) + + def fire_transition(self, trname, comment=None, commentformat=None): + """change the entity's state by firing transition of the given name in + entity's workflow + """ + assert self.current_workflow + tr = self.current_workflow.transition_by_name(trname) + assert tr is not None, 'not a %s transition: %s' % (self.id, trname) + return self._add_trinfo(comment, commentformat, tr.eid) + + def change_state(self, statename, comment=None, commentformat=None, tr=None): + """change the entity's state to the given state (name or entity) in + entity's workflow. This method should only by used by manager to fix an + entity's state when their is no matching transition, otherwise + fire_transition should be used. + """ + assert self.current_workflow + if hasattr(statename, 'eid'): + stateeid = statename.eid + else: + if not isinstance(statename, basestring): + warn('give a state name') + state = self.current_workflow.state_by_eid(statename) + else: + state = self.current_workflow.state_by_name(statename) + if state is None: + raise WorkflowException('not a %s state: %s' % (self.id, + statename)) + stateeid = state.eid + # XXX try to find matching transition? + return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid) + + def subworkflow_input_transition(self): + """return the transition which has went through the current sub-workflow + """ + if self.main_workflow.eid == self.current_workflow.eid: + return # doesn't make sense + subwfentries = [] + for trinfo in reversed(self.workflow_history): + if (trinfo.transition and + trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid): + # entering or leaving a subworkflow + if (subwfentries and + subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid): + # leave + del subwfentries[-1] + else: + # enter + subwfentries.append(trinfo) + if not subwfentries: + return None + return subwfentries[-1].transition + + def clear_all_caches(self): + super(WorkflowableMixIn, self).clear_all_caches() + clear_cache(self, 'cwetype_workflow') + + @deprecated('get transition from current workflow and use its may_be_fired method') + def can_pass_transition(self, trname): + """return the Transition instance if the current user can fire the + transition with the given name, else None + """ + tr = self.current_workflow and self.current_workflow.transition_by_name(trname) + if tr and tr.may_be_fired(self.eid): + return tr + + @property + @deprecated('use printable_state') + def displayable_state(self): + return self.req._(self.state) + +MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn diff -r 24489cbbd697 -r 70c0dd1c3b7d entity.py --- a/entity.py Thu Sep 17 14:03:21 2009 +0200 +++ b/entity.py Thu Sep 17 14:53:18 2009 +0200 @@ -15,12 +15,14 @@ from logilab.common.deprecation import deprecated from logilab.mtconverter import TransformData, TransformError, xml_escape +from rql import parse from rql.utils import rqlvar_maker from cubicweb import Unauthorized from cubicweb.rset import ResultSet from cubicweb.selectors import yes from cubicweb.appobject import AppObject +from cubicweb.rqlrewrite import RQLRewriter from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint, bw_normalize_etype from cubicweb.common.uilib import printable_value, soup2xhtml @@ -82,6 +84,7 @@ except AttributeError: continue + class _metaentity(type): """this metaclass sets the relation tags on the entity class and deals with the `widgets` attribute @@ -163,7 +166,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 +185,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 @@ -457,7 +460,8 @@ return self.mtc_transform(value.getvalue(), attrformat, format, encoding) return u'' - value = printable_value(self.req, attrtype, value, props, displaytime) + value = printable_value(self.req, attrtype, value, props, + displaytime=displaytime) if format == 'text/html': value = xml_escape(value) return value @@ -488,13 +492,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 +622,14 @@ self[str(selected[i-1][0])] = rset[i] # handle relations for i in xrange(lastattr, len(rset)): - rtype, x = selected[i-1][0] + rtype, role = selected[i-1][0] value = rset[i] if value is None: rrset = ResultSet([], rql, {'x': self.eid}) self.req.decorate_rset(rrset) else: rrset = self.req.eid_rset(value) - self.set_related_cache(rtype, x, rrset) + self.set_related_cache(rtype, role, rrset) def get_value(self, name): """get value for the attribute relation , query the repository @@ -725,24 +722,13 @@ # generic vocabulary methods ############################################## - @deprecated('see new form api') - def vocabulary(self, rtype, role='subject', limit=None): - """vocabulary functions must return a list of couples - (label, eid) that will typically be used to fill the - edition view's combobox. - - If `eid` is None in one of these couples, it should be - interpreted as a separator in case vocabulary results are grouped - """ - from logilab.common.testlib import mock_object - form = self.vreg.select('forms', 'edition', self.req, entity=self) - field = mock_object(name=rtype, role=role) - return form.form_field_vocabulary(field, limit) - def unrelated_rql(self, rtype, targettype, role, ordermethod=None, vocabconstraints=True): """build a rql to fetch `targettype` entities unrelated to this entity - using (rtype, role) relation + using (rtype, role) relation. + + Consider relation permissions so that returned entities may be actually + linked by `rtype`. """ ordermethod = ordermethod or 'fetch_unrelated_order' if isinstance(rtype, basestring): @@ -755,8 +741,17 @@ objtype, subjtype = self.e_schema, targettype if self.has_eid(): restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar] + args = {'x': self.eid} + if role == 'subject': + securitycheck_args = {'fromeid': self.eid} + else: + securitycheck_args = {'toeid': self.eid} else: restriction = [] + args = {} + securitycheck_args = {} + insertsecurity = (rtype.has_local_role('add') and not + rtype.has_perm(self.req, 'add', **securitycheck_args)) constraints = rtype.rproperty(subjtype, objtype, 'constraints') if vocabconstraints: # RQLConstraint is a subclass for RQLVocabularyConstraint, so they @@ -773,20 +768,29 @@ if not ' ORDERBY ' in rql: before, after = rql.split(' WHERE ', 1) rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after) - return rql + if insertsecurity: + rqlexprs = rtype.get_rqlexprs('add') + rewriter = RQLRewriter(self.req) + rqlst = self.req.vreg.parse(self.req, rql, args) + for select in rqlst.children: + rewriter.rewrite(select, [((searchedvar, searchedvar), rqlexprs)], + select.solutions, args) + rql = rqlst.as_string() + return rql, args def unrelated(self, rtype, targettype, role='subject', limit=None, ordermethod=None): """return a result set of target type objects that may be related by a given relation, with self as subject or object """ - rql = self.unrelated_rql(rtype, targettype, role, ordermethod) + try: + rql, args = self.unrelated_rql(rtype, targettype, role, ordermethod) + except Unauthorized: + return self.req.empty_rset() if limit is not None: before, after = rql.split(' WHERE ', 1) rql = '%s LIMIT %s WHERE %s' % (before, limit, after) - if self.has_eid(): - return self.req.execute(rql, {'x': self.eid}) - return self.req.execute(rql) + return self.req.execute(rql, args, tuple(args)) # relations cache handling ################################################ @@ -837,6 +841,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): @@ -937,6 +946,20 @@ words += entity.get_words() return words + @deprecated('[3.2] see new form api') + def vocabulary(self, rtype, role='subject', limit=None): + """vocabulary functions must return a list of couples + (label, eid) that will typically be used to fill the + edition view's combobox. + + If `eid` is None in one of these couples, it should be + interpreted as a separator in case vocabulary results are grouped + """ + from logilab.common.testlib import mock_object + form = self.vreg.select('forms', 'edition', self.req, entity=self) + field = mock_object(name=rtype, role=role) + return form.form_field_vocabulary(field, limit) + # attribute and relation descriptors ########################################## diff -r 24489cbbd697 -r 70c0dd1c3b7d etwist/server.py --- a/etwist/server.py Thu Sep 17 14:03:21 2009 +0200 +++ b/etwist/server.py Thu Sep 17 14:53:18 2009 +0200 @@ -15,7 +15,7 @@ from urlparse import urlsplit, urlunsplit import hotshot -from twisted.application import service, strports +from twisted.application import strports from twisted.internet import reactor, task, threads from twisted.internet.defer import maybeDeferred from twisted.web2 import channel, http, server, iweb diff -r 24489cbbd697 -r 70c0dd1c3b7d ext/xhtml2fo.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ext/xhtml2fo.py Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,142 @@ +from cubicweb.utils import can_do_pdf_conversion +assert can_do_pdf_conversion() + +from xml.etree.ElementTree import QName, fromstring +from pysixt.standard.xhtml_xslfo.transformer import XHTML2FOTransformer +from pysixt.utils.xslfo.standard import cm +from pysixt.utils.xslfo import SimplePageMaster +from pysixt.standard.xhtml_xslfo.default_styling import default_styles +from pysixt.standard.xhtml_xslfo import XHTML_NS + + +class ReportTransformer(XHTML2FOTransformer): + """ + Class transforming an XHTML input tree into a FO document + displaying reports (one report for each
+ element in the input tree. + """ + + def __init__(self, section, + page_width=21.0, page_height=29.7, + margin_top=1.0, margin_bottom=1.0, + margin_left=1.0, margin_right=1.0, + header_footer_height=0.75, + standard_font_size=11.0, default_lang=u"fr" ): + """ + Initializes a transformer turning an XHTML input tree + containing
elements representing + main content sections into a FO output tree displaying the + reports. + + page_width: float - width of the page (in cm) + page_height: float - height of the page (in cm) + margin_top: float - top margin of the page (in cm) + margin_bottom: float - bottom margin of the page (in cm) + margin_left: float - left margin of the page (in cm) + margin_right: float - right margin of the page (in cm) + header_footer_height: float - height of the header or the footer of the + page that the page number (if any) will be + inserted in. + standard_font_size: float - standard size of the font (in pt) + default_lang: u"" - default language (used for hyphenation) + """ + self.section = section + self.page_width = page_width + self.page_height = page_height + + self.page_tmargin = margin_top + self.page_bmargin = margin_bottom + self.page_lmargin = margin_left + self.page_rmargin = margin_right + + self.hf_height = header_footer_height + + self.font_size = standard_font_size + self.lang = default_lang + + XHTML2FOTransformer.__init__(self) + + + def define_pagemasters(self): + """ + Defines the page masters for the FO output document. + """ + pm = SimplePageMaster(u"page-report") + pm.set_page_dims( self.page_width*cm, self.page_height*cm ) + pm.set_page_margins({u'top' : self.page_tmargin*cm, + u'bottom': self.page_bmargin*cm, + u'left' : self.page_lmargin*cm, + u'right' : self.page_rmargin*cm }) + pm.add_peripheral_region(u"end",self.hf_height) + dims = {} + dims[u"bottom"] = self.hf_height + 0.25 + pm.set_main_region_margins(dims) + return [pm] + + def _visit_report(self, in_elt, _out_elt, params): + """ + Specific visit function for the input
elements whose class is + "report". The _root_visit method of this class selects these input + elements and asks the process of these elements with this specific + visit function. + """ + + ps = self.create_pagesequence(u"page-report") + props = { u"force-page-count": u"no-force", + u"initial-page-number": u"1", + u"format": u"1", } + self._output_properties(ps,props) + + sc = self.create_staticcontent(ps, u"end") + sc_bl = self.create_block(sc) + attrs = { u"hyphenate": u"false", } + attrs[u"font-size"] = u"%.1fpt" %(self.font_size*0.7) + attrs[u"language"] = self.lang + attrs[u"text-align"] = u"center" + self._output_properties(sc_bl,attrs) + sc_bl.text = u"Page" + u" " # ### Should be localised! + pn = self.create_pagenumber(sc_bl) + pn.tail = u"/" + lpn = self.create_pagenumbercitation( sc_bl, + u"last-block-of-report-%d" % params[u"context_pos"] + ) + + + fl = self.create_flow(ps,u"body") + bl = self.create_block(fl) + + # Sets on the highest block element the properties of the XHTML body + # element. These properties (at the least the inheritable ones) will + # be inherited by all the future FO elements. + bodies = list(self.in_tree.getiterator(QName(XHTML_NS,u"body"))) + if len(bodies) > 0: + attrs = self._extract_properties([bodies[0]]) + else: + attrs = default_styles[u"body"].copy() + attrs[u"font-size"] = u"%.1fpt" %self.font_size + attrs[u"language"] = self.lang + self._output_properties(bl,attrs) + + # Processes the report content + self._copy_text(in_elt,bl) + self._process_nodes(in_elt.getchildren(),bl) + + # Inserts an empty block at the end of the report in order to be able + # to compute the last page number of this report. + last_bl = self.create_block(bl) + props = { u"keep-with-previous": u"always", } + props[u"id"] = u"last-block-of-report-%d" % params[u"context_pos"] + self._output_properties(last_bl,props) + + + def _root_visit(self): + """ + Visit function called when starting the process of the input tree. + """ + content = [ d for d in self.in_tree.getiterator(QName(XHTML_NS,u"div")) + if d.get(u"id") == self.section ] + # Asks the process of the report elements with a specific visit + # function + self._process_nodes(content, self.fo_root, + with_function=self._visit_report) + diff -r 24489cbbd697 -r 70c0dd1c3b7d gettext.py --- a/gettext.py Thu Sep 17 14:03:21 2009 +0200 +++ b/gettext.py Thu Sep 17 14:53:18 2009 +0200 @@ -8,7 +8,6 @@ languages. L10N refers to the adaptation of your program, once internationalized, to the local language and cultural habits. -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ # This module represents the integration of work, contributions, feedback, and @@ -47,7 +46,7 @@ # find this format documented anywhere. -import copy, os, re, struct, sys +import locale, copy, os, re, struct, sys from errno import ENOENT @@ -78,7 +77,10 @@ Python lambda function that implements an equivalent expression. """ # Security check, allow only the "n" identifier - from StringIO import StringIO + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO import token, tokenize tokens = tokenize.generate_tokens(StringIO(plural).readline) try: @@ -172,6 +174,7 @@ def __init__(self, fp=None): self._info = {} self._charset = None + self._output_charset = None self._fallback = None if fp is not None: self._parse(fp) @@ -190,6 +193,21 @@ return self._fallback.gettext(message) return message + def pgettext(self, context, message): + if self._fallback: + return self._fallback.pgettext(context, message) + return message + + def lgettext(self, message): + if self._fallback: + return self._fallback.lgettext(message) + return message + + def lpgettext(self, context, message): + if self._fallback: + return self._fallback.lpgettext(context, message) + return message + def ngettext(self, msgid1, msgid2, n): if self._fallback: return self._fallback.ngettext(msgid1, msgid2, n) @@ -198,11 +216,40 @@ else: return msgid2 + def npgettext(self, context, msgid1, msgid2, n): + if self._fallback: + return self._fallback.npgettext(context, msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + + def lngettext(self, msgid1, msgid2, n): + if self._fallback: + return self._fallback.lngettext(msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + + def lnpgettext(self, context, msgid1, msgid2, n): + if self._fallback: + return self._fallback.lnpgettext(context, msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + def ugettext(self, message): if self._fallback: return self._fallback.ugettext(message) return unicode(message) + def upgettext(self, context, message): + if self._fallback: + return self._fallback.upgettext(context, message) + return unicode(message) + def ungettext(self, msgid1, msgid2, n): if self._fallback: return self._fallback.ungettext(msgid1, msgid2, n) @@ -211,15 +258,49 @@ else: return unicode(msgid2) + def unpgettext(self, context, msgid1, msgid2, n): + if self._fallback: + return self._fallback.unpgettext(context, msgid1, msgid2, n) + if n == 1: + return unicode(msgid1) + else: + return unicode(msgid2) + def info(self): return self._info def charset(self): return self._charset - def install(self, unicode=False): + def output_charset(self): + return self._output_charset + + def set_output_charset(self, charset): + self._output_charset = charset + + def install(self, unicode=False, names=None): import __builtin__ __builtin__.__dict__['_'] = unicode and self.ugettext or self.gettext + if hasattr(names, "__contains__"): + if "gettext" in names: + __builtin__.__dict__['gettext'] = __builtin__.__dict__['_'] + if "pgettext" in names: + __builtin__.__dict__['pgettext'] = (unicode and self.upgettext + or self.pgettext) + if "ngettext" in names: + __builtin__.__dict__['ngettext'] = (unicode and self.ungettext + or self.ngettext) + if "npgettext" in names: + __builtin__.__dict__['npgettext'] = \ + (unicode and self.unpgettext or self.npgettext) + if "lgettext" in names: + __builtin__.__dict__['lgettext'] = self.lgettext + if "lpgettext" in names: + __builtin__.__dict__['lpgettext'] = self.lpgettext + if "lngettext" in names: + __builtin__.__dict__['lngettext'] = self.lngettext + if "lnpgettext" in names: + __builtin__.__dict__['lnpgettext'] = self.lnpgettext class GNUTranslations(NullTranslations): @@ -227,6 +308,10 @@ LE_MAGIC = 0x950412deL BE_MAGIC = 0xde120495L + # The encoding of a msgctxt and a msgid in a .mo file is + # msgctxt + "\x04" + msgid (gettext version >= 0.15) + CONTEXT_ENCODING = "%s\x04%s" + def _parse(self, fp): """Override this method to support alternative .mo formats.""" unpack = struct.unpack @@ -262,18 +347,19 @@ # See if we're looking at GNU .mo conventions for metadata if mlen == 0: # Catalog description - # don't handle multi-lines fields here, and skip - # lines which don't look like a header description - # (e.g. "header: value") lastk = k = None for item in tmsg.splitlines(): item = item.strip() - if not item or not ':' in item: + if not item: continue - k, v = item.split(':', 1) - k = k.strip().lower() - v = v.strip() - self._info[k] = v + if ':' in item: + k, v = item.split(':', 1) + k = k.strip().lower() + v = v.strip() + self._info[k] = v + lastk = k + elif lastk: + self._info[lastk] += '\n' + item if k == 'content-type': self._charset = v.split('charset=')[1] elif k == 'plural-forms': @@ -289,7 +375,7 @@ # cause no problems since us-ascii should always be a subset of # the charset encoding. We may want to fall back to 8-bit msgids # if the Unicode conversion fails. - if msg.find('\x00') >= 0: + if '\x00' in msg: # Plural forms msgid1, msgid2 = msg.split('\x00') tmsg = tmsg.split('\x00') @@ -315,14 +401,56 @@ return self._fallback.gettext(message) return message # Encode the Unicode tmsg back to an 8-bit string, if possible - if self._charset: + if self._output_charset: + return tmsg.encode(self._output_charset) + elif self._charset: + return tmsg.encode(self._charset) + return tmsg + + def pgettext(self, context, message): + ctxt_msg_id = self.CONTEXT_ENCODING % (context, message) + missing = object() + tmsg = self._catalog.get(ctxt_msg_id, missing) + if tmsg is missing: + if self._fallback: + return self._fallback.pgettext(context, message) + return message + # Encode the Unicode tmsg back to an 8-bit string, if possible + if self._output_charset: + return tmsg.encode(self._output_charset) + elif self._charset: return tmsg.encode(self._charset) return tmsg + def lgettext(self, message): + missing = object() + tmsg = self._catalog.get(message, missing) + if tmsg is missing: + if self._fallback: + return self._fallback.lgettext(message) + return message + if self._output_charset: + return tmsg.encode(self._output_charset) + return tmsg.encode(locale.getpreferredencoding()) + + def lpgettext(self, context, message): + ctxt_msg_id = self.CONTEXT_ENCODING % (context, message) + missing = object() + tmsg = self._catalog.get(ctxt_msg_id, missing) + if tmsg is missing: + if self._fallback: + return self._fallback.lpgettext(context, message) + return message + if self._output_charset: + return tmsg.encode(self._output_charset) + return tmsg.encode(locale.getpreferredencoding()) + def ngettext(self, msgid1, msgid2, n): try: tmsg = self._catalog[(msgid1, self.plural(n))] - if self._charset: + if self._output_charset: + return tmsg.encode(self._output_charset) + elif self._charset: return tmsg.encode(self._charset) return tmsg except KeyError: @@ -333,6 +461,52 @@ else: return msgid2 + def npgettext(self, context, msgid1, msgid2, n): + ctxt_msg_id = self.CONTEXT_ENCODING % (context, msgid1) + try: + tmsg = self._catalog[(ctxt_msg_id, self.plural(n))] + if self._output_charset: + return tmsg.encode(self._output_charset) + elif self._charset: + return tmsg.encode(self._charset) + return tmsg + except KeyError: + if self._fallback: + return self._fallback.npgettext(context, msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + + def lngettext(self, msgid1, msgid2, n): + try: + tmsg = self._catalog[(msgid1, self.plural(n))] + if self._output_charset: + return tmsg.encode(self._output_charset) + return tmsg.encode(locale.getpreferredencoding()) + except KeyError: + if self._fallback: + return self._fallback.lngettext(msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + + def lnpgettext(self, context, msgid1, msgid2, n): + ctxt_msg_id = self.CONTEXT_ENCODING % (context, msgid1) + try: + tmsg = self._catalog[(ctxt_msg_id, self.plural(n))] + if self._output_charset: + return tmsg.encode(self._output_charset) + return tmsg.encode(locale.getpreferredencoding()) + except KeyError: + if self._fallback: + return self._fallback.lnpgettext(context, msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + def ugettext(self, message): missing = object() tmsg = self._catalog.get(message, missing) @@ -342,6 +516,18 @@ return unicode(message) return tmsg + def upgettext(self, context, message): + ctxt_message_id = self.CONTEXT_ENCODING % (context, message) + missing = object() + tmsg = self._catalog.get(ctxt_message_id, missing) + if tmsg is missing: + # XXX logilab patch for compat w/ catalog generated by cw < 3.5 + return self.ugettext(message) + if self._fallback: + return self._fallback.upgettext(context, message) + return unicode(message) + return tmsg + def ungettext(self, msgid1, msgid2, n): try: tmsg = self._catalog[(msgid1, self.plural(n))] @@ -354,6 +540,19 @@ tmsg = unicode(msgid2) return tmsg + def unpgettext(self, context, msgid1, msgid2, n): + ctxt_message_id = self.CONTEXT_ENCODING % (context, msgid1) + try: + tmsg = self._catalog[(ctxt_message_id, self.plural(n))] + except KeyError: + if self._fallback: + return self._fallback.unpgettext(context, msgid1, msgid2, n) + if n == 1: + tmsg = unicode(msgid1) + else: + tmsg = unicode(msgid2) + return tmsg + # Locate a .mo file using the gettext strategy def find(domain, localedir=None, languages=None, all=0): @@ -397,7 +596,7 @@ _translations = {} def translation(domain, localedir=None, languages=None, - class_=None, fallback=False): + class_=None, fallback=False, codeset=None): if class_ is None: class_ = GNUTranslations mofiles = find(domain, localedir, languages, all=1) @@ -414,9 +613,12 @@ t = _translations.get(key) if t is None: t = _translations.setdefault(key, class_(open(mofile, 'rb'))) - # Copy the translation object to allow setting fallbacks. - # All other instance data is shared with the cached object. + # Copy the translation object to allow setting fallbacks and + # output charset. All other instance data is shared with the + # cached object. t = copy.copy(t) + if codeset: + t.set_output_charset(codeset) if result is None: result = t else: @@ -424,13 +626,16 @@ return result -def install(domain, localedir=None, unicode=False): - translation(domain, localedir, fallback=True).install(unicode) +def install(domain, localedir=None, unicode=False, codeset=None, names=None): + t = translation(domain, localedir, fallback=True, codeset=codeset) + t.install(unicode, names) # a mapping b/w domains and locale directories _localedirs = {} +# a mapping b/w domains and codesets +_localecodesets = {} # current global domain, `messages' used for compatibility w/ GNU gettext _current_domain = 'messages' @@ -443,22 +648,55 @@ def bindtextdomain(domain, localedir=None): + global _localedirs if localedir is not None: _localedirs[domain] = localedir return _localedirs.get(domain, _default_localedir) +def bind_textdomain_codeset(domain, codeset=None): + global _localecodesets + if codeset is not None: + _localecodesets[domain] = codeset + return _localecodesets.get(domain) + + def dgettext(domain, message): try: - t = translation(domain, _localedirs.get(domain, None)) + t = translation(domain, _localedirs.get(domain, None), + codeset=_localecodesets.get(domain)) except IOError: return message return t.gettext(message) +def dpgettext(domain, context, message): + try: + t = translation(domain, _localedirs.get(domain, None), + codeset=_localecodesets.get(domain)) + except IOError: + return message + return t.pgettext(context, message) + +def ldgettext(domain, message): + try: + t = translation(domain, _localedirs.get(domain, None), + codeset=_localecodesets.get(domain)) + except IOError: + return message + return t.lgettext(message) + +def ldpgettext(domain, context, message): + try: + t = translation(domain, _localedirs.get(domain, None), + codeset=_localecodesets.get(domain)) + except IOError: + return message + return t.lpgettext(context, message) def dngettext(domain, msgid1, msgid2, n): try: - t = translation(domain, _localedirs.get(domain, None)) + t = translation(domain, _localedirs.get(domain, None), + codeset=_localecodesets.get(domain)) except IOError: if n == 1: return msgid1 @@ -466,14 +704,62 @@ return msgid2 return t.ngettext(msgid1, msgid2, n) +def dnpgettext(domain, context, msgid1, msgid2, n): + try: + t = translation(domain, _localedirs.get(domain, None), + codeset=_localecodesets.get(domain)) + except IOError: + if n == 1: + return msgid1 + else: + return msgid2 + return t.npgettext(context, msgid1, msgid2, n) + +def ldngettext(domain, msgid1, msgid2, n): + try: + t = translation(domain, _localedirs.get(domain, None), + codeset=_localecodesets.get(domain)) + except IOError: + if n == 1: + return msgid1 + else: + return msgid2 + return t.lngettext(msgid1, msgid2, n) + +def ldnpgettext(domain, context, msgid1, msgid2, n): + try: + t = translation(domain, _localedirs.get(domain, None), + codeset=_localecodesets.get(domain)) + except IOError: + if n == 1: + return msgid1 + else: + return msgid2 + return t.lnpgettext(context, msgid1, msgid2, n) def gettext(message): return dgettext(_current_domain, message) +def pgettext(context, message): + return dpgettext(_current_domain, context, message) + +def lgettext(message): + return ldgettext(_current_domain, message) + +def lpgettext(context, message): + return ldpgettext(_current_domain, context, message) def ngettext(msgid1, msgid2, n): return dngettext(_current_domain, msgid1, msgid2, n) +def npgettext(context, msgid1, msgid2, n): + return dnpgettext(_current_domain, context, msgid1, msgid2, n) + +def lngettext(msgid1, msgid2, n): + return ldngettext(_current_domain, msgid1, msgid2, n) + +def lnpgettext(context, msgid1, msgid2, n): + return ldnpgettext(_current_domain, context, msgid1, msgid2, n) # dcgettext() has been deemed unnecessary and is not implemented. diff -r 24489cbbd697 -r 70c0dd1c3b7d goa/gaesource.py --- a/goa/gaesource.py Thu Sep 17 14:03:21 2009 +0200 +++ b/goa/gaesource.py Thu Sep 17 14:53:18 2009 +0200 @@ -149,7 +149,7 @@ # ISource interface ####################################################### def compile_rql(self, rql): - rqlst = self.repo.querier._rqlhelper.parse(rql) + rqlst = self.repo.vreg.parse(rql) rqlst.restricted_vars = () rqlst.children[0].solutions = self._sols return rqlst diff -r 24489cbbd697 -r 70c0dd1c3b7d goa/goactl.py --- a/goa/goactl.py Thu Sep 17 14:03:21 2009 +0200 +++ b/goa/goactl.py Thu Sep 17 14:53:18 2009 +0200 @@ -8,7 +8,6 @@ __docformat__ = "restructuredtext en" from os.path import exists, join, split, basename, normpath, abspath - from logilab.common.clcommands import register_commands from cubicweb import CW_SOFTWARE_ROOT, BadCommandUsage @@ -19,9 +18,9 @@ from logilab import common as lgc from logilab import constraint as lgcstr from logilab import mtconverter as lgmtc -import rql, yams, yapps, simplejson, dateutil, vobject, docutils, roman +import rql, yams, yapps, simplejson, docutils, roman -SLINK_DIRECTORIES = ( +SLINK_DIRECTORIES = [ (lgc.__path__[0], 'logilab/common'), (lgmtc.__path__[0], 'logilab/mtconverter'), (lgcstr.__path__[0], 'logilab/constraint'), @@ -29,8 +28,6 @@ (simplejson.__path__[0], 'simplejson'), (yams.__path__[0], 'yams'), (yapps.__path__[0], 'yapps'), - (dateutil.__path__[0], 'dateutil'), - (vobject.__path__[0], 'vobject'), (docutils.__path__[0], 'docutils'), (roman.__file__.replace('.pyc', '.py'), 'roman.py'), @@ -42,7 +39,15 @@ (join(CW_SOFTWARE_ROOT, 'i18n'), join('cubes', 'shared', 'i18n')), (join(CW_SOFTWARE_ROOT, 'goa', 'tools'), 'tools'), (join(CW_SOFTWARE_ROOT, 'goa', 'bin'), 'bin'), - ) + ] + +try: + import dateutil + import vobject + SLINK_DIRECTORIES.extend([ (dateutil.__path__[0], 'dateutil'), + (vobject.__path__[0], 'vobject') ] ) +except ImportError: + pass COPY_CW_FILES = ( '__init__.py', @@ -54,6 +59,7 @@ 'cwconfig.py', 'entity.py', 'interfaces.py', + 'rqlrewrite.py', 'rset.py', 'schema.py', 'schemaviewer.py', @@ -78,7 +84,6 @@ 'server/pool.py', 'server/querier.py', 'server/repository.py', - 'server/rqlrewrite.py', 'server/securityhooks.py', 'server/session.py', 'server/serverconfig.py', diff -r 24489cbbd697 -r 70c0dd1c3b7d i18n/en.po --- a/i18n/en.po Thu Sep 17 14:03:21 2009 +0200 +++ b/i18n/en.po Thu Sep 17 14:53:18 2009 +0200 @@ -5,7 +5,7 @@ msgstr "" "Project-Id-Version: 2.0\n" "POT-Creation-Date: 2006-01-12 17:35+CET\n" -"PO-Revision-Date: 2009-08-05 08:39+0200\n" +"PO-Revision-Date: 2009-09-17 11:53+0200\n" "Last-Translator: Sylvain Thenault \n" "Language-Team: English \n" "MIME-Version: 1.0\n" @@ -107,10 +107,6 @@ msgstr "" #, python-format -msgid "%s is not the initial state (%s) for this entity" -msgstr "" - -#, python-format msgid "%s not estimated" msgstr "" @@ -197,6 +193,15 @@ msgid "Attributes" msgstr "" +# schema pot file, generated on 2009-09-16 16:46:55 +# +# singular and plural forms for each entity type +msgid "BaseTransition" +msgstr "Transition (abstract)" + +msgid "BaseTransition_plural" +msgstr "Transitions (abstract)" + msgid "Bookmark" msgstr "Bookmark" @@ -351,6 +356,9 @@ msgid "Interval_plural" msgstr "Intervals" +msgid "New BaseTransition" +msgstr "XXX" + msgid "New Bookmark" msgstr "New bookmark" @@ -358,7 +366,7 @@ msgstr "New attribute" msgid "New CWCache" -msgstr "" +msgstr "New cache" msgid "New CWConstraint" msgstr "New constraint" @@ -399,12 +407,21 @@ msgid "New State" msgstr "New state" +msgid "New SubWorkflowExitPoint" +msgstr "New subworkflow exit-point" + msgid "New TrInfo" msgstr "New transition information" msgid "New Transition" msgstr "New transition" +msgid "New Workflow" +msgstr "New workflow" + +msgid "New WorkflowTransition" +msgstr "New workflow-transition" + msgid "No query has been executed" msgstr "" @@ -472,6 +489,12 @@ msgid "String_plural" msgstr "Strings" +msgid "SubWorkflowExitPoint" +msgstr "Subworkflow exit-point" + +msgid "SubWorkflowExitPoint_plural" +msgstr "subworkflow exit-points" + msgid "Subject:" msgstr "" @@ -492,12 +515,8 @@ msgid "The view %s could not be found" msgstr "" -msgid "There is no workflow defined for this entity." -msgstr "" - -#, python-format -msgid "This %s" -msgstr "" +msgid "This BaseTransition" +msgstr "This abstract transition" msgid "This Bookmark" msgstr "This bookmark" @@ -506,7 +525,7 @@ msgstr "This attribute" msgid "This CWCache" -msgstr "" +msgstr "This cache" msgid "This CWConstraint" msgstr "This constraint" @@ -547,12 +566,21 @@ msgid "This State" msgstr "This state" +msgid "This SubWorkflowExitPoint" +msgstr "This subworkflow exit-point" + msgid "This TrInfo" msgstr "This transition information" msgid "This Transition" msgstr "This transition" +msgid "This Workflow" +msgstr "This workflow" + +msgid "This WorkflowTransition" +msgstr "This workflow-transition" + msgid "Time" msgstr "Time" @@ -584,9 +612,21 @@ msgid "What's new?" msgstr "" +msgid "Workflow" +msgstr "Workflow" + msgid "Workflow history" msgstr "" +msgid "WorkflowTransition" +msgstr "Workflow-transition" + +msgid "WorkflowTransition_plural" +msgstr "Workflow-transitions" + +msgid "Workflow_plural" +msgstr "Workflows" + msgid "You are not connected to an instance !" msgstr "" @@ -622,9 +662,6 @@ msgid "[%s supervision] changes summary" msgstr "" -msgid "__msg state changed" -msgstr "state changed" - msgid "" "a RQL expression which should return some results, else the transition won't " "be available. This query may use X and U variables that will respectivly " @@ -644,12 +681,12 @@ msgid "about this site" msgstr "" +msgid "abstract base class for transitions" +msgstr "" + msgid "access type" msgstr "" -msgid "account state" -msgstr "" - msgid "action(s) on this selection" msgstr "" @@ -662,6 +699,12 @@ msgid "actions_addentity_description" msgstr "" +msgid "actions_addrelated" +msgstr "" + +msgid "actions_addrelated_description" +msgstr "" + msgid "actions_cancel" msgstr "cancel the selection" @@ -848,8 +891,8 @@ msgid "add State allowed_transition Transition subject" msgstr "allowed transition" -msgid "add State state_of CWEType object" -msgstr "state" +msgid "add State allowed_transition WorkflowTransition subject" +msgstr "workflow-transition" msgid "add Transition condition RQLExpression subject" msgstr "condition" @@ -860,63 +903,28 @@ msgid "add Transition destination_state State subject" msgstr "destination state" -msgid "add Transition transition_of CWEType object" -msgstr "transition" - -msgid "add a Bookmark" -msgstr "add a bookmark" - -msgid "add a CWAttribute" -msgstr "add an attribute" - -msgid "add a CWCache" -msgstr "add a cubicweb cache" - -msgid "add a CWConstraint" -msgstr "add a constraint" - -msgid "add a CWConstraintType" -msgstr "add a constraint type" - +msgid "add WorkflowTransition condition RQLExpression subject" +msgstr "workflow-transition" + +msgid "add WorkflowTransition subworkflow_exit SubWorkflowExitPoint subject" +msgstr "subworkflow exit-point" + +msgctxt "inlined:CWRelation.from_entity.subject" msgid "add a CWEType" msgstr "add an entity type" -msgid "add a CWGroup" -msgstr "add a group" - -msgid "add a CWPermission" -msgstr "add a permission" - -msgid "add a CWProperty" -msgstr "add a property" - +msgctxt "inlined:CWRelation.to_entity.subject" +msgid "add a CWEType" +msgstr "add an entity type" + +msgctxt "inlined:CWRelation.relation_type.subject" msgid "add a CWRType" msgstr "add a relation type" -msgid "add a CWRelation" -msgstr "add a relation" - -msgid "add a CWUser" -msgstr "add a user" - +msgctxt "inlined:CWUser.use_email.subject" msgid "add a EmailAddress" msgstr "add an email address" -msgid "add a ExternalUri" -msgstr "and an external uri" - -msgid "add a RQLExpression" -msgstr "add a rql expression" - -msgid "add a State" -msgstr "add a state" - -msgid "add a TrInfo" -msgstr "add a transition information" - -msgid "add a Transition" -msgstr "add a transition" - msgid "add a new permission" msgstr "" @@ -931,6 +939,24 @@ msgid "add_permission" msgstr "can be added by" +# subject and object forms for each relation type +# (no object form for final relation types) +msgctxt "CWEType" +msgid "add_permission" +msgstr "" + +msgctxt "CWRType" +msgid "add_permission" +msgstr "" + +msgctxt "CWGroup" +msgid "add_permission_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "add_permission_object" +msgstr "" + msgid "add_permission_object" msgstr "has permission to add" @@ -944,12 +970,26 @@ "(toeid)s" msgstr "" +msgid "addrelated" +msgstr "" + +msgid "address" +msgstr "" + +msgctxt "EmailAddress" msgid "address" msgstr "" msgid "alias" msgstr "" +msgctxt "EmailAddress" +msgid "alias" +msgstr "" + +msgid "allow to set a specific workflow for an entity" +msgstr "" + msgid "allowed transition from this state" msgstr "" @@ -959,6 +999,22 @@ msgid "allowed_transition" msgstr "allowed transition" +msgctxt "State" +msgid "allowed_transition" +msgstr "" + +msgctxt "BaseTransition" +msgid "allowed_transition_object" +msgstr "" + +msgctxt "Transition" +msgid "allowed_transition_object" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "allowed_transition_object" +msgstr "" + msgid "allowed_transition_object" msgstr "incoming states" @@ -1038,6 +1094,14 @@ msgid "bookmarked_by" msgstr "bookmarked by" +msgctxt "Bookmark" +msgid "bookmarked_by" +msgstr "bookmarked by" + +msgctxt "CWUser" +msgid "bookmarked_by_object" +msgstr "" + msgid "bookmarked_by_object" msgstr "has bookmarks" @@ -1122,6 +1186,28 @@ msgid "by relation" msgstr "" +msgid "by_transition" +msgstr "" + +msgctxt "TrInfo" +msgid "by_transition" +msgstr "by transition" + +msgctxt "BaseTransition" +msgid "by_transition_object" +msgstr "" + +msgctxt "Transition" +msgid "by_transition_object" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "by_transition_object" +msgstr "" + +msgid "by_transition_object" +msgstr "transition information" + msgid "calendar" msgstr "" @@ -1152,6 +1238,9 @@ msgid "can't display data, unexpected error: %s" msgstr "" +msgid "can't have multiple exits on the same state" +msgstr "" + #, python-format msgid "" "can't set inlined=%(inlined)s, %(stype)s %(rtype)s %(otype)s has cardinality=" @@ -1164,9 +1253,14 @@ msgid "cancel this insert" msgstr "" -msgid "canonical" -msgstr "" - +msgid "cardinality" +msgstr "" + +msgctxt "CWAttribute" +msgid "cardinality" +msgstr "cardinality" + +msgctxt "CWRelation" msgid "cardinality" msgstr "" @@ -1192,9 +1286,14 @@ msgid "comment" msgstr "" -msgid "comment:" -msgstr "" - +msgctxt "TrInfo" +msgid "comment" +msgstr "" + +msgid "comment_format" +msgstr "format" + +msgctxt "TrInfo" msgid "comment_format" msgstr "format" @@ -1246,6 +1345,12 @@ msgid "components_navigation_description" msgstr "pagination component for large resultsets" +msgid "components_pdfview" +msgstr "" + +msgid "components_pdfview_description" +msgstr "" + msgid "components_rqlinput" msgstr "rql input box" @@ -1255,12 +1360,32 @@ msgid "composite" msgstr "" +msgctxt "CWRelation" +msgid "composite" +msgstr "" + +msgid "condition" +msgstr "" + +msgctxt "BaseTransition" +msgid "condition" +msgstr "" + +msgctxt "Transition" +msgid "condition" +msgstr "" + +msgctxt "WorkflowTransition" msgid "condition" msgstr "" msgid "condition:" msgstr "" +msgctxt "RQLExpression" +msgid "condition_object" +msgstr "" + msgid "condition_object" msgstr "condition of" @@ -1270,6 +1395,18 @@ msgid "constrained_by" msgstr "constrained by" +msgctxt "CWAttribute" +msgid "constrained_by" +msgstr "constrained by" + +msgctxt "CWRelation" +msgid "constrained_by" +msgstr "constrained by" + +msgctxt "CWConstraint" +msgid "constrained_by_object" +msgstr "" + msgid "constrained_by_object" msgstr "constraints" @@ -1385,6 +1522,10 @@ msgid "created_by" msgstr "created by" +msgctxt "CWUser" +msgid "created_by_object" +msgstr "" + msgid "created_by_object" msgstr "has created" @@ -1445,23 +1586,32 @@ msgid "creating RQLExpression (Transition %(linkto)s condition RQLExpression)" msgstr "creating rql expression for transition %(linkto)s" +msgid "" +"creating RQLExpression (WorkflowTransition %(linkto)s condition " +"RQLExpression)" +msgstr "creating rql expression for workflow-transition %(linkto)s" + msgid "creating State (State allowed_transition Transition %(linkto)s)" msgstr "creating a state able to trigger transition %(linkto)s" -msgid "creating State (State state_of CWEType %(linkto)s)" -msgstr "creating state for the %(linkto)s entity type" - msgid "creating State (Transition %(linkto)s destination_state State)" msgstr "creating destination state for transition %(linkto)s" +msgid "" +"creating SubWorkflowExitPoint (WorkflowTransition %(linkto)s " +"subworkflow_exit SubWorkflowExitPoint)" +msgstr "creating subworkflow exit-point for workflow-transition %(linkto)s" + msgid "creating Transition (State %(linkto)s allowed_transition Transition)" msgstr "creating triggerable transition for state %(linkto)s" msgid "creating Transition (Transition destination_state State %(linkto)s)" msgstr "creating transition leading to state %(linkto)s" -msgid "creating Transition (Transition transition_of CWEType %(linkto)s)" -msgstr "creating transition for the %(linkto)s entity type" +msgid "" +"creating WorkflowTransition (State %(linkto)s allowed_transition " +"WorkflowTransition)" +msgstr "creating workflow-transition leading to state %(linkto)s" msgid "creation" msgstr "" @@ -1475,6 +1625,14 @@ msgid "cstrtype" msgstr "constraint's type" +msgctxt "CWConstraint" +msgid "cstrtype" +msgstr "" + +msgctxt "CWConstraintType" +msgid "cstrtype_object" +msgstr "" + msgid "cstrtype_object" msgstr "used by" @@ -1488,6 +1646,20 @@ msgid "currently attached file: %s" msgstr "" +msgid "custom_workflow" +msgstr "custom workflow" + +msgctxt "CWUser" +msgid "custom_workflow" +msgstr "custom workflow" + +msgctxt "Workflow" +msgid "custom_workflow_object" +msgstr "" + +msgid "custom_workflow_object" +msgstr "custom workflow of" + msgid "cwetype-schema-image" msgstr "schema" @@ -1524,6 +1696,30 @@ msgid "default text format for rich text fields." msgstr "" +msgid "default user workflow" +msgstr "" + +msgid "default workflow for an entity type" +msgstr "" + +msgid "default_workflow" +msgstr "default workflow" + +msgctxt "CWEType" +msgid "default_workflow" +msgstr "default workflow" + +msgctxt "Workflow" +msgid "default_workflow_object" +msgstr "" + +msgid "default_workflow_object" +msgstr "default workflow of" + +msgid "defaultval" +msgstr "default value" + +msgctxt "CWAttribute" msgid "defaultval" msgstr "default value" @@ -1558,6 +1754,9 @@ msgid "define an entity type, used to build the instance schema" msgstr "" +msgid "define how we get out from a sub-workflow" +msgstr "" + msgid "" "defines what's the property is applied for. You must select this first to be " "able to set value" @@ -1581,6 +1780,22 @@ msgid "delete_permission" msgstr "can be deleted by" +msgctxt "CWEType" +msgid "delete_permission" +msgstr "delete permission" + +msgctxt "CWRType" +msgid "delete_permission" +msgstr "" + +msgctxt "CWGroup" +msgid "delete_permission_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "delete_permission_object" +msgstr "" + msgid "delete_permission_object" msgstr "has permission to delete" @@ -1600,9 +1815,84 @@ msgid "description" msgstr "" +msgctxt "CWEType" +msgid "description" +msgstr "" + +msgctxt "CWRelation" +msgid "description" +msgstr "" + +msgctxt "Workflow" +msgid "description" +msgstr "" + +msgctxt "CWAttribute" +msgid "description" +msgstr "" + +msgctxt "Transition" +msgid "description" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "description" +msgstr "" + +msgctxt "State" +msgid "description" +msgstr "" + +msgctxt "CWRType" +msgid "description" +msgstr "" + +msgctxt "BaseTransition" +msgid "description" +msgstr "" + msgid "description_format" msgstr "format" +msgctxt "CWEType" +msgid "description_format" +msgstr "" + +msgctxt "CWRelation" +msgid "description_format" +msgstr "" + +msgctxt "Workflow" +msgid "description_format" +msgstr "format" + +msgctxt "CWAttribute" +msgid "description_format" +msgstr "format" + +msgctxt "Transition" +msgid "description_format" +msgstr "format" + +msgctxt "WorkflowTransition" +msgid "description_format" +msgstr "format" + +msgctxt "State" +msgid "description_format" +msgstr "format" + +msgctxt "CWRType" +msgid "description_format" +msgstr "format" + +msgctxt "BaseTransition" +msgid "description_format" +msgstr "format" + +msgid "destination state" +msgstr "" + msgid "destination state for this transition" msgstr "" @@ -1612,6 +1902,18 @@ msgid "destination_state" msgstr "destination state" +msgctxt "Transition" +msgid "destination_state" +msgstr "" + +msgctxt "SubWorkflowExitPoint" +msgid "destination_state" +msgstr "destination state" + +msgctxt "State" +msgid "destination_state_object" +msgstr "" + msgid "destination_state_object" msgstr "destination of" @@ -1640,6 +1942,9 @@ msgid "display the component or not" msgstr "" +msgid "display the pdf icon or not" +msgstr "" + msgid "" "distinct label to distinguate between other permission entity of the same " "name" @@ -1655,6 +1960,9 @@ msgid "download icon" msgstr "" +msgid "download page as pdf" +msgstr "" + msgid "download schema as owl" msgstr "" @@ -1712,6 +2020,9 @@ msgid "entity edited" msgstr "" +msgid "entity has no workflow set" +msgstr "" + msgid "entity linked" msgstr "" @@ -1723,10 +2034,7 @@ "configuration" msgstr "" -msgid "entity types which may use this state" -msgstr "" - -msgid "entity types which may use this transition" +msgid "entity types which may use this workflow" msgstr "" msgid "error while embedding page" @@ -1746,15 +2054,33 @@ msgid "eta_date" msgstr "" +msgid "exit_point" +msgstr "" + +msgid "exit_point_object" +msgstr "" + +#, python-format +msgid "exiting from subworkflow %s" +msgstr "" + msgid "expected:" msgstr "" msgid "expression" msgstr "" +msgctxt "RQLExpression" +msgid "expression" +msgstr "" + msgid "exprtype" msgstr "expression's type" +msgctxt "RQLExpression" +msgid "exprtype" +msgstr "" + msgid "external page" msgstr "" @@ -1806,6 +2132,18 @@ msgid "final" msgstr "" +msgctxt "CWEType" +msgid "final" +msgstr "" + +msgctxt "CWRType" +msgid "final" +msgstr "" + +msgid "firstname" +msgstr "" + +msgctxt "CWUser" msgid "firstname" msgstr "" @@ -1818,6 +2156,14 @@ msgid "for_user" msgstr "for user" +msgctxt "CWProperty" +msgid "for_user" +msgstr "" + +msgctxt "CWUser" +msgid "for_user_object" +msgstr "" + msgid "for_user_object" msgstr "use properties" @@ -1834,6 +2180,18 @@ msgid "from_entity" msgstr "from entity" +msgctxt "CWAttribute" +msgid "from_entity" +msgstr "from entity" + +msgctxt "CWRelation" +msgid "from_entity" +msgstr "from entity" + +msgctxt "CWEType" +msgid "from_entity_object" +msgstr "" + msgid "from_entity_object" msgstr "subjet relation" @@ -1843,6 +2201,14 @@ msgid "from_state" msgstr "from state" +msgctxt "TrInfo" +msgid "from_state" +msgstr "from state" + +msgctxt "State" +msgid "from_state_object" +msgstr "" + msgid "from_state_object" msgstr "transitions from this state" @@ -1852,9 +2218,17 @@ msgid "fulltext_container" msgstr "" +msgctxt "CWRType" +msgid "fulltext_container" +msgstr "fulltext container" + msgid "fulltextindexed" msgstr "fulltext indexed" +msgctxt "CWAttribute" +msgid "fulltextindexed" +msgstr "" + msgid "generic plot" msgstr "" @@ -1873,6 +2247,10 @@ msgid "granted to groups" msgstr "" +#, python-format +msgid "graphical representation of %s" +msgstr "" + msgid "graphical representation of the instance'schema" msgstr "" @@ -1961,12 +2339,103 @@ msgid "id of main template used to render pages" msgstr "" +msgid "identical to" +msgstr "" + msgid "identical_to" msgstr "identical to" msgid "identity" msgstr "" +msgctxt "CWRelation" +msgid "identity_object" +msgstr "" + +msgctxt "Bookmark" +msgid "identity_object" +msgstr "" + +msgctxt "CWAttribute" +msgid "identity_object" +msgstr "" + +msgctxt "CWConstraintType" +msgid "identity_object" +msgstr "" + +msgctxt "State" +msgid "identity_object" +msgstr "" + +msgctxt "BaseTransition" +msgid "identity_object" +msgstr "" + +msgctxt "CWEType" +msgid "identity_object" +msgstr "" + +msgctxt "Workflow" +msgid "identity_object" +msgstr "" + +msgctxt "CWGroup" +msgid "identity_object" +msgstr "" + +msgctxt "TrInfo" +msgid "identity_object" +msgstr "" + +msgctxt "CWConstraint" +msgid "identity_object" +msgstr "" + +msgctxt "CWUser" +msgid "identity_object" +msgstr "" + +msgctxt "Transition" +msgid "identity_object" +msgstr "" + +msgctxt "CWRType" +msgid "identity_object" +msgstr "" + +msgctxt "SubWorkflowExitPoint" +msgid "identity_object" +msgstr "" + +msgctxt "ExternalUri" +msgid "identity_object" +msgstr "" + +msgctxt "CWCache" +msgid "identity_object" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "identity_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "identity_object" +msgstr "" + +msgctxt "CWPermission" +msgid "identity_object" +msgstr "" + +msgctxt "EmailAddress" +msgid "identity_object" +msgstr "" + +msgctxt "CWProperty" +msgid "identity_object" +msgstr "" + msgid "identity_object" msgstr "identity" @@ -1987,12 +2456,28 @@ msgid "in_group" msgstr "in group" +msgctxt "CWUser" +msgid "in_group" +msgstr "in group" + +msgctxt "CWGroup" +msgid "in_group_object" +msgstr "" + msgid "in_group_object" msgstr "contains" msgid "in_state" msgstr "in state" +msgctxt "CWUser" +msgid "in_state" +msgstr "in state" + +msgctxt "State" +msgid "in_state_object" +msgstr "" + msgid "in_state_object" msgstr "state of" @@ -2012,6 +2497,10 @@ msgid "indexed" msgstr "" +msgctxt "CWAttribute" +msgid "indexed" +msgstr "" + msgid "indicate the current state of an entity" msgstr "" @@ -2027,18 +2516,30 @@ msgid "initial estimation %s" msgstr "" -msgid "initial state for entities of this type" +msgid "initial state for this workflow" msgstr "" msgid "initial_state" msgstr "initial state" +msgctxt "Workflow" +msgid "initial_state" +msgstr "initial state" + +msgctxt "State" +msgid "initial_state_object" +msgstr "" + msgid "initial_state_object" msgstr "initial state of" msgid "inlined" msgstr "" +msgctxt "CWRType" +msgid "inlined" +msgstr "" + msgid "instance schema" msgstr "" @@ -2048,6 +2549,10 @@ msgid "internationalizable" msgstr "" +msgctxt "CWAttribute" +msgid "internationalizable" +msgstr "" + #, python-format msgid "invalid action %r" msgstr "" @@ -2083,9 +2588,17 @@ msgid "is_instance_of" msgstr "" +msgctxt "CWEType" msgid "is_instance_of_object" msgstr "" +msgid "is_instance_of_object" +msgstr "is instance of" + +msgctxt "CWEType" +msgid "is_object" +msgstr "" + msgid "is_object" msgstr "has instances" @@ -2101,6 +2614,10 @@ msgid "label" msgstr "" +msgctxt "CWPermission" +msgid "label" +msgstr "" + msgid "language of the user interface" msgstr "" @@ -2110,6 +2627,10 @@ msgid "last_login_time" msgstr "last login time" +msgctxt "CWUser" +msgid "last_login_time" +msgstr "last login time" + msgid "latest modification time of an entity" msgstr "" @@ -2138,13 +2659,16 @@ msgid "link a relation definition to its subject entity type" msgstr "" -msgid "link a state to one or more entity type" +msgid "link a state to one or more workflow" msgstr "" msgid "link a transition information to its object" msgstr "" -msgid "link a transition to one or more entity type" +msgid "link a transition to one or more workflow" +msgstr "" + +msgid "link a workflow to one or more entity type" msgstr "" msgid "link to each item in" @@ -2162,6 +2686,10 @@ msgid "login" msgstr "" +msgctxt "CWUser" +msgid "login" +msgstr "" + msgid "login or email" msgstr "" @@ -2181,6 +2709,10 @@ msgid "mainvars" msgstr "" +msgctxt "RQLExpression" +msgid "mainvars" +msgstr "" + msgid "manage" msgstr "" @@ -2196,6 +2728,9 @@ msgid "managers" msgstr "" +msgid "mandatory relation" +msgstr "" + msgid "march" msgstr "" @@ -2242,6 +2777,50 @@ msgid "name" msgstr "" +msgctxt "CWEType" +msgid "name" +msgstr "" + +msgctxt "Transition" +msgid "name" +msgstr "" + +msgctxt "Workflow" +msgid "name" +msgstr "" + +msgctxt "CWGroup" +msgid "name" +msgstr "" + +msgctxt "CWConstraintType" +msgid "name" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "name" +msgstr "" + +msgctxt "State" +msgid "name" +msgstr "" + +msgctxt "CWPermission" +msgid "name" +msgstr "" + +msgctxt "CWRType" +msgid "name" +msgstr "" + +msgctxt "BaseTransition" +msgid "name" +msgstr "" + +msgctxt "CWCache" +msgid "name" +msgstr "" + msgid "name of the cache" msgstr "" @@ -2347,6 +2926,14 @@ msgid "ordernum" msgstr "order" +msgctxt "CWAttribute" +msgid "ordernum" +msgstr "" + +msgctxt "CWRelation" +msgid "ordernum" +msgstr "" + msgid "owl" msgstr "" @@ -2356,6 +2943,10 @@ msgid "owned_by" msgstr "owned by" +msgctxt "CWUser" +msgid "owned_by_object" +msgstr "" + msgid "owned_by_object" msgstr "owns" @@ -2381,6 +2972,10 @@ msgid "path" msgstr "" +msgctxt "Bookmark" +msgid "path" +msgstr "" + msgid "permission" msgstr "" @@ -2402,6 +2997,10 @@ msgid "pkey" msgstr "key" +msgctxt "CWProperty" +msgid "pkey" +msgstr "" + msgid "please correct errors below" msgstr "" @@ -2414,6 +3013,20 @@ msgid "powered by CubicWeb" msgstr "" +msgid "prefered_form" +msgstr "" + +msgctxt "EmailAddress" +msgid "prefered_form" +msgstr "" + +msgctxt "EmailAddress" +msgid "prefered_form_object" +msgstr "" + +msgid "prefered_form_object" +msgstr "" + msgid "preferences" msgstr "" @@ -2426,6 +3039,14 @@ msgid "primary_email" msgstr "primary email" +msgctxt "CWUser" +msgid "primary_email" +msgstr "primary email" + +msgctxt "EmailAddress" +msgid "primary_email_object" +msgstr "" + msgid "primary_email_object" msgstr "primary email of" @@ -2442,17 +3063,39 @@ msgstr "" msgid "read_perm" -msgstr "" +msgstr "read perm" msgid "read_permission" msgstr "can be read by" +msgctxt "CWEType" +msgid "read_permission" +msgstr "read permission" + +msgctxt "CWRType" +msgid "read_permission" +msgstr "read permission" + +msgctxt "CWGroup" +msgid "read_permission_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "read_permission_object" +msgstr "" + msgid "read_permission_object" msgstr "has permission to delete" msgid "registry" msgstr "" +msgid "related entity has no state" +msgstr "" + +msgid "related entity has no workflow set" +msgstr "" + #, python-format msgid "relation %(relname)s of %(ent)s" msgstr "" @@ -2460,6 +3103,18 @@ msgid "relation_type" msgstr "relation type" +msgctxt "CWAttribute" +msgid "relation_type" +msgstr "relation type" + +msgctxt "CWRelation" +msgid "relation_type" +msgstr "relation type" + +msgctxt "CWRType" +msgid "relation_type_object" +msgstr "" + msgid "relation_type_object" msgstr "relation definitions" @@ -2472,71 +3127,53 @@ msgid "relative url of the bookmarked page" msgstr "" -msgid "remove this Bookmark" -msgstr "remove this bookmark" - -msgid "remove this CWAttribute" -msgstr "remove this attribute" - -msgid "remove this CWCache" -msgstr "remove this cubicweb cache" - -msgid "remove this CWConstraint" -msgstr "remove this constraint" - -msgid "remove this CWConstraintType" -msgstr "remove this constraint type" - +msgctxt "inlined:CWRelation:from_entity:subject" +msgid "remove this CWEType" +msgstr "remove this entity type" + +msgctxt "inlined:CWRelation:to_entity:subject" msgid "remove this CWEType" msgstr "remove this entity type" -msgid "remove this CWGroup" -msgstr "remove this group" - -msgid "remove this CWPermission" -msgstr "remove this permission" - -msgid "remove this CWProperty" -msgstr "remove this property" - +msgctxt "inlined:CWRelation:relation_type:subject" msgid "remove this CWRType" msgstr "remove this relation type" -msgid "remove this CWRelation" -msgstr "remove this relation" - -msgid "remove this CWUser" -msgstr "remove this user" - +msgctxt "inlined:CWUser:use_email:subject" msgid "remove this EmailAddress" msgstr "remove this email address" -msgid "remove this ExternalUri" -msgstr "" - -msgid "remove this RQLExpression" -msgstr "remove this RQL expression" - -msgid "remove this State" -msgstr "remove this state" - -msgid "remove this TrInfo" -msgstr "remove this transition information" - -msgid "remove this Transition" -msgstr "remove this transition" - msgid "require_group" msgstr "require the group" +msgctxt "BaseTransition" +msgid "require_group" +msgstr "require group" + +msgctxt "Transition" +msgid "require_group" +msgstr "require group" + +msgctxt "CWPermission" +msgid "require_group" +msgstr "require group" + +msgctxt "WorkflowTransition" +msgid "require_group" +msgstr "require group" + +msgctxt "CWGroup" +msgid "require_group_object" +msgstr "" + msgid "require_group_object" msgstr "required by" msgid "require_permission" -msgstr "" +msgstr "require permission" msgid "require_permission_object" -msgstr "" +msgstr "required by" msgid "required attribute" msgstr "" @@ -2653,6 +3290,9 @@ msgid "semantic description of this transition" msgstr "" +msgid "semantic description of this workflow" +msgstr "" + msgid "send email" msgstr "" @@ -2705,11 +3345,22 @@ msgid "sparql xml" msgstr "" +msgid "special transition allowing to go through a sub-workflow" +msgstr "" + msgid "specializes" msgstr "" +msgctxt "CWEType" +msgid "specializes" +msgstr "" + +msgctxt "CWEType" +msgid "specializes_object" +msgstr "" + msgid "specializes_object" -msgstr "" +msgstr "specialized by" msgid "startup views" msgstr "" @@ -2717,9 +3368,28 @@ msgid "state" msgstr "" +msgid "state doesn't belong to entity's current workflow" +msgstr "" + +msgid "state doesn't belong to entity's workflow" +msgstr "" + +msgid "" +"state doesn't belong to entity's workflow. You may want to set a custom " +"workflow for this entity first." +msgstr "" + msgid "state_of" msgstr "state of" +msgctxt "State" +msgid "state_of" +msgstr "" + +msgctxt "Workflow" +msgid "state_of_object" +msgstr "" + msgid "state_of_object" msgstr "use states" @@ -2742,12 +3412,65 @@ msgid "subject_plural:" msgstr "subjects:" +msgid "subworkflow" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "subworkflow" +msgstr "" + +msgid "subworkflow state" +msgstr "" + +msgid "subworkflow_exit" +msgstr "subworkflow exit" + +msgctxt "WorkflowTransition" +msgid "subworkflow_exit" +msgstr "" + +msgctxt "SubWorkflowExitPoint" +msgid "subworkflow_exit_object" +msgstr "" + +msgid "subworkflow_exit_object" +msgstr "subworkflow exit of" + +msgctxt "Workflow" +msgid "subworkflow_object" +msgstr "" + +msgid "subworkflow_object" +msgstr "subworkflow of" + +msgid "subworkflow_state" +msgstr "subworkflow state" + +msgctxt "SubWorkflowExitPoint" +msgid "subworkflow_state" +msgstr "subworkflow state" + +msgctxt "State" +msgid "subworkflow_state_object" +msgstr "" + +msgid "subworkflow_state_object" +msgstr "" + msgid "sunday" msgstr "" msgid "surname" msgstr "" +msgctxt "CWUser" +msgid "surname" +msgstr "" + +msgid "symetric" +msgstr "" + +msgctxt "CWRType" msgid "symetric" msgstr "" @@ -2806,6 +3529,10 @@ msgid "timestamp" msgstr "" +msgctxt "CWCache" +msgid "timestamp" +msgstr "" + msgid "timestamp of the latest source synchronization." msgstr "" @@ -2815,6 +3542,10 @@ msgid "title" msgstr "" +msgctxt "Bookmark" +msgid "title" +msgstr "" + msgid "to" msgstr "" @@ -2828,6 +3559,18 @@ msgid "to_entity" msgstr "to entity" +msgctxt "CWAttribute" +msgid "to_entity" +msgstr "to entity" + +msgctxt "CWRelation" +msgid "to_entity" +msgstr "to entity" + +msgctxt "CWEType" +msgid "to_entity_object" +msgstr "" + msgid "to_entity_object" msgstr "object relations" @@ -2837,6 +3580,14 @@ msgid "to_state" msgstr "to state" +msgctxt "TrInfo" +msgid "to_state" +msgstr "" + +msgctxt "State" +msgid "to_state_object" +msgstr "" + msgid "to_state_object" msgstr "transitions to this state" @@ -2846,13 +3597,34 @@ msgid "toggle check boxes" msgstr "" -#, python-format -msgid "transition from %s to %s does not exist or is not allowed" +msgid "transition doesn't belong to entity's workflow" +msgstr "" + +msgid "transition isn't allowed" +msgstr "" + +msgid "transition may not be fired" msgstr "" msgid "transition_of" msgstr "transition of" +msgctxt "BaseTransition" +msgid "transition_of" +msgstr "transition of" + +msgctxt "Transition" +msgid "transition_of" +msgstr "transition of" + +msgctxt "WorkflowTransition" +msgid "transition_of" +msgstr "" + +msgctxt "Workflow" +msgid "transition_of_object" +msgstr "" + msgid "transition_of_object" msgstr "use transitions" @@ -2925,6 +3697,10 @@ msgid "upassword" msgstr "password" +msgctxt "CWUser" +msgid "upassword" +msgstr "" + msgid "update" msgstr "" @@ -2934,6 +3710,18 @@ msgid "update_permission" msgstr "can be updated by" +msgctxt "CWEType" +msgid "update_permission" +msgstr "" + +msgctxt "CWGroup" +msgid "update_permission_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "update_permission_object" +msgstr "" + msgid "update_permission_object" msgstr "has permission to update" @@ -2944,6 +3732,10 @@ msgid "uri" msgstr "" +msgctxt "ExternalUri" +msgid "uri" +msgstr "" + msgid "use template languages" msgstr "" @@ -2955,6 +3747,14 @@ msgid "use_email" msgstr "use email" +msgctxt "CWUser" +msgid "use_email" +msgstr "" + +msgctxt "EmailAddress" +msgid "use_email_object" +msgstr "" + msgid "use_email_object" msgstr "used by" @@ -3005,6 +3805,14 @@ msgid "value" msgstr "" +msgctxt "CWConstraint" +msgid "value" +msgstr "" + +msgctxt "CWProperty" +msgid "value" +msgstr "" + msgid "value associated to this key is not editable manually" msgstr "" @@ -3051,17 +3859,54 @@ msgid "wf_info_for" msgstr "record for" +msgctxt "TrInfo" +msgid "wf_info_for" +msgstr "" + +msgctxt "CWUser" +msgid "wf_info_for_object" +msgstr "" + msgid "wf_info_for_object" msgstr "workflow history" msgid "" "when multiple addresses are equivalent (such as python-projects@logilab.org " -"and python-projects@lists.logilab.org), set this to true on one of them " -"which is the preferred form." +"and python-projects@lists.logilab.org), set this to indicate which is the " +"preferred form." +msgstr "" + +msgid "workflow" msgstr "" #, python-format -msgid "workflow for %s" +msgid "workflow changed to \"%s\"" +msgstr "" + +msgid "workflow has no initial state" +msgstr "" + +msgid "workflow history item" +msgstr "" + +msgid "workflow to which this state belongs" +msgstr "" + +msgid "workflow to which this transition belongs" +msgstr "" + +msgid "workflow_of" +msgstr "" + +msgctxt "Workflow" +msgid "workflow_of" +msgstr "" + +msgctxt "CWEType" +msgid "workflow_of_object" +msgstr "" + +msgid "workflow_of_object" msgstr "" msgid "xbel" @@ -3081,3 +3926,132 @@ msgid "you should probably delete that property" msgstr "" + +#~ msgid "add a BaseTransition" +#~ msgstr "XXX" + +#~ msgid "add a Bookmark" +#~ msgstr "add a bookmark" + +#~ msgid "add a CWAttribute" +#~ msgstr "add an attribute" + +#~ msgid "add a CWCache" +#~ msgstr "add a cubicweb cache" + +#~ msgid "add a CWConstraint" +#~ msgstr "add a constraint" + +#~ msgid "add a CWConstraintType" +#~ msgstr "add a constraint type" + +#~ msgid "add a CWEType" +#~ msgstr "add an entity type" + +#~ msgid "add a CWGroup" +#~ msgstr "add a group" + +#~ msgid "add a CWPermission" +#~ msgstr "add a permission" + +#~ msgid "add a CWProperty" +#~ msgstr "add a property" + +#~ msgid "add a CWRType" +#~ msgstr "add a relation type" + +#~ msgid "add a CWRelation" +#~ msgstr "add a relation" + +#~ msgid "add a CWUser" +#~ msgstr "add a user" + +#~ msgid "add a EmailAddress" +#~ msgstr "add an email address" + +#~ msgid "add a ExternalUri" +#~ msgstr "and an external uri" + +#~ msgid "add a RQLExpression" +#~ msgstr "add a rql expression" + +#~ msgid "add a State" +#~ msgstr "add a state" + +#~ msgid "add a SubWorkflowExitPoint" +#~ msgstr "add a subworkflow exit-point" + +#~ msgid "add a TrInfo" +#~ msgstr "add a transition information" + +#~ msgid "add a Transition" +#~ msgstr "add a transition" + +#~ msgid "add a Workflow" +#~ msgstr "add a workflow" + +#~ msgid "add a WorkflowTransition" +#~ msgstr "add a workflow-transition" + +#~ msgid "remove this Bookmark" +#~ msgstr "remove this bookmark" + +#~ msgid "remove this CWAttribute" +#~ msgstr "remove this attribute" + +#~ msgid "remove this CWCache" +#~ msgstr "remove this cubicweb cache" + +#~ msgid "remove this CWConstraint" +#~ msgstr "remove this constraint" + +#~ msgid "remove this CWConstraintType" +#~ msgstr "remove this constraint type" + +#~ msgid "remove this CWEType" +#~ msgstr "remove this entity type" + +#~ msgid "remove this CWGroup" +#~ msgstr "remove this group" + +#~ msgid "remove this CWPermission" +#~ msgstr "remove this permission" + +#~ msgid "remove this CWProperty" +#~ msgstr "remove this property" + +#~ msgid "remove this CWRType" +#~ msgstr "remove this relation type" + +#~ msgid "remove this CWRelation" +#~ msgstr "remove this relation" + +#~ msgid "remove this CWUser" +#~ msgstr "remove this user" + +#~ msgid "remove this EmailAddress" +#~ msgstr "remove this email address" + +#~ msgid "remove this ExternalUri" +#~ msgstr "remove this external uri" + +#~ msgid "remove this RQLExpression" +#~ msgstr "remove this RQL expression" + +#~ msgid "remove this State" +#~ msgstr "remove this state" + +#~ msgid "remove this SubWorkflowExitPoint" +#~ msgstr "remove this subworkflow exit-point" + +#~ msgid "remove this TrInfo" +#~ msgstr "remove this transition information" + +#~ msgid "remove this Transition" +#~ msgstr "remove this transition" + +#~ msgid "remove this Workflow" +#~ msgstr "remove this workflow" + +#~ msgid "remove this WorkflowTransition" +#~ msgstr "remove this workflow-transition" diff -r 24489cbbd697 -r 70c0dd1c3b7d i18n/es.po --- a/i18n/es.po Thu Sep 17 14:03:21 2009 +0200 +++ b/i18n/es.po Thu Sep 17 14:53:18 2009 +0200 @@ -112,10 +112,6 @@ msgstr "%s reporte de errores" #, python-format -msgid "%s is not the initial state (%s) for this entity" -msgstr "" - -#, python-format msgid "%s not estimated" msgstr "%s no estimado(s)" @@ -205,6 +201,15 @@ msgid "Attributes" msgstr "Atributos" +# schema pot file, generated on 2009-09-16 16:46:55 +# +# singular and plural forms for each entity type +msgid "BaseTransition" +msgstr "" + +msgid "BaseTransition_plural" +msgstr "" + msgid "Bookmark" msgstr "Favorito" @@ -359,6 +364,9 @@ msgid "Interval_plural" msgstr "Duraciones" +msgid "New BaseTransition" +msgstr "" + msgid "New Bookmark" msgstr "Agregar a Favoritos" @@ -407,12 +415,21 @@ msgid "New State" msgstr "Agregar Estado" +msgid "New SubWorkflowExitPoint" +msgstr "" + msgid "New TrInfo" msgstr "Agregar Información de Transición" msgid "New Transition" msgstr "Agregar transición" +msgid "New Workflow" +msgstr "" + +msgid "New WorkflowTransition" +msgstr "" + msgid "No query has been executed" msgstr "Ninguna búsqueda ha sido ejecutada" @@ -480,6 +497,12 @@ msgid "String_plural" msgstr "Cadenas de caracteres" +msgid "SubWorkflowExitPoint" +msgstr "" + +msgid "SubWorkflowExitPoint_plural" +msgstr "" + msgid "Subject:" msgstr "Sujeto:" @@ -500,12 +523,8 @@ msgid "The view %s could not be found" msgstr "La vista %s no ha podido ser encontrada" -msgid "There is no workflow defined for this entity." -msgstr "No hay workflow para este entidad" - -#, python-format -msgid "This %s" -msgstr "Este %s" +msgid "This BaseTransition" +msgstr "" msgid "This Bookmark" msgstr "Este favorito" @@ -555,12 +574,21 @@ msgid "This State" msgstr "Este estado" +msgid "This SubWorkflowExitPoint" +msgstr "" + msgid "This TrInfo" msgstr "Esta información de transición" msgid "This Transition" msgstr "Esta transición" +msgid "This Workflow" +msgstr "" + +msgid "This WorkflowTransition" +msgstr "" + msgid "Time" msgstr "Hora" @@ -592,9 +620,21 @@ msgid "What's new?" msgstr "Lo último en el sitio" +msgid "Workflow" +msgstr "" + msgid "Workflow history" msgstr "Histórico del Workflow" +msgid "WorkflowTransition" +msgstr "" + +msgid "WorkflowTransition_plural" +msgstr "" + +msgid "Workflow_plural" +msgstr "" + msgid "You are not connected to an instance !" msgstr "" @@ -628,7 +668,8 @@ msgid "" "You have no access to this view or it can not be used to display the current " "data." -msgstr "No tiene acceso a esta vista o No se puede utilizare para los datos actuales." +msgstr "" +"No tiene acceso a esta vista o No se puede utilizare para los datos actuales." msgid "" "You're not authorized to access this page. If you think you should, please " @@ -641,9 +682,6 @@ msgid "[%s supervision] changes summary" msgstr "[%s supervision] descripción de cambios" -msgid "__msg state changed" -msgstr "El estado a cambiado" - msgid "" "a RQL expression which should return some results, else the transition won't " "be available. This query may use X and U variables that will respectivly " @@ -666,12 +704,12 @@ msgid "about this site" msgstr "Sobre este Espacio" +msgid "abstract base class for transitions" +msgstr "" + msgid "access type" msgstr "Tipo de Acceso" -msgid "account state" -msgstr "Estado de la Cuenta" - msgid "action(s) on this selection" msgstr "acción(es) en esta selección" @@ -684,6 +722,12 @@ msgid "actions_addentity_description" msgstr "" +msgid "actions_addrelated" +msgstr "" + +msgid "actions_addrelated_description" +msgstr "" + msgid "actions_cancel" msgstr "Anular" @@ -829,28 +873,28 @@ msgstr "Definición de atributo" msgid "add CWEType add_permission RQLExpression subject" -msgstr "Agregar una autorización" +msgstr "Expresión RQL de agregación" msgid "add CWEType delete_permission RQLExpression subject" -msgstr "Eliminar una autorización" +msgstr "Expresión RQL de eliminación" msgid "add CWEType read_permission RQLExpression subject" -msgstr "Definir una expresión RQL de lectura" +msgstr "Expresión RQL de lectura" msgid "add CWEType update_permission RQLExpression subject" msgstr "Definir una expresión RQL de actualización" msgid "add CWProperty for_user CWUser object" -msgstr "Agregar Propiedad" +msgstr "Propiedad" msgid "add CWRType add_permission RQLExpression subject" -msgstr "Agregar expresión RQL de agregación" +msgstr "Expresión RQL de agregación" msgid "add CWRType delete_permission RQLExpression subject" -msgstr "Agregar expresión RQL de eliminación" +msgstr "Expresión RQL de eliminación" msgid "add CWRType read_permission RQLExpression subject" -msgstr "Agregar expresión RQL de lectura" +msgstr "Expresión RQL de lectura" msgid "add CWRelation constrained_by CWConstraint subject" msgstr "Restricción" @@ -859,85 +903,50 @@ msgstr "Definición de relación" msgid "add CWUser in_group CWGroup object" -msgstr "Agregar usuario" +msgstr "Usuario" msgid "add CWUser use_email EmailAddress subject" -msgstr "Agregar email" +msgstr "Email" msgid "add State allowed_transition Transition object" -msgstr "Agregar un estado en entrada" +msgstr "Estado en entrada" msgid "add State allowed_transition Transition subject" -msgstr "Agregar una transición en salida" - -msgid "add State state_of CWEType object" -msgstr "Agregar un estado" +msgstr "Transición en salida" + +msgid "add State allowed_transition WorkflowTransition subject" +msgstr "" msgid "add Transition condition RQLExpression subject" -msgstr "Agregar una Restricción" +msgstr "Restricción" msgid "add Transition destination_state State object" -msgstr "Agregar una transición de entrada" +msgstr "Transición de entrada" msgid "add Transition destination_state State subject" -msgstr "Agregar el estado de salida" - -msgid "add Transition transition_of CWEType object" -msgstr "Agregar una transición" - -msgid "add a Bookmark" -msgstr "Agregar un Favorito" - -msgid "add a CWAttribute" -msgstr "Agregar un tipo de relación" - -msgid "add a CWCache" -msgstr "Agregar un cache" - -msgid "add a CWConstraint" -msgstr "Agregar una Restricción" - -msgid "add a CWConstraintType" -msgstr "Agregar un tipo de Restricción" - +msgstr "Estado de salida" + +msgid "add WorkflowTransition condition RQLExpression subject" +msgstr "" + +msgid "add WorkflowTransition subworkflow_exit SubWorkflowExitPoint subject" +msgstr "" + +msgctxt "inlined:CWRelation.from_entity.subject" msgid "add a CWEType" -msgstr "Agregar un tipo de entidad" - -msgid "add a CWGroup" -msgstr "Agregar un grupo de usuarios" - -msgid "add a CWPermission" -msgstr "Agregar una autorización" - -msgid "add a CWProperty" -msgstr "Agregar una propiedad" - +msgstr "" + +msgctxt "inlined:CWRelation.to_entity.subject" +msgid "add a CWEType" +msgstr "" + +msgctxt "inlined:CWRelation.relation_type.subject" msgid "add a CWRType" -msgstr "Agregar un tipo de relación" - -msgid "add a CWRelation" -msgstr "Agregar una relación" - -msgid "add a CWUser" -msgstr "Agregar un usuario" - +msgstr "" + +msgctxt "inlined:CWUser.use_email.subject" msgid "add a EmailAddress" -msgstr "Agregar un email" - -msgid "add a ExternalUri" -msgstr "" - -msgid "add a RQLExpression" -msgstr "Agregar una expresión rql" - -msgid "add a State" -msgstr "Agregar un estado" - -msgid "add a TrInfo" -msgstr "Agregar una información de transición" - -msgid "add a Transition" -msgstr "Agregar una transición" +msgstr "" msgid "add a new permission" msgstr "Agregar una autorización" @@ -953,6 +962,24 @@ msgid "add_permission" msgstr "Autorización para agregar" +# subject and object forms for each relation type +# (no object form for final relation types) +msgctxt "CWEType" +msgid "add_permission" +msgstr "" + +msgctxt "CWRType" +msgid "add_permission" +msgstr "" + +msgctxt "CWGroup" +msgid "add_permission_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "add_permission_object" +msgstr "" + msgid "add_permission_object" msgstr "tiene la autorización para agregar" @@ -968,12 +995,26 @@ "Relación agregada %(rtype)s de %(frometype)s #%(fromeid)s hacia %(toetype)s #" "%(toeid)s" +msgid "addrelated" +msgstr "" + msgid "address" msgstr "dirección" +msgctxt "EmailAddress" +msgid "address" +msgstr "" + msgid "alias" msgstr "alias" +msgctxt "EmailAddress" +msgid "alias" +msgstr "" + +msgid "allow to set a specific workflow for an entity" +msgstr "" + msgid "allowed transition from this state" msgstr "transición autorizada desde este estado" @@ -983,6 +1024,22 @@ msgid "allowed_transition" msgstr "transición autorizada" +msgctxt "State" +msgid "allowed_transition" +msgstr "" + +msgctxt "BaseTransition" +msgid "allowed_transition_object" +msgstr "" + +msgctxt "Transition" +msgid "allowed_transition_object" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "allowed_transition_object" +msgstr "" + msgid "allowed_transition_object" msgstr "Estados de entrada" @@ -1064,6 +1121,14 @@ msgid "bookmarked_by" msgstr "está en los favoritos de" +msgctxt "Bookmark" +msgid "bookmarked_by" +msgstr "" + +msgctxt "CWUser" +msgid "bookmarked_by_object" +msgstr "" + msgid "bookmarked_by_object" msgstr "selecciona en sus favoritos a" @@ -1149,6 +1214,28 @@ msgid "by relation" msgstr "por relación" +msgid "by_transition" +msgstr "" + +msgctxt "TrInfo" +msgid "by_transition" +msgstr "" + +msgctxt "BaseTransition" +msgid "by_transition_object" +msgstr "" + +msgctxt "Transition" +msgid "by_transition_object" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "by_transition_object" +msgstr "" + +msgid "by_transition_object" +msgstr "" + msgid "calendar" msgstr "mostrar un calendario" @@ -1179,6 +1266,9 @@ msgid "can't display data, unexpected error: %s" msgstr "imposible de mostrar los datos, a causa del siguiente error: %s" +msgid "can't have multiple exits on the same state" +msgstr "" + #, python-format msgid "" "can't set inlined=%(inlined)s, %(stype)s %(rtype)s %(otype)s has cardinality=" @@ -1193,12 +1283,17 @@ msgid "cancel this insert" msgstr "Cancelar esta inserción" -msgid "canonical" -msgstr "canónico" - msgid "cardinality" msgstr "cardinalidad" +msgctxt "CWAttribute" +msgid "cardinality" +msgstr "" + +msgctxt "CWRelation" +msgid "cardinality" +msgstr "" + msgid "category" msgstr "categoria" @@ -1221,12 +1316,17 @@ msgid "comment" msgstr "Comentario" -msgid "comment:" -msgstr "Comentario:" +msgctxt "TrInfo" +msgid "comment" +msgstr "" msgid "comment_format" msgstr "Formato" +msgctxt "TrInfo" +msgid "comment_format" +msgstr "" + msgid "components" msgstr "Componentes" @@ -1278,6 +1378,12 @@ "Componente que permite distribuir sobre varias páginas las búsquedas que " "arrojan mayores resultados que un número previamente elegido" +msgid "components_pdfview" +msgstr "" + +msgid "components_pdfview_description" +msgstr "" + msgid "components_rqlinput" msgstr "Barra rql" @@ -1287,12 +1393,32 @@ msgid "composite" msgstr "composite" +msgctxt "CWRelation" +msgid "composite" +msgstr "" + msgid "condition" msgstr "condición" +msgctxt "BaseTransition" +msgid "condition" +msgstr "" + +msgctxt "Transition" +msgid "condition" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "condition" +msgstr "" + msgid "condition:" msgstr "condición:" +msgctxt "RQLExpression" +msgid "condition_object" +msgstr "" + msgid "condition_object" msgstr "condición de" @@ -1302,6 +1428,18 @@ msgid "constrained_by" msgstr "Restricción hecha por" +msgctxt "CWAttribute" +msgid "constrained_by" +msgstr "" + +msgctxt "CWRelation" +msgid "constrained_by" +msgstr "" + +msgctxt "CWConstraint" +msgid "constrained_by_object" +msgstr "" + msgid "constrained_by_object" msgstr "ha restringido" @@ -1433,6 +1571,10 @@ msgid "created_by" msgstr "creado por" +msgctxt "CWUser" +msgid "created_by_object" +msgstr "" + msgid "created_by_object" msgstr "ha creado" @@ -1501,23 +1643,32 @@ msgid "creating RQLExpression (Transition %(linkto)s condition RQLExpression)" msgstr "Creación de una expresión RQL para la transición %(linkto)s" +msgid "" +"creating RQLExpression (WorkflowTransition %(linkto)s condition " +"RQLExpression)" +msgstr "" + msgid "creating State (State allowed_transition Transition %(linkto)s)" msgstr "Creación de un estado que pueda ir hacia la transición %(linkto)s" -msgid "creating State (State state_of CWEType %(linkto)s)" -msgstr "Creación de un estado por el tipo %(linkto)s" - msgid "creating State (Transition %(linkto)s destination_state State)" msgstr "Creación de un estado destinación de la transición %(linkto)s" +msgid "" +"creating SubWorkflowExitPoint (WorkflowTransition %(linkto)s " +"subworkflow_exit SubWorkflowExitPoint)" +msgstr "" + msgid "creating Transition (State %(linkto)s allowed_transition Transition)" msgstr "Creación de una transición autorizada desde el estado %(linkto)s" msgid "creating Transition (Transition destination_state State %(linkto)s)" msgstr "Creación de un transición hacia el estado %(linkto)s" -msgid "creating Transition (Transition transition_of CWEType %(linkto)s)" -msgstr "Creación de una transición para el tipo %(linkto)s" +msgid "" +"creating WorkflowTransition (State %(linkto)s allowed_transition " +"WorkflowTransition)" +msgstr "" msgid "creation" msgstr "Creación" @@ -1531,6 +1682,14 @@ msgid "cstrtype" msgstr "Tipo de condición" +msgctxt "CWConstraint" +msgid "cstrtype" +msgstr "" + +msgctxt "CWConstraintType" +msgid "cstrtype_object" +msgstr "" + msgid "cstrtype_object" msgstr "utilizado por" @@ -1544,6 +1703,20 @@ msgid "currently attached file: %s" msgstr "archivo adjunto: %s" +msgid "custom_workflow" +msgstr "" + +msgctxt "CWUser" +msgid "custom_workflow" +msgstr "" + +msgctxt "Workflow" +msgid "custom_workflow_object" +msgstr "" + +msgid "custom_workflow_object" +msgstr "" + msgid "cwetype-schema-image" msgstr "Esquema" @@ -1580,9 +1753,33 @@ msgid "default text format for rich text fields." msgstr "Formato de texto como opción por defecto para los campos texto" +msgid "default user workflow" +msgstr "" + +msgid "default workflow for an entity type" +msgstr "" + +msgid "default_workflow" +msgstr "" + +msgctxt "CWEType" +msgid "default_workflow" +msgstr "" + +msgctxt "Workflow" +msgid "default_workflow_object" +msgstr "" + +msgid "default_workflow_object" +msgstr "" + msgid "defaultval" msgstr "Valor por defecto" +msgctxt "CWAttribute" +msgid "defaultval" +msgstr "" + msgid "define a CubicWeb user" msgstr "Define un usuario CubicWeb" @@ -1614,6 +1811,9 @@ msgid "define an entity type, used to build the instance schema" msgstr "" +msgid "define how we get out from a sub-workflow" +msgstr "" + msgid "" "defines what's the property is applied for. You must select this first to be " "able to set value" @@ -1639,6 +1839,22 @@ msgid "delete_permission" msgstr "Autorización de eliminar" +msgctxt "CWEType" +msgid "delete_permission" +msgstr "" + +msgctxt "CWRType" +msgid "delete_permission" +msgstr "" + +msgctxt "CWGroup" +msgid "delete_permission_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "delete_permission_object" +msgstr "" + msgid "delete_permission_object" msgstr "posee la autorización de eliminar" @@ -1660,9 +1876,84 @@ msgid "description" msgstr "Descripción" +msgctxt "CWEType" +msgid "description" +msgstr "" + +msgctxt "CWRelation" +msgid "description" +msgstr "" + +msgctxt "Workflow" +msgid "description" +msgstr "" + +msgctxt "CWAttribute" +msgid "description" +msgstr "" + +msgctxt "Transition" +msgid "description" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "description" +msgstr "" + +msgctxt "State" +msgid "description" +msgstr "" + +msgctxt "CWRType" +msgid "description" +msgstr "" + +msgctxt "BaseTransition" +msgid "description" +msgstr "" + msgid "description_format" msgstr "Formato" +msgctxt "CWEType" +msgid "description_format" +msgstr "" + +msgctxt "CWRelation" +msgid "description_format" +msgstr "" + +msgctxt "Workflow" +msgid "description_format" +msgstr "" + +msgctxt "CWAttribute" +msgid "description_format" +msgstr "" + +msgctxt "Transition" +msgid "description_format" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "description_format" +msgstr "" + +msgctxt "State" +msgid "description_format" +msgstr "" + +msgctxt "CWRType" +msgid "description_format" +msgstr "" + +msgctxt "BaseTransition" +msgid "description_format" +msgstr "" + +msgid "destination state" +msgstr "" + msgid "destination state for this transition" msgstr "Estado destino para esta transición" @@ -1672,6 +1963,18 @@ msgid "destination_state" msgstr "Estado destino" +msgctxt "Transition" +msgid "destination_state" +msgstr "" + +msgctxt "SubWorkflowExitPoint" +msgid "destination_state" +msgstr "" + +msgctxt "State" +msgid "destination_state_object" +msgstr "" + msgid "destination_state_object" msgstr "Destino de" @@ -1700,6 +2003,9 @@ msgid "display the component or not" msgstr "Mostrar el componente o no" +msgid "display the pdf icon or not" +msgstr "" + msgid "" "distinct label to distinguate between other permission entity of the same " "name" @@ -1717,6 +2023,9 @@ msgid "download icon" msgstr "ícono de descarga" +msgid "download page as pdf" +msgstr "" + msgid "download schema as owl" msgstr "Descargar esquema en OWL" @@ -1774,6 +2083,9 @@ msgid "entity edited" msgstr "entidad modificada" +msgid "entity has no workflow set" +msgstr "" + msgid "entity linked" msgstr "entidad asociada" @@ -1787,11 +2099,8 @@ "Tipo de entidad utilizada para definir una configuración de seguridad " "avanzada" -msgid "entity types which may use this state" -msgstr "Tipo de entidades que pueden utilizar este estado" - -msgid "entity types which may use this transition" -msgstr "Entidades que pueden utilizar esta transición" +msgid "entity types which may use this workflow" +msgstr "" msgid "error while embedding page" msgstr "Error durante la inclusión de la página" @@ -1813,15 +2122,33 @@ msgid "eta_date" msgstr "fecha de fin" +msgid "exit_point" +msgstr "" + +msgid "exit_point_object" +msgstr "" + +#, python-format +msgid "exiting from subworkflow %s" +msgstr "" + msgid "expected:" msgstr "Previsto :" msgid "expression" msgstr "Expresión" +msgctxt "RQLExpression" +msgid "expression" +msgstr "" + msgid "exprtype" msgstr "Tipo de la expresión" +msgctxt "RQLExpression" +msgid "exprtype" +msgstr "" + msgid "external page" msgstr "Página externa" @@ -1873,9 +2200,21 @@ msgid "final" msgstr "Final" +msgctxt "CWEType" +msgid "final" +msgstr "" + +msgctxt "CWRType" +msgid "final" +msgstr "" + msgid "firstname" msgstr "Nombre" +msgctxt "CWUser" +msgid "firstname" +msgstr "" + msgid "foaf" msgstr "Amigo de un Amigo, FOAF" @@ -1885,6 +2224,14 @@ msgid "for_user" msgstr "Para el usuario" +msgctxt "CWProperty" +msgid "for_user" +msgstr "" + +msgctxt "CWUser" +msgid "for_user_object" +msgstr "" + msgid "for_user_object" msgstr "Utiliza las propiedades" @@ -1901,6 +2248,18 @@ msgid "from_entity" msgstr "De la entidad" +msgctxt "CWAttribute" +msgid "from_entity" +msgstr "" + +msgctxt "CWRelation" +msgid "from_entity" +msgstr "" + +msgctxt "CWEType" +msgid "from_entity_object" +msgstr "" + msgid "from_entity_object" msgstr "Relación sujeto" @@ -1910,6 +2269,14 @@ msgid "from_state" msgstr "De el estado" +msgctxt "TrInfo" +msgid "from_state" +msgstr "" + +msgctxt "State" +msgid "from_state_object" +msgstr "" + msgid "from_state_object" msgstr "Transiciones desde este estado" @@ -1919,9 +2286,17 @@ msgid "fulltext_container" msgstr "Contenedor de texto indexado" +msgctxt "CWRType" +msgid "fulltext_container" +msgstr "" + msgid "fulltextindexed" msgstr "Indexación de texto" +msgctxt "CWAttribute" +msgid "fulltextindexed" +msgstr "" + msgid "generic plot" msgstr "Trazado de curbas estándares" @@ -1940,6 +2315,10 @@ msgid "granted to groups" msgstr "Otorgado a los grupos" +#, python-format +msgid "graphical representation of %s" +msgstr "" + msgid "graphical representation of the instance'schema" msgstr "" @@ -2034,12 +2413,103 @@ msgid "id of main template used to render pages" msgstr "ID del template principal" +msgid "identical to" +msgstr "" + msgid "identical_to" msgstr "idéntico a" msgid "identity" msgstr "es idéntico a" +msgctxt "CWRelation" +msgid "identity_object" +msgstr "" + +msgctxt "Bookmark" +msgid "identity_object" +msgstr "" + +msgctxt "CWAttribute" +msgid "identity_object" +msgstr "" + +msgctxt "CWConstraintType" +msgid "identity_object" +msgstr "" + +msgctxt "State" +msgid "identity_object" +msgstr "" + +msgctxt "BaseTransition" +msgid "identity_object" +msgstr "" + +msgctxt "CWEType" +msgid "identity_object" +msgstr "" + +msgctxt "Workflow" +msgid "identity_object" +msgstr "" + +msgctxt "CWGroup" +msgid "identity_object" +msgstr "" + +msgctxt "TrInfo" +msgid "identity_object" +msgstr "" + +msgctxt "CWConstraint" +msgid "identity_object" +msgstr "" + +msgctxt "CWUser" +msgid "identity_object" +msgstr "" + +msgctxt "Transition" +msgid "identity_object" +msgstr "" + +msgctxt "CWRType" +msgid "identity_object" +msgstr "" + +msgctxt "SubWorkflowExitPoint" +msgid "identity_object" +msgstr "" + +msgctxt "ExternalUri" +msgid "identity_object" +msgstr "" + +msgctxt "CWCache" +msgid "identity_object" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "identity_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "identity_object" +msgstr "" + +msgctxt "CWPermission" +msgid "identity_object" +msgstr "" + +msgctxt "EmailAddress" +msgid "identity_object" +msgstr "" + +msgctxt "CWProperty" +msgid "identity_object" +msgstr "" + msgid "identity_object" msgstr "es idéntico a" @@ -2062,12 +2532,28 @@ msgid "in_group" msgstr "En el grupo" +msgctxt "CWUser" +msgid "in_group" +msgstr "" + +msgctxt "CWGroup" +msgid "in_group_object" +msgstr "" + msgid "in_group_object" msgstr "Miembros" msgid "in_state" msgstr "estado" +msgctxt "CWUser" +msgid "in_state" +msgstr "" + +msgctxt "State" +msgid "in_state_object" +msgstr "" + msgid "in_state_object" msgstr "estado de" @@ -2087,6 +2573,10 @@ msgid "indexed" msgstr "Indexado" +msgctxt "CWAttribute" +msgid "indexed" +msgstr "" + msgid "indicate the current state of an entity" msgstr "Indica el estado actual de una entidad" @@ -2103,18 +2593,30 @@ msgid "initial estimation %s" msgstr "Estimación inicial %s" -msgid "initial state for entities of this type" -msgstr "Estado inicial para las entidades de este tipo" +msgid "initial state for this workflow" +msgstr "" msgid "initial_state" msgstr "estado inicial" +msgctxt "Workflow" +msgid "initial_state" +msgstr "" + +msgctxt "State" +msgid "initial_state_object" +msgstr "" + msgid "initial_state_object" msgstr "es el estado inicial de" msgid "inlined" msgstr "Puesto en línea" +msgctxt "CWRType" +msgid "inlined" +msgstr "" + msgid "instance schema" msgstr "" @@ -2124,6 +2626,10 @@ msgid "internationalizable" msgstr "Internacionalizable" +msgctxt "CWAttribute" +msgid "internationalizable" +msgstr "" + #, python-format msgid "invalid action %r" msgstr "Acción %r invalida" @@ -2163,9 +2669,17 @@ msgid "is_instance_of" msgstr "es una instancia de" +msgctxt "CWEType" +msgid "is_instance_of_object" +msgstr "" + msgid "is_instance_of_object" msgstr "tiene como instancias" +msgctxt "CWEType" +msgid "is_object" +msgstr "" + msgid "is_object" msgstr "tiene por instancia" @@ -2181,6 +2695,10 @@ msgid "label" msgstr "Etiqueta" +msgctxt "CWPermission" +msgid "label" +msgstr "" + msgid "language of the user interface" msgstr "Idioma para la interface del usuario" @@ -2190,6 +2708,10 @@ msgid "last_login_time" msgstr "Ultima fecha de conexión" +msgctxt "CWUser" +msgid "last_login_time" +msgstr "" + msgid "latest modification time of an entity" msgstr "Fecha de la última modificación de una entidad " @@ -2223,14 +2745,17 @@ msgid "link a relation definition to its subject entity type" msgstr "liga una definición de relación a su tipo de entidad" -msgid "link a state to one or more entity type" -msgstr "liga un estado a una o mas entidades" +msgid "link a state to one or more workflow" +msgstr "" msgid "link a transition information to its object" msgstr "liga una transcion de informacion a los objetos asociados" -msgid "link a transition to one or more entity type" -msgstr "liga una transición a una o mas tipos de entidad" +msgid "link a transition to one or more workflow" +msgstr "" + +msgid "link a workflow to one or more entity type" +msgstr "" msgid "link to each item in" msgstr "ligar hacia cada elemento en" @@ -2247,6 +2772,10 @@ msgid "login" msgstr "Clave de acesso" +msgctxt "CWUser" +msgid "login" +msgstr "" + msgid "login or email" msgstr "Clave de acesso o dirección de correo" @@ -2266,6 +2795,10 @@ msgid "mainvars" msgstr "Principales variables" +msgctxt "RQLExpression" +msgid "mainvars" +msgstr "" + msgid "manage" msgstr "Administracion del Sitio" @@ -2281,6 +2814,9 @@ msgid "managers" msgstr "editores" +msgid "mandatory relation" +msgstr "" + msgid "march" msgstr "Marzo" @@ -2327,6 +2863,50 @@ msgid "name" msgstr "Nombre" +msgctxt "CWEType" +msgid "name" +msgstr "" + +msgctxt "Transition" +msgid "name" +msgstr "" + +msgctxt "Workflow" +msgid "name" +msgstr "" + +msgctxt "CWGroup" +msgid "name" +msgstr "" + +msgctxt "CWConstraintType" +msgid "name" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "name" +msgstr "" + +msgctxt "State" +msgid "name" +msgstr "" + +msgctxt "CWPermission" +msgid "name" +msgstr "" + +msgctxt "CWRType" +msgid "name" +msgstr "" + +msgctxt "BaseTransition" +msgid "name" +msgstr "" + +msgctxt "CWCache" +msgid "name" +msgstr "" + msgid "name of the cache" msgstr "Nombre del Cache" @@ -2438,6 +3018,14 @@ msgid "ordernum" msgstr "orden" +msgctxt "CWAttribute" +msgid "ordernum" +msgstr "" + +msgctxt "CWRelation" +msgid "ordernum" +msgstr "" + msgid "owl" msgstr "owl" @@ -2447,6 +3035,10 @@ msgid "owned_by" msgstr "pertenece a" +msgctxt "CWUser" +msgid "owned_by_object" +msgstr "" + msgid "owned_by_object" msgstr "pertenece al objeto" @@ -2471,6 +3063,10 @@ msgid "path" msgstr "Ruta" +msgctxt "Bookmark" +msgid "path" +msgstr "" + msgid "permission" msgstr "Permiso" @@ -2492,6 +3088,10 @@ msgid "pkey" msgstr "pkey" +msgctxt "CWProperty" +msgid "pkey" +msgstr "" + msgid "please correct errors below" msgstr "Favor de corregir errores" @@ -2504,6 +3104,20 @@ msgid "powered by CubicWeb" msgstr "" +msgid "prefered_form" +msgstr "" + +msgctxt "EmailAddress" +msgid "prefered_form" +msgstr "" + +msgctxt "EmailAddress" +msgid "prefered_form_object" +msgstr "" + +msgid "prefered_form_object" +msgstr "" + msgid "preferences" msgstr "Preferencias" @@ -2516,6 +3130,14 @@ msgid "primary_email" msgstr "Dirección de email principal" +msgctxt "CWUser" +msgid "primary_email" +msgstr "" + +msgctxt "EmailAddress" +msgid "primary_email_object" +msgstr "" + msgid "primary_email_object" msgstr "Dirección de email principal (objeto)" @@ -2537,12 +3159,34 @@ msgid "read_permission" msgstr "Permiso de lectura" +msgctxt "CWEType" +msgid "read_permission" +msgstr "" + +msgctxt "CWRType" +msgid "read_permission" +msgstr "" + +msgctxt "CWGroup" +msgid "read_permission_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "read_permission_object" +msgstr "" + msgid "read_permission_object" msgstr "Objeto_permiso_lectura" msgid "registry" msgstr "" +msgid "related entity has no state" +msgstr "" + +msgid "related entity has no workflow set" +msgstr "" + #, python-format msgid "relation %(relname)s of %(ent)s" msgstr "relación %(relname)s de %(ent)s" @@ -2550,6 +3194,18 @@ msgid "relation_type" msgstr "tipo de relación" +msgctxt "CWAttribute" +msgid "relation_type" +msgstr "" + +msgctxt "CWRelation" +msgid "relation_type" +msgstr "" + +msgctxt "CWRType" +msgid "relation_type_object" +msgstr "" + msgid "relation_type_object" msgstr "Definición" @@ -2562,63 +3218,45 @@ msgid "relative url of the bookmarked page" msgstr "Url relativa de la pagina" -msgid "remove this Bookmark" -msgstr "Eliminar este Favorito" - -msgid "remove this CWAttribute" -msgstr "Eliminar este atributo" - -msgid "remove this CWCache" -msgstr "Eliminar esta cache de aplicación" - -msgid "remove this CWConstraint" -msgstr "Eliminar esta restricción" - -msgid "remove this CWConstraintType" -msgstr "Eliminar este tipo de restricción" - +msgctxt "inlined:CWRelation:from_entity:subject" +msgid "remove this CWEType" +msgstr "" + +msgctxt "inlined:CWRelation:to_entity:subject" msgid "remove this CWEType" -msgstr "Eliminar este tipo de entidad" - -msgid "remove this CWGroup" -msgstr "Eliminar este grupo" - -msgid "remove this CWPermission" -msgstr "Eliminar este permiso" - -msgid "remove this CWProperty" -msgstr "Eliminar esta propiedad" - +msgstr "" + +msgctxt "inlined:CWRelation:relation_type:subject" msgid "remove this CWRType" -msgstr "Eliminar esta definición de relación" - -msgid "remove this CWRelation" -msgstr "Eliminar esta relación" - -msgid "remove this CWUser" -msgstr "Eliminar este usuario" - +msgstr "" + +msgctxt "inlined:CWUser:use_email:subject" msgid "remove this EmailAddress" -msgstr "Eliminar este correo electronico" - -msgid "remove this ExternalUri" -msgstr "" - -msgid "remove this RQLExpression" -msgstr "Eliminar esta expresión RQL" - -msgid "remove this State" -msgstr "Eliminar este estado" - -msgid "remove this TrInfo" -msgstr "Eliminar información de esta transición" - -msgid "remove this Transition" -msgstr "Eliminar esta transición" +msgstr "" msgid "require_group" msgstr "Requiere grupo" +msgctxt "BaseTransition" +msgid "require_group" +msgstr "" + +msgctxt "Transition" +msgid "require_group" +msgstr "" + +msgctxt "CWPermission" +msgid "require_group" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "require_group" +msgstr "" + +msgctxt "CWGroup" +msgid "require_group_object" +msgstr "" + msgid "require_group_object" msgstr "Requerido por grupo" @@ -2747,6 +3385,9 @@ msgid "semantic description of this transition" msgstr "descripcion semantica de esta transición" +msgid "semantic description of this workflow" +msgstr "" + msgid "send email" msgstr "enviar email" @@ -2803,9 +3444,20 @@ msgid "sparql xml" msgstr "" +msgid "special transition allowing to go through a sub-workflow" +msgstr "" + msgid "specializes" msgstr "derivado de" +msgctxt "CWEType" +msgid "specializes" +msgstr "" + +msgctxt "CWEType" +msgid "specializes_object" +msgstr "" + msgid "specializes_object" msgstr "objeto_derivado" @@ -2815,9 +3467,28 @@ msgid "state" msgstr "estado" +msgid "state doesn't belong to entity's current workflow" +msgstr "" + +msgid "state doesn't belong to entity's workflow" +msgstr "" + +msgid "" +"state doesn't belong to entity's workflow. You may want to set a custom " +"workflow for this entity first." +msgstr "" + msgid "state_of" msgstr "estado_de" +msgctxt "State" +msgid "state_of" +msgstr "" + +msgctxt "Workflow" +msgid "state_of_object" +msgstr "" + msgid "state_of_object" msgstr "objeto_estado_de" @@ -2840,15 +3511,68 @@ msgid "subject_plural:" msgstr "sujetos:" +msgid "subworkflow" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "subworkflow" +msgstr "" + +msgid "subworkflow state" +msgstr "" + +msgid "subworkflow_exit" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "subworkflow_exit" +msgstr "" + +msgctxt "SubWorkflowExitPoint" +msgid "subworkflow_exit_object" +msgstr "" + +msgid "subworkflow_exit_object" +msgstr "" + +msgctxt "Workflow" +msgid "subworkflow_object" +msgstr "" + +msgid "subworkflow_object" +msgstr "" + +msgid "subworkflow_state" +msgstr "" + +msgctxt "SubWorkflowExitPoint" +msgid "subworkflow_state" +msgstr "" + +msgctxt "State" +msgid "subworkflow_state_object" +msgstr "" + +msgid "subworkflow_state_object" +msgstr "" + msgid "sunday" msgstr "domingo" msgid "surname" msgstr "apellido" +msgctxt "CWUser" +msgid "surname" +msgstr "" + msgid "symetric" msgstr "simetrico" +msgctxt "CWRType" +msgid "symetric" +msgstr "" + msgid "system entities" msgstr "entidades de sistema" @@ -2904,6 +3628,10 @@ msgid "timestamp" msgstr "fecha" +msgctxt "CWCache" +msgid "timestamp" +msgstr "" + msgid "timestamp of the latest source synchronization." msgstr "fecha de la ultima sincronización de la fuente." @@ -2913,6 +3641,10 @@ msgid "title" msgstr "titulo" +msgctxt "Bookmark" +msgid "title" +msgstr "" + msgid "to" msgstr "a" @@ -2926,6 +3658,18 @@ msgid "to_entity" msgstr "hacia entidad" +msgctxt "CWAttribute" +msgid "to_entity" +msgstr "" + +msgctxt "CWRelation" +msgid "to_entity" +msgstr "" + +msgctxt "CWEType" +msgid "to_entity_object" +msgstr "" + msgid "to_entity_object" msgstr "hacia entidad objeto" @@ -2935,6 +3679,14 @@ msgid "to_state" msgstr "hacia el estado" +msgctxt "TrInfo" +msgid "to_state" +msgstr "" + +msgctxt "State" +msgid "to_state_object" +msgstr "" + msgid "to_state_object" msgstr "hacia objeto estado" @@ -2944,13 +3696,34 @@ msgid "toggle check boxes" msgstr "cambiar valor" -#, python-format -msgid "transition from %s to %s does not exist or is not allowed" +msgid "transition doesn't belong to entity's workflow" +msgstr "" + +msgid "transition isn't allowed" +msgstr "" + +msgid "transition may not be fired" msgstr "" msgid "transition_of" msgstr "transicion de" +msgctxt "BaseTransition" +msgid "transition_of" +msgstr "" + +msgctxt "Transition" +msgid "transition_of" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "transition_of" +msgstr "" + +msgctxt "Workflow" +msgid "transition_of_object" +msgstr "" + msgid "transition_of_object" msgstr "objeto de transición" @@ -3023,6 +3796,10 @@ msgid "upassword" msgstr "clave de acceso" +msgctxt "CWUser" +msgid "upassword" +msgstr "" + msgid "update" msgstr "modificación" @@ -3032,6 +3809,18 @@ msgid "update_permission" msgstr "Permiso de modificación" +msgctxt "CWEType" +msgid "update_permission" +msgstr "" + +msgctxt "CWGroup" +msgid "update_permission_object" +msgstr "" + +msgctxt "RQLExpression" +msgid "update_permission_object" +msgstr "" + msgid "update_permission_object" msgstr "objeto de autorización de modificaciones" @@ -3042,6 +3831,10 @@ msgid "uri" msgstr "" +msgctxt "ExternalUri" +msgid "uri" +msgstr "" + msgid "use template languages" msgstr "utilizar plantillas de lenguaje" @@ -3055,6 +3848,14 @@ msgid "use_email" msgstr "correo electrónico" +msgctxt "CWUser" +msgid "use_email" +msgstr "" + +msgctxt "EmailAddress" +msgid "use_email_object" +msgstr "" + msgid "use_email_object" msgstr "objeto email utilizado" @@ -3113,6 +3914,14 @@ msgid "value" msgstr "valor" +msgctxt "CWConstraint" +msgid "value" +msgstr "" + +msgctxt "CWProperty" +msgid "value" +msgstr "" + msgid "value associated to this key is not editable manually" msgstr "el valor asociado a este elemento no es editable manualmente" @@ -3159,21 +3968,55 @@ msgid "wf_info_for" msgstr "historial de" +msgctxt "TrInfo" +msgid "wf_info_for" +msgstr "" + +msgctxt "CWUser" +msgid "wf_info_for_object" +msgstr "" + msgid "wf_info_for_object" msgstr "historial de transiciones" msgid "" "when multiple addresses are equivalent (such as python-projects@logilab.org " -"and python-projects@lists.logilab.org), set this to true on one of them " -"which is the preferred form." -msgstr "" -"cuando multiples direcciones de correo son equivalentes (como python-" -"projects@logilab.org y python-projects@lists.logilab.org), establecer esto " -"como verdadero en una de ellas es la forma preferida " +"and python-projects@lists.logilab.org), set this to indicate which is the " +"preferred form." +msgstr "" + +msgid "workflow" +msgstr "" #, python-format -msgid "workflow for %s" -msgstr "workflow para %s" +msgid "workflow changed to \"%s\"" +msgstr "" + +msgid "workflow has no initial state" +msgstr "" + +msgid "workflow history item" +msgstr "" + +msgid "workflow to which this state belongs" +msgstr "" + +msgid "workflow to which this transition belongs" +msgstr "" + +msgid "workflow_of" +msgstr "" + +msgctxt "Workflow" +msgid "workflow_of" +msgstr "" + +msgctxt "CWEType" +msgid "workflow_of_object" +msgstr "" + +msgid "workflow_of_object" +msgstr "" msgid "xbel" msgstr "xbel" @@ -3193,6 +4036,165 @@ msgid "you should probably delete that property" msgstr "deberia probablamente suprimir esta propriedad" +#~ msgid "There is no workflow defined for this entity." +#~ msgstr "No hay workflow para este entidad" + +#~ msgid "This %s" +#~ msgstr "Este %s" + #~ msgid "" #~ "You have no access to this view or it's not applyable to current data" #~ msgstr "No tiene acceso a esta vista o No es aplicable a los datos actuales" + +#~ msgid "__msg state changed" +#~ msgstr "El estado a cambiado" + +#~ msgid "account state" +#~ msgstr "Estado de la Cuenta" + +#~ msgid "add State state_of CWEType object" +#~ msgstr "Estado" + +#~ msgid "add Transition transition_of CWEType object" +#~ msgstr "Transición" + +#~ msgid "add a Bookmark" +#~ msgstr "Agregar un Favorito" + +#~ msgid "add a CWAttribute" +#~ msgstr "Agregar un tipo de relación" + +#~ msgid "add a CWCache" +#~ msgstr "Agregar un cache" + +#~ msgid "add a CWConstraint" +#~ msgstr "Agregar una Restricción" + +#~ msgid "add a CWConstraintType" +#~ msgstr "Agregar un tipo de Restricción" + +#~ msgid "add a CWEType" +#~ msgstr "Agregar un tipo de entidad" + +#~ msgid "add a CWGroup" +#~ msgstr "Agregar un grupo de usuarios" + +#~ msgid "add a CWPermission" +#~ msgstr "Agregar una autorización" + +#~ msgid "add a CWProperty" +#~ msgstr "Agregar una propiedad" + +#~ msgid "add a CWRType" +#~ msgstr "Agregar un tipo de relación" + +#~ msgid "add a CWRelation" +#~ msgstr "Agregar una relación" + +#~ msgid "add a CWUser" +#~ msgstr "Agregar un usuario" + +#~ msgid "add a EmailAddress" +#~ msgstr "Agregar un email" + +#~ msgid "add a RQLExpression" +#~ msgstr "Agregar una expresión rql" + +#~ msgid "add a State" +#~ msgstr "Agregar un estado" + +#~ msgid "add a TrInfo" +#~ msgstr "Agregar una información de transición" + +#~ msgid "add a Transition" +#~ msgstr "Agregar una transición" + +#~ msgid "canonical" +#~ msgstr "canónico" + +#~ msgid "comment:" +#~ msgstr "Comentario:" + +#~ msgid "creating State (State state_of CWEType %(linkto)s)" +#~ msgstr "Creación de un estado por el tipo %(linkto)s" + +#~ msgid "creating Transition (Transition transition_of CWEType %(linkto)s)" +#~ msgstr "Creación de una transición para el tipo %(linkto)s" + +#~ msgid "entity types which may use this state" +#~ msgstr "Tipo de entidades que pueden utilizar este estado" + +#~ msgid "entity types which may use this transition" +#~ msgstr "Entidades que pueden utilizar esta transición" + +#~ msgid "initial state for entities of this type" +#~ msgstr "Estado inicial para las entidades de este tipo" + +#~ msgid "link a state to one or more entity type" +#~ msgstr "liga un estado a una o mas entidades" + +#~ msgid "link a transition to one or more entity type" +#~ msgstr "liga una transición a una o mas tipos de entidad" + +#~ msgid "remove this Bookmark" +#~ msgstr "Eliminar este Favorito" + +#~ msgid "remove this CWAttribute" +#~ msgstr "Eliminar este atributo" + +#~ msgid "remove this CWCache" +#~ msgstr "Eliminar esta cache de aplicación" + +#~ msgid "remove this CWConstraint" +#~ msgstr "Eliminar esta restricción" + +#~ msgid "remove this CWConstraintType" +#~ msgstr "Eliminar este tipo de restricción" + +#~ msgid "remove this CWEType" +#~ msgstr "Eliminar este tipo de entidad" + +#~ msgid "remove this CWGroup" +#~ msgstr "Eliminar este grupo" + +#~ msgid "remove this CWPermission" +#~ msgstr "Eliminar este permiso" + +#~ msgid "remove this CWProperty" +#~ msgstr "Eliminar esta propiedad" + +#~ msgid "remove this CWRType" +#~ msgstr "Eliminar esta definición de relación" + +#~ msgid "remove this CWRelation" +#~ msgstr "Eliminar esta relación" + +#~ msgid "remove this CWUser" +#~ msgstr "Eliminar este usuario" + +#~ msgid "remove this EmailAddress" +#~ msgstr "Eliminar este correo electronico" + +#~ msgid "remove this RQLExpression" +#~ msgstr "Eliminar esta expresión RQL" + +#~ msgid "remove this State" +#~ msgstr "Eliminar este estado" + +#~ msgid "remove this TrInfo" +#~ msgstr "Eliminar información de esta transición" + +#~ msgid "remove this Transition" +#~ msgstr "Eliminar esta transición" + +#~ msgid "" +#~ "when multiple addresses are equivalent (such as python-projects@logilab." +#~ "org and python-projects@lists.logilab.org), set this to true on one of " +#~ "them which is the preferred form." +#~ msgstr "" +#~ "cuando multiples direcciones de correo son equivalentes (como python-" +#~ "projects@logilab.org y python-projects@lists.logilab.org), establecer " +#~ "esto como verdadero en una de ellas es la forma preferida " + +#~ msgid "workflow for %s" +#~ msgstr "workflow para %s" diff -r 24489cbbd697 -r 70c0dd1c3b7d i18n/fr.po --- a/i18n/fr.po Thu Sep 17 14:03:21 2009 +0200 +++ b/i18n/fr.po Thu Sep 17 14:53:18 2009 +0200 @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: cubicweb 2.46.0\n" -"PO-Revision-Date: 2009-08-05 08:37+0200\n" +"PO-Revision-Date: 2009-09-17 12:03+0200\n" "Last-Translator: Logilab Team \n" "Language-Team: fr \n" "MIME-Version: 1.0\n" @@ -112,10 +112,6 @@ msgstr "%s rapport d'erreur" #, python-format -msgid "%s is not the initial state (%s) for this entity" -msgstr "%s n'est pas l'état initial (%s) de cette entité" - -#, python-format msgid "%s not estimated" msgstr "%s non estimé(s)" @@ -204,6 +200,15 @@ msgid "Attributes" msgstr "Attributs" +# schema pot file, generated on 2009-09-16 16:46:55 +# +# singular and plural forms for each entity type +msgid "BaseTransition" +msgstr "Transition (abstraite)" + +msgid "BaseTransition_plural" +msgstr "Transitions (abstraites)" + msgid "Bookmark" msgstr "Signet" @@ -358,6 +363,9 @@ msgid "Interval_plural" msgstr "Durées" +msgid "New BaseTransition" +msgstr "XXX" + msgid "New Bookmark" msgstr "Nouveau signet" @@ -406,12 +414,21 @@ msgid "New State" msgstr "Nouvel état" +msgid "New SubWorkflowExitPoint" +msgstr "Nouvelle sortie de sous-workflow" + msgid "New TrInfo" msgstr "Nouvelle information de transition" msgid "New Transition" msgstr "Nouvelle transition" +msgid "New Workflow" +msgstr "Nouveau workflow" + +msgid "New WorkflowTransition" +msgstr "Nouvelle transition workflow" + msgid "No query has been executed" msgstr "Aucune requête n'a été éxécuté" @@ -479,6 +496,12 @@ msgid "String_plural" msgstr "Chaînes de caractères" +msgid "SubWorkflowExitPoint" +msgstr "Sortie de sous-workflow" + +msgid "SubWorkflowExitPoint_plural" +msgstr "Sorties de sous-workflow" + msgid "Subject:" msgstr "Sujet :" @@ -499,12 +522,8 @@ msgid "The view %s could not be found" msgstr "La vue %s est introuvable" -msgid "There is no workflow defined for this entity." -msgstr "Il n'y a pas de workflow défini pour ce type d'entité" - -#, python-format -msgid "This %s" -msgstr "Ce %s" +msgid "This BaseTransition" +msgstr "Cette transition abstraite" msgid "This Bookmark" msgstr "Ce signet" @@ -554,12 +573,21 @@ msgid "This State" msgstr "Cet état" +msgid "This SubWorkflowExitPoint" +msgstr "Cette sortie de sous-workflow" + msgid "This TrInfo" msgstr "Cette information de transition" msgid "This Transition" msgstr "Cette transition" +msgid "This Workflow" +msgstr "Ce workflow" + +msgid "This WorkflowTransition" +msgstr "Cette transition workflow" + msgid "Time" msgstr "Heure" @@ -591,9 +619,21 @@ msgid "What's new?" msgstr "Nouveautés" +msgid "Workflow" +msgstr "Workflow" + msgid "Workflow history" msgstr "Historique des changements d'état" +msgid "WorkflowTransition" +msgstr "Transition workflow" + +msgid "WorkflowTransition_plural" +msgstr "Transitions workflow" + +msgid "Workflow_plural" +msgstr "Workflows" + msgid "You are not connected to an instance !" msgstr "Vous n'êtes pas connecté à une instance" @@ -627,7 +667,8 @@ msgid "" "You have no access to this view or it can not be used to display the current " "data." -msgstr "Vous n'avez pas accès à cette vue ou elle ne peut pas afficher ces données." +msgstr "" +"Vous n'avez pas accès à cette vue ou elle ne peut pas afficher ces données." msgid "" "You're not authorized to access this page. If you think you should, please " @@ -640,9 +681,6 @@ msgid "[%s supervision] changes summary" msgstr "[%s supervision] description des changements" -msgid "__msg state changed" -msgstr "l'état a été changé" - msgid "" "a RQL expression which should return some results, else the transition won't " "be available. This query may use X and U variables that will respectivly " @@ -671,12 +709,12 @@ msgid "about this site" msgstr "à propos de ce site" +msgid "abstract base class for transitions" +msgstr "classe de base abstraite pour les transitions" + msgid "access type" msgstr "type d'accès" -msgid "account state" -msgstr "état du compte" - msgid "action(s) on this selection" msgstr "action(s) sur cette sélection" @@ -689,6 +727,12 @@ msgid "actions_addentity_description" msgstr "" +msgid "actions_addrelated" +msgstr "menu ajouter" + +msgid "actions_addrelated_description" +msgstr "" + msgid "actions_cancel" msgstr "annuler la sélection" @@ -867,82 +911,47 @@ msgstr "utilisateur" msgid "add CWUser use_email EmailAddress subject" -msgstr "ajouter une addresse email" +msgstr "addresse email" msgid "add State allowed_transition Transition object" -msgstr "ajouter un état en entrée" +msgstr "état en entrée" msgid "add State allowed_transition Transition subject" -msgstr "ajouter une transition en sortie" - -msgid "add State state_of CWEType object" -msgstr "ajouter un état" +msgstr "transition en sortie" + +msgid "add State allowed_transition WorkflowTransition subject" +msgstr "transition workflow en sortie" msgid "add Transition condition RQLExpression subject" -msgstr "ajouter une condition" +msgstr "condition" msgid "add Transition destination_state State object" -msgstr "ajouter une transition en entrée" +msgstr "transition en entrée" msgid "add Transition destination_state State subject" -msgstr "ajouter l'état de sortie" - -msgid "add Transition transition_of CWEType object" -msgstr "ajouter une transition" - -msgid "add a Bookmark" -msgstr "ajouter un signet" - -msgid "add a CWAttribute" -msgstr "ajouter un type de relation" - -msgid "add a CWCache" -msgstr "ajouter un cache applicatif" - -msgid "add a CWConstraint" -msgstr "ajouter une contrainte" - -msgid "add a CWConstraintType" -msgstr "ajouter un type de contrainte" - +msgstr "état de sortie" + +msgid "add WorkflowTransition condition RQLExpression subject" +msgstr "condition" + +msgid "add WorkflowTransition subworkflow_exit SubWorkflowExitPoint subject" +msgstr "sortie de sous-workflow" + +msgctxt "inlined:CWRelation.from_entity.subject" msgid "add a CWEType" -msgstr "ajouter un type d'entité" - -msgid "add a CWGroup" -msgstr "ajouter un groupe d'utilisateurs" - -msgid "add a CWPermission" -msgstr "ajouter une permission" - -msgid "add a CWProperty" -msgstr "ajouter une propriété" - +msgstr "ajouter un type d'entité sujet" + +msgctxt "inlined:CWRelation.to_entity.subject" +msgid "add a CWEType" +msgstr "ajouter un type d'entité objet" + +msgctxt "inlined:CWRelation.relation_type.subject" msgid "add a CWRType" msgstr "ajouter un type de relation" -msgid "add a CWRelation" -msgstr "ajouter une relation" - -msgid "add a CWUser" -msgstr "ajouter un utilisateur" - +msgctxt "inlined:CWUser.use_email.subject" msgid "add a EmailAddress" -msgstr "ajouter une adresse email" - -msgid "add a ExternalUri" -msgstr "ajouter une Uri externe" - -msgid "add a RQLExpression" -msgstr "ajouter une expression rql" - -msgid "add a State" -msgstr "ajouter un état" - -msgid "add a TrInfo" -msgstr "ajouter une information de transition" - -msgid "add a Transition" -msgstr "ajouter une transition" +msgstr "ajouter une adresse électronique" msgid "add a new permission" msgstr "ajouter une permission" @@ -956,7 +965,25 @@ # subject and object forms for each relation type # (no object form for final relation types) msgid "add_permission" -msgstr "permission d'ajouter" +msgstr "peut ajouter" + +# subject and object forms for each relation type +# (no object form for final relation types) +msgctxt "CWEType" +msgid "add_permission" +msgstr "permission d'ajout" + +msgctxt "CWRType" +msgid "add_permission" +msgstr "permission d'ajout" + +msgctxt "CWGroup" +msgid "add_permission_object" +msgstr "a la permission d'ajouter" + +msgctxt "RQLExpression" +msgid "add_permission_object" +msgstr "a la permission d'ajouter" msgid "add_permission_object" msgstr "a la permission d'ajouter" @@ -973,12 +1000,26 @@ "ajout de la relation %(rtype)s de %(frometype)s #%(fromeid)s vers %(toetype)" "s #%(toeid)s" +msgid "addrelated" +msgstr "ajouter" + +msgid "address" +msgstr "adresse électronique" + +msgctxt "EmailAddress" msgid "address" msgstr "adresse électronique" msgid "alias" msgstr "alias" +msgctxt "EmailAddress" +msgid "alias" +msgstr "alias" + +msgid "allow to set a specific workflow for an entity" +msgstr "permet de spécifier un workflow donné pour une entité" + msgid "allowed transition from this state" msgstr "transition autorisée depuis cet état" @@ -988,6 +1029,22 @@ msgid "allowed_transition" msgstr "transition autorisée" +msgctxt "State" +msgid "allowed_transition" +msgstr "transitions autorisées" + +msgctxt "BaseTransition" +msgid "allowed_transition_object" +msgstr "transition autorisée de" + +msgctxt "Transition" +msgid "allowed_transition_object" +msgstr "transition autorisée de" + +msgctxt "WorkflowTransition" +msgid "allowed_transition_object" +msgstr "transition autorisée de" + msgid "allowed_transition_object" msgstr "états en entrée" @@ -1069,6 +1126,14 @@ msgid "bookmarked_by" msgstr "utilisé par" +msgctxt "Bookmark" +msgid "bookmarked_by" +msgstr "utilisé par" + +msgctxt "CWUser" +msgid "bookmarked_by_object" +msgstr "utilise le(s) signet(s)" + msgid "bookmarked_by_object" msgstr "a pour signets" @@ -1155,6 +1220,28 @@ msgid "by relation" msgstr "via la relation" +msgid "by_transition" +msgstr "transition" + +msgctxt "TrInfo" +msgid "by_transition" +msgstr "transition" + +msgctxt "BaseTransition" +msgid "by_transition_object" +msgstr "a pour information" + +msgctxt "Transition" +msgid "by_transition_object" +msgstr "a pour information" + +msgctxt "WorkflowTransition" +msgid "by_transition_object" +msgstr "a pour information" + +msgid "by_transition_object" +msgstr "changement d'états" + msgid "calendar" msgstr "afficher un calendrier" @@ -1185,6 +1272,9 @@ msgid "can't display data, unexpected error: %s" msgstr "impossible d'afficher les données à cause de l'erreur suivante: %s" +msgid "can't have multiple exits on the same state" +msgstr "ne peut avoir plusieurs sorties sur le même état" + #, python-format msgid "" "can't set inlined=%(inlined)s, %(stype)s %(rtype)s %(otype)s has cardinality=" @@ -1199,9 +1289,14 @@ msgid "cancel this insert" msgstr "annuler cette insertion" -msgid "canonical" -msgstr "canonique" - +msgid "cardinality" +msgstr "cardinalité" + +msgctxt "CWAttribute" +msgid "cardinality" +msgstr "cardinalité" + +msgctxt "CWRelation" msgid "cardinality" msgstr "cardinalité" @@ -1227,9 +1322,14 @@ msgid "comment" msgstr "commentaire" -msgid "comment:" -msgstr "commentaire :" - +msgctxt "TrInfo" +msgid "comment" +msgstr "commentaire" + +msgid "comment_format" +msgstr "format" + +msgctxt "TrInfo" msgid "comment_format" msgstr "format" @@ -1284,6 +1384,12 @@ "composant permettant de présenter sur plusieurs pages les requêtes renvoyant " "plus d'un certain nombre de résultat" +msgid "components_pdfview" +msgstr "icône pdf" + +msgid "components_pdfview_description" +msgstr "l'icône pdf pour obtenir la page courant au format PDF" + msgid "components_rqlinput" msgstr "barre rql" @@ -1293,12 +1399,32 @@ msgid "composite" msgstr "composite" +msgctxt "CWRelation" +msgid "composite" +msgstr "composite" + +msgid "condition" +msgstr "condition" + +msgctxt "BaseTransition" +msgid "condition" +msgstr "condition" + +msgctxt "Transition" +msgid "condition" +msgstr "condition" + +msgctxt "WorkflowTransition" msgid "condition" msgstr "condition" msgid "condition:" msgstr "condition :" +msgctxt "RQLExpression" +msgid "condition_object" +msgstr "condition de" + msgid "condition_object" msgstr "condition de" @@ -1308,6 +1434,18 @@ msgid "constrained_by" msgstr "contraint par" +msgctxt "CWAttribute" +msgid "constrained_by" +msgstr "contraint par" + +msgctxt "CWRelation" +msgid "constrained_by" +msgstr "contraint par" + +msgctxt "CWConstraint" +msgid "constrained_by_object" +msgstr "contrainte de" + msgid "constrained_by_object" msgstr "contrainte de" @@ -1440,6 +1578,10 @@ msgid "created_by" msgstr "créé par" +msgctxt "CWUser" +msgid "created_by_object" +msgstr "a créé" + msgid "created_by_object" msgstr "a créé" @@ -1508,23 +1650,32 @@ msgid "creating RQLExpression (Transition %(linkto)s condition RQLExpression)" msgstr "création d'une expression RQL pour la transition %(linkto)s" +msgid "" +"creating RQLExpression (WorkflowTransition %(linkto)s condition " +"RQLExpression)" +msgstr "création d'une expression RQL pour la transition workflow %(linkto)s" + msgid "creating State (State allowed_transition Transition %(linkto)s)" msgstr "création d'un état pouvant aller vers la transition %(linkto)s" -msgid "creating State (State state_of CWEType %(linkto)s)" -msgstr "création d'un état pour le type %(linkto)s" - msgid "creating State (Transition %(linkto)s destination_state State)" msgstr "création d'un état destination de la transition %(linkto)s" +msgid "" +"creating SubWorkflowExitPoint (WorkflowTransition %(linkto)s " +"subworkflow_exit SubWorkflowExitPoint)" +msgstr "création d'un point de sortie de la transition workflow %(linkto)s" + msgid "creating Transition (State %(linkto)s allowed_transition Transition)" msgstr "création d'une transition autorisée depuis l'état %(linkto)s" msgid "creating Transition (Transition destination_state State %(linkto)s)" msgstr "création d'une transition vers l'état %(linkto)s" -msgid "creating Transition (Transition transition_of CWEType %(linkto)s)" -msgstr "création d'une transition pour le type %(linkto)s" +msgid "" +"creating WorkflowTransition (State %(linkto)s allowed_transition " +"WorkflowTransition)" +msgstr "création d'une transition workflow autorisée depuis l'état %(linkto)s" msgid "creation" msgstr "création" @@ -1538,6 +1689,14 @@ msgid "cstrtype" msgstr "type de constrainte" +msgctxt "CWConstraint" +msgid "cstrtype" +msgstr "type" + +msgctxt "CWConstraintType" +msgid "cstrtype_object" +msgstr "type des contraintes" + msgid "cstrtype_object" msgstr "utilisé par" @@ -1551,6 +1710,20 @@ msgid "currently attached file: %s" msgstr "fichie actuellement attaché %s" +msgid "custom_workflow" +msgstr "workflow spécifique" + +msgctxt "CWUser" +msgid "custom_workflow" +msgstr "workflow spécifique" + +msgctxt "Workflow" +msgid "custom_workflow_object" +msgstr "workflow spécifique de" + +msgid "custom_workflow_object" +msgstr "workflow de" + msgid "cwetype-schema-image" msgstr "schéma" @@ -1587,6 +1760,30 @@ msgid "default text format for rich text fields." msgstr "format de texte par défaut pour les champs textes" +msgid "default user workflow" +msgstr "workflow par défaut des utilisateurs" + +msgid "default workflow for an entity type" +msgstr "workflow par défaut pour un type d'entité" + +msgid "default_workflow" +msgstr "workflow par défaut" + +msgctxt "CWEType" +msgid "default_workflow" +msgstr "workflow par défaut" + +msgctxt "Workflow" +msgid "default_workflow_object" +msgstr "workflow par défaut de" + +msgid "default_workflow_object" +msgstr "workflow par défaut de" + +msgid "defaultval" +msgstr "valeur par défaut" + +msgctxt "CWAttribute" msgid "defaultval" msgstr "valeur par défaut" @@ -1626,6 +1823,9 @@ msgid "define an entity type, used to build the instance schema" msgstr "définit un type d'entité" +msgid "define how we get out from a sub-workflow" +msgstr "définit comment sortir d'un sous-workflow" + msgid "" "defines what's the property is applied for. You must select this first to be " "able to set value" @@ -1651,6 +1851,22 @@ msgid "delete_permission" msgstr "permission de supprimer" +msgctxt "CWEType" +msgid "delete_permission" +msgstr "permission de supprimer" + +msgctxt "CWRType" +msgid "delete_permission" +msgstr "permission de supprimer" + +msgctxt "CWGroup" +msgid "delete_permission_object" +msgstr "peut supprimer" + +msgctxt "RQLExpression" +msgid "delete_permission_object" +msgstr "peut supprimer" + msgid "delete_permission_object" msgstr "a la permission de supprimer" @@ -1672,9 +1888,84 @@ msgid "description" msgstr "description" +msgctxt "CWEType" +msgid "description" +msgstr "description" + +msgctxt "CWRelation" +msgid "description" +msgstr "description" + +msgctxt "Workflow" +msgid "description" +msgstr "description" + +msgctxt "CWAttribute" +msgid "description" +msgstr "description" + +msgctxt "Transition" +msgid "description" +msgstr "description" + +msgctxt "WorkflowTransition" +msgid "description" +msgstr "description" + +msgctxt "State" +msgid "description" +msgstr "description" + +msgctxt "CWRType" +msgid "description" +msgstr "description" + +msgctxt "BaseTransition" +msgid "description" +msgstr "description" + msgid "description_format" msgstr "format" +msgctxt "CWEType" +msgid "description_format" +msgstr "format" + +msgctxt "CWRelation" +msgid "description_format" +msgstr "format" + +msgctxt "Workflow" +msgid "description_format" +msgstr "format" + +msgctxt "CWAttribute" +msgid "description_format" +msgstr "format" + +msgctxt "Transition" +msgid "description_format" +msgstr "format" + +msgctxt "WorkflowTransition" +msgid "description_format" +msgstr "format" + +msgctxt "State" +msgid "description_format" +msgstr "format" + +msgctxt "CWRType" +msgid "description_format" +msgstr "format" + +msgctxt "BaseTransition" +msgid "description_format" +msgstr "format" + +msgid "destination state" +msgstr "état de destination" + msgid "destination state for this transition" msgstr "états accessibles par cette transition" @@ -1684,6 +1975,18 @@ msgid "destination_state" msgstr "état de destination" +msgctxt "Transition" +msgid "destination_state" +msgstr "état de destination" + +msgctxt "SubWorkflowExitPoint" +msgid "destination_state" +msgstr "état de destination" + +msgctxt "State" +msgid "destination_state_object" +msgstr "état final de" + msgid "destination_state_object" msgstr "destination de" @@ -1712,6 +2015,9 @@ msgid "display the component or not" msgstr "afficher le composant ou non" +msgid "display the pdf icon or not" +msgstr "afficher l'icône pdf ou non" + msgid "" "distinct label to distinguate between other permission entity of the same " "name" @@ -1729,6 +2035,9 @@ msgid "download icon" msgstr "icône de téléchargement" +msgid "download page as pdf" +msgstr "télécharger la page au format PDF" + msgid "download schema as owl" msgstr "télécharger le schéma OWL" @@ -1786,6 +2095,9 @@ msgid "entity edited" msgstr "entité éditée" +msgid "entity has no workflow set" +msgstr "l'entité n'a pas de workflow" + msgid "entity linked" msgstr "entité liée" @@ -1798,11 +2110,8 @@ msgstr "" "type d'entité à utiliser pour définir une configuration de sécurité avancée" -msgid "entity types which may use this state" -msgstr "type d'entités opuvant utiliser cet état" - -msgid "entity types which may use this transition" -msgstr "entités qui peuvent utiliser cette transition" +msgid "entity types which may use this workflow" +msgstr "types d'entité pouvant utiliser ce workflow" msgid "error while embedding page" msgstr "erreur pendant l'inclusion de la page" @@ -1824,15 +2133,33 @@ msgid "eta_date" msgstr "date de fin" +msgid "exit_point" +msgstr "état de sortie" + +msgid "exit_point_object" +msgstr "état de sortie de" + +#, python-format +msgid "exiting from subworkflow %s" +msgstr "sortie du sous-workflow %s" + msgid "expected:" msgstr "attendu :" msgid "expression" msgstr "expression" +msgctxt "RQLExpression" +msgid "expression" +msgstr "rql de l'expression" + msgid "exprtype" msgstr "type de l'expression" +msgctxt "RQLExpression" +msgid "exprtype" +msgstr "type" + msgid "external page" msgstr "page externe" @@ -1884,6 +2211,18 @@ msgid "final" msgstr "final" +msgctxt "CWEType" +msgid "final" +msgstr "final" + +msgctxt "CWRType" +msgid "final" +msgstr "final" + +msgid "firstname" +msgstr "prénom" + +msgctxt "CWUser" msgid "firstname" msgstr "prénom" @@ -1896,6 +2235,14 @@ msgid "for_user" msgstr "pour l'utilisateur" +msgctxt "CWProperty" +msgid "for_user" +msgstr "propriété de l'utilisateur" + +msgctxt "CWUser" +msgid "for_user_object" +msgstr "a pour préférence" + msgid "for_user_object" msgstr "utilise les propriétés" @@ -1912,6 +2259,18 @@ msgid "from_entity" msgstr "de l'entité" +msgctxt "CWAttribute" +msgid "from_entity" +msgstr "attribut de l'entité" + +msgctxt "CWRelation" +msgid "from_entity" +msgstr "relation de l'entité" + +msgctxt "CWEType" +msgid "from_entity_object" +msgstr "entité de" + msgid "from_entity_object" msgstr "relation sujet" @@ -1921,6 +2280,14 @@ msgid "from_state" msgstr "de l'état" +msgctxt "TrInfo" +msgid "from_state" +msgstr "état de départ" + +msgctxt "State" +msgid "from_state_object" +msgstr "état de départ de" + msgid "from_state_object" msgstr "transitions depuis cet état" @@ -1930,9 +2297,17 @@ msgid "fulltext_container" msgstr "conteneur du texte indexé" +msgctxt "CWRType" +msgid "fulltext_container" +msgstr "objet à indexer" + msgid "fulltextindexed" msgstr "indexation du texte" +msgctxt "CWAttribute" +msgid "fulltextindexed" +msgstr "texte indexé" + msgid "generic plot" msgstr "tracé de courbes standard" @@ -1951,6 +2326,10 @@ msgid "granted to groups" msgstr "accordée aux groupes" +#, python-format +msgid "graphical representation of %s" +msgstr "représentation graphique de %s" + msgid "graphical representation of the instance'schema" msgstr "représentation graphique du schéma de l'instance" @@ -2046,12 +2425,103 @@ msgid "id of main template used to render pages" msgstr "id du template principal" +msgid "identical to" +msgstr "identique à" + msgid "identical_to" msgstr "identique à" msgid "identity" msgstr "est identique à" +msgctxt "CWRelation" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "Bookmark" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWAttribute" +msgid "identity_object" +msgstr "est indentique à" + +msgctxt "CWConstraintType" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "State" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "BaseTransition" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWEType" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "Workflow" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWGroup" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "TrInfo" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWConstraint" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWUser" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "Transition" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWRType" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "SubWorkflowExitPoint" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "ExternalUri" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWCache" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "WorkflowTransition" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "RQLExpression" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWPermission" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "EmailAddress" +msgid "identity_object" +msgstr "est identique à" + +msgctxt "CWProperty" +msgid "identity_object" +msgstr "est identique à" + msgid "identity_object" msgstr "est identique à" @@ -2074,12 +2544,28 @@ msgid "in_group" msgstr "dans le groupe" +msgctxt "CWUser" +msgid "in_group" +msgstr "fait partie du groupe" + +msgctxt "CWGroup" +msgid "in_group_object" +msgstr "contient les utilisateurs" + msgid "in_group_object" msgstr "membres" msgid "in_state" msgstr "état" +msgctxt "CWUser" +msgid "in_state" +msgstr "dans l'état" + +msgctxt "State" +msgid "in_state_object" +msgstr "état des entités" + msgid "in_state_object" msgstr "état de" @@ -2099,6 +2585,10 @@ msgid "indexed" msgstr "index" +msgctxt "CWAttribute" +msgid "indexed" +msgstr "indexé" + msgid "indicate the current state of an entity" msgstr "indique l'état courant d'une entité" @@ -2115,15 +2605,27 @@ msgid "initial estimation %s" msgstr "estimation initiale %s" -msgid "initial state for entities of this type" -msgstr "état initial pour les entités de ce type" +msgid "initial state for this workflow" +msgstr "état initial pour ce workflow" msgid "initial_state" msgstr "état initial" +msgctxt "Workflow" +msgid "initial_state" +msgstr "état initial" + +msgctxt "State" msgid "initial_state_object" msgstr "état initial de" +msgid "initial_state_object" +msgstr "état initial de" + +msgid "inlined" +msgstr "mise en ligne" + +msgctxt "CWRType" msgid "inlined" msgstr "mise en ligne" @@ -2136,6 +2638,10 @@ msgid "internationalizable" msgstr "internationalisable" +msgctxt "CWAttribute" +msgid "internationalizable" +msgstr "internationalisable" + #, python-format msgid "invalid action %r" msgstr "action %r invalide" @@ -2176,7 +2682,15 @@ msgid "is_instance_of" msgstr "est une instance de" +msgctxt "CWEType" msgid "is_instance_of_object" +msgstr "type de l'entité" + +msgid "is_instance_of_object" +msgstr "type de" + +msgctxt "CWEType" +msgid "is_object" msgstr "type de" msgid "is_object" @@ -2194,6 +2708,10 @@ msgid "label" msgstr "libellé" +msgctxt "CWPermission" +msgid "label" +msgstr "libellé" + msgid "language of the user interface" msgstr "langue pour l'interface utilisateur" @@ -2203,6 +2721,10 @@ msgid "last_login_time" msgstr "dernière date de connexion" +msgctxt "CWUser" +msgid "last_login_time" +msgstr "dernière date de connexion" + msgid "latest modification time of an entity" msgstr "date de dernière modification d'une entité" @@ -2236,14 +2758,17 @@ msgid "link a relation definition to its subject entity type" msgstr "lie une définition de relation à son type d'entité sujet" -msgid "link a state to one or more entity type" -msgstr "lier un état à une ou plusieurs entités" +msgid "link a state to one or more workflow" +msgstr "lie un état à un ou plusieurs workflow" msgid "link a transition information to its object" msgstr "lié une enregistrement de transition vers l'objet associé" -msgid "link a transition to one or more entity type" -msgstr "lie une transition à un ou plusieurs types d'entités" +msgid "link a transition to one or more workflow" +msgstr "lie une transition à un ou plusieurs workflow" + +msgid "link a workflow to one or more entity type" +msgstr "lie un workflow à un ou plusieurs types d'entité" msgid "link to each item in" msgstr "lier vers chaque élément dans" @@ -2260,6 +2785,10 @@ msgid "login" msgstr "identifiant" +msgctxt "CWUser" +msgid "login" +msgstr "identifiant" + msgid "login or email" msgstr "identifiant ou email" @@ -2279,6 +2808,10 @@ msgid "mainvars" msgstr "variables principales" +msgctxt "RQLExpression" +msgid "mainvars" +msgstr "variables principales" + msgid "manage" msgstr "gestion du site" @@ -2294,6 +2827,9 @@ msgid "managers" msgstr "administrateurs" +msgid "mandatory relation" +msgstr "relation obligatoire" + msgid "march" msgstr "mars" @@ -2340,6 +2876,50 @@ msgid "name" msgstr "nom" +msgctxt "CWEType" +msgid "name" +msgstr "nom" + +msgctxt "Transition" +msgid "name" +msgstr "nom" + +msgctxt "Workflow" +msgid "name" +msgstr "nom" + +msgctxt "CWGroup" +msgid "name" +msgstr "nom" + +msgctxt "CWConstraintType" +msgid "name" +msgstr "nom" + +msgctxt "WorkflowTransition" +msgid "name" +msgstr "nom" + +msgctxt "State" +msgid "name" +msgstr "nom" + +msgctxt "CWPermission" +msgid "name" +msgstr "nom" + +msgctxt "CWRType" +msgid "name" +msgstr "nom" + +msgctxt "BaseTransition" +msgid "name" +msgstr "nom" + +msgctxt "CWCache" +msgid "name" +msgstr "nom" + msgid "name of the cache" msgstr "nom du cache applicatif" @@ -2447,6 +3027,14 @@ msgid "ordernum" msgstr "ordre" +msgctxt "CWAttribute" +msgid "ordernum" +msgstr "numéro d'ordre" + +msgctxt "CWRelation" +msgid "ordernum" +msgstr "numéro d'ordre" + msgid "owl" msgstr "owl" @@ -2456,6 +3044,10 @@ msgid "owned_by" msgstr "appartient à" +msgctxt "CWUser" +msgid "owned_by_object" +msgstr "propriétaire de" + msgid "owned_by_object" msgstr "possède" @@ -2482,6 +3074,10 @@ msgid "path" msgstr "chemin" +msgctxt "Bookmark" +msgid "path" +msgstr "chemin" + msgid "permission" msgstr "permission" @@ -2503,6 +3099,10 @@ msgid "pkey" msgstr "clé" +msgctxt "CWProperty" +msgid "pkey" +msgstr "code de la propriété" + msgid "please correct errors below" msgstr "veuillez corriger les erreurs ci-dessous" @@ -2513,7 +3113,21 @@ msgstr "vues possibles" msgid "powered by CubicWeb" -msgstr "" +msgstr "utilise la technologie CubicWeb" + +msgid "prefered_form" +msgstr "forme préférée" + +msgctxt "EmailAddress" +msgid "prefered_form" +msgstr "forme préférée" + +msgctxt "EmailAddress" +msgid "prefered_form_object" +msgstr "forme préférée de" + +msgid "prefered_form_object" +msgstr "forme préférée à" msgid "preferences" msgstr "préférences" @@ -2527,6 +3141,14 @@ msgid "primary_email" msgstr "adresse email principale" +msgctxt "CWUser" +msgid "primary_email" +msgstr "email principal" + +msgctxt "EmailAddress" +msgid "primary_email_object" +msgstr "adresse principale de" + msgid "primary_email_object" msgstr "adresse email principale (object)" @@ -2548,12 +3170,34 @@ msgid "read_permission" msgstr "permission de lire" +msgctxt "CWEType" +msgid "read_permission" +msgstr "permission d'ajouter" + +msgctxt "CWRType" +msgid "read_permission" +msgstr "permission d'ajouter" + +msgctxt "CWGroup" +msgid "read_permission_object" +msgstr "peut lire" + +msgctxt "RQLExpression" +msgid "read_permission_object" +msgstr "peut lire" + msgid "read_permission_object" msgstr "a la permission de lire" msgid "registry" msgstr "registre" +msgid "related entity has no state" +msgstr "l'entité lié n'a pas d'état" + +msgid "related entity has no workflow set" +msgstr "l'entité lié n'a pas de workflow" + #, python-format msgid "relation %(relname)s of %(ent)s" msgstr "relation %(relname)s de %(ent)s" @@ -2561,6 +3205,18 @@ msgid "relation_type" msgstr "type de relation" +msgctxt "CWAttribute" +msgid "relation_type" +msgstr "type de relation" + +msgctxt "CWRelation" +msgid "relation_type" +msgstr "type de relation" + +msgctxt "CWRType" +msgid "relation_type_object" +msgstr "définition" + msgid "relation_type_object" msgstr "définition" @@ -2573,63 +3229,45 @@ msgid "relative url of the bookmarked page" msgstr "url relative de la page" -msgid "remove this Bookmark" -msgstr "supprimer ce signet" - -msgid "remove this CWAttribute" -msgstr "supprimer cet attribut" - -msgid "remove this CWCache" -msgstr "supprimer ce cache applicatif" - -msgid "remove this CWConstraint" -msgstr "supprimer cette contrainte" - -msgid "remove this CWConstraintType" -msgstr "supprimer ce type de contrainte" - +msgctxt "inlined:CWRelation:from_entity:subject" +msgid "remove this CWEType" +msgstr "supprimer ce type d'entité" + +msgctxt "inlined:CWRelation:to_entity:subject" msgid "remove this CWEType" msgstr "supprimer ce type d'entité" -msgid "remove this CWGroup" -msgstr "supprimer ce groupe" - -msgid "remove this CWPermission" -msgstr "supprimer cette permission" - -msgid "remove this CWProperty" -msgstr "supprimer cette propriété" - +msgctxt "inlined:CWRelation:relation_type:subject" msgid "remove this CWRType" -msgstr "supprimer cette définition de relation" - -msgid "remove this CWRelation" -msgstr "supprimer cette relation" - -msgid "remove this CWUser" -msgstr "supprimer cet utilisateur" - +msgstr "supprimer ce type de relation" + +msgctxt "inlined:CWUser:use_email:subject" msgid "remove this EmailAddress" -msgstr "supprimer cette adresse email" - -msgid "remove this ExternalUri" -msgstr "supprimer cette Uri externe" - -msgid "remove this RQLExpression" -msgstr "supprimer cette expression rql" - -msgid "remove this State" -msgstr "supprimer cet état" - -msgid "remove this TrInfo" -msgstr "retirer cette information de transition" - -msgid "remove this Transition" -msgstr "supprimer cette transition" +msgstr "supprimer cette adresse électronique" msgid "require_group" msgstr "nécessite le groupe" +msgctxt "BaseTransition" +msgid "require_group" +msgstr "restreinte au groupe" + +msgctxt "Transition" +msgid "require_group" +msgstr "restreinte au groupe" + +msgctxt "CWPermission" +msgid "require_group" +msgstr "restreinte au groupe" + +msgctxt "WorkflowTransition" +msgid "require_group" +msgstr "restreinte au groupe" + +msgctxt "CWGroup" +msgid "require_group_object" +msgstr "dé" + msgid "require_group_object" msgstr "à les droits" @@ -2763,6 +3401,9 @@ msgid "semantic description of this transition" msgstr "description sémantique de cette transition" +msgid "semantic description of this workflow" +msgstr "description sémantique de ce workflow" + msgid "send email" msgstr "envoyer un courriel" @@ -2818,9 +3459,20 @@ msgid "sparql xml" msgstr "XML Sparql" +msgid "special transition allowing to go through a sub-workflow" +msgstr "transition spécial permettant d'aller dans un sous-workfow" + msgid "specializes" msgstr "dérive de" +msgctxt "CWEType" +msgid "specializes" +msgstr "spécialise" + +msgctxt "CWEType" +msgid "specializes_object" +msgstr "parent de" + msgid "specializes_object" msgstr "parent de" @@ -2830,9 +3482,30 @@ msgid "state" msgstr "état" +msgid "state doesn't belong to entity's current workflow" +msgstr "l'état n'appartient pas au workflow courant de l'entité" + +msgid "state doesn't belong to entity's workflow" +msgstr "l'état n'appartient pas au workflow de l'entité" + +msgid "" +"state doesn't belong to entity's workflow. You may want to set a custom " +"workflow for this entity first." +msgstr "" +"l'état n'appartient pas au workflow courant de l'entité. Vous désirez peut-" +"être spécifier que cette entité doit utiliser ce workflow." + msgid "state_of" msgstr "état de" +msgctxt "State" +msgid "state_of" +msgstr "état de" + +msgctxt "Workflow" +msgid "state_of_object" +msgstr "contient les états" + msgid "state_of_object" msgstr "a pour état" @@ -2855,12 +3528,65 @@ msgid "subject_plural:" msgstr "sujets :" +msgid "subworkflow" +msgstr "sous-workflow" + +msgctxt "WorkflowTransition" +msgid "subworkflow" +msgstr "sous-workflow" + +msgid "subworkflow state" +msgstr "état de sous-workflow" + +msgid "subworkflow_exit" +msgstr "sortie de sous-workflow" + +msgctxt "WorkflowTransition" +msgid "subworkflow_exit" +msgstr "sortie du sous-workflow" + +msgctxt "SubWorkflowExitPoint" +msgid "subworkflow_exit_object" +msgstr "états de sortie" + +msgid "subworkflow_exit_object" +msgstr "états de sortie" + +msgctxt "Workflow" +msgid "subworkflow_object" +msgstr "" + +msgid "subworkflow_object" +msgstr "utilisé par la transition" + +msgid "subworkflow_state" +msgstr "état du sous-workflow" + +msgctxt "SubWorkflowExitPoint" +msgid "subworkflow_state" +msgstr "état" + +msgctxt "State" +msgid "subworkflow_state_object" +msgstr "" + +msgid "subworkflow_state_object" +msgstr "état de sortie de" + msgid "sunday" msgstr "dimanche" msgid "surname" msgstr "nom" +msgctxt "CWUser" +msgid "surname" +msgstr "nom de famille" + +msgid "symetric" +msgstr "symétrique" + +msgctxt "CWRType" msgid "symetric" msgstr "symétrique" @@ -2920,6 +3646,10 @@ msgid "timestamp" msgstr "date" +msgctxt "CWCache" +msgid "timestamp" +msgstr "valide depuis" + msgid "timestamp of the latest source synchronization." msgstr "date de la dernière synchronisation avec la source." @@ -2929,6 +3659,10 @@ msgid "title" msgstr "titre" +msgctxt "Bookmark" +msgid "title" +msgstr "libellé" + msgid "to" msgstr "à" @@ -2942,6 +3676,18 @@ msgid "to_entity" msgstr "vers l'entité" +msgctxt "CWAttribute" +msgid "to_entity" +msgstr "pour l'entité" + +msgctxt "CWRelation" +msgid "to_entity" +msgstr "pour l'entité" + +msgctxt "CWEType" +msgid "to_entity_object" +msgstr "relation objet" + msgid "to_entity_object" msgstr "relation objet" @@ -2951,8 +3697,16 @@ msgid "to_state" msgstr "vers l'état" +msgctxt "TrInfo" +msgid "to_state" +msgstr "état de destination" + +msgctxt "State" msgid "to_state_object" -msgstr "transitions vers cette état" +msgstr "transition vers cet état" + +msgid "to_state_object" +msgstr "transitions vers cet état" msgid "todo_by" msgstr "à faire par" @@ -2960,13 +3714,34 @@ msgid "toggle check boxes" msgstr "inverser les cases à cocher" -#, python-format -msgid "transition from %s to %s does not exist or is not allowed" -msgstr "la transition de %s à %s n'existe pas ou n'est pas permise" +msgid "transition doesn't belong to entity's workflow" +msgstr "la transition n'appartient pas au workflow de l'entité" + +msgid "transition isn't allowed" +msgstr "la transition n'est pas autorisée" + +msgid "transition may not be fired" +msgstr "la transition ne peut-être déclenchée" msgid "transition_of" msgstr "transition de" +msgctxt "BaseTransition" +msgid "transition_of" +msgstr "transition de" + +msgctxt "Transition" +msgid "transition_of" +msgstr "transition de" + +msgctxt "WorkflowTransition" +msgid "transition_of" +msgstr "transition de" + +msgctxt "Workflow" +msgid "transition_of_object" +msgstr "a pour transition" + msgid "transition_of_object" msgstr "a pour transition" @@ -3039,6 +3814,10 @@ msgid "upassword" msgstr "mot de passe" +msgctxt "CWUser" +msgid "upassword" +msgstr "mot de passe" + msgid "update" msgstr "modification" @@ -3048,6 +3827,18 @@ msgid "update_permission" msgstr "permission de modification" +msgctxt "CWEType" +msgid "update_permission" +msgstr "permission de modifier" + +msgctxt "CWGroup" +msgid "update_permission_object" +msgstr "peut modifier" + +msgctxt "RQLExpression" +msgid "update_permission_object" +msgstr "peut modifier" + msgid "update_permission_object" msgstr "à la permission de modifier" @@ -3058,6 +3849,10 @@ msgid "uri" msgstr "uri" +msgctxt "ExternalUri" +msgid "uri" +msgstr "uri" + msgid "use template languages" msgstr "utiliser les langages de template" @@ -3071,6 +3866,14 @@ msgid "use_email" msgstr "adresse électronique" +msgctxt "CWUser" +msgid "use_email" +msgstr "utilise l'adresse électronique" + +msgctxt "EmailAddress" +msgid "use_email_object" +msgstr "utilisée par" + msgid "use_email_object" msgstr "adresse utilisée par" @@ -3127,6 +3930,14 @@ msgid "value" msgstr "valeur" +msgctxt "CWConstraint" +msgid "value" +msgstr "contrainte" + +msgctxt "CWProperty" +msgid "value" +msgstr "valeur" + msgid "value associated to this key is not editable manually" msgstr "la valeur associée à cette clé n'est pas éditable manuellement" @@ -3174,21 +3985,55 @@ msgid "wf_info_for" msgstr "historique de" +msgctxt "TrInfo" +msgid "wf_info_for" +msgstr "information de transition" + +msgctxt "CWUser" +msgid "wf_info_for_object" +msgstr "historique des transitions" + msgid "wf_info_for_object" msgstr "historique des transitions" msgid "" "when multiple addresses are equivalent (such as python-projects@logilab.org " -"and python-projects@lists.logilab.org), set this to true on one of them " -"which is the preferred form." +"and python-projects@lists.logilab.org), set this to indicate which is the " +"preferred form." msgstr "" -"quand plusieurs adresses sont équivalentes (comme python-projects@logilab." -"org et python-projects@lists.logilab.org), mettez cette propriété à vrai sur " -"l'une d'entre-elle qui sera la forme canonique" + +msgid "workflow" +msgstr "workflow" #, python-format -msgid "workflow for %s" -msgstr "workflow pour %s" +msgid "workflow changed to \"%s\"" +msgstr "workflow changé à \"%s\"" + +msgid "workflow has no initial state" +msgstr "le workflow n'a pas d'état initial" + +msgid "workflow history item" +msgstr "entrée de l'historique de workflow" + +msgid "workflow to which this state belongs" +msgstr "workflow auquel cet état appartient" + +msgid "workflow to which this transition belongs" +msgstr "workflow auquel cette transition appartient" + +msgid "workflow_of" +msgstr "workflow de" + +msgctxt "Workflow" +msgid "workflow_of" +msgstr "workflow de" + +msgctxt "CWEType" +msgid "workflow_of_object" +msgstr "a pour workflow" + +msgid "workflow_of_object" +msgstr "a pour workflow" msgid "xbel" msgstr "xbel" @@ -3208,10 +4053,180 @@ msgid "you should probably delete that property" msgstr "vous devriez probablement supprimer cette propriété" -#~ msgid "" -#~ "You have no access to this view or it's not applyable to current data" -#~ msgstr "" -#~ "Vous n'avez pas accès à cette vue ou elle ne s'applique pas aux données" - -#~ msgid "download image" -#~ msgstr "image de téléchargement" +#~ msgid "This %s" +#~ msgstr "Ce %s" + +#~ msgid "add a BaseTransition" +#~ msgstr "XXX" + +#~ msgid "add a Bookmark" +#~ msgstr "ajouter un signet" + +#~ msgid "add a CWAttribute" +#~ msgstr "ajouter un type de relation" + +#~ msgid "add a CWCache" +#~ msgstr "ajouter un cache applicatif" + +#~ msgid "add a CWConstraint" +#~ msgstr "ajouter une contrainte" + +#~ msgid "add a CWConstraintType" +#~ msgstr "ajouter un type de contrainte" + +#~ msgid "add a CWEType" +#~ msgstr "ajouter un type d'entité" + +#~ msgid "add a CWGroup" +#~ msgstr "ajouter un groupe d'utilisateurs" + +#~ msgid "add a CWPermission" +#~ msgstr "ajouter une permission" + +#~ msgid "add a CWProperty" +#~ msgstr "ajouter une propriété" + +#~ msgid "add a CWRType" +#~ msgstr "ajouter un type de relation" + +#~ msgid "add a CWRelation" +#~ msgstr "ajouter une relation" + +#~ msgid "add a CWUser" +#~ msgstr "ajouter un utilisateur" + +#~ msgid "add a EmailAddress" +#~ msgstr "ajouter une adresse email" + +#~ msgid "add a ExternalUri" +#~ msgstr "ajouter une Uri externe" + +#~ msgid "add a RQLExpression" +#~ msgstr "ajouter une expression rql" + +#~ msgid "add a State" +#~ msgstr "ajouter un état" + +#~ msgid "add a SubWorkflowExitPoint" +#~ msgstr "ajouter une sortie de sous-workflow" + +#~ msgid "add a TrInfo" +#~ msgstr "ajouter une information de transition" + +#~ msgid "add a Transition" +#~ msgstr "ajouter une transition" + +#~ msgid "add a Workflow" +#~ msgstr "ajouter un workflow" + +#~ msgid "add a WorkflowTransition" +#~ msgstr "ajouter une transition workflow" + +#~ msgctxt "CWRelation" +#~ msgid "created_by" +#~ msgstr "créée par" + +#~ msgctxt "Bookmark" +#~ msgid "created_by" +#~ msgstr "créé par" + +#~ msgctxt "CWAttribute" +#~ msgid "created_by" +#~ msgstr "créé par" + +#~ msgctxt "CWConstraintType" +#~ msgid "created_by" +#~ msgstr "créé par" + +#~ msgctxt "RQLExpression" +#~ msgid "created_by" +#~ msgstr "créée par" + +#~ msgctxt "BaseTransition" +#~ msgid "created_by" +#~ msgstr "créée par" + +#~ msgctxt "CWEType" +#~ msgid "created_by" +#~ msgstr "créé par" + +#~ msgctxt "Workflow" +#~ msgid "created_by" +#~ msgstr "créé par" + +#~ msgctxt "CWGroup" +#~ msgid "created_by" +#~ msgstr "créé par" + +#~ msgctxt "TrInfo" +#~ msgid "created_by" +#~ msgstr "créée par" + +#~ msgid "default workflow for this entity type" +#~ msgstr "workflow par défaut pour un type d'entité" + +#~ msgid "remove this BaseTransition" +#~ msgstr "XXX" + +#~ msgid "remove this Bookmark" +#~ msgstr "supprimer ce signet" + +#~ msgid "remove this CWAttribute" +#~ msgstr "supprimer cet attribut" + +#~ msgid "remove this CWCache" +#~ msgstr "supprimer ce cache applicatif" + +#~ msgid "remove this CWConstraint" +#~ msgstr "supprimer cette contrainte" + +#~ msgid "remove this CWConstraintType" +#~ msgstr "supprimer ce type de contrainte" + +#~ msgid "remove this CWEType" +#~ msgstr "supprimer ce type d'entité" + +#~ msgid "remove this CWGroup" +#~ msgstr "supprimer ce groupe" + +#~ msgid "remove this CWPermission" +#~ msgstr "supprimer cette permission" + +#~ msgid "remove this CWProperty" +#~ msgstr "supprimer cette propriété" + +#~ msgid "remove this CWRType" +#~ msgstr "supprimer cette définition de relation" + +#~ msgid "remove this CWRelation" +#~ msgstr "supprimer cette relation" + +#~ msgid "remove this CWUser" +#~ msgstr "supprimer cet utilisateur" + +#~ msgid "remove this EmailAddress" +#~ msgstr "supprimer cette adresse email" + +#~ msgid "remove this ExternalUri" +#~ msgstr "supprimer cette Uri externe" + +#~ msgid "remove this RQLExpression" +#~ msgstr "supprimer cette expression rql" + +#~ msgid "remove this State" +#~ msgstr "supprimer cet état" + +#~ msgid "remove this SubWorkflowExitPoint" +#~ msgstr "supprimer ce point de sortie" + +#~ msgid "remove this TrInfo" +#~ msgstr "retirer cette information de transition" + +#~ msgid "remove this Transition" +#~ msgstr "supprimer cette transition" + +#~ msgid "remove this Workflow" +#~ msgstr "supprimer ce workflow" + +#~ msgid "remove this WorkflowTransition" +#~ msgstr "supprimer cette transition workflow" diff -r 24489cbbd697 -r 70c0dd1c3b7d interfaces.py --- a/interfaces.py Thu Sep 17 14:03:21 2009 +0200 +++ b/interfaces.py Thu Sep 17 14:53:18 2009 +0200 @@ -37,25 +37,22 @@ class IWorkflowable(Interface): """interface for entities dealing with a specific workflow""" + # XXX to be completed, see cw.entities.wfobjs.WorkflowableMixIn @property def state(self): - """return current state""" + """return current state name""" def change_state(self, stateeid, trcomment=None, trcommentformat=None): - """change the entity's state according to a state defined in given - parameters - """ - - def can_pass_transition(self, trname): - """return true if the current user can pass the transition with the - given name + """change the entity's state to the state of the given name in entity's + workflow """ def latest_trinfo(self): """return the latest transition information for this entity """ + class IProgress(Interface): """something that has a cost, a state and a progression diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.42.0_Any.py --- a/misc/migration/2.42.0_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -synchronize_rschema('created_by') -synchronize_rschema('owned_by') diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.42.1_Any.py --- a/misc/migration/2.42.1_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -if confirm('remove deprecated database constraints?'): - execute = session.system_sql - session.set_pool() - dbhelper = session.pool.source('system').dbhelper - cu = session.pool['system'] - for table in dbhelper.list_tables(cu): - if table.endswith('_relation'): - try: - execute('ALTER TABLE %s DROP CONSTRAINT %s_fkey1' % (table, table)) - execute('ALTER TABLE %s DROP CONSTRAINT %s_fkey2' % (table, table)) - except: - continue - checkpoint() - -if 'inline_view' in schema: - # inline_view attribute should have been deleted for a while now.... - drop_attribute('CWRelation', 'inline_view') - diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.43.0_Any.py --- a/misc/migration/2.43.0_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -synchronize_permissions('EmailAddress') diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.44.0_Any.py --- a/misc/migration/2.44.0_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -change_relation_props('CWAttribute', 'cardinality', 'String', internationalizable=True) -change_relation_props('CWRelation', 'cardinality', 'String', internationalizable=True) - -drop_relation_definition('CWPermission', 'require_state', 'State') - -if confirm('cleanup require_permission relation'): - try: - newrschema = fsschema.rschema('require_permission') - except KeyError: - newrschema = None - for rsubj, robj in schema.rschema('require_permission').rdefs(): - if newrschema is None or not newrschema.has_rdef(rsubj, robj): - print 'removing', rsubj, 'require_permission', robj - drop_relation_definition(rsubj, 'require_permission', robj, ask_confirm=False) diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.45.0_Any.py --- a/misc/migration/2.45.0_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -# following functions have been renamed, but keep old definition for bw compat -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -sql('''CREATE AGGREGATE group_concat ( - basetype = anyelement, - sfunc = array_append, - stype = anyarray, - finalfunc = comma_join, - initcond = '{}' -)''') - -sql('''CREATE FUNCTION text_limit_size (fulltext text, maxsize integer) RETURNS text AS $$ -BEGIN - RETURN limit_size(fulltext, 'text/plain', maxsize); -END -$$ LANGUAGE plpgsql; -''') - - -synchronize_rschema('bookmarked_by') diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.46.0_Any.py --- a/misc/migration/2.46.0_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" - - -rql('SET X value "navtop" WHERE X pkey ~= "contentnavigation.%.context", X value "header"') -rql('SET X value "navcontenttop" WHERE X pkey ~= "contentnavigation%.context", X value "incontext"') -rql('SET X value "navcontentbottom" WHERE X pkey ~= "contentnavigation%.context", X value "footer"') -checkpoint() - -if 'require_permission' in schema: - synchronize_rschema('require_permission') diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.47.0_Any.py --- a/misc/migration/2.47.0_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -synchronize_permissions('primary_email') -synchronize_rschema('wf_info_for') -synchronize_rschema('use_email') - diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.48.8_Any.py --- a/misc/migration/2.48.8_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -for etype in ('CWRType', 'CWAttribute', 'CWRelation', 'CWConstraint', 'CWConstraintType'): - synchronize_permissions(etype) diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.49.3_Any.py --- a/misc/migration/2.49.3_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -add_entity_type('Decimal') diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/2.50.0_Any.py --- a/misc/migration/2.50.0_Any.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -add_relation_type('specializes') diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/3.5.0_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.5.0_Any.py Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,10 @@ +add_relation_type('prefered_form') + +rql('SET X prefered_form Y WHERE Y canonical TRUE, X identical_to Y') +checkpoint() + +drop_attribute('EmailAddress', 'canonical') +drop_relation_definition('EmailAddress', 'identical_to', 'EmailAddress') + +if 'see_also' in schema: + sync_schema_props_perms('see_also', syncprops=False, syncrdefs=False) diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Thu Sep 17 14:03:21 2009 +0200 +++ b/misc/migration/bootstrapmigration_repository.py Thu Sep 17 14:53:18 2009 +0200 @@ -30,6 +30,47 @@ 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 explicit 'State allowed_transition Transition' since it should be + # infered due to yams inheritance. However we've to disable the schema + # sync hook first to avoid to destroy existing data... + from cubicweb.server.schemahooks import after_del_relation_type + repo.hm.unregister_hook(after_del_relation_type, + 'after_delete_relation', 'relation_type') + try: + drop_relation_definition('State', 'allowed_transition', 'Transition') + finally: + repo.hm.register_hook(after_del_relation_type, + 'after_delete_relation', 'relation_type') + 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 +82,3 @@ if applcubicwebversion < (3, 2, 0) and cubicwebversion >= (3, 2, 0): add_cube('card', update_database=False) - -if applcubicwebversion < (2, 47, 0) and cubicwebversion >= (2, 47, 0): - from cubicweb.server import schemaserial - schemaserial.HAS_FULLTEXT_CONTAINER = False - session.set_shared_data('do-not-insert-is_instance_of', True) - add_attribute('CWRType', 'fulltext_container') - schemaserial.HAS_FULLTEXT_CONTAINER = True - - - -if applcubicwebversion < (2, 50, 0) and cubicwebversion >= (2, 50, 0): - session.set_shared_data('do-not-insert-is_instance_of', True) - add_relation_type('is_instance_of') - # fill the relation using an efficient sql query instead of using rql - sql('INSERT INTO is_instance_of_relation ' - ' SELECT * from is_relation') - checkpoint() - session.set_shared_data('do-not-insert-is_instance_of', False) - -if applcubicwebversion < (2, 42, 0) and cubicwebversion >= (2, 42, 0): - sql('ALTER TABLE entities ADD COLUMN mtime TIMESTAMP') - sql('UPDATE entities SET mtime=CURRENT_TIMESTAMP') - sql('CREATE INDEX entities_mtime_idx ON entities(mtime)') - sql('''CREATE TABLE deleted_entities ( - eid INTEGER PRIMARY KEY NOT NULL, - type VARCHAR(64) NOT NULL, - source VARCHAR(64) NOT NULL, - dtime TIMESTAMP NOT NULL, - extid VARCHAR(256) -)''') - sql('CREATE INDEX deleted_entities_type_idx ON deleted_entities(type)') - sql('CREATE INDEX deleted_entities_dtime_idx ON deleted_entities(dtime)') - sql('CREATE INDEX deleted_entities_extid_idx ON deleted_entities(extid)') - checkpoint() diff -r 24489cbbd697 -r 70c0dd1c3b7d misc/migration/postcreate.py --- a/misc/migration/postcreate.py Thu Sep 17 14:03:21 2009 +0200 +++ b/misc/migration/postcreate.py Thu Sep 17 14:53:18 2009 +0200 @@ -6,41 +6,59 @@ :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ -activatedeid = add_state(_('activated'), 'CWUser', initial=True) -deactivatedeid = add_state(_('deactivated'), 'CWUser') -add_transition(_('deactivate'), 'CWUser', - (activatedeid,), deactivatedeid, - requiredgroups=('managers',)) -add_transition(_('activate'), 'CWUser', - (deactivatedeid,), activatedeid, - requiredgroups=('managers',)) +# insert versions +create_entity('CWProperty', pkey=u'system.version.cubicweb', + value=unicode(config.cubicweb_version())) +for cube in config.cubes(): + create_entity('CWProperty', pkey=u'system.version.%s' % cube.lower(), + value=unicode(config.cube_version(cube))) -# need this since we already have at least one user in the database (the default admin) -rql('SET X in_state S WHERE X is CWUser, S eid %s' % activatedeid) +# some entities have been added before schema entities, fix the 'is' and +# 'is_instance_of' relations +for rtype in ('is', 'is_instance_of'): + sql('INSERT INTO %s_relation ' + 'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET ' + 'WHERE X.type=ET.cw_name AND NOT EXISTS(' + ' SELECT 1 from is_relation ' + ' WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype) + +# user workflow +userwf = add_workflow(_('default user workflow'), 'CWUser') +activated = userwf.add_state(_('activated'), initial=True) +deactivated = userwf.add_state(_('deactivated')) +userwf.add_transition(_('deactivate'), (activated,), deactivated, + requiredgroups=('managers',)) +userwf.add_transition(_('activate'), (deactivated,), activated, + requiredgroups=('managers',)) # create anonymous user if all-in-one config and anonymous user has been specified if hasattr(config, 'anonymous_user'): anonlogin, anonpwd = config.anonymous_user() if anonlogin: rql('INSERT CWUser X: X login %(login)s, X upassword %(pwd)s,' - 'X in_state S, X in_group G WHERE G name "guests", S name "activated"', + 'X in_group G WHERE G name "guests"', {'login': unicode(anonlogin), 'pwd': anonpwd}) -cfg = config.persistent_options_configuration() -if interactive_mode: - cfg.input_config(inputlevel=0) +# need this since we already have at least one user in the database (the default admin) +for user in rql('Any X WHERE X is CWUser').entities(): + session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', + {'x': user.eid, 's': activated.eid}, 'x') -for section, options in cfg.options_by_section(): - for optname, optdict, value in options: - key = '%s.%s' % (section, optname) - default = cfg.option_default(optname, optdict) - # only record values differing from default - if value != default: - rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value}) +# on interactive mode, ask for level 0 persistent options +if interactive_mode: + cfg = config.persistent_options_configuration() + cfg.input_config(inputlevel=0) + for section, options in cfg.options_by_section(): + for optname, optdict, value in options: + key = '%s.%s' % (section, optname) + default = cfg.option_default(optname, optdict) + # only record values differing from default + if value != default: + rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value}) # add PERM_USE_TEMPLATE_FORMAT permission from cubicweb.schema import PERM_USE_TEMPLATE_FORMAT -eid = add_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT, - label=_('use template languages')) +usetmplperm = create_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT, + label=_('use template languages')) rql('SET X require_group G WHERE G name "managers", X eid %(x)s', - {'x': eid}, 'x') + {'x': usetmplperm.eid}, 'x') diff -r 24489cbbd697 -r 70c0dd1c3b7d rqlrewrite.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rqlrewrite.py Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,480 @@ +"""RQL rewriting utilities : insert rql expression snippets into rql syntax +tree. + +This is used for instance for read security checking in the repository. + +:organization: Logilab +:copyright: 2007-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 +""" +__docformat__ = "restructuredtext en" + +from rql import nodes as n, stmts, TypeResolverException + +from logilab.common.compat import any + +from cubicweb import Unauthorized, server, typed_eid +from cubicweb.server.ssplanner import add_types_restriction + + +def remove_solutions(origsolutions, solutions, defined): + """when a rqlst has been generated from another by introducing security + assertions, this method returns solutions which are contained in orig + solutions + """ + newsolutions = [] + for origsol in origsolutions: + for newsol in solutions[:]: + for var, etype in origsol.items(): + try: + if newsol[var] != etype: + try: + defined[var].stinfo['possibletypes'].remove(newsol[var]) + except KeyError: + pass + break + except KeyError: + # variable has been rewritten + continue + else: + newsolutions.append(newsol) + solutions.remove(newsol) + return newsolutions + + +class Unsupported(Exception): pass + + +class RQLRewriter(object): + """insert some rql snippets into another rql syntax tree + + this class *isn't thread safe* + """ + + def __init__(self, session): + self.session = session + vreg = session.vreg + self.schema = vreg.schema + self.annotate = vreg.rqlhelper.annotate + self._compute_solutions = vreg.solutions + + def compute_solutions(self): + self.annotate(self.select) + try: + self._compute_solutions(self.session, self.select, self.kwargs) + except TypeResolverException: + raise Unsupported(str(self.select)) + if len(self.select.solutions) < len(self.solutions): + raise Unsupported() + + def rewrite(self, select, snippets, solutions, kwargs): + """ + snippets: (varmap, list of rql expression) + with varmap a *tuple* (select var, snippet var) + """ + if server.DEBUG: + print '---- rewrite', select, snippets, solutions + self.select = self.insert_scope = select + self.solutions = solutions + self.kwargs = kwargs + self.u_varname = None + self.removing_ambiguity = False + self.exists_snippet = {} + self.pending_keys = [] + # we have to annotate the rqlst before inserting snippets, even though + # we'll have to redo it latter + self.annotate(select) + self.insert_snippets(snippets) + if not self.exists_snippet and self.u_varname: + # U has been inserted than cancelled, cleanup + select.undefine_variable(select.defined_vars[self.u_varname]) + # clean solutions according to initial solutions + newsolutions = remove_solutions(solutions, select.solutions, + select.defined_vars) + assert len(newsolutions) >= len(solutions), ( + 'rewritten rql %s has lost some solutions, there is probably ' + 'something wrong in your schema permission (for instance using a ' + 'RQLExpression which insert a relation which doesn\'t exists in ' + 'the schema)\nOrig solutions: %s\nnew solutions: %s' % ( + select, solutions, newsolutions)) + if len(newsolutions) > len(solutions): + newsolutions = self.remove_ambiguities(snippets, newsolutions) + select.solutions = newsolutions + add_types_restriction(self.schema, select) + if server.DEBUG: + print '---- rewriten', select + + def insert_snippets(self, snippets, varexistsmap=None): + self.rewritten = {} + for varmap, rqlexprs in snippets: + if varexistsmap is not None and not varmap in varexistsmap: + continue + self.varmap = varmap + selectvar, snippetvar = varmap + assert snippetvar in 'SOX' + self.revvarmap = {snippetvar: selectvar} + self.varinfo = vi = {} + try: + vi['const'] = typed_eid(selectvar) # XXX gae + vi['rhs_rels'] = vi['lhs_rels'] = {} + except ValueError: + vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo + if varexistsmap is None: + vi['rhs_rels'] = dict( (r.r_type, r) for r in sti['rhsrelations']) + vi['lhs_rels'] = dict( (r.r_type, r) for r in sti['relations'] + if not r in sti['rhsrelations']) + else: + vi['rhs_rels'] = vi['lhs_rels'] = {} + parent = None + inserted = False + for rqlexpr in rqlexprs: + self.current_expr = rqlexpr + if varexistsmap is None: + try: + new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent) + except Unsupported: + import traceback + traceback.print_exc() + continue + inserted = True + if new is not None: + self.exists_snippet[rqlexpr] = new + parent = parent or new + else: + # called to reintroduce snippet due to ambiguity creation, + # so skip snippets which are not introducing this ambiguity + exists = varexistsmap[varmap] + if self.exists_snippet[rqlexpr] is exists: + self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists) + if varexistsmap is None and not inserted: + # no rql expression found matching rql solutions. User has no access right + raise Unauthorized() + + def insert_snippet(self, varmap, snippetrqlst, parent=None): + new = snippetrqlst.where.accept(self) + if new is not None: + if self.varinfo.get('stinfo', {}).get('optrelations'): + assert parent is None + self.insert_scope = self.snippet_subquery(varmap, new) + self.insert_pending() + self.insert_scope = self.select + return + new = n.Exists(new) + if parent is None: + self.insert_scope.add_restriction(new) + else: + grandpa = parent.parent + or_ = n.Or(parent, new) + grandpa.replace(parent, or_) + if not self.removing_ambiguity: + try: + self.compute_solutions() + except Unsupported: + # some solutions have been lost, can't apply this rql expr + if parent is None: + self.select.remove_node(new, undefine=True) + else: + parent.parent.replace(or_, or_.children[0]) + self._cleanup_inserted(new) + raise + else: + self.insert_scope = new + self.insert_pending() + self.insert_scope = self.select + return new + self.insert_pending() + + def insert_pending(self): + """pending_keys hold variable referenced by U has__permission X + relation. + + Once the snippet introducing this has been inserted and solutions + recomputed, we have to insert snippet defined for of entity + types taken by X + """ + while self.pending_keys: + key, action = self.pending_keys.pop() + try: + varname = self.rewritten[key] + except KeyError: + try: + varname = self.revvarmap[key[-1]] + except KeyError: + # variable isn't used anywhere else, we can't insert security + raise Unauthorized() + ptypes = self.select.defined_vars[varname].stinfo['possibletypes'] + if len(ptypes) > 1: + # XXX dunno how to handle this + self.session.error( + 'cant check security of %s, ambigous type for %s in %s', + self.select, varname, key[0]) # key[0] == the rql expression + raise Unauthorized() + etype = iter(ptypes).next() + eschema = self.schema.eschema(etype) + if not eschema.has_perm(self.session, action): + rqlexprs = eschema.get_rqlexprs(action) + if not rqlexprs: + raise Unauthorised() + self.insert_snippets([((varname, 'X'), rqlexprs)]) + + def snippet_subquery(self, varmap, transformedsnippet): + """introduce the given snippet in a subquery""" + subselect = stmts.Select() + selectvar, snippetvar = varmap + subselect.append_selected(n.VariableRef( + subselect.get_variable(selectvar))) + aliases = [selectvar] + subselect.add_restriction(transformedsnippet.copy(subselect)) + stinfo = self.varinfo['stinfo'] + for rel in stinfo['relations']: + rschema = self.schema.rschema(rel.r_type) + if rschema.is_final() or (rschema.inlined and + not rel in stinfo['rhsrelations']): + self.select.remove_node(rel) + rel.children[0].name = selectvar + subselect.add_restriction(rel.copy(subselect)) + for vref in rel.children[1].iget_nodes(n.VariableRef): + subselect.append_selected(vref.copy(subselect)) + aliases.append(vref.name) + if self.u_varname: + # generate an identifier for the substitution + argname = subselect.allocate_varname() + while argname in self.kwargs: + argname = subselect.allocate_varname() + subselect.add_constant_restriction(subselect.get_variable(self.u_varname), + 'eid', unicode(argname), 'Substitute') + self.kwargs[argname] = self.session.user.eid + add_types_restriction(self.schema, subselect, subselect, + solutions=self.solutions) + myunion = stmts.Union() + myunion.append(subselect) + aliases = [n.VariableRef(self.select.get_variable(name, i)) + for i, name in enumerate(aliases)] + self.select.add_subquery(n.SubQuery(aliases, myunion), check=False) + self._cleanup_inserted(transformedsnippet) + try: + self.compute_solutions() + except Unsupported: + # some solutions have been lost, can't apply this rql expr + self.select.remove_subquery(new, undefine=True) + raise + return subselect + + def remove_ambiguities(self, snippets, newsolutions): + # the snippet has introduced some ambiguities, we have to resolve them + # "manually" + variantes = self.build_variantes(newsolutions) + # insert "is" where necessary + varexistsmap = {} + self.removing_ambiguity = True + for (erqlexpr, varmap, oldvarname), etype in variantes[0].iteritems(): + varname = self.rewritten[(erqlexpr, varmap, oldvarname)] + var = self.select.defined_vars[varname] + exists = var.references()[0].scope + exists.add_constant_restriction(var, 'is', etype, 'etype') + varexistsmap[varmap] = exists + # insert ORED exists where necessary + for variante in variantes[1:]: + self.insert_snippets(snippets, varexistsmap) + for key, etype in variante.iteritems(): + varname = self.rewritten[key] + try: + var = self.select.defined_vars[varname] + except KeyError: + # not a newly inserted variable + continue + exists = var.references()[0].scope + exists.add_constant_restriction(var, 'is', etype, 'etype') + # recompute solutions + #select.annotated = False # avoid assertion error + self.compute_solutions() + # clean solutions according to initial solutions + return remove_solutions(self.solutions, self.select.solutions, + self.select.defined_vars) + + def build_variantes(self, newsolutions): + variantes = set() + for sol in newsolutions: + variante = [] + for key, newvar in self.rewritten.iteritems(): + variante.append( (key, sol[newvar]) ) + variantes.add(tuple(variante)) + # rebuild variantes as dict + variantes = [dict(variante) for variante in variantes] + # remove variable which have always the same type + for key in self.rewritten: + it = iter(variantes) + etype = it.next()[key] + for variante in it: + if variante[key] != etype: + break + else: + for variante in variantes: + del variante[key] + return variantes + + def _cleanup_inserted(self, node): + # cleanup inserted variable references + for vref in node.iget_nodes(n.VariableRef): + vref.unregister_reference() + if not vref.variable.stinfo['references']: + # no more references, undefine the variable + del self.select.defined_vars[vref.name] + + def _may_be_shared(self, relation, target, searchedvarname): + """return True if the snippet relation can be skipped to use a relation + from the original query + """ + # if cardinality is in '?1', we can ignore the relation and use variable + # from the original query + rschema = self.schema.rschema(relation.r_type) + if target == 'object': + cardindex = 0 + ttypes_func = rschema.objects + rprop = rschema.rproperty + else: # target == 'subject': + cardindex = 1 + ttypes_func = rschema.subjects + rprop = lambda x, y, z: rschema.rproperty(y, x, z) + for etype in self.varinfo['stinfo']['possibletypes']: + for ttype in ttypes_func(etype): + if rprop(etype, ttype, 'cardinality')[cardindex] in '+*': + return False + return True + + def _use_outer_term(self, snippet_varname, term): + key = (self.current_expr, self.varmap, snippet_varname) + if key in self.rewritten: + insertedvar = self.select.defined_vars.pop(self.rewritten[key]) + for inserted_vref in insertedvar.references(): + inserted_vref.parent.replace(inserted_vref, term.copy(self.select)) + self.rewritten[key] = term.name + + def _get_varname_or_term(self, vname): + if vname == 'U': + if self.u_varname is None: + select = self.select + self.u_varname = select.allocate_varname() + # generate an identifier for the substitution + argname = select.allocate_varname() + while argname in self.kwargs: + argname = select.allocate_varname() + # insert "U eid %(u)s" + var = select.get_variable(self.u_varname) + select.add_constant_restriction(select.get_variable(self.u_varname), + 'eid', unicode(argname), 'Substitute') + self.kwargs[argname] = self.session.user.eid + return self.u_varname + key = (self.current_expr, self.varmap, vname) + try: + return self.rewritten[key] + except KeyError: + self.rewritten[key] = newvname = self.select.allocate_varname() + return newvname + + # visitor methods ########################################################## + + def _visit_binary(self, node, cls): + newnode = cls() + for c in node.children: + new = c.accept(self) + if new is None: + continue + newnode.append(new) + if len(newnode.children) == 0: + return None + if len(newnode.children) == 1: + return newnode.children[0] + return newnode + + def _visit_unary(self, node, cls): + newc = node.children[0].accept(self) + if newc is None: + return None + newnode = cls() + newnode.append(newc) + return newnode + + def visit_and(self, node): + return self._visit_binary(node, n.And) + + def visit_or(self, node): + return self._visit_binary(node, n.Or) + + def visit_not(self, node): + return self._visit_unary(node, n.Not) + + def visit_exists(self, node): + return self._visit_unary(node, n.Exists) + + def visit_relation(self, node): + lhs, rhs = node.get_variable_parts() + if node.r_type in ('has_add_permission', 'has_update_permission', + 'has_delete_permission', 'has_read_permission'): + assert lhs.name == 'U' + action = node.r_type.split('_')[1] + key = (self.current_expr, self.varmap, rhs.name) + self.pending_keys.append( (key, action) ) + return + if lhs.name in self.revvarmap: + # on lhs + # see if we can reuse this relation + rels = self.varinfo['lhs_rels'] + if (node.r_type in rels and isinstance(rhs, n.VariableRef) + and rhs.name != 'U' and not rels[node.r_type].neged(strict=True) + and self._may_be_shared(node, 'object', lhs.name)): + # ok, can share variable + term = rels[node.r_type].children[1].children[0] + self._use_outer_term(rhs.name, term) + return + elif isinstance(rhs, n.VariableRef) and rhs.name in self.revvarmap and lhs.name != 'U': + # on rhs + # see if we can reuse this relation + rels = self.varinfo['rhs_rels'] + if (node.r_type in rels and not rels[node.r_type].neged(strict=True) + and self._may_be_shared(node, 'subject', rhs.name)): + # ok, can share variable + term = rels[node.r_type].children[0] + self._use_outer_term(lhs.name, term) + return + rel = n.Relation(node.r_type, node.optional) + for c in node.children: + rel.append(c.accept(self)) + return rel + + def visit_comparison(self, node): + cmp_ = n.Comparison(node.operator) + for c in node.children: + cmp_.append(c.accept(self)) + return cmp_ + + def visit_mathexpression(self, node): + cmp_ = n.MathExpression(node.operator) + for c in cmp.children: + cmp_.append(c.accept(self)) + return cmp_ + + def visit_function(self, node): + """generate filter name for a function""" + function_ = n.Function(node.name) + for c in node.children: + function_.append(c.accept(self)) + return function_ + + def visit_constant(self, node): + """generate filter name for a constant""" + return n.Constant(node.value, node.type) + + def visit_variableref(self, node): + """get the sql name for a variable reference""" + if node.name in self.revvarmap: + if self.varinfo.get('const') is not None: + return n.Constant(self.varinfo['const'], 'Int') # XXX gae + return n.VariableRef(self.select.get_variable( + self.revvarmap[node.name])) + vname_or_term = self._get_varname_or_term(node.name) + if isinstance(vname_or_term, basestring): + return n.VariableRef(self.select.get_variable(vname_or_term)) + # shared term + return vname_or_term.copy(self.select) diff -r 24489cbbd697 -r 70c0dd1c3b7d schema.py --- a/schema.py Thu Sep 17 14:03:21 2009 +0200 +++ b/schema.py Thu Sep 17 14:53:18 2009 +0200 @@ -44,6 +44,7 @@ 'owned_by', 'created_by', 'is', 'is_instance_of', 'identity', 'eid', 'creation_date', 'modification_date', 'has_text', 'cwuri', )) +SYSTEM_RTYPES = set(('require_permission', 'custom_workflow', 'in_state', 'wf_info_for')) # set of entity and relation types used to build the schema SCHEMA_TYPES = set(( @@ -100,7 +101,7 @@ etype = ETYPE_NAME_MAP[etype] return etype -def display_name(req, key, form=''): +def display_name(req, key, form='', context=None): """return a internationalized string for the key (schema entity or relation name) in a given form """ @@ -110,8 +111,12 @@ if form: key = key + '_' + form # ensure unicode - # added .lower() in case no translation are available - return unicode(req._(key)).lower() + # .lower() in case no translation are available XXX done whatever a translation is there or not! + if context is not None: + return unicode(req.pgettext(context, key)).lower() + else: + return unicode(req._(key)).lower() + __builtins__['display_name'] = deprecated('display_name should be imported from cubicweb.schema')(display_name) def ERSchema_display_name(self, req, form=''): @@ -642,6 +647,8 @@ if len(self.rqlst.defined_vars[mainvar].references()) <= 2: _LOGGER.warn('You did not use the %s variable in your RQL ' 'expression %s', mainvar, self) + # syntax tree used by read security (inserted in queries when necessary + self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0] def __str__(self): return self.full_rql @@ -767,8 +774,6 @@ class ERQLExpression(RQLExpression): def __init__(self, expression, mainvars=None, eid=None): RQLExpression.__init__(self, expression, mainvars or 'X', eid) - # syntax tree used by read security (inserted in queries when necessary - self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0] @property def full_rql(self): @@ -847,23 +852,30 @@ This is the default metaclass for WorkflowableEntityType """ def __new__(mcs, name, bases, classdict): - abstract = classdict.pop('abstract', False) - defclass = super(workflowable_definition, mcs).__new__(mcs, name, bases, classdict) + abstract = classdict.pop('__abstract__', False) + cls = super(workflowable_definition, mcs).__new__(mcs, name, bases, + classdict) if not abstract: - existing_rels = set(rdef.name for rdef in defclass.__relations__) - if 'in_state' not in existing_rels and 'wf_info_for' not in existing_rels: - in_state = ybo.SubjectRelation('State', cardinality='1*', - # XXX automatize this - constraints=[RQLConstraint('S is ET, O state_of ET')], - description=_('account state')) - yams_add_relation(defclass.__relations__, in_state, 'in_state') - wf_info_for = ybo.ObjectRelation('TrInfo', cardinality='1*', composite='object') - yams_add_relation(defclass.__relations__, wf_info_for, 'wf_info_for') - return defclass + make_workflowable(cls) + return cls + +def make_workflowable(cls, in_state_descr=None): + existing_rels = set(rdef.name for rdef in cls.__relations__) + # let relation types defined in cw.schemas.workflow carrying + # cardinality, constraints and other relation definition properties + if 'custom_workflow' not in existing_rels: + rdef = ybo.SubjectRelation('Workflow') + yams_add_relation(cls.__relations__, rdef, 'custom_workflow') + if 'in_state' not in existing_rels: + rdef = ybo.SubjectRelation('State', description=in_state_descr) + yams_add_relation(cls.__relations__, rdef, 'in_state') + if 'wf_info_for' not in existing_rels: + rdef = ybo.ObjectRelation('TrInfo') + yams_add_relation(cls.__relations__, rdef, 'wf_info_for') class WorkflowableEntityType(ybo.EntityType): __metaclass__ = workflowable_definition - abstract = True + __abstract__ = True PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType diff -r 24489cbbd697 -r 70c0dd1c3b7d schemas/base.py --- a/schemas/base.py Thu Sep 17 14:03:21 2009 +0200 +++ b/schemas/base.py Thu Sep 17 14:53:18 2009 +0200 @@ -52,11 +52,10 @@ alias = String(fulltextindexed=True, maxsize=56) address = String(required=True, fulltextindexed=True, indexed=True, unique=True, maxsize=128) - canonical = Boolean(default=False, - description=_('when multiple addresses are equivalent \ + prefered_form = SubjectRelation('EmailAddress', cardinality='?*', + description=_('when multiple addresses are equivalent \ (such as python-projects@logilab.org and python-projects@lists.logilab.org), set this \ -to true on one of them which is the preferred form.')) - identical_to = SubjectRelation('EmailAddress') +to indicate which is the preferred form.')) class use_email(RelationType): """ """ @@ -71,9 +70,7 @@ """the prefered email""" permissions = use_email.permissions -class identical_to(RelationType): - """identical_to""" - symetric = True +class prefered_form(RelationType): permissions = { 'read': ('managers', 'users', 'guests',), # XXX should have update permissions on both subject and object, @@ -207,10 +204,6 @@ } -class see_also(RelationType): - """generic relation to link one entity to another""" - symetric = True - class ExternalUri(EntityType): """a URI representing an object in external data store""" uri = String(required=True, unique=True, maxsize=256, @@ -254,3 +247,30 @@ name = String(required=True, unique=True, indexed=True, maxsize=128, description=_('name of the cache')) timestamp = Datetime(default='NOW') + + +# "abtract" relation types, not used in cubicweb itself + +class identical_to(RelationType): + """identical to""" + symetric = True + permissions = { + 'read': ('managers', 'users', 'guests',), + # XXX should have update permissions on both subject and object, + # though by doing this we will probably have no way to add + # this relation in the web ui. The easiest way to acheive this + # is probably to be able to have "U has_update_permission O" as + # RQLConstraint of the relation definition, though this is not yet + # possible + 'add': ('managers', RRQLExpression('U has_update_permission S'),), + 'delete': ('managers', RRQLExpression('U has_update_permission S'),), + } + +class see_also(RelationType): + """generic relation to link one entity to another""" + symetric = True + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers', RRQLExpression('U has_update_permission S'),), + 'delete': ('managers', RRQLExpression('U has_update_permission S'),), + } diff -r 24489cbbd697 -r 70c0dd1c3b7d schemas/workflow.py --- a/schemas/workflow.py Thu Sep 17 14:03:21 2009 +0200 +++ b/schemas/workflow.py Thu Sep 17 14:53:18 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 an entity type""" + permissions = META_RTYPE_PERMS + + subject = 'CWEType' + object = 'Workflow' + cardinality = '?*' + constraints = [RQLConstraint('S final FALSE, O workflow_of S')] + class State(EntityType): """used to associate simple states to an entity type and/or to define @@ -24,23 +53,18 @@ description = RichString(fulltextindexed=True, default_format='text/rest', description=_('semantic description of this state')) - state_of = SubjectRelation('CWEType', cardinality='+*', - description=_('entity types which may use this state'), - constraints=[RQLConstraint('O final FALSE')]) - allowed_transition = SubjectRelation('Transition', cardinality='**', - constraints=[RQLConstraint('S state_of ET, O transition_of ET')], + # XXX should be on BaseTransition w/ AND/OR selectors when we will + # implements #345274 + allowed_transition = SubjectRelation('BaseTransition', cardinality='**', + constraints=[RQLConstraint('S state_of WF, O transition_of WF')], description=_('allowed transitions from this state')) - - initial_state = ObjectRelation('CWEType', cardinality='?*', - # S initial_state O, O state_of S - constraints=[RQLConstraint('O state_of S')], - description=_('initial state for entities of this type')) + state_of = SubjectRelation('Workflow', cardinality='1*', + description=_('workflow to which this state belongs'), + constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N')]) -class Transition(EntityType): - """use to define a transition from one or multiple states to a destination - states in workflow's definitions. - """ +class BaseTransition(EntityType): + """abstract base class for transitions""" permissions = META_ETYPE_PERMS name = String(required=True, indexed=True, internationalizable=True, @@ -57,47 +81,108 @@ require_group = SubjectRelation('CWGroup', cardinality='**', description=_('group in which a user should be to be ' 'allowed to pass this transition')) - transition_of = SubjectRelation('CWEType', cardinality='+*', - description=_('entity types which may use this transition'), - constraints=[RQLConstraint('O final FALSE')]) + transition_of = SubjectRelation('Workflow', cardinality='1*', + description=_('workflow to which this transition belongs'), + constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N')]) + + +class Transition(BaseTransition): + """use to define a transition from one or multiple states to a destination + states in workflow's definitions. + """ + __specializes_schema__ = True + destination_state = SubjectRelation('State', cardinality='1*', - constraints=[RQLConstraint('S transition_of ET, O state_of ET')], + constraints=[RQLConstraint('S transition_of WF, O state_of WF')], description=_('destination state for this transition')) -class TrInfo(EntityType): - permissions = META_ETYPE_PERMS +class WorkflowTransition(BaseTransition): + """special transition allowing to go through a sub-workflow""" + __specializes_schema__ = True + + subworkflow = SubjectRelation('Workflow', cardinality='1*', + constraints=[RQLConstraint('S transition_of WF, WF workflow_of ET, O workflow_of ET')]) + subworkflow_exit = SubjectRelation('SubWorkflowExitPoint', cardinality='+1', + composite='subject') + + +class SubWorkflowExitPoint(EntityType): + """define how we get out from a sub-workflow""" + subworkflow_state = SubjectRelation('State', cardinality='1*', + constraints=[RQLConstraint('T subworkflow_exit S, T subworkflow WF, O state_of WF')], + description=_('subworkflow state')) + destination_state = SubjectRelation('State', cardinality='1*', + constraints=[RQLConstraint('T subworkflow_exit S, T transition_of WF, O state_of WF')], + description=_('destination state')) - from_state = SubjectRelation('State', cardinality='?*') + +# XXX should we allow managers to delete TrInfo? + +class TrInfo(EntityType): + """workflow history item""" + # 'add' security actually done by hooks + permissions = { + 'read': ('managers', 'users', 'guests',), # XXX U has_read_permission O ? + 'add': ('managers', 'users', 'guests',), + 'delete': (), + 'update': ('managers', 'owners',), + } + + from_state = SubjectRelation('State', cardinality='1*') to_state = SubjectRelation('State', cardinality='1*') + # make by_transition optional because we want to allow managers to set + # entity into an arbitrary state without having to respect wf transition + by_transition = SubjectRelation('BaseTransition', cardinality='?*') comment = RichString(fulltextindexed=True) # get actor and date time using owned_by and creation_date +class from_state(RelationType): + permissions = HOOKS_RTYPE_PERMS.copy() + inlined = True -class from_state(RelationType): - permissions = HOOKS_RTYPE_PERMS +class to_state(RelationType): + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers',), + 'delete': (), + } inlined = True -class to_state(RelationType): - permissions = HOOKS_RTYPE_PERMS + +class by_transition(RelationType): + # 'add' security actually done by hooks + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers', 'users', 'guests',), + 'delete': (), + } inlined = True -class wf_info_for(RelationType): - """link a transition information to its object""" - permissions = { - 'read': ('managers', 'users', 'guests',),# RRQLExpression('U has_read_permission O')), - 'add': (), # handled automatically, no one should add one explicitly - 'delete': ('managers',), # RRQLExpression('U has_delete_permission O') - } - inlined = True - composite = 'object' - fulltext_container = composite +class workflow_of(RelationType): + """link a workflow to one or more entity type""" + permissions = META_RTYPE_PERMS class state_of(RelationType): - """link a state to one or more entity type""" + """link a state to one or more workflow""" permissions = META_RTYPE_PERMS + class transition_of(RelationType): - """link a transition to one or more entity type""" + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + +class subworkflow(RelationType): + """link a transition to one or more workflow""" permissions = META_RTYPE_PERMS + inlined = True + +class exit_point(RelationType): + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + +class subworkflow_state(RelationType): + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + inlined = True class initial_state(RelationType): """indicate which state should be used by default when an entity using @@ -115,16 +200,42 @@ """allowed transition from this state""" permissions = META_RTYPE_PERMS + +# "abstract" relations, set by WorkflowableEntityType ########################## + +class custom_workflow(RelationType): + """allow to set a specific workflow for an entity""" + permissions = META_RTYPE_PERMS + + cardinality = '?*' + constraints = [RQLConstraint('S is ET, O workflow_of ET')] + object = 'Workflow' + + +class wf_info_for(RelationType): + """link a transition information to its object""" + # 'add' security actually done by hooks + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers', 'users', 'guests',), + 'delete': (), + } + inlined = True + + cardinality='1*' + composite = 'object' + fulltext_container = composite + subject = 'TrInfo' + + class in_state(RelationType): """indicate the current state of an entity""" + permissions = HOOKS_RTYPE_PERMS + # not inlined intentionnaly since when using ldap sources, user'state # has to be stored outside the CWUser table inlined = False - # add/delete perms given to managers/users, after what most of the job - # is done by workflow enforcment - permissions = { - 'read': ('managers', 'users', 'guests',), - 'add': ('managers', 'users',), # XXX has_update_perm - 'delete': ('managers', 'users',), - } + cardinality = '1*' + constraints = [RQLConstraint('S is ET, O state_of WF, WF workflow_of ET')] + object = 'State' diff -r 24489cbbd697 -r 70c0dd1c3b7d selectors.py --- a/selectors.py Thu Sep 17 14:03:21 2009 +0200 +++ b/selectors.py Thu Sep 17 14:53:18 2009 +0200 @@ -960,7 +960,14 @@ """ def __init__(self, scorefunc, once_is_enough=False): super(score_entity, self).__init__(once_is_enough) - self.score_entity = scorefunc + def intscore(*args, **kwargs): + score = scorefunc(*args, **kwargs) + if not score: + return 0 + if isinstance(score, (int, long)): + return score + return 1 + self.score_entity = intscore # XXX DEPRECATED ############################################################## diff -r 24489cbbd697 -r 70c0dd1c3b7d server/__init__.py --- a/server/__init__.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/__init__.py Thu Sep 17 14:53:18 2009 +0200 @@ -180,22 +180,6 @@ handler = config.migration_handler(schema, interactive=False, cnx=cnx, repo=repo) initialize_schema(config, schema, handler) - # insert versions - handler.cmd_add_entity('CWProperty', pkey=u'system.version.cubicweb', - value=unicode(config.cubicweb_version())) - for cube in config.cubes(): - handler.cmd_add_entity('CWProperty', - pkey=u'system.version.%s' % cube.lower(), - value=unicode(config.cube_version(cube))) - # some entities have been added before schema entities, fix the 'is' and - # 'is_instance_of' relations - for rtype in ('is', 'is_instance_of'): - handler.sqlexec( - 'INSERT INTO %s_relation ' - 'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET ' - 'WHERE X.type=ET.cw_name AND NOT EXISTS(' - ' SELECT 1 from is_relation ' - ' WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype) # yoo ! cnx.commit() config.enabled_sources = None diff -r 24489cbbd697 -r 70c0dd1c3b7d server/hooks.py --- a/server/hooks.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/hooks.py Thu Sep 17 14:53:18 2009 +0200 @@ -13,8 +13,8 @@ from cubicweb import UnknownProperty, ValidationError, BadConnectionId from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation -from cubicweb.server.hookhelper import (check_internal_entity, previous_state, - get_user_sessions, rproperty) +from cubicweb.server.hookhelper import (check_internal_entity, + get_user_sessions, rproperty) from cubicweb.server.repository import FTIndexEntityOp # special relations that don't have to be checked for integrity, usually @@ -37,7 +37,8 @@ # from the database (eg during tests) if eschema.eid is None: eschema.eid = session.unsafe_execute( - 'Any X WHERE X is CWEType, X name %(name)s', {'name': str(etype)})[0][0] + 'Any X WHERE X is CWEType, X name %(name)s', + {'name': str(etype)})[0][0] return eschema.eid @@ -417,51 +418,129 @@ # workflow handling ########################################################### -def before_add_in_state(session, fromeid, rtype, toeid): - """check the transition is allowed and record transition information +from cubicweb.entities.wfobjs import WorkflowTransition, WorkflowException + +def _change_state(session, x, oldstate, newstate): + nocheck = session.transaction_data.setdefault('skip-security', set()) + nocheck.add((x, 'in_state', oldstate)) + nocheck.add((x, 'in_state', newstate)) + # delete previous state first in case we're using a super session + fromsource = session.describe(x)[1] + # don't try to remove previous state if in_state isn't stored in the system + # source + if fromsource == 'system' or \ + not session.repo.sources_by_uri[fromsource].support_relation('in_state'): + session.delete_relation(x, 'in_state', oldstate) + session.add_relation(x, 'in_state', newstate) + + +def before_add_trinfo(session, entity): + """check the transition is allowed, add missing information. Expect that: + * wf_info_for inlined relation is set + * by_transition or to_state (managers only) inlined relation is set """ - assert rtype == 'in_state' - state = previous_state(session, fromeid) - etype = session.describe(fromeid)[0] - if not (session.is_super_session or 'managers' in session.user.groups): - if not state is None: - entity = session.entity_from_eid(fromeid) - # we should find at least one transition going to this state - try: - iter(state.transitions(entity, toeid)).next() - except StopIteration: - _ = session._ - msg = _('transition from %s to %s does not exist or is not allowed') % ( - _(state.name), _(session.entity_from_eid(toeid).name)) - raise ValidationError(fromeid, {'in_state': msg}) + # first retreive entity to which the state change apply + try: + foreid = entity['wf_info_for'] + except KeyError: + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'wf_info_for': msg}) + forentity = session.entity_from_eid(foreid) + # then check it has a workflow set, unless we're in the process of changing + # entity's workflow + if session.transaction_data.get((forentity.eid, 'customwf')): + wfeid = session.transaction_data[(forentity.eid, 'customwf')] + wf = session.entity_from_eid(wfeid) + else: + wf = forentity.current_workflow + if wf is None: + msg = session._('related entity has no workflow set') + raise ValidationError(entity.eid, {None: msg}) + # then check it has a state set + fromstate = forentity.current_state + if fromstate is None: + msg = session._('related entity has no state') + raise ValidationError(entity.eid, {None: msg}) + # True if we are coming back from subworkflow + swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) + cowpowers = session.is_super_session or 'managers' in session.user.groups + # no investigate the requested state change... + try: + treid = entity['by_transition'] + except KeyError: + # no transition set, check user is a manager and destination state is + # specified (and valid) + if not cowpowers: + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'by_transition': msg}) + deststateeid = entity.get('to_state') + if not deststateeid: + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'by_transition': msg}) + deststate = wf.state_by_eid(deststateeid) + if not cowpowers and deststate is None: + msg = entity.req._("state doesn't belong to entity's workflow") + raise ValidationError(entity.eid, {'to_state': msg}) + else: + # check transition is valid and allowed, unless we're coming back from + # subworkflow + tr = session.entity_from_eid(treid) + if swtr is None: + if tr is None: + msg = session._("transition doesn't belong to entity's workflow") + raise ValidationError(entity.eid, {'by_transition': msg}) + if not tr.has_input_state(fromstate): + msg = session._("transition isn't allowed") + raise ValidationError(entity.eid, {'by_transition': msg}) + if not tr.may_be_fired(foreid): + msg = session._("transition may not be fired") + raise ValidationError(entity.eid, {'by_transition': msg}) + if entity.get('to_state'): + deststateeid = entity['to_state'] + if not cowpowers and deststateeid != tr.destination().eid: + msg = session._("transition isn't allowed") + raise ValidationError(entity.eid, {'by_transition': msg}) + if swtr is None: + deststate = session.entity_from_eid(deststateeid) + if not cowpowers and deststate is None: + msg = entity.req._("state doesn't belong to entity's workflow") + raise ValidationError(entity.eid, {'to_state': msg}) else: - # not a transition - # check state is initial state if the workflow defines one - isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s', - {'etype': etype}) - if isrset and not toeid == isrset[0][0]: - _ = session._ - msg = _('%s is not the initial state (%s) for this entity') % ( - _(session.entity_from_eid(toeid).name), _(isrset.get_entity(0,0).name)) - raise ValidationError(fromeid, {'in_state': msg}) - eschema = session.repo.schema[etype] - if not 'wf_info_for' in eschema.object_relations(): - # workflow history not activated for this entity type - return - rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s' - args = {'comment': session.get_shared_data('trcomment', None, pop=True), - 'e': fromeid, 'ds': toeid} - cformat = session.get_shared_data('trcommentformat', None, pop=True) - if cformat is not None: - args['comment_format'] = cformat - rql += ', T comment_format %(comment_format)s' - restriction = ['DS eid %(ds)s, E eid %(e)s'] - if not state is None: # not a transition - rql += ', T from_state FS' - restriction.append('FS eid %(fs)s') - args['fs'] = state.eid - rql = '%s WHERE %s' % (rql, ', '.join(restriction)) - session.unsafe_execute(rql, args, 'e') + deststateeid = tr.destination().eid + # everything is ok, add missing information on the trinfo entity + entity['from_state'] = fromstate.eid + entity['to_state'] = deststateeid + nocheck = session.transaction_data.setdefault('skip-security', set()) + nocheck.add((entity.eid, 'from_state', fromstate.eid)) + nocheck.add((entity.eid, 'to_state', deststateeid)) + +def after_add_trinfo(session, entity): + """change related entity state""" + _change_state(session, entity['wf_info_for'], + entity['from_state'], entity['to_state']) + forentity = session.entity_from_eid(entity['wf_info_for']) + assert forentity.current_state.eid == entity['to_state'], forentity.current_state.name + if forentity.main_workflow.eid != forentity.current_workflow.eid: + # we're in a subworkflow, check if we've reached an exit point + wftr = forentity.subworkflow_input_transition() + if wftr is None: + # inconsistency detected + msg = entity.req._("state doesn't belong to entity's current workflow") + raise ValidationError(entity.eid, {'to_state': msg}) + tostate = wftr.get_exit_point(entity['to_state']) + if tostate is not None: + # reached an exit point + msg = session._('exiting from subworkflow %s') + msg %= session._(forentity.current_workflow.name) + session.transaction_data[(forentity.eid, 'subwfentrytr')] = True + # XXX iirk + req = forentity.req + forentity.req = session.super_session + try: + trinfo = forentity.change_state(tostate, msg, u'text/plain', + tr=wftr) + finally: + forentity.req = req class SetInitialStateOp(PreCommitOperation): @@ -473,26 +552,125 @@ # if there is an initial state and the entity's state is not set, # use the initial state as a default state pendingeids = session.transaction_data.get('pendingeids', ()) - if not entity.eid in pendingeids and not entity.in_state: - rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s', - {'name': entity.id}) - if rset: - session.add_relation(entity.eid, 'in_state', rset[0][0]) + if not entity.eid in pendingeids and not entity.in_state and \ + entity.main_workflow: + state = entity.main_workflow.initial + if state: + # use super session to by-pass security checks + session.super_session.add_relation(entity.eid, 'in_state', + state.eid) def set_initial_state_after_add(session, entity): SetInitialStateOp(session, entity=entity) +def before_add_in_state(session, eidfrom, rtype, eidto): + """check state apply, in case of direct in_state change using unsafe_execute + """ + nocheck = session.transaction_data.setdefault('skip-security', ()) + if (eidfrom, 'in_state', eidto) in nocheck: + # state changed through TrInfo insertion, so we already know it's ok + return + entity = session.entity_from_eid(eidfrom) + mainwf = entity.main_workflow + if mainwf is None: + msg = session._('entity has no workflow set') + raise ValidationError(entity.eid, {None: msg}) + for wf in mainwf.iter_workflows(): + if wf.state_by_eid(eidto): + break + else: + msg = session._("state doesn't belong to entity's workflow. You may " + "want to set a custom workflow for this entity first.") + raise ValidationError(eidfrom, {'in_state': msg}) + if entity.current_workflow and wf.eid != entity.current_workflow.eid: + msg = session._("state doesn't belong to entity's current workflow") + raise ValidationError(eidfrom, {'in_state': msg}) + + +class CheckTrExitPoint(PreCommitOperation): + + def precommit_event(self): + tr = self.session.entity_from_eid(self.treid) + outputs = set() + for ep in tr.subworkflow_exit: + if ep.subwf_state.eid in outputs: + msg = self.session._("can't have multiple exits on the same state") + raise ValidationError(self.treid, {'subworkflow_exit': msg}) + outputs.add(ep.subwf_state.eid) + + +def after_add_subworkflow_exit(session, eidfrom, rtype, eidto): + CheckTrExitPoint(session, treid=eidfrom) + + +class WorkflowChangedOp(PreCommitOperation): + """fix entity current state when changing its workflow""" + + def precommit_event(self): + # notice that enforcement that new workflow apply to the entity's type is + # done by schema rule, no need to check it here + session = self.session + pendingeids = session.transaction_data.get('pendingeids', ()) + if self.eid in pendingeids: + return + entity = session.entity_from_eid(self.eid) + # check custom workflow has not been rechanged to another one in the same + # transaction + mainwf = entity.main_workflow + if mainwf.eid == self.wfeid: + deststate = mainwf.initial + if not deststate: + msg = session._('workflow has no initial state') + raise ValidationError(entity.eid, {'custom_workflow': msg}) + if mainwf.state_by_eid(entity.current_state.eid): + # nothing to do + return + # if there are no history, simply go to new workflow's initial state + if not entity.workflow_history: + if entity.current_state.eid != deststate.eid: + _change_state(session, entity.eid, + entity.current_state.eid, deststate.eid) + return + msg = session._('workflow changed to "%s"') + msg %= session._(mainwf.name) + session.transaction_data[(entity.eid, 'customwf')] = self.wfeid + entity.change_state(deststate, msg, u'text/plain') + + +def set_custom_workflow(session, eidfrom, rtype, eidto): + WorkflowChangedOp(session, eid=eidfrom, wfeid=eidto) + + +def del_custom_workflow(session, eidfrom, rtype, eidto): + entity = session.entity_from_eid(eidfrom) + typewf = entity.cwetype_workflow() + if typewf is not None: + WorkflowChangedOp(session, eid=eidfrom, wfeid=typewf.eid) + + +def after_del_workflow(session, eid): + # workflow cleanup + session.execute('DELETE State X WHERE NOT X state_of Y') + session.execute('DELETE Transition X WHERE NOT X transition_of Y') + + def _register_wf_hooks(hm): """register workflow related hooks on the hooks manager""" if 'in_state' in hm.schema: - hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state') - hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') + hm.register_hook(before_add_trinfo, 'before_add_entity', 'TrInfo') + hm.register_hook(after_add_trinfo, 'after_add_entity', 'TrInfo') + #hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') for eschema in hm.schema.entities(): if 'in_state' in eschema.subject_relations(): hm.register_hook(set_initial_state_after_add, 'after_add_entity', str(eschema)) + hm.register_hook(set_custom_workflow, 'after_add_relation', 'custom_workflow') + hm.register_hook(del_custom_workflow, 'after_delete_relation', 'custom_workflow') + hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow') + hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state') + hm.register_hook(after_add_subworkflow_exit, 'after_add_relation', 'subworkflow_exit') # CWProperty hooks ############################################################# diff -r 24489cbbd697 -r 70c0dd1c3b7d server/hooksmanager.py --- a/server/hooksmanager.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/hooksmanager.py Thu Sep 17 14:53:18 2009 +0200 @@ -268,3 +268,80 @@ 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 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 + session.unsafe_execute( + '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), + {'x': meid, 'e': seid}, ('x', 'e')) + + +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]) + execute = session.unsafe_execute + for rel in self.subject_relations: + if eschema.has_subject_relation(rel): + execute('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): + execute('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') + + +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]) + execute = session.unsafe_execute + for rel in self.subject_relations: + if eschema.has_subject_relation(rel): + execute('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): + execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'R %s X' % (rtype, rel), + {'x': fromeid, 'p': toeid}, 'x') diff -r 24489cbbd697 -r 70c0dd1c3b7d server/migractions.py --- a/server/migractions.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/migractions.py Thu Sep 17 14:53:18 2009 +0200 @@ -116,7 +116,7 @@ config = self.config repo = self.repo_connect() # paths - timestamp = datetime.now().strftime('%Y-%m-%d_%H:%M:%S') + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') instbkdir = osp.join(config.appdatahome, 'backup') if not osp.exists(instbkdir): os.makedirs(instbkdir) @@ -415,11 +415,12 @@ espschema = eschema.specializes() if repospschema and not espschema: self.rqlexec('DELETE X specializes Y WHERE X is CWEType, X name %(x)s', - {'x': str(repoeschema)}) + {'x': str(repoeschema)}, ask_confirm=False) elif not repospschema and espschema: self.rqlexec('SET X specializes Y WHERE X is CWEType, X name %(x)s, ' 'Y is CWEType, Y name %(y)s', - {'x': str(repoeschema), 'y': str(espschema)}) + {'x': str(repoeschema), 'y': str(espschema)}, + ask_confirm=False) self.rqlexecall(ss.updateeschema2rql(eschema), ask_confirm=self.verbosity >= 2) for rschema, targettypes, role in eschema.relation_definitions(True): @@ -959,78 +960,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() @@ -1047,32 +1048,26 @@ prop = self.rqlexec('CWProperty X WHERE X pkey %(k)s', {'k': pkey}, ask_confirm=False).get_entity(0, 0) except: - self.cmd_add_entity('CWProperty', pkey=unicode(pkey), value=value) + self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value) else: self.rqlexec('SET X value %(v)s WHERE X pkey %(k)s', {'k': pkey, 'v': value}, ask_confirm=False) # other data migration commands ########################################### + def cmd_create_entity(self, etype, *args, **kwargs): + """add a new entity of the given type""" + commit = kwargs.pop('commit', False) + self.session.set_pool() + entity = self.session.create_entity(etype, *args, **kwargs) + if commit: + self.commit() + return entity + + @deprecated('use create_entity') def cmd_add_entity(self, etype, *args, **kwargs): """add a new entity of the given type""" - rql = 'INSERT %s X' % etype - relations = [] - restrictions = [] - for rtype, rvar in args: - relations.append('X %s %s' % (rtype, rvar)) - restrictions.append('%s eid %s' % (rvar, kwargs.pop(rvar))) - commit = kwargs.pop('commit', False) - for attr in kwargs: - relations.append('X %s %%(%s)s' % (attr, attr)) - if relations: - rql = '%s: %s' % (rql, ', '.join(relations)) - if restrictions: - rql = '%s WHERE %s' % (rql, ', '.join(restrictions)) - eid = self.rqlexec(rql, kwargs, ask_confirm=self.verbosity>=2).rows[0][0] - if commit: - self.commit() - return eid + return self.cmd_create_entity(etype, *args, **kwargs).eid def sqlexec(self, sql, args=None, ask_confirm=True): """execute the given sql if confirmed diff -r 24489cbbd697 -r 70c0dd1c3b7d server/msplanner.py --- a/server/msplanner.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/msplanner.py Thu Sep 17 14:53:18 2009 +0200 @@ -97,9 +97,6 @@ # str() Constant.value to ensure generated table name won't be unicode Constant._ms_table_key = lambda x: str(x.value) -AbstractSource.dont_cross_relations = () -AbstractSource.cross_relations = () - def need_source_access_relation(vargraph): if not vargraph: return False @@ -347,7 +344,7 @@ # * at least one supported relation specified if not varobj._q_invariant or \ any(imap(source.support_relation, - (r.r_type for r in rels if r.r_type != 'eid'))): + (r.r_type for r in rels if r.r_type not in ('identity', 'eid')))): sourcesterms.setdefault(source, {}).setdefault(varobj, set()).add(i) # if variable is not invariant and is used by a relation # not supported by this source, we'll have to split the diff -r 24489cbbd697 -r 70c0dd1c3b7d server/querier.py --- a/server/querier.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/querier.py Thu Sep 17 14:53:18 2009 +0200 @@ -138,8 +138,8 @@ # various resource accesors self.querier = querier self.schema = querier.schema - self.rqlhelper = querier._rqlhelper self.sqlannotate = querier.sqlgen_annotate + self.rqlhelper = session.vreg.rqlhelper def annotate_rqlst(self): if not self.rqlst.annotated: @@ -265,6 +265,8 @@ myrqlst = select.copy(solutions=lchecksolutions) myunion.append(myrqlst) # in-place rewrite + annotation / simplification + lcheckdef = [((varmap, 'X'), rqlexprs) + for varmap, rqlexprs in lcheckdef] rewrite(myrqlst, lcheckdef, lchecksolutions, self.args) noinvariant.update(noinvariant_vars(restricted, myrqlst, nbtrees)) if () in localchecks: @@ -524,37 +526,33 @@ def set_schema(self, schema): self.schema = schema + repo = self._repo # rql parsing / analysing helper - self._rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid', - 'has_text': 'fti'}) - self._rql_cache = Cache(self._repo.config['rql-cache-size']) + self.solutions = repo.vreg.solutions + self._rql_cache = Cache(repo.config['rql-cache-size']) self.cache_hit, self.cache_miss = 0, 0 # rql planner # note: don't use repo.sources, may not be built yet, and also "admin" # isn't an actual source - if len([uri for uri in self._repo.config.sources() if uri != 'admin']) < 2: + rqlhelper = repo.vreg.rqlhelper + self._parse = rqlhelper.parse + self._annotate = rqlhelper.annotate + if len([uri for uri in repo.config.sources() if uri != 'admin']) < 2: from cubicweb.server.ssplanner import SSPlanner - self._planner = SSPlanner(schema, self._rqlhelper) + self._planner = SSPlanner(schema, rqlhelper) else: from cubicweb.server.msplanner import MSPlanner - self._planner = MSPlanner(schema, self._rqlhelper) + self._planner = MSPlanner(schema, rqlhelper) # sql generation annotator self.sqlgen_annotate = SQLGenAnnotator(schema).annotate def parse(self, rql, annotate=False): """return a rql syntax tree for the given rql""" try: - return self._rqlhelper.parse(unicode(rql), annotate=annotate) + return self._parse(unicode(rql), annotate=annotate) except UnicodeError: raise RQLSyntaxError(rql) - def solutions(self, session, rqlst, args): - assert session is not None - def type_from_eid(eid, type_from_eid=self._repo.type_from_eid, - session=session): - return type_from_eid(eid, session) - self._rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args) - def plan_factory(self, rqlst, args, session): """create an execution plan for an INSERT RQL query""" if rqlst.TYPE == 'insert': @@ -642,7 +640,7 @@ # bother modifying it. This is not necessary on write queries since # a new syntax tree is built from them. rqlst = rqlst.copy() - self._rqlhelper.annotate(rqlst) + self._annotate(rqlst) # make an execution plan plan = self.plan_factory(rqlst, args, session) plan.cache_key = cachekey diff -r 24489cbbd697 -r 70c0dd1c3b7d server/repository.py --- a/server/repository.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/repository.py Thu Sep 17 14:53:18 2009 +0200 @@ -30,7 +30,7 @@ from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP, CW_EVENT_MANAGER, UnknownEid, AuthenticationError, ExecutionError, - ETypeNotSupportedBySources, RTypeNotSupportedBySources, + ETypeNotSupportedBySources, MultiSourcesError, BadConnectionId, Unauthorized, ValidationError, typed_eid) from cubicweb.cwvreg import CubicWebVRegistry @@ -108,6 +108,9 @@ # hooks responsability to ensure they do not violate relation's cardinality if session.is_super_session: return + ensure_card_respected(session.unsafe_execute, session, eidfrom, rtype, eidto) + +def ensure_card_respected(execute, session, eidfrom, rtype, eidto): card = rproperty(session, rtype, eidfrom, eidto, 'cardinality') # one may be tented to check for neweids but this may cause more than one # relation even with '1?' cardinality if thoses relations are added in the @@ -118,14 +121,11 @@ if card[0] in '1?': rschema = session.repo.schema.rschema(rtype) if not rschema.inlined: - session.unsafe_execute( - 'DELETE X %s Y WHERE X eid %%(x)s, NOT Y eid %%(y)s' % rtype, - {'x': eidfrom, 'y': eidto}, 'x') + execute('DELETE X %s Y WHERE X eid %%(x)s,NOT Y eid %%(y)s' % rtype, + {'x': eidfrom, 'y': eidto}, 'x') if card[1] in '1?': - session.unsafe_execute( - 'DELETE X %s Y WHERE NOT X eid %%(x)s, Y eid %%(y)s' % rtype, - {'x': eidfrom, 'y': eidto}, 'y') - + execute('DELETE X %s Y WHERE NOT X eid %%(x)s, Y eid %%(y)s' % rtype, + {'x': eidfrom, 'y': eidto}, 'y') class Repository(object): """a repository provides access to a set of persistent storages for @@ -149,6 +149,7 @@ self._running_threads = [] # initial schema, should be build or replaced latter self.schema = CubicWebSchema(config.appid) + self.vreg.schema = self.schema # until actual schema is loaded... # querier helper, need to be created after sources initialization self.querier = QuerierHelper(self, self.schema) # should we reindex in changes? @@ -192,13 +193,14 @@ config.bootstrap_cubes() self.set_bootstrap_schema(config.load_schema()) # need to load the Any and CWUser entity types - self.vreg.schema = self.schema etdirectory = join(CW_SOFTWARE_ROOT, 'entities') self.vreg.init_registration([etdirectory]) self.vreg.load_file(join(etdirectory, '__init__.py'), '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") @@ -244,15 +246,16 @@ if rebuildinfered: schema.rebuild_infered_relations() self.info('set schema %s %#x', schema.name, id(schema)) - self.debug(', '.join(sorted(str(e) for e in schema.entities()))) + if resetvreg: + # full reload of all appobjects + self.vreg.reset() + self.vreg.set_schema(schema) + else: + self.vreg._set_schema(schema) self.querier.set_schema(schema) for source in self.sources: source.set_schema(schema) self.schema = schema - if resetvreg: - # full reload of all appobjects - self.vreg.reset() - self.vreg.set_schema(schema) self.reset_hooks() def reset_hooks(self): @@ -970,12 +973,21 @@ def locate_relation_source(self, session, subject, rtype, object): subjsource = self.source_from_eid(subject, session) objsource = self.source_from_eid(object, session) - if not (subjsource is objsource and subjsource.support_relation(rtype, 1)): + if not subjsource is objsource: source = self.system_source - if not source.support_relation(rtype, 1): - raise RTypeNotSupportedBySources(rtype) + if not (subjsource.may_cross_relation(rtype) + and objsource.may_cross_relation(rtype)): + raise MultiSourcesError( + "relation %s can't be crossed among sources" + % rtype) + elif not subjsource.support_relation(rtype): + source = self.system_source else: source = subjsource + if not source.support_relation(rtype, True): + raise MultiSourcesError( + "source %s doesn't support write of %s relation" + % (source.uri, rtype)) return source def locate_etype_source(self, etype): diff -r 24489cbbd697 -r 70c0dd1c3b7d server/rqlannotation.py --- a/server/rqlannotation.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/rqlannotation.py Thu Sep 17 14:53:18 2009 +0200 @@ -331,7 +331,7 @@ if isinstance(term, VariableRef) and self.is_ambiguous(term.variable): var = term.variable if len(var.stinfo['relations'] - var.stinfo['typerels']) == 1 \ - or rel.sqlscope is var.sqlscope: + or rel.sqlscope is var.sqlscope or rel.r_type == 'identity': self.restrict(var, frozenset(etypes_func())) try: self.maydeambrels[var].add(rel) diff -r 24489cbbd697 -r 70c0dd1c3b7d server/rqlrewrite.py --- a/server/rqlrewrite.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,396 +0,0 @@ -"""RQL rewriting utilities, used for read security checking - -:organization: Logilab -:copyright: 2007-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 rql import nodes, stmts, TypeResolverException -from cubicweb import Unauthorized, server, typed_eid -from cubicweb.server.ssplanner import add_types_restriction - -def remove_solutions(origsolutions, solutions, defined): - """when a rqlst has been generated from another by introducing security - assertions, this method returns solutions which are contained in orig - solutions - """ - newsolutions = [] - for origsol in origsolutions: - for newsol in solutions[:]: - for var, etype in origsol.items(): - try: - if newsol[var] != etype: - try: - defined[var].stinfo['possibletypes'].remove(newsol[var]) - except KeyError: - pass - break - except KeyError: - # variable has been rewritten - continue - else: - newsolutions.append(newsol) - solutions.remove(newsol) - return newsolutions - -class Unsupported(Exception): pass - -class RQLRewriter(object): - """insert some rql snippets into another rql syntax tree""" - def __init__(self, querier, session): - self.session = session - self.annotate = querier._rqlhelper.annotate - self._compute_solutions = querier.solutions - self.schema = querier.schema - - def compute_solutions(self): - self.annotate(self.select) - try: - self._compute_solutions(self.session, self.select, self.kwargs) - except TypeResolverException: - raise Unsupported() - if len(self.select.solutions) < len(self.solutions): - raise Unsupported() - - def rewrite(self, select, snippets, solutions, kwargs): - if server.DEBUG: - print '---- rewrite', select, snippets, solutions - self.select = select - self.solutions = solutions - self.kwargs = kwargs - self.u_varname = None - self.removing_ambiguity = False - self.exists_snippet = {} - # we have to annotate the rqlst before inserting snippets, even though - # we'll have to redo it latter - self.annotate(select) - self.insert_snippets(snippets) - if not self.exists_snippet and self.u_varname: - # U has been inserted than cancelled, cleanup - select.undefine_variable(select.defined_vars[self.u_varname]) - # clean solutions according to initial solutions - newsolutions = remove_solutions(solutions, select.solutions, - select.defined_vars) - assert len(newsolutions) >= len(solutions), \ - 'rewritten rql %s has lost some solutions, there is probably something '\ - 'wrong in your schema permission (for instance using a '\ - 'RQLExpression which insert a relation which doesn\'t exists in '\ - 'the schema)\nOrig solutions: %s\nnew solutions: %s' % ( - select, solutions, newsolutions) - if len(newsolutions) > len(solutions): - # the snippet has introduced some ambiguities, we have to resolve them - # "manually" - variantes = self.build_variantes(newsolutions) - # insert "is" where necessary - varexistsmap = {} - self.removing_ambiguity = True - for (erqlexpr, mainvar, oldvarname), etype in variantes[0].iteritems(): - varname = self.rewritten[(erqlexpr, mainvar, oldvarname)] - var = select.defined_vars[varname] - exists = var.references()[0].scope - exists.add_constant_restriction(var, 'is', etype, 'etype') - varexistsmap[mainvar] = exists - # insert ORED exists where necessary - for variante in variantes[1:]: - self.insert_snippets(snippets, varexistsmap) - for (erqlexpr, mainvar, oldvarname), etype in variante.iteritems(): - varname = self.rewritten[(erqlexpr, mainvar, oldvarname)] - try: - var = select.defined_vars[varname] - except KeyError: - # not a newly inserted variable - continue - exists = var.references()[0].scope - exists.add_constant_restriction(var, 'is', etype, 'etype') - # recompute solutions - #select.annotated = False # avoid assertion error - self.compute_solutions() - # clean solutions according to initial solutions - newsolutions = remove_solutions(solutions, select.solutions, - select.defined_vars) - select.solutions = newsolutions - add_types_restriction(self.schema, select) - if server.DEBUG: - print '---- rewriten', select - - def build_variantes(self, newsolutions): - variantes = set() - for sol in newsolutions: - variante = [] - for (erqlexpr, mainvar, oldvar), newvar in self.rewritten.iteritems(): - variante.append( ((erqlexpr, mainvar, oldvar), sol[newvar]) ) - variantes.add(tuple(variante)) - # rebuild variantes as dict - variantes = [dict(variante) for variante in variantes] - # remove variable which have always the same type - for erqlexpr, mainvar, oldvar in self.rewritten: - it = iter(variantes) - etype = it.next()[(erqlexpr, mainvar, oldvar)] - for variante in it: - if variante[(erqlexpr, mainvar, oldvar)] != etype: - break - else: - for variante in variantes: - del variante[(erqlexpr, mainvar, oldvar)] - return variantes - - def insert_snippets(self, snippets, varexistsmap=None): - self.rewritten = {} - for varname, erqlexprs in snippets: - if varexistsmap is not None and not varname in varexistsmap: - continue - try: - self.const = typed_eid(varname) - self.varname = self.const - self.rhs_rels = self.lhs_rels = {} - except ValueError: - self.varname = varname - self.const = None - self.varstinfo = stinfo = self.select.defined_vars[varname].stinfo - if varexistsmap is None: - self.rhs_rels = dict( (rel.r_type, rel) for rel in stinfo['rhsrelations']) - self.lhs_rels = dict( (rel.r_type, rel) for rel in stinfo['relations'] - if not rel in stinfo['rhsrelations']) - else: - self.rhs_rels = self.lhs_rels = {} - parent = None - inserted = False - for erqlexpr in erqlexprs: - self.current_expr = erqlexpr - if varexistsmap is None: - try: - new = self.insert_snippet(varname, erqlexpr.snippet_rqlst, parent) - except Unsupported: - continue - inserted = True - if new is not None: - self.exists_snippet[erqlexpr] = new - parent = parent or new - else: - # called to reintroduce snippet due to ambiguity creation, - # so skip snippets which are not introducing this ambiguity - exists = varexistsmap[varname] - if self.exists_snippet[erqlexpr] is exists: - self.insert_snippet(varname, erqlexpr.snippet_rqlst, exists) - if varexistsmap is None and not inserted: - # no rql expression found matching rql solutions. User has no access right - raise Unauthorized() - - def insert_snippet(self, varname, snippetrqlst, parent=None): - new = snippetrqlst.where.accept(self) - if new is not None: - try: - var = self.select.defined_vars[varname] - except KeyError: - # not a variable - pass - else: - if var.stinfo['optrelations']: - # use a subquery - subselect = stmts.Select() - subselect.append_selected(nodes.VariableRef(subselect.get_variable(varname))) - subselect.add_restriction(new.copy(subselect)) - aliases = [varname] - for rel in var.stinfo['relations']: - rschema = self.schema.rschema(rel.r_type) - if rschema.is_final() or (rschema.inlined and not rel in var.stinfo['rhsrelations']): - self.select.remove_node(rel) - rel.children[0].name = varname - subselect.add_restriction(rel.copy(subselect)) - for vref in rel.children[1].iget_nodes(nodes.VariableRef): - subselect.append_selected(vref.copy(subselect)) - aliases.append(vref.name) - if self.u_varname: - # generate an identifier for the substitution - argname = subselect.allocate_varname() - while argname in self.kwargs: - argname = subselect.allocate_varname() - subselect.add_constant_restriction(subselect.get_variable(self.u_varname), - 'eid', unicode(argname), 'Substitute') - self.kwargs[argname] = self.session.user.eid - add_types_restriction(self.schema, subselect, subselect, solutions=self.solutions) - assert parent is None - myunion = stmts.Union() - myunion.append(subselect) - aliases = [nodes.VariableRef(self.select.get_variable(name, i)) - for i, name in enumerate(aliases)] - self.select.add_subquery(nodes.SubQuery(aliases, myunion), check=False) - self._cleanup_inserted(new) - try: - self.compute_solutions() - except Unsupported: - # some solutions have been lost, can't apply this rql expr - self.select.remove_subquery(new, undefine=True) - raise - return - new = nodes.Exists(new) - if parent is None: - self.select.add_restriction(new) - else: - grandpa = parent.parent - or_ = nodes.Or(parent, new) - grandpa.replace(parent, or_) - if not self.removing_ambiguity: - try: - self.compute_solutions() - except Unsupported: - # some solutions have been lost, can't apply this rql expr - if parent is None: - self.select.remove_node(new, undefine=True) - else: - parent.parent.replace(or_, or_.children[0]) - self._cleanup_inserted(new) - raise - return new - - def _cleanup_inserted(self, node): - # cleanup inserted variable references - for vref in node.iget_nodes(nodes.VariableRef): - vref.unregister_reference() - if not vref.variable.stinfo['references']: - # no more references, undefine the variable - del self.select.defined_vars[vref.name] - - def _visit_binary(self, node, cls): - newnode = cls() - for c in node.children: - new = c.accept(self) - if new is None: - continue - newnode.append(new) - if len(newnode.children) == 0: - return None - if len(newnode.children) == 1: - return newnode.children[0] - return newnode - - def _visit_unary(self, node, cls): - newc = node.children[0].accept(self) - if newc is None: - return None - newnode = cls() - newnode.append(newc) - return newnode - - def visit_and(self, et): - return self._visit_binary(et, nodes.And) - - def visit_or(self, ou): - return self._visit_binary(ou, nodes.Or) - - def visit_not(self, node): - return self._visit_unary(node, nodes.Not) - - def visit_exists(self, node): - return self._visit_unary(node, nodes.Exists) - - def visit_relation(self, relation): - lhs, rhs = relation.get_variable_parts() - if lhs.name == 'X': - # on lhs - # see if we can reuse this relation - if relation.r_type in self.lhs_rels and isinstance(rhs, nodes.VariableRef) and rhs.name != 'U': - if self._may_be_shared(relation, 'object'): - # ok, can share variable - term = self.lhs_rels[relation.r_type].children[1].children[0] - self._use_outer_term(rhs.name, term) - return - elif isinstance(rhs, nodes.VariableRef) and rhs.name == 'X' and lhs.name != 'U': - # on rhs - # see if we can reuse this relation - if relation.r_type in self.rhs_rels and self._may_be_shared(relation, 'subject'): - # ok, can share variable - term = self.rhs_rels[relation.r_type].children[0] - self._use_outer_term(lhs.name, term) - return - rel = nodes.Relation(relation.r_type, relation.optional) - for c in relation.children: - rel.append(c.accept(self)) - return rel - - def visit_comparison(self, cmp): - cmp_ = nodes.Comparison(cmp.operator) - for c in cmp.children: - cmp_.append(c.accept(self)) - return cmp_ - - def visit_mathexpression(self, mexpr): - cmp_ = nodes.MathExpression(mexpr.operator) - for c in cmp.children: - cmp_.append(c.accept(self)) - return cmp_ - - def visit_function(self, function): - """generate filter name for a function""" - function_ = nodes.Function(function.name) - for c in function.children: - function_.append(c.accept(self)) - return function_ - - def visit_constant(self, constant): - """generate filter name for a constant""" - return nodes.Constant(constant.value, constant.type) - - def visit_variableref(self, vref): - """get the sql name for a variable reference""" - if vref.name == 'X': - if self.const is not None: - return nodes.Constant(self.const, 'Int') - return nodes.VariableRef(self.select.get_variable(self.varname)) - vname_or_term = self._get_varname_or_term(vref.name) - if isinstance(vname_or_term, basestring): - return nodes.VariableRef(self.select.get_variable(vname_or_term)) - # shared term - return vname_or_term.copy(self.select) - - def _may_be_shared(self, relation, target): - """return True if the snippet relation can be skipped to use a relation - from the original query - """ - # if cardinality is in '?1', we can ignore the relation and use variable - # from the original query - rschema = self.schema.rschema(relation.r_type) - if target == 'object': - cardindex = 0 - ttypes_func = rschema.objects - rprop = rschema.rproperty - else: # target == 'subject': - cardindex = 1 - ttypes_func = rschema.subjects - rprop = lambda x, y, z: rschema.rproperty(y, x, z) - for etype in self.varstinfo['possibletypes']: - for ttype in ttypes_func(etype): - if rprop(etype, ttype, 'cardinality')[cardindex] in '+*': - return False - return True - - def _use_outer_term(self, snippet_varname, term): - key = (self.current_expr, self.varname, snippet_varname) - if key in self.rewritten: - insertedvar = self.select.defined_vars.pop(self.rewritten[key]) - for inserted_vref in insertedvar.references(): - inserted_vref.parent.replace(inserted_vref, term.copy(self.select)) - self.rewritten[key] = term - - def _get_varname_or_term(self, vname): - if vname == 'U': - if self.u_varname is None: - select = self.select - self.u_varname = select.allocate_varname() - # generate an identifier for the substitution - argname = select.allocate_varname() - while argname in self.kwargs: - argname = select.allocate_varname() - # insert "U eid %(u)s" - var = select.get_variable(self.u_varname) - select.add_constant_restriction(select.get_variable(self.u_varname), - 'eid', unicode(argname), 'Substitute') - self.kwargs[argname] = self.session.user.eid - return self.u_varname - key = (self.current_expr, self.varname, vname) - try: - return self.rewritten[key] - except KeyError: - self.rewritten[key] = newvname = self.select.allocate_varname() - return newvname diff -r 24489cbbd697 -r 70c0dd1c3b7d server/schemahooks.py --- a/server/schemahooks.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/schemahooks.py Thu Sep 17 14:53:18 2009 +0200 @@ -759,8 +759,7 @@ def after_del_eetype(session, eid): # workflow cleanup - session.execute('DELETE State X WHERE NOT X state_of Y') - session.execute('DELETE Transition X WHERE NOT X transition_of Y') + session.execute('DELETE Workflow X WHERE NOT X workflow_of Y') def before_del_ertype(session, eid): diff -r 24489cbbd697 -r 70c0dd1c3b7d server/schemaserial.py --- a/server/schemaserial.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/schemaserial.py Thu Sep 17 14:53:18 2009 +0200 @@ -110,7 +110,6 @@ print sql sqlcu.execute(sql) # other table renaming done once schema has been read - # print 'reading schema from the database...' index = {} permsdict = deserialize_ertype_permissions(session) schema.reading_from_database = True diff -r 24489cbbd697 -r 70c0dd1c3b7d server/securityhooks.py --- a/server/securityhooks.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/securityhooks.py Thu Sep 17 14:53:18 2009 +0200 @@ -70,11 +70,17 @@ def before_add_relation(session, fromeid, rtype, toeid): if rtype in BEFORE_ADD_RELATIONS and not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return rschema = session.repo.schema[rtype] rschema.check_perm(session, 'add', fromeid, toeid) def after_add_relation(session, fromeid, rtype, toeid): if not rtype in BEFORE_ADD_RELATIONS and not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return rschema = session.repo.schema[rtype] if rtype in ON_COMMIT_ADD_RELATIONS: CheckRelationPermissionOp(session, action='add', rschema=rschema, @@ -84,6 +90,9 @@ def before_del_relation(session, fromeid, rtype, toeid): if not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return session.repo.schema[rtype].check_perm(session, 'delete', fromeid, toeid) def register_security_hooks(hm): diff -r 24489cbbd697 -r 70c0dd1c3b7d server/serverctl.py --- a/server/serverctl.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/serverctl.py Thu Sep 17 14:53:18 2009 +0200 @@ -312,7 +312,8 @@ # postgres specific stuff if driver == 'postgres': # install plpythonu/plpgsql language if not installed by the cube - for extlang in ('plpythonu', 'plpgsql'): + langs = ('plpgsql',) if sys.platform == 'win32' else ('plpythonu', 'plpgsql') + for extlang in langs: helper.create_language(cursor, extlang) cursor.close() cnx.commit() @@ -676,7 +677,7 @@ import tempfile srcappid = pop_arg(args, 1, msg='No source instance specified !') destappid = pop_arg(args, msg='No destination instance specified !') - output = tempfile.mkstemp(dir='/tmp/')[1] + output = tempfile.mkstemp()[1] if ':' in srcappid: host, srcappid = srcappid.split(':') _remote_dump(host, srcappid, output, self.config.sudo) diff -r 24489cbbd697 -r 70c0dd1c3b7d server/session.py --- a/server/session.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/session.py Thu Sep 17 14:53:18 2009 +0200 @@ -18,7 +18,7 @@ from cubicweb import RequestSessionMixIn, Binary, UnknownEid from cubicweb.dbapi import ConnectionProperties from cubicweb.utils import make_uid -from cubicweb.server.rqlrewrite import RQLRewriter +from cubicweb.rqlrewrite import RQLRewriter ETYPE_PYOBJ_MAP[Binary] = 'Bytes' @@ -210,13 +210,18 @@ vreg = self.vreg language = language or self.user.property_value('ui.language') try: - self._ = self.__ = vreg.config.translations[language] + gettext, pgettext = vreg.config.translations[language] + self._ = self.__ = gettext + self.pgettext = pgettext except KeyError: language = vreg.property_value('ui.language') try: - self._ = self.__ = vreg.config.translations[language] + gettext, pgettext = vreg.config.translations[language] + self._ = self.__ = gettext + self.pgettext = pgettext except KeyError: self._ = self.__ = unicode + self.pgettext = lambda x,y: y self.lang = language def change_property(self, prop, value): @@ -543,7 +548,7 @@ try: return self._threaddata._rewriter except AttributeError: - self._threaddata._rewriter = RQLRewriter(self.repo.querier, self) + self._threaddata._rewriter = RQLRewriter(self) return self._threaddata._rewriter def build_description(self, rqlst, args, result): diff -r 24489cbbd697 -r 70c0dd1c3b7d server/sources/__init__.py --- a/server/sources/__init__.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/sources/__init__.py Thu Sep 17 14:53:18 2009 +0200 @@ -80,6 +80,11 @@ # a reference to the instance'schema (may differs from the source'schema) schema = None + # multi-sources planning control + dont_cross_relations = () + cross_relations = () + + def __init__(self, repo, appschema, source_config, *args, **kwargs): self.repo = repo self.uri = source_config['uri'] @@ -177,6 +182,19 @@ return wsupport return True + def may_cross_relation(self, rtype): + """return True if the relation may be crossed among sources. Rules are: + + * if this source support the relation, can't be crossed unless explicitly + specified in .cross_relations + + * if this source doesn't support the relation, can be crossed unless + explicitly specified in .dont_cross_relations + """ + if self.support_relation(rtype): + return rtype in self.cross_relations + return rtype not in self.dont_cross_relations + def eid2extid(self, eid, session=None): return self.repo.eid2extid(self, eid, session) diff -r 24489cbbd697 -r 70c0dd1c3b7d server/sources/ldapuser.py --- a/server/sources/ldapuser.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/sources/ldapuser.py Thu Sep 17 14:53:18 2009 +0200 @@ -336,7 +336,7 @@ if sol[varname] == 'CWUser': mainvars.append(varname) break - assert mainvars + assert mainvars, rqlst columns, globtransforms = self.prepare_columns(mainvars, rqlst) eidfilters = [] allresults = [] diff -r 24489cbbd697 -r 70c0dd1c3b7d server/sources/native.py --- a/server/sources/native.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/sources/native.py Thu Sep 17 14:53:18 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 @@ -242,7 +231,7 @@ # ISource interface ####################################################### def compile_rql(self, rql): - rqlst = self.repo.querier._rqlhelper.parse(rql) + rqlst = self.repo.vreg.rqlhelper.parse(rql) rqlst.restricted_vars = () rqlst.children[0].solutions = self._sols self.repo.querier.sqlgen_annotate(rqlst) @@ -279,6 +268,9 @@ # can't claim not supporting a relation return True #not rtype == 'content_for' + def may_cross_relation(self, rtype): + return True + def authenticate(self, session, login, password): """return CWUser eid for the given login/password if this account is defined in this source, else raise `AuthenticationError` @@ -549,13 +541,12 @@ """ attrs = {'eid': eid} session.system_sql(self.sqlgen.delete('entities', attrs), attrs) - if self.has_deleted_entitites_table: - if extid is not None: - assert isinstance(extid, str), type(extid) - extid = b64encode(extid) - attrs = {'type': etype, 'eid': eid, 'extid': extid, - 'source': uri, 'dtime': datetime.now()} - session.system_sql(self.sqlgen.insert('deleted_entities', attrs), attrs) + if extid is not None: + assert isinstance(extid, str), type(extid) + extid = b64encode(extid) + attrs = {'type': etype, 'eid': eid, 'extid': extid, + 'source': uri, 'dtime': datetime.now()} + session.system_sql(self.sqlgen.insert('deleted_entities', attrs), attrs) def fti_unindex_entity(self, session, eid): """remove text content for entity with the given eid from the full text diff -r 24489cbbd697 -r 70c0dd1c3b7d server/sources/pyrorql.py --- a/server/sources/pyrorql.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/sources/pyrorql.py Thu Sep 17 14:53:18 2009 +0200 @@ -345,6 +345,7 @@ cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs, 'x') self._query_cache.clear() + entity.clear_all_caches() def delete_entity(self, session, etype, eid): """delete an entity from the source""" @@ -360,6 +361,8 @@ {'x': self.eid2extid(subject, session), 'y': self.eid2extid(object, session)}, ('x', 'y')) self._query_cache.clear() + session.entity_from_eid(subject).clear_all_caches() + session.entity_from_eid(object).clear_all_caches() def delete_relation(self, session, subject, rtype, object): """delete a relation from the source""" @@ -368,6 +371,8 @@ {'x': self.eid2extid(subject, session), 'y': self.eid2extid(object, session)}, ('x', 'y')) self._query_cache.clear() + session.entity_from_eid(subject).clear_all_caches() + session.entity_from_eid(object).clear_all_caches() class RQL2RQL(object): diff -r 24489cbbd697 -r 70c0dd1c3b7d server/sources/rql2sql.py --- a/server/sources/rql2sql.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/sources/rql2sql.py Thu Sep 17 14:53:18 2009 +0200 @@ -336,6 +336,7 @@ self._varmap = varmap self._query_attrs = {} self._state = None + self._not_scope_offset = 0 try: # union query for each rqlst / solution sql = self.union_sql(union) @@ -553,7 +554,11 @@ def visit_not(self, node): self._state.push_scope() + if isinstance(node.children[0], Relation): + self._not_scope_offset += 1 csql = node.children[0].accept(self) + if isinstance(node.children[0], Relation): + self._not_scope_offset -= 1 sqls, tables = self._state.pop_scope() if node in self._state.done or not csql: # already processed or no sql generated by children @@ -651,10 +656,6 @@ sql = self._visit_outer_join_relation(relation, rschema) elif rschema.inlined: sql = self._visit_inlined_relation(relation) -# elif isinstance(relation.parent, Not): -# self._state.done.add(relation.parent) -# # NOT relation -# sql = self._visit_not_relation(relation, rschema) else: # regular (non final) relation sql = self._visit_relation(relation, rschema) @@ -1080,12 +1081,16 @@ # a EXISTS node if var.sqlscope is var.stmt: scope = 0 + # don't consider not_scope_offset if the variable is only used in one + # relation + elif len(var.stinfo['relations']) > 1: + scope = -1 - self._not_scope_offset else: scope = -1 try: sql = self._varmap[var.name] table = sql.split('.', 1)[0] - if scope == -1: + if scope < 0: scope = self._varmap_table_scope(var.stmt, table) self.add_table(table, scope=scope) except KeyError: @@ -1095,7 +1100,8 @@ raise BadRQLQuery(var.stmt.root) table = var.name sql = '%s.%seid' % (table, SQL_PREFIX) - self.add_table('%s%s AS %s' % (SQL_PREFIX, etype, table), table, scope=scope) + self.add_table('%s%s AS %s' % (SQL_PREFIX, etype, table), table, + scope=scope) return sql, table def _inlined_var_sql(self, var, rtype): @@ -1146,8 +1152,8 @@ key = table if key in self._state.tables: return - if scope == -1: - scope = len(self._state.actual_tables) - 1 + if scope < 0: + scope = len(self._state.actual_tables) + scope self._state.tables[key] = (scope, table) self._state.actual_tables[scope].append(table) diff -r 24489cbbd697 -r 70c0dd1c3b7d server/ssplanner.py --- a/server/ssplanner.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/ssplanner.py Thu Sep 17 14:53:18 2009 +0200 @@ -377,7 +377,7 @@ previous FetchStep relations values comes from the latest result, with one columns for - each relation defined in self.r_defs + each relation defined in self.rdefs for one entity definition, we'll construct N entity, where N is the number of the latest result @@ -387,33 +387,35 @@ RELATION = 1 REVERSE_RELATION = 2 - def __init__(self, plan, e_def, r_defs): + def __init__(self, plan, edef, rdefs): Step.__init__(self, plan) # partial entity definition to expand - self.e_def = e_def + self.edef = edef # definition of relations to complete - self.r_defs = r_defs + self.rdefs = rdefs def execute(self): """execute this step""" - base_e_def = self.e_def - result = [] - for row in self.execute_child(): + base_edef = self.edef + edefs = [] + result = self.execute_child() + for row in result: # get a new entity definition for this row - e_def = copy(base_e_def) + edef = copy(base_edef) # complete this entity def using row values - for i in range(len(self.r_defs)): - rtype, rorder = self.r_defs[i] + for i in range(len(self.rdefs)): + rtype, rorder = self.rdefs[i] if rorder == RelationsStep.FINAL: - e_def[rtype] = row[i] + edef[rtype] = row[i] elif rorder == RelationsStep.RELATION: - self.plan.add_relation_def( (e_def, rtype, row[i]) ) - e_def.querier_pending_relations[(rtype, 'subject')] = row[i] + self.plan.add_relation_def( (edef, rtype, row[i]) ) + edef.querier_pending_relations[(rtype, 'subject')] = row[i] else: - self.plan.add_relation_def( (row[i], rtype, e_def) ) - e_def.querier_pending_relations[(rtype, 'object')] = row[i] - result.append(e_def) - self.plan.substitute_entity_def(base_e_def, result) + self.plan.add_relation_def( (row[i], rtype, edef) ) + edef.querier_pending_relations[(rtype, 'object')] = row[i] + edefs.append(edef) + self.plan.substitute_entity_def(base_edef, edefs) + return result class InsertStep(Step): @@ -483,7 +485,8 @@ edefs = {} # insert relations attributes = set([relation.r_type for relation in self.attribute_relations]) - for row in self.execute_child(): + result = self.execute_child() + for row in result: for relation in self.attribute_relations: lhs, rhs = relation.get_variable_parts() eid = typed_eid(row[self.selected_index[str(lhs)]]) @@ -502,8 +505,6 @@ obj = row[self.selected_index[str(relation.children[1])]] repo.glob_add_relation(session, subj, relation.r_type, obj) # update entities - result = [] for eid, edef in edefs.iteritems(): repo.glob_update_entity(session, edef, attributes) - result.append( (eid,) ) return result diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/data/migratedapp/schema.py Thu Sep 17 14:53:18 2009 +0200 @@ -50,9 +50,9 @@ 'PE require_permission P, P name "add_note", ' 'P require_group G'),)} + whatever = Int() # keep it before `date` for unittest_migraction.test_add_attribute_int date = Datetime() type = String(maxsize=1) - whatever = Int() mydate = Date(default='TODAY') shortpara = String(maxsize=64) ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')]) @@ -104,7 +104,7 @@ concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*') connait = SubjectRelation('Personne', symetric=True) -class Societe(EntityType): +class Societe(WorkflowableEntityType): permissions = { 'read': ('managers', 'users', 'guests'), 'update': ('managers', 'owners'), @@ -123,7 +123,6 @@ cp = String(maxsize=12) ville= String(maxsize=32) - in_state = SubjectRelation('State', cardinality='?*') class evaluee(RelationDefinition): subject = ('Personne', 'CWUser', 'Societe') diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/data/schema.py --- a/server/test/data/schema.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/data/schema.py Thu Sep 17 14:53:18 2009 +0200 @@ -34,7 +34,7 @@ depends_on = SubjectRelation('Affaire') require_permission = SubjectRelation('CWPermission') concerne = SubjectRelation(('Societe', 'Note')) - todo_by = SubjectRelation('Personne') + todo_by = SubjectRelation('Personne', cardinality='?*') documented_by = SubjectRelation('Card') @@ -69,7 +69,7 @@ from cubicweb.schemas.base import CWUser CWUser.get_relations('login').next().fulltextindexed = True -class Note(EntityType): +class Note(WorkflowableEntityType): date = String(maxsize=10) type = String(maxsize=6) para = String(maxsize=512) @@ -146,18 +146,6 @@ 'delete': ('managers',), 'add': ('managers',)} - -class in_state(RelationDefinition): - subject = 'Note' - object = 'State' - cardinality = '1*' - constraints=[RQLConstraint('S is ET, O state_of ET')] - -class wf_info_for(RelationDefinition): - subject = 'TrInfo' - object = 'Note' - cardinality = '1*' - class multisource_rel(RelationDefinition): subject = ('Card', 'Note') object = 'Note' diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_extlite.py --- a/server/test/unittest_extlite.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -import threading, os, time - -from logilab.common.testlib import TestCase, unittest_main -from logilab.common.db import get_connection - -class SQLiteTC(TestCase): - sqlite_file = '_extlite_test.sqlite' - - def _cleanup(self): - try: - os.remove(self.sqlite_file) - except: - pass - - def setUp(self): - self._cleanup() - cnx1 = get_connection('sqlite', database=self.sqlite_file) - cu = cnx1.cursor() - cu.execute('CREATE TABLE toto(name integer);') - cnx1.commit() - cnx1.close() - - def tearDown(self): - self._cleanup() - - def test(self): - lock = threading.Lock() - - def run_thread(): - cnx2 = get_connection('sqlite', database=self.sqlite_file) - lock.acquire() - cu = cnx2.cursor() - cu.execute('SELECT name FROM toto') - self.failIf(cu.fetchall()) - cnx2.commit() - lock.release() - time.sleep(0.1) - lock.acquire() - cu.execute('SELECT name FROM toto') - self.failUnless(cu.fetchall()) - lock.release() - - cnx1 = get_connection('sqlite', database=self.sqlite_file) - lock.acquire() - thread = threading.Thread(target=run_thread) - thread.start() - cu = cnx1.cursor() - cu.execute('SELECT name FROM toto') - lock.release() - time.sleep(0.1) - cnx1.commit() - lock.acquire() - cu.execute("INSERT INTO toto(name) VALUES ('toto')") - cnx1.commit() - lock.release() - -if __name__ == '__main__': - unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_hookhelper.py --- a/server/test/unittest_hookhelper.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_hookhelper.py Thu Sep 17 14:53:18 2009 +0200 @@ -49,41 +49,5 @@ op5 = hooks.CheckORelationOp(session) self.assertEquals(session.pending_operations, [op1, op2, op4, op5, op3]) - - def test_in_state_notification(self): - result = [] - # test both email notification and transition_information - # whatever if we can connect to the default stmp server, transaction - # should not fail - def in_state_changed(session, eidfrom, rtype, eidto): - tr = previous_state(session, eidfrom) - if tr is None: - result.append(tr) - return - content = u'trÀnsition from %s to %s' % (tr.name, entity_name(session, eidto)) - result.append(content) - SendMailOp(session, msg=content, recipients=['test@logilab.fr']) - self.hm.register_hook(in_state_changed, - 'before_add_relation', 'in_state') - self.execute('INSERT CWUser X: X login "paf", X upassword "wouf", X in_state S, X in_group G WHERE S name "activated", G name "users"') - self.assertEquals(result, [None]) - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 0, - self.session.pending_operations) - self.commit() - self.execute('SET X in_state S WHERE X login "paf", S name "deactivated"') - self.assertEquals(result, [None, u'trÀnsition from activated to deactivated']) - # one to send the mail, one to close the smtp connection - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 1, - self.session.pending_operations) - self.commit() - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 0, - self.session.pending_operations) - if __name__ == '__main__': unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_hooks.py --- a/server/test/unittest_hooks.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_hooks.py Thu Sep 17 14:53:18 2009 +0200 @@ -37,8 +37,8 @@ 'DELETE CWGroup X WHERE X name "owners"') def test_delete_required_relations_subject(self): - self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y, X in_state S ' - 'WHERE Y name "users", S name "activated"') + self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y ' + 'WHERE Y name "users"') self.commit() self.execute('DELETE X in_group Y WHERE X login "toto", Y name "users"') self.assertRaises(ValidationError, self.commit) @@ -60,18 +60,6 @@ self.assertRaises(ValidationError, self.commit) - def test_delete_if_singlecard1(self): - self.assertEquals(self.repo.schema['in_state'].inlined, False) - ueid = self.create_user('toto') - self.commit() - self.execute('SET X in_state S WHERE S name "deactivated", X eid %(x)s', {'x': ueid}) - rset = self.execute('Any S WHERE X in_state S, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) - self.commit() - self.assertRaises(Exception, self.execute, 'SET X in_state S WHERE S name "deactivated", X eid %s' % ueid) - rset2 = self.execute('Any S WHERE X in_state S, X eid %(x)s', {'x': ueid}) - self.assertEquals(rset.rows, rset2.rows) - def test_inlined(self): self.assertEquals(self.repo.schema['sender'].inlined, True) self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') @@ -155,6 +143,40 @@ self.assertEquals(entity.descr, u'R&D

yo

') + def test_metadata_cwuri(self): + eid = self.execute('INSERT Note X')[0][0] + cwuri = self.execute('Any U WHERE X eid %s, X cwuri U' % eid)[0][0] + self.assertEquals(cwuri, self.repo.config['base-url'] + 'eid/%s' % eid) + + def test_metadata_creation_modification_date(self): + _now = datetime.now() + eid = self.execute('INSERT Note X')[0][0] + creation_date, modification_date = self.execute('Any CD, MD WHERE X eid %s, ' + 'X creation_date CD, ' + 'X modification_date MD' % eid)[0] + self.assertEquals((creation_date - _now).seconds, 0) + self.assertEquals((modification_date - _now).seconds, 0) + + def test_metadata__date(self): + _now = datetime.now() + eid = self.execute('INSERT Note X')[0][0] + creation_date = self.execute('Any D WHERE X eid %s, X creation_date D' % eid)[0][0] + self.assertEquals((creation_date - _now).seconds, 0) + + def test_metadata_created_by(self): + eid = self.execute('INSERT Note X')[0][0] + self.commit() # fire operations + rset = self.execute('Any U WHERE X eid %s, X created_by U' % eid) + self.assertEquals(len(rset), 1) # make sure we have only one creator + self.assertEquals(rset[0][0], self.session.user.eid) + + def test_metadata_owned_by(self): + eid = self.execute('INSERT Note X')[0][0] + self.commit() # fire operations + rset = self.execute('Any U WHERE X eid %s, X owned_by U' % eid) + self.assertEquals(len(rset), 1) # make sure we have only one owner + self.assertEquals(rset[0][0], self.session.user.eid) + class UserGroupHooksTC(RepositoryBasedTC): @@ -480,177 +502,5 @@ 'RT name "prenom", E name "Personne"') self.commit() - -class WorkflowHooksTC(RepositoryBasedTC): - - def setUp(self): - RepositoryBasedTC.setUp(self) - self.s_activated = self.execute('State X WHERE X name "activated"')[0][0] - self.s_deactivated = self.execute('State X WHERE X name "deactivated"')[0][0] - self.s_dummy = self.execute('INSERT State X: X name "dummy", X state_of E WHERE E name "CWUser"')[0][0] - self.create_user('stduser') - # give access to users group on the user's wf transitions - # so we can test wf enforcing on euser (managers don't have anymore this - # enforcement - self.execute('SET X require_group G WHERE G name "users", X transition_of ET, ET name "CWUser"') - self.commit() - - def tearDown(self): - self.execute('DELETE X require_group G WHERE G name "users", X transition_of ET, ET name "CWUser"') - self.commit() - RepositoryBasedTC.tearDown(self) - - def test_set_initial_state(self): - ueid = self.execute('INSERT CWUser E: E login "x", E upassword "x", E in_group G ' - 'WHERE G name "users"')[0][0] - self.failIf(self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', - {'x' : ueid})) - self.commit() - initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', - {'x' : ueid})[0][0] - self.assertEquals(initialstate, u'activated') - - def test_initial_state(self): - cnx = self.login('stduser') - cu = cnx.cursor() - self.assertRaises(ValidationError, cu.execute, - 'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' - 'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'}) - cnx.close() - # though managers can do whatever he want - self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' - 'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'}) - self.commit() - - # test that the workflow is correctly enforced - def test_transition_checking1(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.close() - - def test_transition_checking2(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_dummy}, 'x') - cnx.close() - - def test_transition_checking3(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - cnx.commit() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - # get back now - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.commit() - cnx.close() - - def test_transition_checking4(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - cnx.commit() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_dummy}, 'x') - # get back now - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.commit() - cnx.close() - - def test_transition_information(self): - ueid = self.session.user.eid - self.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - self.commit() - rset = self.execute('TrInfo T ORDERBY T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 2) - tr = rset.get_entity(1, 0) - #tr.complete() - self.assertEquals(tr.comment, None) - self.assertEquals(tr.from_state[0].eid, self.s_activated) - self.assertEquals(tr.to_state[0].eid, self.s_deactivated) - - self.session.set_shared_data('trcomment', u'il est pas sage celui-la') - self.session.set_shared_data('trcommentformat', u'text/plain') - self.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - self.commit() - rset = self.execute('TrInfo T ORDERBY T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 3) - tr = rset.get_entity(2, 0) - #tr.complete() - self.assertEquals(tr.comment, u'il est pas sage celui-la') - self.assertEquals(tr.comment_format, u'text/plain') - self.assertEquals(tr.from_state[0].eid, self.s_deactivated) - self.assertEquals(tr.to_state[0].eid, self.s_activated) - self.assertEquals(tr.owned_by[0].login, 'admin') - - def test_transition_information_on_creation(self): - ueid = self.create_user('toto') - rset = self.execute('TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) - tr = rset.get_entity(0, 0) - #tr.complete() - self.assertEquals(tr.comment, None) - self.assertEquals(tr.from_state, []) - self.assertEquals(tr.to_state[0].eid, self.s_activated) - - def test_std_users_can_create_trinfo(self): - self.create_user('toto') - cnx = self.login('toto') - cu = cnx.cursor() - self.failUnless(cu.execute("INSERT Note X: X type 'a', X in_state S WHERE S name 'todo'")) - cnx.commit() - - def test_metadata_cwuri(self): - eid = self.execute('INSERT Note X')[0][0] - cwuri = self.execute('Any U WHERE X eid %s, X cwuri U' % eid)[0][0] - self.assertEquals(cwuri, self.repo.config['base-url'] + 'eid/%s' % eid) - - def test_metadata_creation_modification_date(self): - _now = datetime.now() - eid = self.execute('INSERT Note X')[0][0] - creation_date, modification_date = self.execute('Any CD, MD WHERE X eid %s, ' - 'X creation_date CD, ' - 'X modification_date MD' % eid)[0] - self.assertEquals((creation_date - _now).seconds, 0) - self.assertEquals((modification_date - _now).seconds, 0) - - def test_metadata__date(self): - _now = datetime.now() - eid = self.execute('INSERT Note X')[0][0] - creation_date = self.execute('Any D WHERE X eid %s, X creation_date D' % eid)[0][0] - self.assertEquals((creation_date - _now).seconds, 0) - - def test_metadata_created_by(self): - eid = self.execute('INSERT Note X')[0][0] - self.commit() # fire operations - rset = self.execute('Any U WHERE X eid %s, X created_by U' % eid) - self.assertEquals(len(rset), 1) # make sure we have only one creator - self.assertEquals(rset[0][0], self.session.user.eid) - - def test_metadata_owned_by(self): - eid = self.execute('INSERT Note X')[0][0] - self.commit() # fire operations - rset = self.execute('Any U WHERE X eid %s, X owned_by U' % eid) - self.assertEquals(len(rset), 1) # make sure we have only one owner - self.assertEquals(rset[0][0], self.session.user.eid) - if __name__ == '__main__': unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_ldapuser.py Thu Sep 17 14:53:18 2009 +0200 @@ -156,7 +156,8 @@ self.patch_authenticate() cnx = self.login('syt', 'dummypassword') cu = cnx.cursor() - cu.execute('SET X in_state S WHERE X login "alf", S name "deactivated"') + alf = cu.execute('Any X WHERE X login "alf"').get_entity(0, 0) + alf.fire_transition('deactivate') try: cnx.commit() alf = self.execute('CWUser X WHERE X login "alf"').get_entity(0, 0) @@ -172,7 +173,8 @@ finally: # restore db state self.restore_connection() - self.execute('SET X in_state S WHERE X login "alf", S name "activated"') + alf = self.execute('Any X WHERE X login "alf"').get_entity(0, 0) + alf.fire_transition('activate') self.execute('DELETE X in_group G WHERE X login "syt", G name "managers"') def test_same_column_names(self): diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_migractions.py Thu Sep 17 14:53:18 2009 +0200 @@ -45,19 +45,27 @@ assert self.cnx is self.mh._cnx assert self.session is self.mh.session, (self.session.id, self.mh.session.id) + def test_add_attribute_int(self): self.failIf('whatever' in self.schema) - paraordernum = self.mh.rqlexec('Any O WHERE X name "Note", RT name "para", RDEF from_entity X, RDEF relation_type RT, RDEF ordernum O')[0][0] + orderdict = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, ' + 'RDEF relation_type RT, RDEF ordernum O, RT name RTN')) self.mh.cmd_add_attribute('Note', 'whatever') self.failUnless('whatever' in self.schema) self.assertEquals(self.schema['whatever'].subjects(), ('Note',)) self.assertEquals(self.schema['whatever'].objects(), ('Int',)) - paraordernum2 = self.mh.rqlexec('Any O WHERE X name "Note", RT name "para", RDEF from_entity X, RDEF relation_type RT, RDEF ordernum O')[0][0] - self.assertEquals(paraordernum2, paraordernum+1) + orderdict2 = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, ' + 'RDEF relation_type RT, RDEF ordernum O, RT name RTN')) + whateverorder = migrschema['whatever'].rproperty('Note', 'Int', 'order') + for k, v in orderdict.iteritems(): + if v >= whateverorder: + orderdict[k] = v+1 + orderdict['whatever'] = whateverorder + self.assertDictEquals(orderdict, orderdict2) #self.assertEquals([r.type for r in self.schema['Note'].ordered_relations()], # ['modification_date', 'creation_date', 'owned_by', # 'eid', 'ecrit_par', 'inline1', 'date', 'type', - # 'whatever', 'para', 'in_basket']) + # 'whatever', 'date', 'in_basket']) # NB: commit instead of rollback make following test fail with py2.5 # this sounds like a pysqlite/2.5 bug (the same eid is affected to # two different entities) @@ -106,23 +114,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) @@ -132,6 +131,7 @@ self.failUnless(self.execute('CWEType X WHERE X name "Folder2"')) self.failUnless('filed_under2' in self.schema) self.failUnless(self.execute('CWRType X WHERE X name "filed_under2"')) + self.schema.rebuild_infered_relations() self.assertEquals(sorted(str(rs) for rs in self.schema['Folder2'].subject_relations()), ['created_by', 'creation_date', 'cwuri', 'description', 'description_format', @@ -150,22 +150,25 @@ def test_add_drop_entity_type(self): self.mh.cmd_add_entity_type('Folder2') - todoeid = self.mh.cmd_add_state(u'todo', 'Folder2', initial=True) - doneeid = self.mh.cmd_add_state(u'done', 'Folder2') - self.mh.cmd_add_transition(u'redoit', 'Folder2', (doneeid,), todoeid) - self.mh.cmd_add_transition(u'markasdone', 'Folder2', (todoeid,), doneeid) + wf = self.mh.cmd_add_workflow(u'folder2 wf', 'Folder2') + todo = wf.add_state(u'todo', initial=True) + done = wf.add_state(u'done') + wf.add_transition(u'redoit', done, todo) + wf.add_transition(u'markasdone', todo, done) self.commit() eschema = self.schema.eschema('Folder2') self.mh.cmd_drop_entity_type('Folder2') 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) self.mh.cmd_add_relation_type('filed_under2') + self.schema.rebuild_infered_relations() self.failUnless('filed_under2' in self.schema) self.assertEquals(sorted(str(e) for e in self.schema['filed_under2'].subjects()), sorted(str(e) for e in self.schema.entities() if not e.is_final())) @@ -183,8 +186,8 @@ '1*') self.mh.cmd_add_relation_definition('Personne', 'concerne2', 'Note') self.assertEquals(sorted(self.schema['concerne2'].objects()), ['Affaire', 'Note']) - self.mh.add_entity('Personne', nom=u'tot') - self.mh.add_entity('Affaire') + self.mh.create_entity('Personne', nom=u'tot') + self.mh.create_entity('Affaire') self.mh.rqlexec('SET X concerne2 Y WHERE X is Personne, Y is Affaire') self.commit() self.mh.cmd_drop_relation_definition('Personne', 'concerne2', 'Affaire') @@ -219,11 +222,17 @@ self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()), ['Affaire', 'Personne']) self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()), + ['Affaire', 'Division', 'Note', 'SubDivision']) + self.schema.rebuild_infered_relations() # need to be explicitly called once everything is in place + self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()), ['Affaire', 'Note']) self.mh.cmd_add_relation_definition('Affaire', 'concerne', 'Societe') self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()), ['Affaire', 'Personne']) self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()), + ['Affaire', 'Note', 'Societe']) + self.schema.rebuild_infered_relations() # need to be explicitly called once everything is in place + self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()), ['Affaire', 'Division', 'Note', 'Societe', 'SubDivision']) # trick: overwrite self.maxeid to avoid deletion of just reintroduced types self.maxeid = self.execute('Any MAX(X)')[0][0] @@ -451,12 +460,6 @@ ex = self.assertRaises(ConfigurationError, self.mh.cmd_remove_cube, 'file') self.assertEquals(str(ex), "can't remove cube file, used as a dependency") - def test_set_state(self): - user = self.session.user - self.mh.set_state(user.eid, 'deactivated') - user.clear_related_cache('in_state', 'subject') - self.assertEquals(user.state, 'deactivated') - def test_introduce_base_class(self): self.mh.cmd_add_entity_type('Para') self.mh.repo.schema.rebuild_infered_relations() diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_msplanner.py Thu Sep 17 14:53:18 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 @@ -347,7 +348,7 @@ def setUp(self): BaseMSPlannerTC.setUp(self) - self.planner = MSPlanner(self.o.schema, self.o._rqlhelper) + self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper) _test = test_plan @@ -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', @@ -1962,13 +1961,23 @@ None, None, [self.system], {}, [])], {'x': 999998, 'u': 999999}) - def test_nonregr_identity_no_source_access(self): + def test_nonregr_identity_no_source_access_1(self): repo._type_source_cache[999999] = ('CWUser', 'ldap', 999998) self._test('Any S WHERE S identity U, S eid %(s)s, U eid %(u)s', [('OneFetchStep', [('Any 999999 WHERE 999999 identity 999999', [{}])], None, None, [self.system], {}, [])], {'s': 999999, 'u': 999999}) + def test_nonregr_identity_no_source_access_2(self): + repo._type_source_cache[999999] = ('EmailAddress', 'system', 999999) + repo._type_source_cache[999998] = ('CWUser', 'ldap', 999998) + self._test('Any X WHERE O use_email X, ((EXISTS(O identity U)) OR (EXISTS(O in_group G, G name IN("managers", "staff")))) OR (EXISTS(O in_group G2, U in_group G2, NOT G2 name "users")), X eid %(x)s, U eid %(u)s', + [('OneFetchStep', [('Any 999999 WHERE O use_email 999999, ((EXISTS(O identity 999998)) OR (EXISTS(O in_group G, G name IN("managers", "staff")))) OR (EXISTS(O in_group G2, 999998 in_group G2, NOT G2 name "users"))', + [{'G': 'CWGroup', 'G2': 'CWGroup', 'O': 'CWUser'}])], + None, None, [self.system], {}, [])], + {'x': 999999, 'u': 999998}) + + class MSPlannerTwoSameExternalSourcesTC(BasePlannerTC): """test planner related feature on a 3-sources repository: @@ -1980,7 +1989,7 @@ self.setup() self.add_source(FakeCardSource, 'cards') self.add_source(FakeCardSource, 'cards2') - self.planner = MSPlanner(self.o.schema, self.o._rqlhelper) + self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper) assert repo.sources_by_uri['cards2'].support_relation('multisource_crossed_rel') assert 'multisource_crossed_rel' in repo.sources_by_uri['cards2'].cross_relations assert repo.sources_by_uri['cards'].support_relation('multisource_crossed_rel') @@ -2133,7 +2142,7 @@ def setUp(self): self.setup() self.add_source(FakeVCSSource, 'vcs') - self.planner = MSPlanner(self.o.schema, self.o._rqlhelper) + self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper) _test = test_plan def test_multisource_inlined_rel_skipped(self): diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_multisources.py Thu Sep 17 14:53:18 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 @@ -233,11 +233,12 @@ def test_not_relation(self): states = set(tuple(x) for x in self.execute('Any S,SN WHERE S is State, S name SN')) + self.session.user.clear_all_caches() userstate = self.session.user.in_state[0] states.remove((userstate.eid, userstate.name)) notstates = set(tuple(x) for x in self.execute('Any S,SN WHERE S is State, S name SN, NOT X in_state S, X eid %(x)s', {'x': self.session.user.eid}, 'x')) - self.assertEquals(notstates, states) + self.assertSetEquals(notstates, states) aff1 = self.execute('Any X WHERE X is Affaire, X ref "AFFREF"')[0][0] aff1stateeid, aff1statename = self.execute('Any S,SN WHERE X eid %(x)s, X in_state S, S name SN', {'x': aff1}, 'x')[0] self.assertEquals(aff1statename, 'pitetre') @@ -267,6 +268,7 @@ {'x': affaire.eid, 'u': ueid}) def test_nonregr2(self): + self.session.user.fire_transition('deactivate') treid = self.session.user.latest_trinfo().eid rset = self.execute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D', {'x': treid}) diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_querier.py --- a/server/test/unittest_querier.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_querier.py Thu Sep 17 14:53:18 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), ' @@ -290,8 +293,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"') @@ -355,27 +358,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] @@ -471,12 +458,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 @@ -726,9 +718,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']) @@ -1000,20 +992,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) @@ -1033,9 +1023,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] @@ -1135,7 +1122,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) @@ -1265,9 +1252,8 @@ def test_nonregr_set_query(self): ueid = self.execute("INSERT CWUser X: X login 'bob', X upassword 'toto'")[0][0] - self.execute("SET E in_group G, E in_state S, " - "E firstname %(firstname)s, E surname %(surname)s " - "WHERE E eid %(x)s, G name 'users', S name 'activated'", + self.execute("SET E in_group G, E firstname %(firstname)s, E surname %(surname)s " + "WHERE E eid %(x)s, G name 'users'", {'x':ueid, 'firstname': u'jean', 'surname': u'paul'}, 'x') def test_nonregr_u_owned_by_u(self): diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_repository.py --- a/server/test/unittest_repository.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_repository.py Thu Sep 17 14:53:18 2009 +0200 @@ -102,7 +102,7 @@ def test_login_upassword_accent(self): repo = self.repo cnxid = repo.connect(*self.default_user_password()) - repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S, X in_group G WHERE S name "activated", G name "users"', + repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_group G WHERE G name "users"', {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')}) repo.commit(cnxid) repo.close(cnxid) @@ -112,7 +112,7 @@ repo = self.repo cnxid = repo.connect(*self.default_user_password()) # no group - repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S WHERE S name "activated"', + repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s', {'login': u"tutetute", 'passwd': 'tutetute'}) self.assertRaises(ValidationError, repo.commit, cnxid) rset = repo.execute(cnxid, 'CWUser X WHERE X login "tutetute"') @@ -190,16 +190,13 @@ repo = self.repo cnxid = repo.connect(*self.default_user_password()) # rollback state change which trigger TrInfo insertion - ueid = repo._get_session(cnxid).user.eid - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) + user = repo._get_session(cnxid).user + user.fire_transition('deactivate') + rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid}) self.assertEquals(len(rset), 1) - repo.execute(cnxid, 'SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 2) repo.rollback(cnxid) - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) + rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid}) + self.assertEquals(len(rset), 0) def test_transaction_interleaved(self): self.skip('implement me') @@ -342,6 +339,22 @@ # self.set_debug(False) # print 'test time: %.3f (time) %.3f (cpu)' % ((time() - t), clock() - c) + def test_delete_if_singlecard1(self): + note = self.add_entity('Affaire') + p1 = self.add_entity('Personne', nom=u'toto') + self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s', + {'x': note.eid, 'p': p1.eid}) + rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s', + {'x': note.eid}) + self.assertEquals(len(rset), 1) + p2 = self.add_entity('Personne', nom=u'tutu') + self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s', + {'x': note.eid, 'p': p2.eid}) + rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s', + {'x': note.eid}) + self.assertEquals(len(rset), 1) + self.assertEquals(rset.rows[0][0], p2.eid) + class DataHelpersTC(RepositoryBasedTC): @@ -485,11 +498,11 @@ def test_after_add_inline(self): """make sure after__relation hooks are deferred""" + p1 = self.add_entity('Personne', nom=u'toto') self.hm.register_hook(self._after_relation_hook, - 'after_add_relation', 'in_state') - eidp = self.execute('INSERT CWUser X: X login "toto", X upassword "tutu", X in_state S WHERE S name "activated"')[0][0] - eids = self.execute('State X WHERE X name "activated"')[0][0] - self.assertEquals(self.called, [(eidp, 'in_state', eids,)]) + 'after_add_relation', 'ecrit_par') + eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0] + self.assertEquals(self.called, [(eidn, 'ecrit_par', p1.eid,)]) def test_before_delete_inline_relation(self): """make sure before__relation hooks are called directly""" diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_rql2sql.py Thu Sep 17 14:53:18 2009 +0200 @@ -163,7 +163,6 @@ ] ADVANCED= [ - ("Societe S WHERE S nom 'Logilab' OR S nom 'Caesium'", '''SELECT S.cw_eid FROM cw_Societe AS S @@ -339,6 +338,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 +378,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 +470,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 +509,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;', @@ -624,6 +641,13 @@ WHERE X.cw_in_state=S.cw_eid ORDER BY 2) AS T1'''), + ('Any O,AA,AB,AC ORDERBY AC DESC ' + 'WHERE NOT S use_email O, S eid 1, O is EmailAddress, O address AA, O alias AB, O modification_date AC, ' + 'EXISTS(A use_email O, EXISTS(A identity B, NOT B in_group D, D name "guests", D is CWGroup), A is CWUser), B eid 2', + '''SELECT O.cw_eid, O.cw_address, O.cw_alias, O.cw_modification_date +FROM cw_EmailAddress AS O +WHERE NOT EXISTS(SELECT 1 FROM use_email_relation AS rel_use_email0 WHERE rel_use_email0.eid_from=1 AND rel_use_email0.eid_to=O.cw_eid) AND EXISTS(SELECT 1 FROM use_email_relation AS rel_use_email1 WHERE rel_use_email1.eid_to=O.cw_eid AND EXISTS(SELECT 1 FROM cw_CWGroup AS D WHERE rel_use_email1.eid_from=2 AND NOT EXISTS(SELECT 1 FROM in_group_relation AS rel_in_group2 WHERE rel_in_group2.eid_from=2 AND rel_in_group2.eid_to=D.cw_eid) AND D.cw_name=guests)) +ORDER BY 4 DESC'''), ] MULTIPLE_SEL = [ @@ -1029,8 +1053,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 +1131,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 +1404,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 +1416,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 +1553,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 +1565,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 +1614,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 +1626,7 @@ SELECT X.cw_eid FROM appears AS appears0, cw_Folder AS X WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Image AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_State AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Tag AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Transition AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu""") +""") ] for t in self._parse(queries): yield t diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_rqlannotation.py --- a/server/test/unittest_rqlannotation.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_rqlannotation.py Thu Sep 17 14:53:18 2009 +0200 @@ -95,6 +95,11 @@ self.assertEquals(rqlst.defined_vars['X']._q_invariant, False) self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False) + def test_diff_scope_identity_deamb(self): + rqlst = self._prepare('Any X WHERE X concerne Y, Y is Note, EXISTS(Y identity Z, Z migrated_from N)') + self.assertEquals(rqlst.defined_vars['Z']._q_invariant, True) + self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True) + def test_optional_inlined(self): rqlst = self._prepare('Any X,S where X from_state S?') self.assertEquals(rqlst.defined_vars['X']._q_invariant, False) diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_rqlrewrite.py --- a/server/test/unittest_rqlrewrite.py Thu Sep 17 14:03:21 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,194 +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, TestCase -from logilab.common.testlib import mock_object - -from rql import parse, nodes, RQLHelper - -from cubicweb import Unauthorized -from cubicweb.server.rqlrewrite import RQLRewriter -from cubicweb.devtools import repotest, TestServerConfiguration - -config = TestServerConfiguration('data') -config.bootstrap_cubes() -schema = config.load_schema() -schema.add_relation_def(mock_object(subject='Card', name='in_state', object='State', cardinality='1*')) - -rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid', - 'has_text': 'fti'}) - -def setup_module(*args): - repotest.do_monkey_patch() - -def teardown_module(*args): - repotest.undo_monkey_patch() - -def eid_func_map(eid): - return {1: 'CWUser', - 2: 'Card'}[eid] - -def rewrite(rqlst, snippets_map, kwargs): - class FakeQuerier: - schema = schema - @staticmethod - def solutions(sqlcursor, mainrqlst, kwargs): - rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs) - class _rqlhelper: - @staticmethod - def annotate(rqlst): - rqlhelper.annotate(rqlst) - @staticmethod - def simplify(mainrqlst, needcopy=False): - rqlhelper.simplify(rqlst, needcopy) - rewriter = RQLRewriter(FakeQuerier, mock_object(user=(mock_object(eid=1)))) - for v, snippets in snippets_map.items(): - snippets_map[v] = [mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0], - expression='Any X WHERE '+snippet) - for snippet in snippets] - rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs) - solutions = rqlst.children[0].solutions - rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs) - test_vrefs(rqlst.children[0]) - return rewriter.rewritten - -def test_vrefs(node): - vrefmap = {} - for vref in node.iget_nodes(nodes.VariableRef): - vrefmap.setdefault(vref.name, set()).add(vref) - for var in node.defined_vars.itervalues(): - assert not (var.stinfo['references'] ^ vrefmap[var.name]) - assert (var.stinfo['references']) - -class RQLRewriteTC(TestCase): - """a faire: - - * optimisation: detecter les relations utilisees dans les rqlexpressions qui - sont presentes dans la requete de depart pour les reutiliser si possible - - * "has__permission" ? - """ - - def test_base_var(self): - card_constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') - rqlst = parse('Card C') - rewrite(rqlst, {'C': (card_constraint,)}, {}) - self.failUnlessEqual(rqlst.as_string(), - u"Any C WHERE C is Card, B eid %(D)s, " - "EXISTS(C in_state A, B in_group E, F require_state A, " - "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission)") - - def test_multiple_var(self): - card_constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') - affaire_constraints = ('X ref LIKE "PUBLIC%"', 'U in_group G, G name "public"') - kwargs = {'u':2} - rqlst = parse('Any S WHERE S documented_by C, C eid %(u)s') - rewrite(rqlst, {'C': (card_constraint,), 'S': affaire_constraints}, - kwargs) - self.assertTextEquals(rqlst.as_string(), - "Any S WHERE S documented_by C, C eid %(u)s, B eid %(D)s, " - "EXISTS(C in_state A, B in_group E, F require_state A, " - "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission), " - "(EXISTS(S ref LIKE 'PUBLIC%')) OR (EXISTS(B in_group G, G name 'public', G is CWGroup)), " - "S is Affaire") - self.failUnless('D' in kwargs) - - 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') - 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, " - "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)") - - def test_simplified_rqlst(self): - card_constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') - rqlst = parse('Any 2') # this is the simplified rql st for Any X WHERE X eid 12 - rewrite(rqlst, {'2': (card_constraint,)}, {}) - self.failUnlessEqual(rqlst.as_string(), - u"Any 2 WHERE B eid %(C)s, " - "EXISTS(2 in_state A, B in_group D, E require_state A, " - "E name 'read', E require_group D, A is State, D is CWGroup, E is CWPermission)") - - def test_optional_var(self): - card_constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') - rqlst = parse('Any A,C WHERE A documented_by C?') - rewrite(rqlst, {'C': (card_constraint,)}, {}) - self.failUnlessEqual(rqlst.as_string(), - "Any A,C WHERE A documented_by C?, A is Affaire " - "WITH C BEING " - "(Any C WHERE C in_state B, D in_group F, G require_state B, G name 'read', " - "G require_group F, D eid %(A)s, C is Card)") - rqlst = parse('Any A,C,T WHERE A documented_by C?, C title T') - rewrite(rqlst, {'C': (card_constraint,)}, {}) - self.failUnlessEqual(rqlst.as_string(), - "Any A,C,T WHERE A documented_by C?, A is Affaire " - "WITH C,T BEING " - "(Any C,T WHERE C in_state B, D in_group F, G require_state B, G name 'read', " - "G require_group F, C title T, D eid %(A)s, C is Card)") - - def test_relation_optimization(self): - # since Card in_state State as monovalued cardinality, the in_state - # relation used in the rql expression can be ignored and S replaced by - # the variable from the incoming query - card_constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') - rqlst = parse('Card C WHERE C in_state STATE') - rewrite(rqlst, {'C': (card_constraint,)}, {}) - self.failUnlessEqual(rqlst.as_string(), - u"Any C WHERE C in_state STATE, C is Card, A eid %(B)s, " - "EXISTS(A in_group D, E require_state STATE, " - "E name 'read', E require_group D, D is CWGroup, E is CWPermission), " - "STATE is State") - - def test_unsupported_constraint_1(self): - # CWUser doesn't have require_permission - trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"') - rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U') - self.assertRaises(Unauthorized, rewrite, rqlst, {'T': (trinfo_constraint,)}, {}) - - def test_unsupported_constraint_2(self): - trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"') - rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U') - rewrite(rqlst, {'T': (trinfo_constraint, 'X wf_info_for Y, Y in_group G, G name "managers"')}, {}) - self.failUnlessEqual(rqlst.as_string(), - u"Any U,T WHERE U is CWUser, T wf_info_for U, " - "EXISTS(U in_group B, B name 'managers', B is CWGroup), T is TrInfo") - - def test_unsupported_constraint_3(self): - self.skip('raise unauthorized for now') - trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"') - rqlst = parse('Any T WHERE T wf_info_for X') - rewrite(rqlst, {'T': (trinfo_constraint, 'X in_group G, G name "managers"')}, {}) - self.failUnlessEqual(rqlst.as_string(), - u'XXX dunno what should be generated') - - def test_add_ambiguity_exists(self): - constraint = ('X concerne Y') - rqlst = parse('Affaire X') - rewrite(rqlst, {'X': (constraint,)}, {}) - self.failUnlessEqual(rqlst.as_string(), - u"Any X WHERE X is Affaire, (((EXISTS(X concerne A, A is Division)) OR (EXISTS(X concerne D, D is SubDivision))) OR (EXISTS(X concerne C, C is Societe))) OR (EXISTS(X concerne B, B is Note))") - - def test_add_ambiguity_outerjoin(self): - constraint = ('X concerne Y') - rqlst = parse('Any X,C WHERE X? documented_by C') - rewrite(rqlst, {'X': (constraint,)}, {}) - # ambiguity are kept in the sub-query, no need to be resolved using OR - self.failUnlessEqual(rqlst.as_string(), - u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE X concerne A, X is Affaire)") - - - -if __name__ == '__main__': - unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_schemaserial.py Thu Sep 17 14:53:18 2009 +0200 @@ -33,12 +33,17 @@ {'description': u'', 'final': True, 'name': u'String'})]) def test_eschema2rql_specialization(self): - self.assertListEquals(list(specialize2rql(schema)), - [ - ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', - {'x': 'Division', 'et': 'Societe'}), - ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', - {'x': 'SubDivision', 'et': 'Division'})]) + self.assertListEquals(sorted(specialize2rql(schema)), + [('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'BaseTransition', 'x': 'Transition'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'BaseTransition', 'x': 'WorkflowTransition'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'Division', 'x': 'SubDivision'}), + # ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + # {'et': 'File', 'x': 'Image'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'Societe', 'x': 'Division'})]) def test_rschema2rql1(self): self.assertListEquals(list(rschema2rql(schema.rschema('relation_type'))), diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_security.py --- a/server/test/unittest_security.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_security.py Thu Sep 17 14:53:18 2009 +0200 @@ -27,10 +27,10 @@ def test_check_read_access(self): rql = u'Personne U where U nom "managers"' - rqlst = self.repo.querier._rqlhelper.parse(rql).children[0] + rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0] origgroups = self.schema['Personne'].get_groups('read') self.schema['Personne'].set_groups('read', ('users', 'managers')) - self.repo.querier._rqlhelper.compute_solutions(rqlst) + self.repo.vreg.rqlhelper.compute_solutions(rqlst) solution = rqlst.solutions[0] check_read_access(self.schema, self.session.user, rqlst, solution) cnx = self.login('anon') @@ -265,7 +265,7 @@ self.commit() cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() - aff2 = cu.execute("INSERT Affaire X: X sujet 'cool', X in_state S WHERE S name 'pitetre'")[0][0] + aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0] soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0] cu.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1}, ('a', 's')) @@ -347,25 +347,26 @@ def test_attribute_security_rqlexpr(self): # Note.para attribute editable by managers or if the note is in "todo" state - eid = self.execute("INSERT Note X: X para 'bidule', X in_state S WHERE S name 'done'")[0][0] + note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0) self.commit() - self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': eid}, 'x') + note.fire_transition('markasdone') + self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid}, 'x') self.commit() cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid}, 'x') self.assertRaises(Unauthorized, cnx.commit) - eid2 = cu.execute("INSERT Note X: X para 'bidule'")[0][0] + note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0) cnx.commit() - cu.execute("SET X in_state S WHERE X eid %(x)s, S name 'done'", {'x': eid2}, 'x') + note2.fire_transition('markasdone') cnx.commit() - self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': eid2}, 'x')), + self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid}, 'x')), 0) - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid2}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}, 'x') self.assertRaises(Unauthorized, cnx.commit) - cu.execute("SET X in_state S WHERE X eid %(x)s, S name 'todo'", {'x': eid2}, 'x') + note2.fire_transition('redoit') cnx.commit() - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid2}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}, 'x') cnx.commit() def test_attribute_read_security(self): @@ -398,16 +399,14 @@ cu.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"') cnx.commit() self.restore_connection() - self.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') + affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0) + affaire.fire_transition('abort') self.commit() self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')), - 2) + 1) self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",' 'X owned_by U, U login "admin"')), 1) # TrInfo at the above state change - self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",' - 'X owned_by U, U login "iaminusersgrouponly"')), - 1) # TrInfo created at creation time cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() cu.execute('DELETE Affaire X WHERE X ref "ARCT01"') @@ -499,29 +498,34 @@ self.assertRaises(Unauthorized, self.schema['Affaire'].check_perm, session, 'update', eid) cu = cnx.cursor() - cu.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') - cnx.commit() - # though changing a user state (even logged user) is reserved to managers - rql = u"SET X in_state S WHERE X eid %(x)s, S name 'deactivated'" - # XXX wether it should raise Unauthorized or ValidationError is not clear - # the best would probably ValidationError if the transition doesn't exist - # from the current state but Unauthorized if it exists but user can't pass it - self.assertRaises(ValidationError, cu.execute, rql, {'x': cnx.user(self.current_session()).eid}, 'x') + self.schema['Affaire'].set_groups('read', ('users',)) + try: + aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0) + aff.fire_transition('abort') + cnx.commit() + # though changing a user state (even logged user) is reserved to managers + user = cnx.user(self.current_session()) + # XXX wether it should raise Unauthorized or ValidationError is not clear + # the best would probably ValidationError if the transition doesn't exist + # from the current state but Unauthorized if it exists but user can't pass it + self.assertRaises(ValidationError, user.fire_transition, 'deactivate') + finally: + self.schema['Affaire'].set_groups('read', ('managers',)) def test_trinfo_security(self): aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0) self.commit() + aff.fire_transition('abort') + self.commit() # can change tr info comment self.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"', - {'c': u'creation'}) + {'c': u'bouh!'}) self.commit() aff.clear_related_cache('wf_info_for', 'object') - self.assertEquals(aff.latest_trinfo().comment, 'creation') + trinfo = aff.latest_trinfo() + self.assertEquals(trinfo.comment, 'bouh!') # but not from_state/to_state - self.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') - self.commit() aff.clear_related_cache('wf_info_for', role='object') - trinfo = aff.latest_trinfo() self.assertRaises(Unauthorized, self.execute, 'SET TI from_state S WHERE TI eid %(ti)s, S name "ben non"', {'ti': trinfo.eid}, 'ti') diff -r 24489cbbd697 -r 70c0dd1c3b7d server/test/unittest_ssplanner.py --- a/server/test/unittest_ssplanner.py Thu Sep 17 14:03:21 2009 +0200 +++ b/server/test/unittest_ssplanner.py Thu Sep 17 14:53:18 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): @@ -18,47 +18,27 @@ def setUp(self): BasePlannerTC.setUp(self) - self.planner = SSPlanner(self.o.schema, self.o._rqlhelper) + self.planner = SSPlanner(self.o.schema, self.repo.vreg.rqlhelper) self.system = self.o._repo.system_source def tearDown(self): BasePlannerTC.tearDown(self) def test_ordered_ambigous_sol(self): - self._test('Any XN ORDERBY XN WHERE X name XN', - [('OneFetchStep', [('Any XN ORDERBY XN WHERE X name XN', + self._test('Any XN ORDERBY XN WHERE X name XN, X is IN (Basket, File, Folder)', + [('OneFetchStep', [('Any XN ORDERBY XN WHERE X name XN, X is IN(Basket, File, Folder)', [{'X': 'Basket', 'XN': 'String'}, - {'X': 'CWCache', 'XN': 'String'}, - {'X': 'CWConstraintType', 'XN': 'String'}, - {'X': 'CWEType', 'XN': 'String'}, - {'X': 'CWGroup', 'XN': 'String'}, - {'X': 'CWPermission', 'XN': 'String'}, - {'X': 'CWRType', 'XN': 'String'}, {'X': 'File', 'XN': 'String'}, - {'X': 'Folder', 'XN': 'String'}, - {'X': 'Image', 'XN': 'String'}, - {'X': 'State', 'XN': 'String'}, - {'X': 'Tag', u'XN': 'String'}, - {'X': 'Transition', 'XN': 'String'}])], + {'X': 'Folder', 'XN': 'String'}])], None, None, [self.system], None, [])]) def test_groupeded_ambigous_sol(self): - self._test('Any XN,COUNT(X) GROUPBY XN WHERE X name XN', - [('OneFetchStep', [('Any XN,COUNT(X) GROUPBY XN WHERE X name XN', + self._test('Any XN,COUNT(X) GROUPBY XN WHERE X name XN, X is IN (Basket, File, Folder)', + [('OneFetchStep', [('Any XN,COUNT(X) GROUPBY XN WHERE X name XN, X is IN(Basket, File, Folder)', [{'X': 'Basket', 'XN': 'String'}, - {'X': 'CWCache', 'XN': 'String'}, - {'X': 'CWConstraintType', 'XN': 'String'}, - {'X': 'CWEType', 'XN': 'String'}, - {'X': 'CWGroup', 'XN': 'String'}, - {'X': 'CWPermission', 'XN': 'String'}, - {'X': 'CWRType', 'XN': 'String'}, {'X': 'File', 'XN': 'String'}, - {'X': 'Folder', 'XN': 'String'}, - {'X': 'Image', 'XN': 'String'}, - {'X': 'State', 'XN': 'String'}, - {'X': 'Tag', u'XN': 'String'}, - {'X': 'Transition', 'XN': 'String'}])], + {'X': 'Folder', 'XN': 'String'}])], None, None, [self.system], None, [])]) diff -r 24489cbbd697 -r 70c0dd1c3b7d sobjects/email.py --- a/sobjects/email.py Thu Sep 17 14:03:21 2009 +0200 +++ b/sobjects/email.py Thu Sep 17 14:53:18 2009 +0200 @@ -9,6 +9,7 @@ from cubicweb.server.hooksmanager import Hook from cubicweb.server.pool import PreCommitOperation +from cubicweb.server.repository import ensure_card_respected class SetUseEmailRelationOp(PreCommitOperation): """delay this operation to commit to avoid conflict with a late rql query @@ -26,6 +27,11 @@ def precommit_event(self): session = self.session if self.condition(): + # we've to handle cardinaly by ourselves since we're using unsafe_execute + # but use session.execute and not session.unsafe_execute to check we + # can change the relation + ensure_card_respected(session.execute, session, + self.fromeid, self.rtype, self.toeid) session.unsafe_execute( 'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % self.rtype, {'x': self.fromeid, 'y': self.toeid}, 'x') diff -r 24489cbbd697 -r 70c0dd1c3b7d sobjects/supervising.py --- a/sobjects/supervising.py Thu Sep 17 14:03:21 2009 +0200 +++ b/sobjects/supervising.py Thu Sep 17 14:53:18 2009 +0200 @@ -85,27 +85,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: @@ -114,14 +105,10 @@ # skip meta-relations which are set automatically # XXX generate list below using rtags (category = 'generated') if changedescr[1] in ('created_by', 'owned_by', 'is', 'is_instance_of', - 'from_state', 'to_state', 'wf_info_for',) \ + 'from_state', 'to_state', 'by_transition', + 'wf_info_for') \ and changedescr[0] == eid: index['add_relation'].remove(change) - # skip in_state relation if the entity is being created - # XXX this may be automatized by skipping all mandatory relation - # at entity creation time - elif changedescr[1] == 'in_state' and changedescr[0] in added: - index['add_relation'].remove(change) except KeyError: break diff -r 24489cbbd697 -r 70c0dd1c3b7d sobjects/test/unittest_email.py --- a/sobjects/test/unittest_email.py Thu Sep 17 14:03:21 2009 +0200 +++ b/sobjects/test/unittest_email.py Thu Sep 17 14:53:18 2009 +0200 @@ -5,6 +5,7 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ +from cubicweb import Unauthorized from cubicweb.devtools.apptest import EnvBasedTC class EmailAddressHooksTC(EnvBasedTC): @@ -30,6 +31,24 @@ self.assertEquals(self.execute('Any A WHERE U use_email X, U login "admin", X address A')[0][0], 'admin@logilab.fr') + def test_cardinality_check(self): + email1 = self.execute('INSERT EmailAddress E: E address "client@client.com", U use_email E WHERE U login "admin"')[0][0] + self.commit() + self.execute('SET U primary_email E WHERE U login "anon", E address "client@client.com"') + self.commit() + rset = self.execute('Any X WHERE X use_email E, E eid %(e)s', {'e': email1}) + self.failIf(rset.rowcount != 1, rset) + + def test_security_check(self): + self.create_user('toto') + email1 = self.execute('INSERT EmailAddress E: E address "client@client.com", U use_email E WHERE U login "admin"')[0][0] + self.commit() + cnx = self.login('toto') + cu = cnx.cursor() + cu.execute('SET U primary_email E WHERE E eid %(e)s, U login "toto"', + {'e': email1}) + self.assertRaises(Unauthorized, cnx.commit) + if __name__ == '__main__': from logilab.common.testlib import unittest_main diff -r 24489cbbd697 -r 70c0dd1c3b7d sobjects/test/unittest_notification.py --- a/sobjects/test/unittest_notification.py Thu Sep 17 14:03:21 2009 +0200 +++ b/sobjects/test/unittest_notification.py Thu Sep 17 14:53:18 2009 +0200 @@ -9,9 +9,9 @@ from socket import gethostname from logilab.common.testlib import unittest_main, TestCase -from cubicweb.devtools.apptest import EnvBasedTC +from cubicweb.devtools.apptest import EnvBasedTC, MAILBOX -from cubicweb.sobjects.notification import construct_message_id, parse_message_id +from cubicweb.common.mail import construct_message_id, parse_message_id class MessageIdTC(TestCase): def test_base(self): @@ -71,16 +71,14 @@ def test_status_change_view(self): req = self.session() - u = self.create_user('toto', req=req) - assert u.req - assert u.rset - self.execute('SET X in_state S WHERE X eid %s, S name "deactivated"' % u.eid) - v = self.vreg['views'].select('notif_status_change', req, rset=u.rset, row=0) - content = v.render(row=0, comment='yeah', - previous_state='activated', - current_state='deactivated') - # remove date - self.assertEquals(content, + u = self.create_user('toto', req=req)#, commit=False) XXX in cw 3.6, and remove set_pool + req.set_pool() + u.fire_transition('deactivate', comment=u'yeah') + self.failIf(MAILBOX) + self.commit() + self.assertEquals(len(MAILBOX), 1) + email = MAILBOX[0] + self.assertEquals(email.content, ''' admin changed status from to for entity 'toto' @@ -89,7 +87,7 @@ url: http://testing.fr/cubicweb/cwuser/toto ''') - self.assertEquals(v.subject(), 'status changed cwuser #%s (admin)' % u.eid) + self.assertEquals(email.subject, 'status changed cwuser #%s (admin)' % u.eid) if __name__ == '__main__': unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d sobjects/test/unittest_supervising.py --- a/sobjects/test/unittest_supervising.py Thu Sep 17 14:03:21 2009 +0200 +++ b/sobjects/test/unittest_supervising.py Thu Sep 17 14:53:18 2009 +0200 @@ -28,12 +28,11 @@ def test_supervision(self): session = self.session() # do some modification - ueid = self.execute('INSERT CWUser X: X login "toto", X upassword "sosafe", X in_group G, X in_state S ' - 'WHERE G name "users", S name "activated"')[0][0] - self.execute('SET X last_login_time NOW WHERE X eid %(x)s', {'x': ueid}, 'x') - self.execute('SET X in_state S WHERE X login "anon", S name "deactivated"') + user = self.execute('INSERT CWUser X: X login "toto", X upassword "sosafe", X in_group G ' + 'WHERE G name "users"').get_entity(0, 0) + self.execute('SET X last_login_time NOW WHERE X eid %(x)s', {'x': user.eid}, 'x') self.execute('DELETE Card B WHERE B title "une news !"') - self.execute('SET X bookmarked_by U WHERE X is Bookmark, U eid %(x)s', {'x': ueid}, 'x') + self.execute('SET X bookmarked_by U WHERE X is Bookmark, U eid %(x)s', {'x': user.eid}, 'x') self.execute('SET X content "duh?" WHERE X is Comment') self.execute('DELETE X comments Y WHERE Y is Card, Y title "une autre news !"') # check only one supervision email operation @@ -62,17 +61,31 @@ * updated comment #EID (#EID) http://testing.fr/cubicweb/comment/EID -* deleted relation comments from comment #EID to card #EID - -* changed state of cwuser #EID (anon) - from state activated to state deactivated - http://testing.fr/cubicweb/cwuser/anon''', +* deleted relation comments from comment #EID to card #EID''', data) # check prepared email op._prepare_email() self.assertEquals(len(op.to_send), 1) self.assert_(op.to_send[0][0]) self.assertEquals(op.to_send[0][1], ['test@logilab.fr']) + self.commit() + # some other changes ####### + user.fire_transition('deactivate') + sentops = [op for op in session.pending_operations + if isinstance(op, SupervisionMailOp)] + self.assertEquals(len(sentops), 1) + # check view content + op = sentops[0] + view = sentops[0]._get_view() + data = view.render(changes=session.transaction_data.get('pendingchanges')).strip() + data = re.sub('#\d+', '#EID', data) + data = re.sub('/\d+', '/EID', data) + self.assertTextEquals('''user admin has made the following change(s): + +* changed state of cwuser #EID (toto) + from state activated to state deactivated + http://testing.fr/cubicweb/cwuser/toto''', + data) def test_nonregr1(self): session = self.session() diff -r 24489cbbd697 -r 70c0dd1c3b7d test/data/rewrite/__init__.py diff -r 24489cbbd697 -r 70c0dd1c3b7d test/data/rewrite/bootstrap_cubes --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/rewrite/bootstrap_cubes Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,1 @@ +card, person diff -r 24489cbbd697 -r 70c0dd1c3b7d test/data/rewrite/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/rewrite/schema.py Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,42 @@ +from yams.buildobjs import EntityType, RelationDefinition, String, SubjectRelation +from cubicweb.schema import ERQLExpression + +class Affaire(EntityType): + permissions = { + 'read': ('managers', + ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')), + 'add': ('managers', ERQLExpression('X concerne S, S owned_by U')), + 'update': ('managers', 'owners', ERQLExpression('X in_state S, S name in ("pitetre", "en cours")')), + 'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')), + } + ref = String(fulltextindexed=True, indexed=True, + constraints=[SizeConstraint(16)]) + documented_by = SubjectRelation('Card') + concerne = SubjectRelation(('Societe', 'Note')) + + +class Societe(EntityType): + permissions = { + 'read': ('managers', 'users', 'guests'), + 'update': ('managers', 'owners', ERQLExpression('U login L, X nom L')), + 'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')), + 'add': ('managers', 'users',) + } + + +class Division(Societe): + __specializes_schema__ = True + + +class Note(EntityType): + pass + + +class require_permission(RelationDefinition): + subject = ('Card', 'Note', 'Person') + object = 'CWPermission' + + +class require_state(RelationDefinition): + subject = 'CWPermission' + object = 'State' diff -r 24489cbbd697 -r 70c0dd1c3b7d test/unittest_entity.py --- a/test/unittest_entity.py Thu Sep 17 14:03:21 2009 +0200 +++ b/test/unittest_entity.py Thu Sep 17 14:53:18 2009 +0200 @@ -9,9 +9,10 @@ from datetime import datetime -from cubicweb import Binary +from cubicweb import Binary, Unauthorized from cubicweb.devtools.apptest import EnvBasedTC from cubicweb.common.mttransforms import HAS_TAL +from cubicweb.entities import fetch_config class EntityTC(EnvBasedTC): @@ -76,8 +77,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 +86,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') @@ -179,7 +180,6 @@ Societe.fetch_attrs = sfetch_attrs def test_related_rql(self): - from cubicweb.entities import fetch_config Personne = self.vreg['etypes'].etype_class('Personne') Note = self.vreg['etypes'].etype_class('Note') self.failUnless(issubclass(self.vreg['etypes'].etype_class('SubNote'), Note)) @@ -194,7 +194,40 @@ self.assertEquals(p.related_rql('evaluee'), 'Any X,AA ORDERBY Z DESC WHERE X modification_date Z, E eid %(x)s, E evaluee X, X modification_date AA') - def test_entity_unrelated(self): + def test_unrelated_rql_security_1(self): + user = self.request().user + rql = user.unrelated_rql('use_email', 'EmailAddress', 'subject')[0] + self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' + 'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC') + self.create_user('toto') + self.login('toto') + user = self.request().user + rql = user.unrelated_rql('use_email', 'EmailAddress', 'subject')[0] + self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' + 'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC') + user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0) + self.assertRaises(Unauthorized, user.unrelated_rql, 'use_email', 'EmailAddress', 'subject') + self.login('anon') + user = self.request().user + self.assertRaises(Unauthorized, user.unrelated_rql, 'use_email', 'EmailAddress', 'subject') + + def test_unrelated_rql_security_2(self): + email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0) + rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0] + self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ASC ' + 'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD') + #rql = email.unrelated_rql('use_email', 'Person', 'object')[0] + #self.assertEquals(rql, '') + self.login('anon') + email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}, 'x').get_entity(0, 0) + rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0] + self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ' + 'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, ' + 'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') + #rql = email.unrelated_rql('use_email', 'Person', 'object')[0] + #self.assertEquals(rql, '') + + def test_unrelated_base(self): p = self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien') e = self.add_entity('Tag', name=u'x') related = [r.eid for r in e.tags] @@ -206,14 +239,40 @@ unrelated = [r[0] for r in e.unrelated('tags', 'Personne', 'subject')] self.failIf(p.eid in unrelated) - def test_entity_unrelated_limit(self): + def test_unrelated_limit(self): e = self.add_entity('Tag', name=u'x') self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien') - self.add_entity('Personne', nom=u'di mascio', prenom=u'gwen') + self.add_entity('Personne', nom=u'thenault', prenom=u'sylvain') self.assertEquals(len(e.unrelated('tags', 'Personne', 'subject', limit=1)), 1) - def test_new_entity_unrelated(self): + def test_unrelated_security(self): + email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0) + rset = email.unrelated('use_email', 'CWUser', 'object') + self.assertEquals([x.login for x in rset.entities()], [u'admin', u'anon']) + user = self.request().user + rset = user.unrelated('use_email', 'EmailAddress', 'subject') + self.assertEquals([x.address for x in rset.entities()], [u'hop']) + self.create_user('toto') + self.login('toto') + email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}, 'x').get_entity(0, 0) + rset = email.unrelated('use_email', 'CWUser', 'object') + self.assertEquals([x.login for x in rset.entities()], ['toto']) + user = self.request().user + rset = user.unrelated('use_email', 'EmailAddress', 'subject') + self.assertEquals([x.address for x in rset.entities()], ['hop']) + user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0) + rset = user.unrelated('use_email', 'EmailAddress', 'subject') + self.assertEquals([x.address for x in rset.entities()], []) + self.login('anon') + email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}, 'x').get_entity(0, 0) + rset = email.unrelated('use_email', 'CWUser', 'object') + self.assertEquals([x.login for x in rset.entities()], []) + user = self.request().user + rset = user.unrelated('use_email', 'EmailAddress', 'subject') + self.assertEquals([x.address for x in rset.entities()], []) + + def test_unrelated_new_entity(self): e = self.etype_instance('CWUser') unrelated = [r[0] for r in e.unrelated('in_group', 'CWGroup', 'subject')] # should be default groups but owners, i.e. managers, users, guests @@ -328,33 +387,17 @@ self.failUnless(not p1.reverse_evaluee) def test_complete_relation(self): - self.execute('SET RT add_permission G WHERE RT name "wf_info_for", G name "managers"') - self.commit() session = self.session() - try: - eid = session.unsafe_execute( - 'INSERT TrInfo X: X comment "zou", X wf_info_for U, X from_state S1, X to_state S2 ' - 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] - trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') - trinfo.complete() - self.failUnless(trinfo.relation_cached('from_state', 'subject')) - self.failUnless(trinfo.relation_cached('to_state', 'subject')) - self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) - # check with a missing relation - eid = session.unsafe_execute( - 'INSERT TrInfo X: X comment "zou", X wf_info_for U,X to_state S2 ' - 'WHERE U login "admin", S2 name "activated"')[0][0] - trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') - trinfo.complete() - self.failUnless(isinstance(trinfo.creation_date, datetime)) - self.failUnless(trinfo.relation_cached('from_state', 'subject')) - self.failUnless(trinfo.relation_cached('to_state', 'subject')) - self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) - self.assertEquals(trinfo.from_state, []) - finally: - self.rollback() - self.execute('DELETE RT add_permission G WHERE RT name "wf_info_for", G name "managers"') - self.commit() + eid = session.unsafe_execute( + 'INSERT TrInfo X: X comment "zou", X wf_info_for U, X from_state S1, X to_state S2 ' + 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] + trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') + trinfo.complete() + self.failUnless(isinstance(trinfo['creation_date'], datetime)) + self.failUnless(trinfo.relation_cached('from_state', 'subject')) + self.failUnless(trinfo.relation_cached('to_state', 'subject')) + self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) + self.assertEquals(trinfo.by_transition, []) def test_request_cache(self): req = self.request() diff -r 24489cbbd697 -r 70c0dd1c3b7d test/unittest_rqlrewrite.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unittest_rqlrewrite.py Thu Sep 17 14:53:18 2009 +0200 @@ -0,0 +1,193 @@ +""" + +: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, TestCase +from logilab.common.testlib import mock_object + +from rql import parse, nodes, RQLHelper + +from cubicweb import Unauthorized +from cubicweb.rqlrewrite import RQLRewriter +from cubicweb.devtools import repotest, TestServerConfiguration + +config = TestServerConfiguration('data/rewrite') +config.bootstrap_cubes() +schema = config.load_schema() +schema.add_relation_def(mock_object(subject='Card', name='in_state', object='State', cardinality='1*')) + +rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid', + 'has_text': 'fti'}) + +def setup_module(*args): + repotest.do_monkey_patch() + +def teardown_module(*args): + repotest.undo_monkey_patch() + +def eid_func_map(eid): + return {1: 'CWUser', + 2: 'Card'}[eid] + +def rewrite(rqlst, snippets_map, kwargs): + class FakeVReg: + schema = schema + @staticmethod + def solutions(sqlcursor, mainrqlst, kwargs): + rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs) + class rqlhelper: + @staticmethod + def annotate(rqlst): + rqlhelper.annotate(rqlst) + @staticmethod + def simplify(mainrqlst, needcopy=False): + rqlhelper.simplify(rqlst, needcopy) + rewriter = RQLRewriter(mock_object(vreg=FakeVReg, user=(mock_object(eid=1)))) + for v, snippets in snippets_map.items(): + snippets_map[v] = [mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0], + expression='Any X WHERE '+snippet) + for snippet in snippets] + rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs) + solutions = rqlst.children[0].solutions + rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs) + test_vrefs(rqlst.children[0]) + return rewriter.rewritten + +def test_vrefs(node): + vrefmap = {} + for vref in node.iget_nodes(nodes.VariableRef): + vrefmap.setdefault(vref.name, set()).add(vref) + for var in node.defined_vars.itervalues(): + assert not (var.stinfo['references'] ^ vrefmap[var.name]) + assert (var.stinfo['references']) + +class RQLRewriteTC(TestCase): + """a faire: + + * optimisation: detecter les relations utilisees dans les rqlexpressions qui + sont presentes dans la requete de depart pour les reutiliser si possible + + * "has__permission" ? + """ + + def test_base_var(self): + card_constraint = ('X in_state S, U in_group G, P require_state S,' + 'P name "read", P require_group G') + rqlst = parse('Card C') + rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {}) + self.failUnlessEqual(rqlst.as_string(), + u"Any C WHERE C is Card, B eid %(D)s, " + "EXISTS(C in_state A, B in_group E, F require_state A, " + "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission)") + + def test_multiple_var(self): + card_constraint = ('X in_state S, U in_group G, P require_state S,' + 'P name "read", P require_group G') + affaire_constraints = ('X ref LIKE "PUBLIC%"', 'U in_group G, G name "public"') + kwargs = {'u':2} + rqlst = parse('Any S WHERE S documented_by C, C eid %(u)s') + rewrite(rqlst, {('C', 'X'): (card_constraint,), ('S', 'X'): affaire_constraints}, + kwargs) + self.assertTextEquals(rqlst.as_string(), + "Any S WHERE S documented_by C, C eid %(u)s, B eid %(D)s, " + "EXISTS(C in_state A, B in_group E, F require_state A, " + "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission), " + "(EXISTS(S ref LIKE 'PUBLIC%')) OR (EXISTS(B in_group G, G name 'public', G is CWGroup)), " + "S is Affaire") + self.failUnless('D' in kwargs) + + 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, S is in (CWUser, CWGroup)') + rewrite(rqlst, {('C', 'X'): (constraint,)}, {'u':1}) + self.failUnlessEqual(rqlst.as_string(), + "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)") + + def test_simplified_rqlst(self): + card_constraint = ('X in_state S, U in_group G, P require_state S,' + 'P name "read", P require_group G') + rqlst = parse('Any 2') # this is the simplified rql st for Any X WHERE X eid 12 + rewrite(rqlst, {('2', 'X'): (card_constraint,)}, {}) + self.failUnlessEqual(rqlst.as_string(), + u"Any 2 WHERE B eid %(C)s, " + "EXISTS(2 in_state A, B in_group D, E require_state A, " + "E name 'read', E require_group D, A is State, D is CWGroup, E is CWPermission)") + + def test_optional_var(self): + card_constraint = ('X in_state S, U in_group G, P require_state S,' + 'P name "read", P require_group G') + rqlst = parse('Any A,C WHERE A documented_by C?') + rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {}) + self.failUnlessEqual(rqlst.as_string(), + "Any A,C WHERE A documented_by C?, A is Affaire " + "WITH C BEING " + "(Any C WHERE C in_state B, D in_group F, G require_state B, G name 'read', " + "G require_group F, D eid %(A)s, C is Card)") + rqlst = parse('Any A,C,T WHERE A documented_by C?, C title T') + rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {}) + self.failUnlessEqual(rqlst.as_string(), + "Any A,C,T WHERE A documented_by C?, A is Affaire " + "WITH C,T BEING " + "(Any C,T WHERE C in_state B, D in_group F, G require_state B, G name 'read', " + "G require_group F, C title T, D eid %(A)s, C is Card)") + + def test_relation_optimization(self): + # since Card in_state State as monovalued cardinality, the in_state + # relation used in the rql expression can be ignored and S replaced by + # the variable from the incoming query + card_constraint = ('X in_state S, U in_group G, P require_state S,' + 'P name "read", P require_group G') + rqlst = parse('Card C WHERE C in_state STATE') + rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {}) + self.failUnlessEqual(rqlst.as_string(), + u"Any C WHERE C in_state STATE, C is Card, A eid %(B)s, " + "EXISTS(A in_group D, E require_state STATE, " + "E name 'read', E require_group D, D is CWGroup, E is CWPermission), " + "STATE is State") + + def test_unsupported_constraint_1(self): + # CWUser doesn't have require_permission + trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"') + rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U') + self.assertRaises(Unauthorized, rewrite, rqlst, {('T', 'X'): (trinfo_constraint,)}, {}) + + def test_unsupported_constraint_2(self): + trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"') + rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U') + rewrite(rqlst, {('T', 'X'): (trinfo_constraint, 'X wf_info_for Y, Y in_group G, G name "managers"')}, {}) + self.failUnlessEqual(rqlst.as_string(), + u"Any U,T WHERE U is CWUser, T wf_info_for U, " + "EXISTS(U in_group B, B name 'managers', B is CWGroup), T is TrInfo") + + def test_unsupported_constraint_3(self): + self.skip('raise unauthorized for now') + trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"') + rqlst = parse('Any T WHERE T wf_info_for X') + rewrite(rqlst, {('T', 'X'): (trinfo_constraint, 'X in_group G, G name "managers"')}, {}) + self.failUnlessEqual(rqlst.as_string(), + u'XXX dunno what should be generated') + + def test_add_ambiguity_exists(self): + constraint = ('X concerne Y') + rqlst = parse('Affaire X') + rewrite(rqlst, {('X', 'X'): (constraint,)}, {}) + self.failUnlessEqual(rqlst.as_string(), + u"Any X WHERE X is Affaire, ((EXISTS(X concerne A, A is Division)) OR (EXISTS(X concerne C, C is Societe))) OR (EXISTS(X concerne B, B is Note))") + + def test_add_ambiguity_outerjoin(self): + constraint = ('X concerne Y') + rqlst = parse('Any X,C WHERE X? documented_by C') + rewrite(rqlst, {('X', 'X'): (constraint,)}, {}) + # ambiguity are kept in the sub-query, no need to be resolved using OR + self.failUnlessEqual(rqlst.as_string(), + u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE X concerne A, X is Affaire)") + + + +if __name__ == '__main__': + unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d test/unittest_schema.py --- a/test/unittest_schema.py Thu Sep 17 14:03:21 2009 +0200 +++ b/test/unittest_schema.py Thu Sep 17 14:53:18 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', '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', @@ -174,7 +175,7 @@ 'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed', 'has_text', - 'identical_to', 'identity', 'in_group', 'in_state', 'indexed', + 'identity', 'in_group', 'in_state', 'indexed', 'initial_state', 'inlined', 'internationalizable', 'is', 'is_instance_of', 'label', 'last_login_time', 'login', @@ -185,11 +186,11 @@ 'ordernum', 'owned_by', - 'path', 'pkey', 'prenom', 'primary_email', + 'path', 'pkey', 'prefered_form', 'prenom', 'primary_email', 'read_permission', 'relation_type', 'require_group', - 'specializes', 'state_of', 'surname', 'symetric', 'synopsis', + 'specializes', 'state_of', 'subworkflow', 'subworkflow_exit', 'subworkflow_state', 'surname', 'symetric', 'synopsis', 'tags', 'timestamp', 'title', 'to_entity', 'to_state', 'transition_of', 'travaille', 'type', @@ -197,13 +198,13 @@ 'value', - 'wf_info_for', 'wikiid'] + 'wf_info_for', 'wikiid', 'workflow_of'] self.assertListEquals(relations, expected_relations) eschema = schema.eschema('CWUser') rels = sorted(str(r) for r in eschema.subject_relations()) - self.assertListEquals(rels, ['created_by', 'creation_date', 'cwuri', 'eid', + self.assertListEquals(rels, ['created_by', 'creation_date', 'custom_workflow', 'cwuri', 'eid', 'evaluee', 'firstname', 'has_text', 'identity', 'in_group', 'in_state', 'is', 'is_instance_of', 'last_login_time', diff -r 24489cbbd697 -r 70c0dd1c3b7d test/unittest_utils.py --- a/test/unittest_utils.py Thu Sep 17 14:03:21 2009 +0200 +++ b/test/unittest_utils.py Thu Sep 17 14:53:18 2009 +0200 @@ -8,7 +8,11 @@ from logilab.common.testlib import TestCase, unittest_main -from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList +import simplejson +import decimal +import datetime + +from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, CubicWebJsonEncoder class MakeUidTC(TestCase): @@ -48,6 +52,24 @@ l.extend(extension) yield self.assertEquals, l, expected +class JSONEncoerTests(TestCase): + + def encode(self, value): + return simplejson.dumps(value, cls=CubicWebJsonEncoder) + + def test_encoding_dates(self): + self.assertEquals(self.encode(datetime.datetime(2009, 9, 9, 20, 30)), + '"2009/09/09 20:30:00"') + self.assertEquals(self.encode(datetime.date(2009, 9, 9)), + '"2009/09/09"') + self.assertEquals(self.encode(datetime.time(20, 30)), + '"20:30:00"') + + def test_encoding_decimal(self): + self.assertEquals(self.encode(decimal.Decimal('1.2')), '1.2') + + def test_encoding_unknown_stuff(self): + self.assertEquals(self.encode(TestCase), 'null') if __name__ == '__main__': unittest_main() diff -r 24489cbbd697 -r 70c0dd1c3b7d toolsutils.py --- a/toolsutils.py Thu Sep 17 14:03:21 2009 +0200 +++ b/toolsutils.py Thu Sep 17 14:53:18 2009 +0200 @@ -10,9 +10,15 @@ # XXX move most of this in logilab.common (shellutils ?) import os, sys -from os import listdir, makedirs, symlink, environ, chmod, walk, remove +from os import listdir, makedirs, environ, chmod, walk, remove from os.path import exists, join, abspath, normpath +try: + from os import symlink +except ImportError: + def symlink(*args): + raise NotImplementedError + from logilab.common.clcommands import Command as BaseCommand, \ main_run as base_main_run from logilab.common.compat import any diff -r 24489cbbd697 -r 70c0dd1c3b7d utils.py --- a/utils.py Thu Sep 17 14:03:21 2009 +0200 +++ b/utils.py Thu Sep 17 14:53:18 2009 +0200 @@ -11,10 +11,14 @@ import locale from md5 import md5 +import datetime as pydatetime from datetime import datetime, timedelta, date from time import time, mktime from random import randint, seed from calendar import monthrange +import decimal + +import simplejson # initialize random seed from current time seed() @@ -324,9 +328,46 @@ 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 + +class CubicWebJsonEncoder(simplejson.JSONEncoder): + """define a simplejson encoder to be able to encode yams std types""" + def default(self, obj): + if isinstance(obj, pydatetime.datetime): + return obj.strftime('%Y/%m/%d %H:%M:%S') + elif isinstance(obj, pydatetime.date): + return obj.strftime('%Y/%m/%d') + elif isinstance(obj, pydatetime.time): + return obj.strftime('%H:%M:%S') + elif isinstance(obj, decimal.Decimal): + return float(obj) + try: + return simplejson.JSONEncoder.default(self, obj) + except TypeError: + # we never ever want to fail because of an unknown type, + # just return None in those cases. + return None diff -r 24489cbbd697 -r 70c0dd1c3b7d vregistry.py --- a/vregistry.py Thu Sep 17 14:03:21 2009 +0200 +++ b/vregistry.py Thu Sep 17 14:53:18 2009 +0200 @@ -171,7 +171,7 @@ raise `NoSelectableObject` if not object apply """ if len(args) > 1: - warn('only the request param can not be named when calling select', + warn('[3.5] only the request param can not be named when calling select*', DeprecationWarning, stacklevel=3) score, winners = 0, [] for appobject in appobjects: diff -r 24489cbbd697 -r 70c0dd1c3b7d web/__init__.py --- a/web/__init__.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/__init__.py Thu Sep 17 14:53:18 2009 +0200 @@ -16,7 +16,7 @@ from logilab.common.deprecation import deprecated -from cubicweb.common.uilib import urlquote +from urllib import quote as urlquote from cubicweb.web._exceptions import * diff -r 24489cbbd697 -r 70c0dd1c3b7d web/action.py --- a/web/action.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/action.py Thu Sep 17 14:53:18 2009 +0200 @@ -32,8 +32,18 @@ 'useractions', 'siteactions', 'hidden'), help=_('context where this component should be displayed')), } - site_wide = True # don't want user to configuration actions eproperties + site_wide = True # don't want user to configurate actions category = 'moreactions' + # actions in category 'moreactions' can specify a sub-menu in which they should be filed + submenu = None + + def actual_actions(self): + yield self + + def fill_menu(self, box, menu): + """add action(s) to the given submenu of the given box""" + for action in self.actual_actions(): + menu.append(box.box_action(action)) def url(self): """return the url associated with this action""" @@ -45,6 +55,9 @@ if self.category: return 'box' + self.category.capitalize() + def build_action(self, title, path, **kwargs): + return UnregisteredAction(self.req, self.rset, title, path, **kwargs) + class UnregisteredAction(Action): """non registered action used to build boxes. Unless you set them @@ -75,13 +88,12 @@ & partial_may_add_relation()) registered = accepts_compat(Action.registered) - category = 'addrelated' + submenu = 'addrelated' def url(self): current_entity = self.rset.get_entity(self.row or 0, self.col or 0) linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, target(self)) - return self.build_url(vid='creation', etype=self.etype, - __linkto=linkto, + return self.build_url('add/%s' % self.etype, __linkto=linkto, __redirectpath=current_entity.rest_path(), # should not be url quoted! __redirectvid=self.req.form.get('__redirectvid', '')) diff -r 24489cbbd697 -r 70c0dd1c3b7d web/box.py --- a/web/box.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/box.py Thu Sep 17 14:53:18 2009 +0200 @@ -60,7 +60,8 @@ result = [] actions_by_cat = {} for action in actions: - actions_by_cat.setdefault(action.category, []).append((action.title, action)) + actions_by_cat.setdefault(action.category, []).append( + (action.title, action) ) for key, values in actions_by_cat.items(): actions_by_cat[key] = [act for title, act in sorted(values)] for cat in self.categories_in_order: @@ -150,7 +151,7 @@ __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities() def cell_call(self, row, col, **kwargs): - entity = self.entity(row, col) + entity = self.rset.get_entity(row, col) limit = self.req.property_value('navigation.related-limit') + 1 role = get_role(self) self.w(u'") - # creates a bug submission link if SUBMIT_URL is set - submiturl = self.config['submit-url'] - submitmail = self.config['submit-mail'] - if submiturl or submitmail: + # creates a bug submission link if submit-mail is set + if self.config['submit-mail']: form = self.vreg['forms'].select('base', self.req, rset=None, mainform=False) binfo = text_error_description(ex, excinfo, req, eversion, cversions) @@ -248,15 +246,9 @@ # we must use a text area to keep line breaks widget=wdgs.TextArea({'class': 'hidden'})) form.form_add_hidden('__bugreporting', '1') - if submitmail: - form.form_buttons = [wdgs.SubmitButton(MAIL_SUBMIT_MSGID)] - form.action = req.build_url('reportbug') - w(form.form_render()) - if submiturl: - form.form_add_hidden('description_format', 'text/rest') - form.form_buttons = [wdgs.SubmitButton(SUBMIT_MSGID)] - form.action = submiturl - w(form.form_render()) + form.form_buttons = [wdgs.SubmitButton(MAIL_SUBMIT_MSGID)] + form.action = req.build_url('reportbug') + w(form.form_render()) def exc_message(ex, encoding): diff -r 24489cbbd697 -r 70c0dd1c3b7d web/views/primary.py --- a/web/views/primary.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/views/primary.py Thu Sep 17 14:53:18 2009 +0200 @@ -232,9 +232,9 @@ for rtype in ('eid', 'creation_date', 'modification_date', 'cwuri', 'is', 'is_instance_of', 'identity', - 'owned_by', 'created_by', - 'in_state', 'wf_info_for', 'require_permission', - 'from_entity', 'to_entity', + 'owned_by', 'created_by', 'in_state', + 'wf_info_for', 'by_transition', 'from_state', 'to_state', + 'require_permission', 'from_entity', 'to_entity', 'see_also'): uicfg.primaryview_section.tag_subject_of(('*', rtype, '*'), 'hidden') uicfg.primaryview_section.tag_object_of(('*', rtype, '*'), 'hidden') diff -r 24489cbbd697 -r 70c0dd1c3b7d web/views/schema.py --- a/web/views/schema.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/views/schema.py Thu Sep 17 14:53:18 2009 +0200 @@ -12,8 +12,9 @@ from logilab.mtconverter import xml_escape from yams import BASE_TYPES, schema2dot as s2d -from cubicweb.selectors import implements, yes, match_user_groups -from cubicweb.schema import META_RTYPES, SCHEMA_TYPES +from cubicweb.selectors import (implements, yes, match_user_groups, + has_related_entities) +from cubicweb.schema import META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES from cubicweb.schemaviewer import SchemaViewer from cubicweb.view import EntityView, StartupView from cubicweb.common import tags, uilib @@ -22,7 +23,7 @@ from cubicweb.web.views import primary, baseviews, tabs, management ALWAYS_SKIP_TYPES = BASE_TYPES | SCHEMA_TYPES -SKIP_TYPES = ALWAYS_SKIP_TYPES | META_RTYPES +SKIP_TYPES = ALWAYS_SKIP_TYPES | META_RTYPES | SYSTEM_RTYPES SKIP_TYPES.update(set(('Transition', 'State', 'TrInfo', 'CWUser', 'CWGroup', 'CWCache', 'CWProperty', 'CWPermission', @@ -199,7 +200,7 @@ __select__ = implements('CWEType') def cell_call(self, row, col, **kwargs): - entity = self.entity(row, col) + entity = self.rset.get_entity(row, col) final = entity.final if final: self.w(u'') @@ -227,7 +228,7 @@ __select__ = EntityView.__select__ & implements('CWEType') def cell_call(self, row, col): - entity = self.entity(row, col) + entity = self.rset.get_entity(row, col) self.w(u'

%s

' % _('Attributes')) rset = self.req.execute('Any N,F,D,I,J,DE,A ' 'ORDERBY AA WHERE A is CWAttribute, ' @@ -263,18 +264,19 @@ __select__ = EntityView.__select__ & implements('CWEType') def cell_call(self, row, col): - entity = self.entity(row, col) + entity = self.rset.get_entity(row, col) url = entity.absolute_url(vid='schemagraph') self.w(u'%s' % ( xml_escape(url), xml_escape(self.req._('graphical schema for %s') % entity.name))) + class CWETypeSPermView(EntityView): id = 'cwetype-schema-permissions' __select__ = EntityView.__select__ & implements('CWEType') def cell_call(self, row, col): - entity = self.entity(row, col) + entity = self.rset.get_entity(row, col) self.w(u'

%s

' % _('Add permissions')) rset = self.req.execute('Any P WHERE X add_permission P, ' 'X eid %(x)s', @@ -296,18 +298,28 @@ {'x': entity.eid}) self.wview('outofcontext', rset, 'null') + class CWETypeSWorkflowView(EntityView): id = 'cwetype-workflow' - __select__ = EntityView.__select__ & implements('CWEType') + __select__ = (EntityView.__select__ & implements('CWEType') & + has_related_entities('workflow_of', 'object')) def cell_call(self, row, col): - entity = self.entity(row, col) - if entity.reverse_state_of: - self.w(u'%s' % ( - xml_escape(entity.absolute_url(vid='ewfgraph')), - xml_escape(self.req._('graphical workflow for %s') % entity.name))) - else: - self.w(u'

%s

' % _('There is no workflow defined for this entity.')) + entity = self.rset.get_entity(row, col) + if entity.default_workflow: + wf = entity.default_workflow[0] + self.w(u'

%s (%s)

' % (wf.name, self.req._('default'))) + self.wf_image(wf) + for altwf in entity.reverse_workflow_of: + if altwf.eid == wf.eid: + continue + self.w(u'

%s

' % altwf.name) + self.wf_image(altwf) + + def wf_image(self, wf): + self.w(u'%s' % ( + xml_escape(wf.absolute_url(vid='wfgraph')), + xml_escape(self.req._('graphical representation of %s') % wf.name))) # CWRType ###################################################################### @@ -377,7 +389,7 @@ def _generate(self, tmpfile): """display schema information for an entity""" - entity = self.entity(self.row, self.col) + entity = self.rset.get_entity(self.row, self.col) eschema = self.vreg.schema.eschema(entity.name) visitor = OneHopESchemaVisitor(self.req, eschema, skiptypes=skip_types(self.req)) @@ -389,7 +401,7 @@ def _generate(self, tmpfile): """display schema information for an entity""" - entity = self.entity(self.row, self.col) + entity = self.rset.get_entity(self.row, self.col) rschema = self.vreg.schema.rschema(entity.name) visitor = OneHopRSchemaVisitor(self.req, rschema) s2d.schema2dot(outputfile=tmpfile, visitor=visitor) diff -r 24489cbbd697 -r 70c0dd1c3b7d web/views/tableview.py --- a/web/views/tableview.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/views/tableview.py Thu Sep 17 14:53:18 2009 +0200 @@ -144,8 +144,11 @@ actions += self.show_hide_actions(divid, True) self.w(u'
') # close
') # close ') for tab in tabs: - w(u'
' % tab) + w(u'
' % tab) if entity: self.lazyview(tab, eid=entity.eid) else: @@ -153,7 +156,7 @@ role = 'subject' vid = 'gallery' - in this example, entities related to project entity by the'screenshot' + in this example, entities related to project entity by the 'screenshot' relation (where the project is subject of the relation) will be displayed using the 'gallery' view. """ diff -r 24489cbbd697 -r 70c0dd1c3b7d web/views/treeview.py --- a/web/views/treeview.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/views/treeview.py Thu Sep 17 14:53:18 2009 +0200 @@ -24,7 +24,7 @@ css_classes = 'treeview widget' title = _('tree view') - def call(self, subvid=None, treeid=None, initial_load=True): + def call(self, subvid=None, treeid=None, initial_load=True, initial_thru_ajax=False): if subvid is None: subvid = self.req.form.pop('treesubvid', 'oneline') # consume it if treeid is None: @@ -32,17 +32,22 @@ if treeid is None: self.warning('Tree state won\'t be properly restored after next reload') treeid = make_uid('throw away uid') - self.w(u'
    ' % (treeid, self.css_classes)) + toplevel_thru_ajax = self.req.form.pop('treeview_top', False) or initial_thru_ajax + toplevel = toplevel_thru_ajax or (initial_load and not self.req.form.get('fname')) + ulid = ' ' + if toplevel: + ulid = ' id="tree-%s"' % treeid + self.w(u'' % (ulid, self.css_classes)) for rowidx in xrange(len(self.rset)): self.wview(self.itemvid, self.rset, row=rowidx, col=0, vid=subvid, parentvid=self.id, treeid=treeid) self.w(u'
') - if initial_load and not self.req.form.get('fname'): + if toplevel: self.req.add_css('jquery.treeview.css') self.req.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery.treeview.js')) self.req.html_headers.add_onload(u""" -jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid) - +jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid, + jsoncall=toplevel_thru_ajax) class FileTreeView(TreeView): """specific version of the treeview to display file trees @@ -91,14 +96,15 @@ (each item should be expandable if it's not a tree leaf) """ id = 'treeitemview' - __select__ = EntityView.__select__ & implements(ITree) # XXX + default_branch_state_is_open = False + __select__ = EntityView.__select__ & implements(ITree) def open_state(self, eeid, treeid): cookies = self.req.get_cookie() treestate = cookies.get(treecookiename(treeid)) if treestate: return str(eeid) in treestate.value.split(';') - return False + return self.default_branch_state_is_open def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview'): w = self.w diff -r 24489cbbd697 -r 70c0dd1c3b7d web/views/workflow.py --- a/web/views/workflow.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/views/workflow.py Thu Sep 17 14:53:18 2009 +0200 @@ -15,55 +15,59 @@ from logilab.common.graph import escape, GraphGenerator, DotBackend from cubicweb import Unauthorized, view -from cubicweb.selectors import (implements, has_related_entities, +from cubicweb.selectors import (implements, has_related_entities, one_line_rset, relation_possible, match_form_params) 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.views import TmpFileViewMixin, forms +from cubicweb.schema import display_name +from cubicweb.web import uicfg, stdmsgs, action, component, form, action +from cubicweb.web import formfields as ff, formwidgets as fwdgs +from cubicweb.web.views import TmpFileViewMixin, forms, primary +_abaa = uicfg.actionbox_appearsin_addmenu +_abaa.tag_subject_of(('BaseTransition', 'condition', 'RQLExpression'), False) +_abaa.tag_subject_of(('State', 'allowed_transition', 'BaseTransition'), False) +_abaa.tag_object_of(('SubWorkflowExitPoint', 'destination_state', 'State'), + False) # 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] + entity = self.rset.get_entity(row, col) transition = self.req.entity_from_eid(self.req.form['treid']) dest = transition.destination() _ = self.req._ - form = self.vreg.select('forms', 'changestate', self.req, rset=self.rset, - row=row, col=col, entity=entity, - redirect_path=self.redirectpath(entity)) + form = self.vreg['forms'].select('changestate', self.req, entity=entity, + redirect_path=self.redirectpath(entity)) self.w(form.error_message()) self.w(u'

%s %s

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

%s

\n' % msg) - self.w(form.form_render(state=dest.eid, trcomment=u'', - trcomment_format=self.req.property_value('ui.default-text-format'))) + trinfo = self.vreg['etypes'].etype_class('TrInfo')(self.req) + self.initialize_varmaker() + trinfo.eid = self.varmaker.next() + subform = self.vreg['forms'].select('edition', self.req, entity=trinfo, + mainform=False) + subform.field_by_name('by_transition').widget = fwdgs.HiddenInput() + form.form_add_subform(subform) + self.w(form.form_render(wf_info_for=entity.eid, + by_transition=transition.eid)) def redirectpath(self, entity): return entity.rest_path() @@ -111,14 +115,48 @@ def cell_call(self, row, col, view=None): self.wview('wfhistory', self.rset, row=row, col=col, view=view) -# workflow entity types views ################################################# + +# workflow actions ############################################################# + +class WorkflowActions(action.Action): + """fill 'workflow' sub-menu of the actions box""" + id = 'workflow' + __select__ = (action.Action.__select__ & one_line_rset() & + relation_possible('in_state')) + + submenu = _('workflow') + order = 10 + + def fill_menu(self, box, menu): + entity = self.rset.get_entity(self.row or 0, self.col or 0) + menu.label = u'%s: %s' % (self.req._('state'), entity.printable_state) + menu.append_anyway = True + super(WorkflowActions, self).fill_menu(box, menu) + + def actual_actions(self): + entity = self.rset.get_entity(self.row or 0, self.col or 0) + hastr = False + for tr in entity.possible_transitions(): + url = entity.absolute_url(vid='statuschange', treid=tr.eid) + yield self.build_action(self.req._(tr.name), url) + hastr = True + # don't propose to see wf if user can't pass any transition + if hastr: + wfurl = entity.current_workflow.absolute_url() + yield self.build_action(self.req._('view workflow'), wfurl) + if entity.workflow_history: + wfurl = entity.absolute_url(vid='wfhistory') + yield self.build_action(self.req._('view history'), wfurl) + + +# workflow entity types views ################################################## class CellView(view.EntityView): id = 'cell' __select__ = implements('TrInfo') def cell_call(self, row, col, cellvid=None): - self.w(self.entity(row, col).view('reledit', rtype='comment')) + self.w(self.rset.get_entity(row, col).view('reledit', rtype='comment')) class StateInContextView(view.EntityView): @@ -131,32 +169,17 @@ row=row, col=col))) -# workflow images ############################################################# - -class ViewWorkflowAction(action.Action): - id = 'workflow' - __select__ = implements('CWEType') & has_related_entities('state_of', 'object') +class WorkflowPrimaryView(primary.PrimaryView): + __select__ = implements('Workflow') - category = 'mainactions' - title = _('view workflow') - def url(self): - entity = self.rset.get_entity(self.row or 0, self.col or 0) - return entity.absolute_url(vid='workflow') + def render_entity_attributes(self, entity): + self.w(entity.view('reledit', rtype='description')) + self.w(u'%s' % ( + xml_escape(entity.absolute_url(vid='wfgraph')), + xml_escape(self.req._('graphical workflow for %s') % entity.name))) -class CWETypeWorkflowView(view.EntityView): - id = 'workflow' - __select__ = implements('CWEType') - cache_max_age = 60*60*2 # stay in http cache for 2 hours by default - - def cell_call(self, row, col, **kwargs): - entity = self.entity(row, col) - self.w(u'

%s

' % (self.req._('workflow for %s') - % display_name(self.req, entity.name))) - self.w(u'%s' % ( - xml_escape(entity.absolute_url(vid='ewfgraph')), - xml_escape(self.req._('graphical workflow for %s') % entity.name))) - +# workflow images ############################################################## class WorkflowDotPropsHandler(object): def __init__(self, req): @@ -180,7 +203,9 @@ self._('groups:'), ','.join(g.name for g in tr.require_group))) if tr.condition: - descr.append('%s %s'% (self._('condition:'), tr.condition)) + descr.append('%s %s'% ( + self._('condition:'), + ' | '.join(e.expression for e in tr.condition))) if descr: props['label'] += escape('\n'.join(descr)) return props @@ -210,14 +235,14 @@ yield transition.eid, transition.destination().eid, transition -class CWETypeWorkflowImageView(TmpFileViewMixin, view.EntityView): - id = 'ewfgraph' +class WorkflowImageView(TmpFileViewMixin, view.EntityView): + id = 'wfgraph' content_type = 'image/png' - __select__ = implements('CWEType') + __select__ = implements('Workflow') def _generate(self, tmpfile): """display schema information for an entity""" - entity = self.entity(self.row, self.col) + entity = self.rset.get_entity(self.row, self.col) visitor = WorkflowVisitor(entity) prophdlr = WorkflowDotPropsHandler(self.req) generator = GraphGenerator(DotBackend('workflow', 'LR', diff -r 24489cbbd697 -r 70c0dd1c3b7d web/views/xmlrss.py --- a/web/views/xmlrss.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/views/xmlrss.py Thu Sep 17 14:53:18 2009 +0200 @@ -95,8 +95,8 @@ val = self.view('textincontext', rset, row=rowindex, col=colindex) else: - val = self.view('final', rset, displaytime=True, - row=rowindex, col=colindex, format='text/plain') + val = self.view('final', rset, row=rowindex, + col=colindex, format='text/plain') w(simple_sgml_tag(tag, val, **attrs)) w(u' \n') w(u'\n' % self.xml_root) diff -r 24489cbbd697 -r 70c0dd1c3b7d web/webconfig.py --- a/web/webconfig.py Thu Sep 17 14:03:21 2009 +0200 +++ b/web/webconfig.py Thu Sep 17 14:53:18 2009 +0200 @@ -160,18 +160,6 @@ if you want to allow everything', 'group': 'web', 'inputlevel': 1, }), - ('submit-url', - {'type' : 'string', - 'default': Method('default_submit_url'), - 'help': ('URL that may be used to report bug in this instance ' - 'by direct access to the project\'s (jpl) tracker, ' - 'if you want this feature on. The url should looks like ' - 'http://mytracker.com/view?__linkto=concerns:1234:subject&etype=Ticket&type=bug&vid=creation ' - 'where 1234 should be replaced by the eid of your project in ' - 'the tracker. If you have no idea about what I\'am talking ' - 'about, you should probably let no value for this option.'), - 'group': 'web', 'inputlevel': 2, - }), ('submit-mail', {'type' : 'string', 'default': None, @@ -196,16 +184,6 @@ }), )) - def default_submit_url(self): - try: - cube = self.cubes()[0] - cubeeid = self.cube_pkginfo(cube).cube_eid - except Exception: - return None - if cubeeid: - return 'http://intranet.logilab.fr/jpl/view?__linkto=concerns:%s:subject&etype=Ticket&type=bug&vid=creation' % cubeeid - return None - def fckeditor_installed(self): return exists(self.ext_resources['FCKEDITOR_PATH'])