# HG changeset patch # User Sylvain Thénault # Date 1259857063 -3600 # Node ID 94cc7cad3d2d0d8d85a20ccbdf0bcb809c410084 # Parent 92ead039d3d0f5d0ccd941550ed7575d88d8914c# Parent 9b52725d8c534ba40877457b413077a10173bf88 backport stable into default diff -r 92ead039d3d0 -r 94cc7cad3d2d .hgignore --- a/.hgignore Mon Nov 23 14:13:53 2009 +0100 +++ b/.hgignore Thu Dec 03 17:17:43 2009 +0100 @@ -8,3 +8,4 @@ \~$ \#.*?\#$ \.swp$ +^doc/book/en/apidoc$ diff -r 92ead039d3d0 -r 94cc7cad3d2d .hgtags --- a/.hgtags Mon Nov 23 14:13:53 2009 +0100 +++ b/.hgtags Thu Dec 03 17:17:43 2009 +0100 @@ -82,3 +82,13 @@ 37d025b2aa7735dae4a861059014c560b45b19e6 cubicweb-debian-version-3.5.4-1 1eca47d59fd932fe23f643ca239cf2408e5b1856 cubicweb-version-3.5.5 aad818d9d9b6fdb2ffea56c0a9af718c0b69899d cubicweb-debian-version-3.5.5-1 +b79f361839a7251b35eb8378fbc0773de7c8a815 cubicweb-version-3.5.6 +e6225e8e36c6506c774e0a76acc301d8ae1c1028 cubicweb-debian-version-3.5.6-1 +b79f361839a7251b35eb8378fbc0773de7c8a815 cubicweb-version-3.5.6 +4e619e97b3fd70769a0f454963193c10cb87f9d4 cubicweb-version-3.5.6 +e6225e8e36c6506c774e0a76acc301d8ae1c1028 cubicweb-debian-version-3.5.6-1 +5f7c939301a1b915e17eec61c05e8e9ab8bdc182 cubicweb-debian-version-3.5.6-1 +0fc300eb4746e01f2755b9eefd986d58d8366ccf cubicweb-version-3.5.7 +7a96c0544c138a0c5f452e5b2428ce6e2b7cb378 cubicweb-debian-version-3.5.7-1 +1677312fd8a3e8c0a5ae083e3104ca62b7c9a5bb cubicweb-version-3.5.9 +d7f2d32340fb59753548ef29cbc1958ef3a55fc6 cubicweb-debian-version-3.5.9-1 diff -r 92ead039d3d0 -r 94cc7cad3d2d __pkginfo__.py --- a/__pkginfo__.py Mon Nov 23 14:13:53 2009 +0100 +++ b/__pkginfo__.py Thu Dec 03 17:17:43 2009 +0100 @@ -7,7 +7,7 @@ distname = "cubicweb" modname = "cubicweb" -numversion = (3, 5, 5) +numversion = (3, 5, 10) version = '.'.join(str(num) for num in numversion) license = 'LGPL' diff -r 92ead039d3d0 -r 94cc7cad3d2d common/mail.py --- a/common/mail.py Mon Nov 23 14:13:53 2009 +0100 +++ b/common/mail.py Thu Dec 03 17:17:43 2009 +0100 @@ -198,6 +198,11 @@ subject = self.subject() except SkipEmail: continue + except Exception, ex: + # shouldn't make the whole transaction fail because of rendering + # error (unauthorized or such) + self.exception(str(ex)) + continue msg = format_mail(self.user_data, [emailaddr], content, subject, config=self._cw.vreg.config, msgid=msgid, references=refs) yield [emailaddr], msg diff -r 92ead039d3d0 -r 94cc7cad3d2d common/migration.py --- a/common/migration.py Mon Nov 23 14:13:53 2009 +0100 +++ b/common/migration.py Thu Dec 03 17:17:43 2009 +0100 @@ -268,6 +268,7 @@ in interactive mode, display the migration script path, ask for confirmation and execute it if confirmed """ + migrscript = os.path.normpath(migrscript) if migrscript.endswith('.py'): script_mode = 'python' elif migrscript.endswith('.txt') or migrscript.endswith('.rst'): @@ -295,7 +296,7 @@ return func(*args, **kwargs) else: # script_mode == 'doctest' import doctest - doctest.testfile(os.path.abspath(migrscript), module_relative=False, + doctest.testfile(migrscript, module_relative=False, optionflags=doctest.ELLIPSIS, globs=scriptlocals) def cmd_option_renamed(self, oldname, newname): diff -r 92ead039d3d0 -r 94cc7cad3d2d common/mixins.py --- a/common/mixins.py Mon Nov 23 14:13:53 2009 +0100 +++ b/common/mixins.py Thu Dec 03 17:17:43 2009 +0100 @@ -194,7 +194,12 @@ return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() ) +"""pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity +classes which have the relation described by the dict's key. +NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree +(eg without plugged classes). This includes bases Entity and AnyEntity classes. +""" MI_REL_TRIGGERS = { ('primary_email', 'subject'): EmailableMixIn, ('use_email', 'subject'): EmailableMixIn, diff -r 92ead039d3d0 -r 94cc7cad3d2d cwconfig.py --- a/cwconfig.py Mon Nov 23 14:13:53 2009 +0100 +++ b/cwconfig.py Thu Dec 03 17:17:43 2009 +0100 @@ -216,9 +216,12 @@ if os.environ.get('APYCOT_ROOT'): mode = 'test' - CUBES_DIR = '%(APYCOT_ROOT)s/local/share/cubicweb/cubes/' % os.environ - # create __init__ file - file(join(CUBES_DIR, '__init__.py'), 'w').close() + if CWDEV: + CUBES_DIR = '%(APYCOT_ROOT)s/local/share/cubicweb/cubes/' % os.environ + # create __init__ file + file(join(CUBES_DIR, '__init__.py'), 'w').close() + else: + CUBES_DIR = '/usr/share/cubicweb/cubes/' elif (CWDEV and _forced_mode != 'system'): mode = 'user' CUBES_DIR = abspath(normpath(join(CW_SOFTWARE_ROOT, '../cubes'))) @@ -612,7 +615,10 @@ root = os.environ['APYCOT_ROOT'] REGISTRY_DIR = '%s/etc/cubicweb.d/' % root RUNTIME_DIR = tempfile.gettempdir() - MIGRATION_DIR = '%s/local/share/cubicweb/migration/' % root + if CWDEV: + MIGRATION_DIR = '%s/local/share/cubicweb/migration/' % root + else: + MIGRATION_DIR = '/usr/share/cubicweb/migration/' if not exists(REGISTRY_DIR): os.makedirs(REGISTRY_DIR) else: diff -r 92ead039d3d0 -r 94cc7cad3d2d cwvreg.py --- a/cwvreg.py Mon Nov 23 14:13:53 2009 +0100 +++ b/cwvreg.py Thu Dec 03 17:17:43 2009 +0100 @@ -499,7 +499,7 @@ for key, val in propvalues: try: values[key] = self.typed_value(key, val) - except ValueError: + except ValueError, ex: self.warning('%s (you should probably delete that property ' 'from the database)', ex) except UnknownProperty, ex: diff -r 92ead039d3d0 -r 94cc7cad3d2d debian/changelog --- a/debian/changelog Mon Nov 23 14:13:53 2009 +0100 +++ b/debian/changelog Thu Dec 03 17:17:43 2009 +0100 @@ -1,3 +1,33 @@ +cubicweb (3.5.10-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Thu, 03 Dec 2009 15:48:38 +0100 + +cubicweb (3.5.9-1) unstable; urgency=low + + * new upstream release + + -- Nicolas Chauvat Sun, 29 Nov 2009 23:28:47 +0100 + +cubicweb (3.5.8-1) unstable; urgency=low + + * new upstream release + + -- Nicolas Chauvat Sun, 29 Nov 2009 22:43:11 +0100 + +cubicweb (3.5.7-1) unstable; urgency=low + + * new upstream release + + -- Nicolas Chauvat Sat, 28 Nov 2009 11:50:08 +0100 + +cubicweb (3.5.6-1) unstable; urgency=low + + * new upstream release + + -- Adrien Di Mascio Mon, 23 Nov 2009 18:55:01 +0100 + cubicweb (3.5.5-1) unstable; urgency=low * new upstream release diff -r 92ead039d3d0 -r 94cc7cad3d2d debian/control --- a/debian/control Mon Nov 23 14:13:53 2009 +0100 +++ b/debian/control Thu Dec 03 17:17:43 2009 +0100 @@ -4,6 +4,7 @@ Maintainer: Logilab S.A. Uploaders: Sylvain Thenault , Julien Jehannet , + Adrien Di Mascio , Aurélien Campéas , Nicolas Chauvat Build-Depends: debhelper (>= 5), python-dev (>=2.4), python-central (>= 0.5) @@ -76,7 +77,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.44.0), python-yams (>= 0.25.0), python-rql (>= 0.22.3), python-lxml +Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.45.2), python-yams (>= 0.25.0), python-rql (>= 0.22.3), python-lxml Recommends: python-simpletal (>= 4.0) Conflicts: cubicweb-core Replaces: cubicweb-core diff -r 92ead039d3d0 -r 94cc7cad3d2d debian/rules --- a/debian/rules Mon Nov 23 14:13:53 2009 +0100 +++ b/debian/rules Thu Dec 03 17:17:43 2009 +0100 @@ -8,7 +8,7 @@ PY_VERSION:=$(shell pyversions -d) build: build-stamp -build-stamp: +build-stamp: dh_testdir # XXX doesn't work if logilab-doctools, logilab-xml are not in build depends # and I can't get pbuilder find them in its chroot :( @@ -17,7 +17,7 @@ python setup.py build touch build-stamp -clean: +clean: dh_testdir dh_testroot rm -f build-stamp configure-stamp @@ -82,6 +82,6 @@ binary-arch: -binary: binary-indep +binary: binary-indep .PHONY: build clean binary binary-indep binary-arch diff -r 92ead039d3d0 -r 94cc7cad3d2d devtools/__init__.py diff -r 92ead039d3d0 -r 94cc7cad3d2d devtools/devctl.py --- a/devtools/devctl.py Mon Nov 23 14:13:53 2009 +0100 +++ b/devtools/devctl.py Thu Dec 03 17:17:43 2009 +0100 @@ -21,9 +21,10 @@ from logilab.common.shellutils import ASK from logilab.common.clcommands import register_commands +from cubicweb.__pkginfo__ import version as cubicwebversion from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage -from cubicweb.__pkginfo__ import version as cubicwebversion from cubicweb.toolsutils import Command, copy_skeleton, underline_title +from cubicweb.schema import CONSTRAINTS from cubicweb.web.webconfig import WebConfiguration from cubicweb.server.serverconfig import ServerConfiguration @@ -138,6 +139,8 @@ libschema = {} afs = uicfg.autoform_section appearsin_addmenu = uicfg.actionbox_appearsin_addmenu + for cstrtype in CONSTRAINTS: + add_msg(w, cstrtype) done = set() for eschema in sorted(schema.entities()): etype = eschema.type diff -r 92ead039d3d0 -r 94cc7cad3d2d devtools/testlib.py --- a/devtools/testlib.py Mon Nov 23 14:13:53 2009 +0100 +++ b/devtools/testlib.py Thu Dec 03 17:17:43 2009 +0100 @@ -24,7 +24,7 @@ from logilab.common.decorators import cached, classproperty, clear_cache from logilab.common.deprecation import deprecated -from cubicweb import NoSelectableObject, AuthenticationError +from cubicweb import ValidationError, NoSelectableObject, AuthenticationError from cubicweb import cwconfig, devtools, web, server from cubicweb.dbapi import repo_connect, ConnectionProperties, ProgrammingError from cubicweb.sobjects import notification @@ -673,10 +673,13 @@ try: validatorclass = self.vid_validators[view.__regid__] except KeyError: - if template is None: - default_validator = htmlparser.HTMLValidator + if view.content_type in ('text/html', 'application/xhtml+xml'): + if template is None: + default_validator = htmlparser.HTMLValidator + else: + default_validator = htmlparser.DTDValidator else: - default_validator = htmlparser.DTDValidator + default_validator = None validatorclass = self.content_type_validators.get(view.content_type, default_validator) if validatorclass is None: @@ -779,7 +782,7 @@ rset = cu.execute('%s X' % etype) edict[str(etype)] = set(row[0] for row in rset.rows) existingrels = {} - ignored_relations = SYSTEM_RELATIONS | self.ignored_relations + ignored_relations = SYSTEM_RELATIONS + self.ignored_relations for rschema in self.schema.relations(): if rschema.final or rschema in ignored_relations: continue @@ -788,7 +791,11 @@ q = make_relations_queries(self.schema, edict, cu, ignored_relations, existingrels=existingrels) for rql, args in q: - cu.execute(rql, args) + try: + cu.execute(rql, args) + except ValidationError, ex: + # failed to satisfy some constraint + print 'error in automatic db population', ex self.post_populate(cu) self.commit() diff -r 92ead039d3d0 -r 94cc7cad3d2d doc/book/en/development/devweb/facets.rst --- a/doc/book/en/development/devweb/facets.rst Mon Nov 23 14:13:53 2009 +0100 +++ b/doc/book/en/development/devweb/facets.rst Thu Dec 03 17:17:43 2009 +0100 @@ -1,3 +1,129 @@ The facets system ----------------- -XXX feed me \ No newline at end of file +XXX feed me more (below is the extracted of adim blog) + + +Recently, for internal purposes, we've made a little cubicweb application to +help us +organizing visits to find new office locations. Here's an *excerpt* of the +schema: + +.. sourcecode:: python + + class Office(WorkflowableEntityType): + price = Int(description='euros / m2 / HC / HT') + surface = Int(description='m2') + description = RichString(fulltextindexed=True) + has_address = SubjectRelation('PostalAddress', cardinality='1?', composite='subject') + proposed_by = SubjectRelation('Agency') + comments = ObjectRelation('Comment', cardinality='1*', composite='object') + screenshots = SubjectRelation(('File', 'Image'), cardinality='*1', + composite='subject') + +The two other entity types defined in the schema are `Visit` and `Agency` but we +can also guess from the above that this application uses the two cubes +`comment`_ and +`addressbook`_ (remember, cubicweb is only a game where you assemble cubes !). + +While we know that just defining the schema in enough to have a full, usable, +(testable !) application, we also know that every application needs to be +customized to fulfill the needs it was built for. So in this case, what we +needed most was some custom filters that would let us restrict searches +according +to surfaces, prices or zipcodes. Fortunately for us, Cubicweb provides the +**facets** (image_) mechanism and a few base classes that make the task quite +easy: + +.. sourcecode:: python + + class PostalCodeFacet(RelationFacet): + id = 'postalcode-facet' # every registered class must have an id + __select__ = implements('Office') # this facet should only be selected when + # visualizing offices + rtype = 'has_address' # this facet is a filter on the entity linked to + # the office thrhough the relation + # has_address + target_attr = 'postalcode' # the filter's key is the attribute "postal_code" + # of the target PostalAddress entity + +This is a typical `RelationFacet`: we want to be able to filter offices +according +to the attribute `postalcode` of their associated `PostalAdress`. Each line in +the class is explained by the comment on its right. + +Now, here is the code to define a filter based on the `surface` attribute of the +`Office`: + +.. sourcecode:: python + + class SurfaceFacet(AttributeFacet): + id = 'surface-facet' # every registered class must have an id + __select__ = implements('Office') # this facet should only be selected when + # visualizing offices + rtype = 'surface' # the filter's key is the attribute "surface" + comparator = '>=' # override the default value of operator since + # we want to filter according to a + # minimal + # value, not an exact one + + def rset_vocabulary(self, ___): + """override the default vocabulary method since we want to hard-code + our threshold values. + Not overriding would generate a filter box with all existing surfaces + defined in the database. + """ + return [('> 200', '200'), ('> 250', '250'), + ('> 275', '275'), ('> 300', '300')] + + +And that's it: we have two filter boxes automatically displayed on each page +presenting more than one office. The `price` facet is basically the same as the +`surface` one but with a different vocabulary and with ``rtype = 'price'``. + +(The cube also benefits from the builtin google map views defined by +cubicweb but that's for another blog). + +.. _image: http://www.cubicweb.org/image/197646?vid=download +.. _comment: http://www.cubicweb.org/project/cubicweb-comment +.. _addressbook: http://www.cubicweb.org/project/cubicweb-addressbook + +CubicWeb has this really nice builtin `facet`_ system to +define restrictions `filters`_ really as easily as possible. + +We've just added two new kind of facets in CubicWeb : + +- The **RangeFacet** which displays a slider using `jquery`_ + to choose a lower bound and an upper bound. The **RangeWidget** + works with either numerical values or date values + +- The **HasRelationFacet** which displays a simple checkbox and + lets you refine your selection in order to get only entities + that actually use this relation. + +.. image :: http://www.cubicweb.org/Image/343498?vid=download + + +Here's an example of code that defines a facet to filter +musical works according to their composition date: + +.. sourcecode:: python + + class CompositionDateFacet(DateRangeFacet): + # 1. make sure this facet is displayed only on Track selection + __select__ = DateRangeFacet.__select__ & implements('Track') + # 2. give the facet an id (required by CubicWeb) + id = 'compdate-facet' + # 3. specify the attribute name that actually stores the date in the DB + rtype = 'composition_date' + +And that's it, on each page displaying tracks, you'll be able to filter them +according to their composition date with a jquery slider. + +All this, brought by CubicWeb (in the next 3.3 version) + +.. _facet: http://en.wikipedia.org/wiki/Faceted_browser +.. _filters: http://www.cubicweb.org/blogentry/154152 +.. _jquery: http://www.jqueryui.com/ + +To use **HasRelationFacet** on a reverse relation add ``role = 'object'`` in +it's definitions. diff -r 92ead039d3d0 -r 94cc7cad3d2d doc/book/en/development/devweb/internationalization.rst --- a/doc/book/en/development/devweb/internationalization.rst Mon Nov 23 14:13:53 2009 +0100 +++ b/doc/book/en/development/devweb/internationalization.rst Thu Dec 03 17:17:43 2009 +0100 @@ -20,6 +20,9 @@ String internationalization ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +User defined string +``````````````````` + In the Python code and cubicweb-tal templates translatable strings can be marked in one of the following ways : @@ -63,15 +66,40 @@ Translations in cubicweb-tal template can also be done with TAL tags `i18n:content` and `i18n:replace`. -.. note:: - - We dont need to mark the translation strings of entities/relations - used by a particular instance's schema as they are generated - automatically. If you need to add messages on top of those that can be found in the source, you can create a file named `i18n/static-messages.pot`. +Generated string +```````````````` + +We do not need to mark the translation strings of entities/relations used by a +particular instance's schema as they are generated automatically. String for +various actions are also generated. + +For exemple the following schema :: + + Class EntityA(EntityType): + relationa2b = SubjectRelation('EntityB') + + class EntityB(EntityType): + pass + +May generate the following message :: + + creating EntityB (EntityA %(linkto)s relation_a2b EntityB) + +This message will be used in views of ``EntityA`` for creation of a new +``EntityB`` with a preset relation ``relation_a2b`` between the current +``EntityA`` and the new ``EntityB``. The opposite message :: + + creating EntityA (EntityA relation_a2b %(linkto)s EntityA) + +Is used for similar creation of an ``EntityA`` from a view of ``EntityB``. + +In the translated string you can use ``%(linkto)s`` for reference to the source +``entity``. + Handle the translation catalog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff -r 92ead039d3d0 -r 94cc7cad3d2d entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Mon Nov 23 14:13:53 2009 +0100 +++ b/entities/test/unittest_wfobjs.py Thu Dec 03 17:17:43 2009 +0100 @@ -37,12 +37,18 @@ 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'}) + self.assertEquals(ex.errors, {'name': 'workflow already have a state of that name'}) # no pb if not in the same workflow wf2 = add_wf(self, 'Company') foo = wf2.add_state(u'foo', initial=True) self.commit() + # gnark gnark + bar = wf.add_state(u'bar') + self.commit() + print '*'*80 + bar.set_attributes(name=u'foo') + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'name': 'workflow already have a state of that name'}) def test_duplicated_transition(self): wf = add_wf(self, 'Company') @@ -51,8 +57,19 @@ 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'}) + self.assertEquals(ex.errors, {'name': 'workflow already have a transition of that name'}) + # no pb if not in the same workflow + wf2 = 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',)) + self.commit() + # gnark gnark + biz = wf.add_transition(u'biz', (bar,), foo) + self.commit() + biz.set_attributes(name=u'baz') + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'name': 'workflow already have a transition of that name'}) class WorkflowTC(CubicWebTC): @@ -375,7 +392,7 @@ 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'}) + self.assertEquals(ex.errors, {'custom_workflow': 'workflow isn\'t a workflow for this type'}) def test_del_custom_wf(self): """member in some state shared by the new workflow, nothing has to be diff -r 92ead039d3d0 -r 94cc7cad3d2d entities/wfobjs.py --- a/entities/wfobjs.py Mon Nov 23 14:13:53 2009 +0100 +++ b/entities/wfobjs.py Thu Dec 03 17:17:43 2009 +0100 @@ -34,6 +34,7 @@ return any(et for et in self.reverse_default_workflow if et.name == etype) + # XXX define parent() instead? what if workflow of multiple types? def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted @@ -101,7 +102,7 @@ self._cw.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 + assert not self.initial, "Initial state already defined as %s" % self.initial self._cw.execute('SET WF initial_state S ' 'WHERE S eid %(s)s, WF eid %(wf)s', {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) @@ -245,6 +246,9 @@ def destination(self): return self.destination_state[0] + def parent(self): + return self.workflow + class WorkflowTransition(BaseTransition): """customized class for WorkflowTransition entities""" @@ -310,6 +314,9 @@ def destination(self): return self.destination_state and self.destination_state[0] or None + def parent(self): + return self.reverse_subworkflow_exit[0] + class State(AnyEntity): """customized class for State entities""" @@ -322,13 +329,8 @@ # 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(), {} - return super(State, self).after_deletion_path() + def parent(self): + return self.workflow class TrInfo(AnyEntity): @@ -353,13 +355,8 @@ 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 - """ - if self.for_entity: - return self.for_entity.rest_path(), {} - return 'view', {} + def parent(self): + return self.for_entity class WorkflowableMixIn(object): @@ -431,6 +428,7 @@ def possible_transitions(self, type='normal'): """generates transition that MAY be fired for the given entity, expected to be in this state + used only by the UI """ if self.current_state is None or self.current_workflow is None: return diff -r 92ead039d3d0 -r 94cc7cad3d2d entity.py --- a/entity.py Mon Nov 23 14:13:53 2009 +0100 +++ b/entity.py Thu Dec 03 17:17:43 2009 +0100 @@ -100,8 +100,18 @@ attr = 'reverse_%s' % rschema.type setattr(cls, attr, ObjectRelation(rschema)) if mixins: - cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object]) - cls.debug('plugged %s mixins on %s', mixins, etype) + # see etype class instantation in cwvreg.ETypeRegistry.etype_class method: + # due to class dumping, cls is the generated top level class with actual + # user class as (only) parent. Since we want to be able to override mixins + # method from this user class, we have to take care to insert mixins after that + # class + # + # note that we don't plug mixins as user class parent since it causes pb + # with some cases of entity classes inheritance. + mixins.insert(0, cls.__bases__[0]) + mixins += cls.__bases__[1:] + cls.__bases__ = tuple(mixins) + cls.info('plugged %s mixins on %s', mixins, cls) @classmethod def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X', @@ -657,6 +667,7 @@ rdef = rtype.role_rdef(self.e_schema, targettype, role) insertsecurity = (rdef.has_local_role('add') and not rdef.has_perm(self._cw, 'add', **securitycheck_args)) + # XXX consider constraint.mainvars to check if constraint apply if vocabconstraints: # RQLConstraint is a subclass for RQLVocabularyConstraint, so they # will be included as well @@ -777,15 +788,26 @@ kwargs, 'x') def set_relations(self, _cw_unsafe=False, **kwargs): + """add relations to the given object. To set a relation where this entity + is the object of the relation, use 'reverse_' as argument name. + + Values may be an entity, a list of entity, or None (meaning that all + relations of the given type from or to this object should be deleted). + """ if _cw_unsafe: execute = self.req.unsafe_execute else: execute = self.req.execute + # XXX update cache for attr, values in kwargs.iteritems(): if attr.startswith('reverse_'): restr = 'Y %s X' % attr[len('reverse_'):] else: restr = 'X %s Y' % attr + if values is None: + execute('DELETE %s WHERE X eid %%(x)s' % restr, + {'x': self.eid}, 'x') + continue if not isinstance(values, (tuple, list, set, frozenset)): values = (values,) execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % ( diff -r 92ead039d3d0 -r 94cc7cad3d2d hooks/integrity.py --- a/hooks/integrity.py Mon Nov 23 14:13:53 2009 +0100 +++ b/hooks/integrity.py Thu Dec 03 17:17:43 2009 +0100 @@ -9,7 +9,7 @@ __docformat__ = "restructuredtext en" from cubicweb import ValidationError -from cubicweb.schema import RQLVocabularyConstraint +from cubicweb.schema import RQLConstraint, RQLUniqueConstraint from cubicweb.selectors import entity_implements from cubicweb.common.uilib import soup2xhtml from cubicweb.server import hook @@ -146,6 +146,7 @@ events = ('after_add_relation',) def __call__(self): + # XXX get only RQL[Unique]Constraints? constraints = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto, 'constraints') if constraints: @@ -167,7 +168,7 @@ for attr in entity.edited_attributes: if schema.rschema(attr).final: constraints = [c for c in entity.rdef(attr).constraints - if isinstance(c, RQLVocabularyConstraint)] + if isinstance(c, (RQLUniqueConstraint, RQLConstraint))] if constraints: _CheckConstraintsOp(self._cw, constraints=constraints, rdef=(entity.eid, attr, None)) diff -r 92ead039d3d0 -r 94cc7cad3d2d i18n/en.po --- a/i18n/en.po Mon Nov 23 14:13:53 2009 +0100 +++ b/i18n/en.po Thu Dec 03 17:17:43 2009 +0100 @@ -226,6 +226,9 @@ msgid "Boolean_plural" msgstr "Booleans" +msgid "BoundConstraint" +msgstr "bound constraint" + msgid "Browse by category" msgstr "" @@ -329,6 +332,9 @@ msgid "Do you want to delete the following element(s) ?" msgstr "" +msgid "Download page as pdf" +msgstr "" + msgid "EmailAddress" msgstr "Email address" @@ -353,6 +359,12 @@ msgid "Float_plural" msgstr "Floats" +# schema pot file, generated on 2009-12-03 09:22:35 +# +# singular and plural forms for each entity type +msgid "FormatConstraint" +msgstr "format constraint" + msgid "From:" msgstr "" @@ -368,6 +380,9 @@ msgid "Interval" msgstr "Interval" +msgid "IntervalBoundConstraint" +msgstr "interval constraint" + msgid "Interval_plural" msgstr "Intervals" @@ -455,18 +470,30 @@ msgid "Please note that this is only a shallow copy" msgstr "" +msgid "RQLConstraint" +msgstr "RQL constraint" + msgid "RQLExpression" msgstr "RQL expression" msgid "RQLExpression_plural" msgstr "RQL expressions" +msgid "RQLUniqueConstraint" +msgstr "RQL unique constraint" + +msgid "RQLVocabularyConstraint" +msgstr "RQL vocabulary constraint" + msgid "Read permissions" msgstr "" msgid "Recipients:" msgstr "" +msgid "RegexpConstraint" +msgstr "regular expression constrainte" + msgid "Registry's content" msgstr "" @@ -489,6 +516,9 @@ msgid "Server" msgstr "" +msgid "SizeConstraint" +msgstr "size constraint" + msgid "Startup views" msgstr "" @@ -498,6 +528,9 @@ msgid "State_plural" msgstr "States" +msgid "StaticVocabularyConstraint" +msgstr "vocabulary constraint" + msgid "String" msgstr "String" @@ -634,6 +667,9 @@ msgid "Unable to find anything named \"%s\" in the schema !" msgstr "" +msgid "UniqueConstraint" +msgstr "unique constraint" + msgid "Update permissions" msgstr "" @@ -895,9 +931,6 @@ msgid "add" msgstr "" -msgid "add BaseTransition transition_of Workflow object" -msgstr "" - msgid "add Bookmark bookmarked_by CWUser object" msgstr "bookmark" @@ -1388,12 +1421,6 @@ msgid "components_etypenavigation_description" msgstr "permit to filter search results by entity type" -msgid "components_help" -msgstr "help button" - -msgid "components_help_description" -msgstr "the help button on the top right-hand corner" - msgid "components_loggeduserlink" msgstr "user link" @@ -1415,12 +1442,6 @@ 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" @@ -1498,6 +1519,12 @@ msgid "contentnavigation_breadcrumbs_description" msgstr "breadcrumbs bar that display a path locating the page in the site" +msgid "contentnavigation_metadata" +msgstr "entity's metadata" + +msgid "contentnavigation_metadata_description" +msgstr "" + msgid "contentnavigation_prevnext" msgstr "previous / next entity" @@ -1514,6 +1541,12 @@ "section containing entities related by the \"see also\" relation on entities " "supporting it." +msgid "contentnavigation_view_page_as_pdf" +msgstr "icon to display page as pdf" + +msgid "contentnavigation_view_page_as_pdf_description" +msgstr "" + msgid "contentnavigation_wfhistory" msgstr "workflow history" @@ -1595,10 +1628,6 @@ msgid "created_by_object" msgstr "has created" -msgid "" -"creating BaseTransition (BaseTransition transition_of Workflow %(linkto)s)" -msgstr "" - msgid "creating Bookmark (Bookmark bookmarked_by CWUser %(linkto)s)" msgstr "creating bookmark for %(linkto)s" @@ -1723,6 +1752,9 @@ msgid "csv export" msgstr "" +msgid "ctxtoolbar" +msgstr "toolbar" + #, python-format msgid "currently attached file: %s" msgstr "" @@ -1966,6 +1998,9 @@ msgid "destination state for this transition" msgstr "" +msgid "destination state must be in the same workflow as our parent transition" +msgstr "" + msgid "destination state of a transition" msgstr "" @@ -2017,9 +2052,6 @@ 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" @@ -2035,9 +2067,6 @@ msgid "download icon" msgstr "" -msgid "download page as pdf" -msgstr "" - msgid "download schema as owl" msgstr "" @@ -2129,6 +2158,9 @@ msgid "eta_date" msgstr "" +msgid "exit state must a subworkflow state" +msgstr "" + msgid "exit_point" msgstr "" @@ -2228,6 +2260,10 @@ msgid "follow" msgstr "" +#, python-format +msgid "follow this link for more information on this %s" +msgstr "" + msgid "for_user" msgstr "for user" @@ -3338,6 +3374,12 @@ msgid "state" msgstr "" +msgid "state and transition don't belong the the same workflow" +msgstr "" + +msgid "state doesn't apply to this entity's type" +msgstr "" + msgid "state doesn't belong to entity's current workflow" msgstr "" @@ -3349,6 +3391,9 @@ "workflow for this entity first." msgstr "" +msgid "state doesn't belong to this workflow" +msgstr "" + msgid "state_of" msgstr "state of" @@ -3389,6 +3434,10 @@ msgid "subworkflow" msgstr "" +msgid "" +"subworkflow isn't a workflow for the same types as the transition's workflow" +msgstr "" + msgid "subworkflow state" msgstr "" @@ -3567,6 +3616,10 @@ msgid "toggle check boxes" msgstr "" +#, python-format +msgid "transition %s isn't allowed from %s" +msgstr "" + msgid "transition doesn't belong to entity's workflow" msgstr "" @@ -3856,6 +3909,12 @@ msgid "workflow" msgstr "" +msgid "workflow already have a state of that name" +msgstr "" + +msgid "workflow already have a transition of that name" +msgstr "" + #, python-format msgid "workflow changed to \"%s\"" msgstr "" @@ -3866,6 +3925,12 @@ msgid "workflow history item" msgstr "" +msgid "workflow isn't a workflow for this type" +msgstr "" + +msgid "workflow isn't a workflow of this type" +msgstr "" + msgid "workflow to which this state belongs" msgstr "" @@ -3904,6 +3969,12 @@ msgid "you should probably delete that property" msgstr "" +#~ msgid "components_help" +#~ msgstr "help button" + +#~ msgid "components_help_description" +#~ msgstr "the help button on the top right-hand corner" + #~ msgctxt "inlined:CWRelation:from_entity:subject" #~ msgid "remove this CWEType" #~ msgstr "remove this entity type" diff -r 92ead039d3d0 -r 94cc7cad3d2d i18n/es.po --- a/i18n/es.po Mon Nov 23 14:13:53 2009 +0100 +++ b/i18n/es.po Thu Dec 03 17:17:43 2009 +0100 @@ -234,6 +234,9 @@ msgid "Boolean_plural" msgstr "Booleanos" +msgid "BoundConstraint" +msgstr "" + msgid "Browse by category" msgstr "Busca por categoría" @@ -337,6 +340,9 @@ msgid "Do you want to delete the following element(s) ?" msgstr "Desea suprimir el(los) elemento(s) siguiente(s)" +msgid "Download page as pdf" +msgstr "" + msgid "EmailAddress" msgstr "Correo Electrónico" @@ -361,6 +367,12 @@ msgid "Float_plural" msgstr "Números flotantes" +# schema pot file, generated on 2009-12-03 09:22:35 +# +# singular and plural forms for each entity type +msgid "FormatConstraint" +msgstr "" + msgid "From:" msgstr "De: " @@ -376,6 +388,9 @@ msgid "Interval" msgstr "Duración" +msgid "IntervalBoundConstraint" +msgstr "" + msgid "Interval_plural" msgstr "Duraciones" @@ -463,18 +478,30 @@ msgid "Please note that this is only a shallow copy" msgstr "Recuerde que no es más que una copia superficial" +msgid "RQLConstraint" +msgstr "" + msgid "RQLExpression" msgstr "Expresión RQL" msgid "RQLExpression_plural" msgstr "Expresiones RQL" +msgid "RQLUniqueConstraint" +msgstr "" + +msgid "RQLVocabularyConstraint" +msgstr "" + msgid "Read permissions" msgstr "Autorización de leer" msgid "Recipients:" msgstr "Destinatarios" +msgid "RegexpConstraint" +msgstr "" + msgid "Registry's content" msgstr "" @@ -497,6 +524,9 @@ msgid "Server" msgstr "Servidor" +msgid "SizeConstraint" +msgstr "" + msgid "Startup views" msgstr "Vistas de Inicio" @@ -506,6 +536,9 @@ msgid "State_plural" msgstr "Estados" +msgid "StaticVocabularyConstraint" +msgstr "" + msgid "String" msgstr "Cadena de caracteres" @@ -642,6 +675,9 @@ msgid "Unable to find anything named \"%s\" in the schema !" msgstr "No encontramos el nombre \"%s\" en el esquema" +msgid "UniqueConstraint" +msgstr "" + msgid "Update permissions" msgstr "Autorización de modificar" @@ -918,9 +954,6 @@ msgid "add" msgstr "Agregar" -msgid "add BaseTransition transition_of Workflow object" -msgstr "" - msgid "add Bookmark bookmarked_by CWUser object" msgstr "Agregar a los favoritos " @@ -1420,12 +1453,6 @@ msgid "components_etypenavigation_description" msgstr "Permite filtrar por tipo de entidad los resultados de búsqueda" -msgid "components_help" -msgstr "Botón de ayuda" - -msgid "components_help_description" -msgstr "El botón de ayuda, en el encabezado de página" - msgid "components_loggeduserlink" msgstr "Liga usuario" @@ -1448,12 +1475,6 @@ "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" @@ -1531,6 +1552,12 @@ msgid "contentnavigation_breadcrumbs_description" msgstr "Muestra un camino que permite localizar la página actual en el sitio" +msgid "contentnavigation_metadata" +msgstr "" + +msgid "contentnavigation_metadata_description" +msgstr "" + msgid "contentnavigation_prevnext" msgstr "Elemento anterior / siguiente" @@ -1547,6 +1574,12 @@ "sección que muestra las entidades ligadas por la relación \"vea también\" , " "si la entidad soporta esta relación." +msgid "contentnavigation_view_page_as_pdf" +msgstr "" + +msgid "contentnavigation_view_page_as_pdf_description" +msgstr "" + msgid "contentnavigation_wfhistory" msgstr "Histórico del workflow." @@ -1644,10 +1677,6 @@ msgid "created_by_object" msgstr "ha creado" -msgid "" -"creating BaseTransition (BaseTransition transition_of Workflow %(linkto)s)" -msgstr "" - msgid "creating Bookmark (Bookmark bookmarked_by CWUser %(linkto)s)" msgstr "Creando Favorito" @@ -1780,6 +1809,9 @@ msgid "csv export" msgstr "Exportar CSV" +msgid "ctxtoolbar" +msgstr "" + #, python-format msgid "currently attached file: %s" msgstr "archivo adjunto: %s" @@ -2027,6 +2059,9 @@ msgid "destination state for this transition" msgstr "Estado destino para esta transición" +msgid "destination state must be in the same workflow as our parent transition" +msgstr "" + msgid "destination state of a transition" msgstr "Estado destino de una transición" @@ -2078,9 +2113,6 @@ 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" @@ -2098,9 +2130,6 @@ msgid "download icon" msgstr "ícono de descarga" -msgid "download page as pdf" -msgstr "" - msgid "download schema as owl" msgstr "Descargar esquema en OWL" @@ -2197,6 +2226,9 @@ msgid "eta_date" msgstr "fecha de fin" +msgid "exit state must a subworkflow state" +msgstr "" + msgid "exit_point" msgstr "" @@ -2296,6 +2328,10 @@ msgid "follow" msgstr "Seguir la liga" +#, python-format +msgid "follow this link for more information on this %s" +msgstr "" + msgid "for_user" msgstr "Para el usuario" @@ -3437,6 +3473,12 @@ msgid "state" msgstr "estado" +msgid "state and transition don't belong the the same workflow" +msgstr "" + +msgid "state doesn't apply to this entity's type" +msgstr "" + msgid "state doesn't belong to entity's current workflow" msgstr "" @@ -3448,6 +3490,9 @@ "workflow for this entity first." msgstr "" +msgid "state doesn't belong to this workflow" +msgstr "" + msgid "state_of" msgstr "estado_de" @@ -3488,6 +3533,10 @@ msgid "subworkflow" msgstr "" +msgid "" +"subworkflow isn't a workflow for the same types as the transition's workflow" +msgstr "" + msgid "subworkflow state" msgstr "" @@ -3666,6 +3715,10 @@ msgid "toggle check boxes" msgstr "cambiar valor" +#, python-format +msgid "transition %s isn't allowed from %s" +msgstr "" + msgid "transition doesn't belong to entity's workflow" msgstr "" @@ -3965,6 +4018,12 @@ msgid "workflow" msgstr "" +msgid "workflow already have a state of that name" +msgstr "" + +msgid "workflow already have a transition of that name" +msgstr "" + #, python-format msgid "workflow changed to \"%s\"" msgstr "" @@ -3975,6 +4034,12 @@ msgid "workflow history item" msgstr "" +msgid "workflow isn't a workflow for this type" +msgstr "" + +msgid "workflow isn't a workflow of this type" +msgstr "" + msgid "workflow to which this state belongs" msgstr "" @@ -4089,6 +4154,12 @@ #~ msgid "comment:" #~ msgstr "Comentario:" +#~ msgid "components_help" +#~ msgstr "Botón de ayuda" + +#~ msgid "components_help_description" +#~ msgstr "El botón de ayuda, en el encabezado de página" + #~ msgid "creating State (State state_of CWEType %(linkto)s)" #~ msgstr "Creación de un estado por el tipo %(linkto)s" diff -r 92ead039d3d0 -r 94cc7cad3d2d i18n/fr.po --- a/i18n/fr.po Mon Nov 23 14:13:53 2009 +0100 +++ b/i18n/fr.po Thu Dec 03 17:17:43 2009 +0100 @@ -233,6 +233,9 @@ msgid "Boolean_plural" msgstr "Booléen" +msgid "BoundConstraint" +msgstr "contrainte de bornes" + msgid "Browse by category" msgstr "Naviguer par catégorie" @@ -336,6 +339,9 @@ msgid "Do you want to delete the following element(s) ?" msgstr "Voulez vous supprimer le(s) élément(s) suivant(s)" +msgid "Download page as pdf" +msgstr "télécharger la page au format PDF" + msgid "EmailAddress" msgstr "Adresse électronique" @@ -360,6 +366,12 @@ msgid "Float_plural" msgstr "Nombres flottants" +# schema pot file, generated on 2009-12-03 09:22:35 +# +# singular and plural forms for each entity type +msgid "FormatConstraint" +msgstr "contrainte de format" + msgid "From:" msgstr "De :" @@ -375,6 +387,9 @@ msgid "Interval" msgstr "Durée" +msgid "IntervalBoundConstraint" +msgstr "contrainte d'interval" + msgid "Interval_plural" msgstr "Durées" @@ -462,18 +477,30 @@ msgid "Please note that this is only a shallow copy" msgstr "Attention, cela n'effectue qu'une copie de surface" +msgid "RQLConstraint" +msgstr "contrainte rql" + msgid "RQLExpression" msgstr "Expression RQL" msgid "RQLExpression_plural" msgstr "Expressions RQL" +msgid "RQLUniqueConstraint" +msgstr "contrainte rql d'unicité" + +msgid "RQLVocabularyConstraint" +msgstr "contrainte rql de vocabulaire" + msgid "Read permissions" msgstr "Permissions de lire" msgid "Recipients:" msgstr "Destinataires :" +msgid "RegexpConstraint" +msgstr "contrainte expression régulière" + msgid "Registry's content" msgstr "Contenu du registre" @@ -496,6 +523,9 @@ msgid "Server" msgstr "Serveur" +msgid "SizeConstraint" +msgstr "contrainte de taille" + msgid "Startup views" msgstr "Vues de départ" @@ -505,6 +535,9 @@ msgid "State_plural" msgstr "États" +msgid "StaticVocabularyConstraint" +msgstr "contrainte de vocabulaire" + msgid "String" msgstr "Chaîne de caractères" @@ -641,6 +674,9 @@ msgid "Unable to find anything named \"%s\" in the schema !" msgstr "Rien de nommé \"%s\" dans le schéma" +msgid "UniqueConstraint" +msgstr "contrainte d'unicité" + msgid "Update permissions" msgstr "Permissions de modifier" @@ -753,7 +789,7 @@ msgstr "actions" msgid "actions_about" -msgstr "" +msgstr "à propos" msgid "actions_about_description" msgstr "" @@ -777,7 +813,7 @@ msgstr "" msgid "actions_changelog" -msgstr "" +msgstr "changements récents" msgid "actions_changelog_description" msgstr "" @@ -819,7 +855,7 @@ msgstr "" msgid "actions_help" -msgstr "" +msgstr "aide" msgid "actions_help_description" msgstr "" @@ -861,7 +897,7 @@ msgstr "" msgid "actions_poweredby" -msgstr "" +msgstr "powered by" msgid "actions_poweredby_description" msgstr "" @@ -923,9 +959,6 @@ msgid "add" msgstr "ajouter" -msgid "add BaseTransition transition_of Workflow object" -msgstr "" - msgid "add Bookmark bookmarked_by CWUser object" msgstr "signet" @@ -1425,12 +1458,6 @@ msgid "components_etypenavigation_description" msgstr "permet de filtrer par type d'entité les résultats d'une recherche" -msgid "components_help" -msgstr "bouton aide" - -msgid "components_help_description" -msgstr "le bouton d'aide, dans l'en-tête de page" - msgid "components_loggeduserlink" msgstr "lien utilisateur" @@ -1454,12 +1481,6 @@ "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" @@ -1538,6 +1559,12 @@ msgstr "" "affiche un chemin permettant de localiser la page courante dans le site" +msgid "contentnavigation_metadata" +msgstr "méta-données de l'entité" + +msgid "contentnavigation_metadata_description" +msgstr "" + msgid "contentnavigation_prevnext" msgstr "élément précedent / suivant" @@ -1554,6 +1581,12 @@ "section affichant les entités liées par la relation \"voir aussi\" si " "l'entité supporte cette relation." +msgid "contentnavigation_view_page_as_pdf" +msgstr "icône pdf" + +msgid "contentnavigation_view_page_as_pdf_description" +msgstr "l'icône pdf pour obtenir la page courant au format PDF" + msgid "contentnavigation_wfhistory" msgstr "historique du workflow." @@ -1651,10 +1684,6 @@ msgid "created_by_object" msgstr "a créé" -msgid "" -"creating BaseTransition (BaseTransition transition_of Workflow %(linkto)s)" -msgstr "" - msgid "creating Bookmark (Bookmark bookmarked_by CWUser %(linkto)s)" msgstr "création d'un signet pour %(linkto)s" @@ -1787,6 +1816,9 @@ msgid "csv export" msgstr "export CSV" +msgid "ctxtoolbar" +msgstr "barre d'outils" + #, python-format msgid "currently attached file: %s" msgstr "fichie actuellement attaché %s" @@ -2039,6 +2071,11 @@ msgid "destination state for this transition" msgstr "états accessibles par cette transition" +msgid "destination state must be in the same workflow as our parent transition" +msgstr "" +"l'état de destination doit être dans le même workflow que la transition " +"parente" + msgid "destination state of a transition" msgstr "état d'arrivée d'une transition" @@ -2093,9 +2130,6 @@ 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" @@ -2113,9 +2147,6 @@ 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" @@ -2211,6 +2242,9 @@ msgid "eta_date" msgstr "date de fin" +msgid "exit state must a subworkflow state" +msgstr "l'état de sortie doit être un état du sous-workflow" + msgid "exit_point" msgstr "état de sortie" @@ -2310,6 +2344,10 @@ msgid "follow" msgstr "suivre le lien" +#, python-format +msgid "follow this link for more information on this %s" +msgstr "suivez ce lien pour plus d'information sur ce %s" + msgid "for_user" msgstr "pour l'utilisateur" @@ -3455,6 +3493,12 @@ msgid "state" msgstr "état" +msgid "state and transition don't belong the the same workflow" +msgstr "l'état et la transition n'appartiennent pas au même workflow" + +msgid "state doesn't apply to this entity's type" +msgstr "cet état ne s'applique pas à ce type d'entité" + msgid "state doesn't belong to entity's current workflow" msgstr "l'état n'appartient pas au workflow courant de l'entité" @@ -3468,6 +3512,9 @@ "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 doesn't belong to this workflow" +msgstr "l'état n'appartient pas à ce workflow" + msgid "state_of" msgstr "état de" @@ -3508,6 +3555,12 @@ msgid "subworkflow" msgstr "sous-workflow" +msgid "" +"subworkflow isn't a workflow for the same types as the transition's workflow" +msgstr "" +"le sous-workflow ne s'applique pas aux mêmes types que le workflow de cette " +"transition" + msgid "subworkflow state" msgstr "état de sous-workflow" @@ -3687,6 +3740,10 @@ msgid "toggle check boxes" msgstr "inverser les cases à cocher" +#, python-format +msgid "transition %s isn't allowed from %s" +msgstr "la transition %s n'est pas autorisée depuis l'état %s" + msgid "transition doesn't belong to entity's workflow" msgstr "la transition n'appartient pas au workflow de l'entité" @@ -3828,7 +3885,7 @@ msgstr "a la permission de modifier" msgid "updated" -msgstr "" +msgstr "mis à jour" #, python-format msgid "updated %(etype)s #%(eid)s (%(title)s)" @@ -3988,6 +4045,12 @@ msgid "workflow" msgstr "workflow" +msgid "workflow already have a state of that name" +msgstr "le workflow a déja un état du même nom" + +msgid "workflow already have a transition of that name" +msgstr "le workflow a déja une transition du même nom" + #, python-format msgid "workflow changed to \"%s\"" msgstr "workflow changé à \"%s\"" @@ -3998,6 +4061,12 @@ msgid "workflow history item" msgstr "entrée de l'historique de workflow" +msgid "workflow isn't a workflow for this type" +msgstr "le workflow ne s'applique pas à ce type d'entité" + +msgid "workflow isn't a workflow of this type" +msgstr "" + msgid "workflow to which this state belongs" msgstr "workflow auquel cet état appartient" @@ -4036,9 +4105,18 @@ msgid "you should probably delete that property" msgstr "vous devriez probablement supprimer cette propriété" +#~ msgid "components_help" +#~ msgstr "bouton aide" + +#~ msgid "components_help_description" +#~ msgstr "le bouton d'aide, dans l'en-tête de page" + #~ msgid "destination state" #~ msgstr "état de destination" +#~ msgid "display the pdf icon or not" +#~ msgstr "afficher l'icône pdf ou non" + #~ msgctxt "inlined:CWRelation:from_entity:subject" #~ msgid "remove this CWEType" #~ msgstr "supprimer ce type d'entité" diff -r 92ead039d3d0 -r 94cc7cad3d2d misc/migration/3.5.10_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.5.10_Any.py Thu Dec 03 17:17:43 2009 +0100 @@ -0,0 +1,5 @@ +sync_schema_props_perms('state_of') +sync_schema_props_perms('transition_of') +for etype in ('State', 'BaseTransition', 'Transition', 'WorkflowTransition'): + sync_schema_props_perms((etype, 'name', 'String')) + diff -r 92ead039d3d0 -r 94cc7cad3d2d rqlrewrite.py --- a/rqlrewrite.py Mon Nov 23 14:13:53 2009 +0100 +++ b/rqlrewrite.py Thu Dec 03 17:17:43 2009 +0100 @@ -285,7 +285,7 @@ if not eschema.has_perm(self.session, action): rqlexprs = eschema.get_rqlexprs(action) if not rqlexprs: - raise Unauthorised() + raise Unauthorized() self.insert_snippets([((varname, 'X'), rqlexprs)]) def snippet_subquery(self, varmap, transformedsnippet): diff -r 92ead039d3d0 -r 94cc7cad3d2d rset.py --- a/rset.py Mon Nov 23 14:13:53 2009 +0100 +++ b/rset.py Thu Dec 03 17:17:43 2009 +0100 @@ -501,7 +501,6 @@ @cached def column_types(self, col): """return the list of different types in the column with the given col - index default to 0 (ie the first column) :type col: int :param col: the index of the desired column diff -r 92ead039d3d0 -r 94cc7cad3d2d schema.py --- a/schema.py Mon Nov 23 14:13:53 2009 +0100 +++ b/schema.py Thu Dec 03 17:17:43 2009 +0100 @@ -120,6 +120,37 @@ __builtins__['display_name'] = deprecated('[3.4] display_name should be imported from cubicweb.schema')(display_name) + +# rql expression utilities function ############################################ + +def guess_rrqlexpr_mainvars(expression): + defined = set(split_expression(expression)) + mainvars = [] + if 'S' in defined: + mainvars.append('S') + if 'O' in defined: + mainvars.append('O') + if 'U' in defined: + mainvars.append('U') + if not mainvars: + raise Exception('unable to guess selection variables') + return ','.join(mainvars) + +def split_expression(rqlstring): + for expr in rqlstring.split(','): + for word in expr.split(): + yield word + +def normalize_expression(rqlstring): + """normalize an rql expression to ease schema synchronization (avoid + suppressing and reinserting an expression if only a space has been added/removed + for instance) + """ + return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) + + +# Schema objects definition ################################################### + def ERSchema_display_name(self, req, form='', context=None): """return a internationalized string for the entity/relation type name in a given form @@ -271,16 +302,6 @@ RelationDefinitionSchema.check_permission_definitions = check_permission_definitions -def system_etypes(schema): - """return system entity types only: skip final, schema and application entities - """ - for eschema in schema.entities(): - if eschema.final or eschema.schema_entity(): - continue - yield eschema.type - -# Schema objects definition ################################################### - class CubicWebEntitySchema(EntitySchema): """a entity has a type, a set of subject and or object relations the entity schema defines the possible relations for a given type and some @@ -536,25 +557,35 @@ # Possible constraints ######################################################## -class RQLVocabularyConstraint(BaseConstraint): - """the rql vocabulary constraint : - - limit the proposed values to a set of entities returned by a rql query, - but this is not enforced at the repository level - - restriction is additional rql restriction that will be added to - a predefined query, where the S and O variables respectivly represent - the subject and the object of the relation +class BaseRQLConstraint(BaseConstraint): + """base class for rql constraints """ - def __init__(self, restriction): - self.restriction = restriction + def __init__(self, restriction, mainvars=None): + self.restriction = normalize_expression(restriction) + if mainvars is None: + mainvars = guess_rrqlexpr_mainvars(restriction) + else: + normmainvars = [] + for mainvar in mainvars.split(','): + mainvar = mainvar.strip() + if not mainvar.isalpha(): + raise Exception('bad mainvars %s' % mainvars) + normmainvars.append(mainvar) + assert mainvars, 'bad mainvars %s' % mainvars + mainvars = ','.join(sorted(normmainvars)) + self.mainvars = mainvars def serialize(self): - return self.restriction + # start with a comma for bw compat, see below + return ';' + self.mainvars + ';' + self.restriction def deserialize(cls, value): - return cls(value) + # XXX < 3.5.10 bw compat + if not value.startswith(';'): + return cls(value) + _, mainvars, restriction = value.split(';', 2) + return cls(restriction, mainvars) deserialize = classmethod(deserialize) def check(self, entity, rtype, value): @@ -568,60 +599,104 @@ pass # this is a vocabulary constraint, not enforce XXX why? def __str__(self): - return self.restriction + return '%s(Any %s WHERE %s)' % (self.__class__.__name__, self.mainvars, + self.restriction) def __repr__(self): - return '<%s : %s>' % (self.__class__.__name__, repr(self.restriction)) + return '<%s @%#x>' % (self.__str__(), id(self)) -class RQLConstraint(RQLVocabularyConstraint): - """the rql constraint is similar to the RQLVocabularyConstraint but - are also enforced at the repository level +class RQLVocabularyConstraint(BaseRQLConstraint): + """the rql vocabulary constraint : + + limit the proposed values to a set of entities returned by a rql query, + but this is not enforced at the repository level + + restriction is additional rql restriction that will be added to + a predefined query, where the S and O variables respectivly represent + the subject and the object of the relation + + mainvars is a string that should be used as selection variable (eg + `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be + done to guess it according to variable used in the expression. """ - def exec_query(self, session, eidfrom, eidto): - if eidto is None: - rql = 'Any S WHERE S eid %(s)s, ' + self.restriction - return session.unsafe_execute(rql, {'s': eidfrom}, 's', - build_descr=False) - rql = 'Any S,O WHERE S eid %(s)s, O eid %(o)s, ' + self.restriction - return session.unsafe_execute(rql, {'s': eidfrom, 'o': eidto}, - ('s', 'o'), build_descr=False) - def error(self, eid, rtype, msg): - raise ValidationError(eid, {rtype: msg}) + + +class RepoEnforcedRQLConstraintMixIn(object): + + def __init__(self, restriction, mainvars=None, msg=None): + super(RepoEnforcedRQLConstraintMixIn, self).__init__(restriction, mainvars) + self.msg = msg + + def serialize(self): + # start with a semicolon for bw compat, see below + return ';%s;%s\n%s' % (self.mainvars, self.restriction, + self.msg or '') + + def deserialize(cls, value): + # XXX < 3.5.10 bw compat + if not value.startswith(';'): + return cls(value) + value, msg = value.split('\n', 1) + _, mainvars, restriction = value.split(';', 2) + return cls(restriction, mainvars, msg) + deserialize = classmethod(deserialize) def repo_check(self, session, eidfrom, rtype, eidto=None): """raise ValidationError if the relation doesn't satisfy the constraint """ - if not self.exec_query(session, eidfrom, eidto): - # XXX at this point dunno if the validation error `occured` on - # eidfrom or eidto (from user interface point of view) - self.error(eidfrom, rtype, 'constraint %s failed' % self) + if not self.match_condition(session, eidfrom, eidto): + # XXX at this point if both or neither of S and O are in mainvar we + # dunno if the validation error `occured` on eidfrom or eidto (from + # user interface point of view) + if eidto is None or 'S' in self.mainvars or not 'O' in self.mainvars: + maineid = eidfrom + else: + maineid = eidto + if self.msg: + msg = session._(self.msg) + else: + msg = '%(constraint)s %(restriction)s failed' % { + 'constraint': session._(self.type()), + 'restriction': self.restriction} + raise ValidationError(maineid, {rtype: msg}) + + def exec_query(self, session, eidfrom, eidto): + if eidto is None: + # checking constraint for an attribute relation + restriction = 'S eid %(s)s, ' + self.restriction + args, ck = {'s': eidfrom}, 's' + else: + restriction = 'S eid %(s)s, O eid %(o)s, ' + self.restriction + args, ck = {'s': eidfrom, 'o': eidto}, ('s', 'o') + rql = 'Any %s WHERE %s' % (self.mainvars, restriction) + if self.distinct_query: + rql = 'DISTINCT ' + rql + return session.unsafe_execute(rql, args, ck, build_descr=False) -class RQLUniqueConstraint(RQLConstraint): +class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint): + """the rql constraint is similar to the RQLVocabularyConstraint but + are also enforced at the repository level + """ + distinct_query = False + + def match_condition(self, session, eidfrom, eidto): + return self.exec_query(session, eidfrom, eidto) + + +class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint): """the unique rql constraint check that the result of the query isn't greater than one """ - def repo_check(self, session, eidfrom, rtype, eidto=None): - """raise ValidationError if the relation doesn't satisfy the constraint - """ - if len(self.exec_query(session, eidfrom, eidto)) > 1: - # XXX at this point dunno if the validation error `occured` on - # eidfrom or eidto (from user interface point of view) - self.error(eidfrom, rtype, 'unique constraint %s failed' % self) - + distinct_query = True -def split_expression(rqlstring): - for expr in rqlstring.split(','): - for word in expr.split(): - yield word + # XXX turns mainvars into a required argument in __init__, since we've no + # way to guess it correctly (eg if using S,O or U the constraint will + # always be satisfied since we've to use a DISTINCT query) -def normalize_expression(rqlstring): - """normalize an rql expression to ease schema synchronization (avoid - suppressing and reinserting an expression if only a space has been added/removed - for instance) - """ - return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) + def match_condition(self, session, eidfrom, eidto): + return len(self.exec_query(session, eidfrom, eidto)) <= 1 class RQLExpression(object): @@ -793,22 +868,11 @@ return self._check(session, x=eid) return self._check(session) -PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression) class RRQLExpression(RQLExpression): def __init__(self, expression, mainvars=None, eid=None): if mainvars is None: - defined = set(split_expression(expression)) - mainvars = [] - if 'S' in defined: - mainvars.append('S') - if 'O' in defined: - mainvars.append('O') - if 'U' in defined: - mainvars.append('U') - if not mainvars: - raise Exception('unable to guess selection variables') - mainvars = ','.join(mainvars) + mainvars = guess_rrqlexpr_mainvars(expression) RQLExpression.__init__(self, expression, mainvars, eid) # graph of links between variable, used by rql rewriter self.vargraph = {} @@ -851,7 +915,6 @@ kwargs['o'] = toeid return self._check(session, **kwargs) -PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression) # workflow extensions ######################################################### @@ -888,13 +951,13 @@ __metaclass__ = workflowable_definition __abstract__ = True -PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType # schema loading ############################################################## CONSTRAINTS['RQLConstraint'] = RQLConstraint CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint +CONSTRAINTS.pop('MultipleStaticVocabularyConstraint', None) # don't want this in cw yams schema PyFileReader.context.update(CONSTRAINTS) @@ -1009,7 +1072,12 @@ stmts.Select.set_statement_type = bw_set_statement_type # XXX deprecated + from yams.constraints import format_constraint format_constraint = deprecated('[3.4] use RichString instead of format_constraint')(format_constraint) from yams.buildobjs import RichString + +PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression) +PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression) +PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType PyFileReader.context['format_constraint'] = format_constraint diff -r 92ead039d3d0 -r 94cc7cad3d2d schemas/_regproc.mysql.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schemas/_regproc.mysql.sql Thu Dec 03 17:17:43 2009 +0100 @@ -0,0 +1,22 @@ +/* -*- sql -*- + + mysql specific registered procedures, + +*/ + +/* XXX limit_size version dealing with format as postgres version does. + XXX mysql doesn't support overloading, each function should have a different name + + NOTE: fulltext renamed since it cause a mysql name conflict + */ + +CREATE FUNCTION text_limit_size(vfulltext TEXT, maxsize INT) +RETURNS TEXT +NO SQL +BEGIN + IF LENGTH(vfulltext) < maxsize THEN + RETURN vfulltext; + ELSE + RETURN SUBSTRING(vfulltext from 1 for maxsize) || '...'; + END IF; +END ;; diff -r 92ead039d3d0 -r 94cc7cad3d2d schemas/_regproc.postgres.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schemas/_regproc.postgres.sql Thu Dec 03 17:17:43 2009 +0100 @@ -0,0 +1,47 @@ +/* -*- sql -*- + + postgres specific registered procedures, + require the plpgsql language installed + +*/ + +CREATE FUNCTION comma_join (anyarray) RETURNS text AS $$ + SELECT array_to_string($1, ', ') +$$ LANGUAGE SQL;; + +CREATE AGGREGATE group_concat ( + basetype = anyelement, + sfunc = array_append, + stype = anyarray, + finalfunc = comma_join, + initcond = '{}' +);; + + + +CREATE FUNCTION limit_size (fulltext text, format text, maxsize integer) RETURNS text AS $$ +DECLARE + plaintext text; +BEGIN + IF char_length(fulltext) < maxsize THEN + RETURN fulltext; + END IF; + IF format = 'text/html' OR format = 'text/xhtml' OR format = 'text/xml' THEN + plaintext := regexp_replace(fulltext, '<[\\w/][^>]+>', '', 'g'); + ELSE + plaintext := fulltext; + END IF; + IF char_length(plaintext) < maxsize THEN + RETURN plaintext; + ELSE + RETURN substring(plaintext from 1 for maxsize) || '...'; + END IF; +END +$$ LANGUAGE plpgsql;; + + +CREATE FUNCTION text_limit_size (fulltext text, maxsize integer) RETURNS text AS $$ +BEGIN + RETURN limit_size(fulltext, 'text/plain', maxsize); +END +$$ LANGUAGE plpgsql;; diff -r 92ead039d3d0 -r 94cc7cad3d2d schemas/_regproc.sql.mysql --- a/schemas/_regproc.sql.mysql Mon Nov 23 14:13:53 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -/* -*- sql -*- - - mysql specific registered procedures, - -*/ - -/* XXX limit_size version dealing with format as postgres version does. - XXX mysql doesn't support overloading, each function should have a different name - - NOTE: fulltext renamed since it cause a mysql name conflict - */ - -CREATE FUNCTION text_limit_size(vfulltext TEXT, maxsize INT) -RETURNS TEXT -NO SQL -BEGIN - IF LENGTH(vfulltext) < maxsize THEN - RETURN vfulltext; - ELSE - RETURN SUBSTRING(vfulltext from 1 for maxsize) || '...'; - END IF; -END ;; diff -r 92ead039d3d0 -r 94cc7cad3d2d schemas/_regproc.sql.postgres --- a/schemas/_regproc.sql.postgres Mon Nov 23 14:13:53 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -/* -*- sql -*- - - postgres specific registered procedures, - require the plpgsql language installed - -*/ - -CREATE FUNCTION comma_join (anyarray) RETURNS text AS $$ - SELECT array_to_string($1, ', ') -$$ LANGUAGE SQL;; - -CREATE AGGREGATE group_concat ( - basetype = anyelement, - sfunc = array_append, - stype = anyarray, - finalfunc = comma_join, - initcond = '{}' -);; - - - -CREATE FUNCTION limit_size (fulltext text, format text, maxsize integer) RETURNS text AS $$ -DECLARE - plaintext text; -BEGIN - IF char_length(fulltext) < maxsize THEN - RETURN fulltext; - END IF; - IF format = 'text/html' OR format = 'text/xhtml' OR format = 'text/xml' THEN - plaintext := regexp_replace(fulltext, '<[\\w/][^>]+>', '', 'g'); - ELSE - plaintext := fulltext; - END IF; - IF char_length(plaintext) < maxsize THEN - RETURN plaintext; - ELSE - RETURN substring(plaintext from 1 for maxsize) || '...'; - END IF; -END -$$ LANGUAGE plpgsql;; - - -CREATE FUNCTION text_limit_size (fulltext text, maxsize integer) RETURNS text AS $$ -BEGIN - RETURN limit_size(fulltext, 'text/plain', maxsize); -END -$$ LANGUAGE plpgsql;; diff -r 92ead039d3d0 -r 94cc7cad3d2d schemas/workflow.py --- a/schemas/workflow.py Mon Nov 23 14:13:53 2009 +0100 +++ b/schemas/workflow.py Thu Dec 03 17:17:43 2009 +0100 @@ -27,7 +27,8 @@ constraints=[RQLConstraint('O final FALSE')]) initial_state = SubjectRelation('State', cardinality='?*', - constraints=[RQLConstraint('O state_of S')], + constraints=[RQLConstraint('O state_of S', + msg=_('state doesn\'t belong to this workflow'))], description=_('initial state for this workflow')) @@ -38,7 +39,8 @@ subject = 'CWEType' object = 'Workflow' cardinality = '?*' - constraints = [RQLConstraint('S final FALSE, O workflow_of S')] + constraints = [RQLConstraint('S final FALSE, O workflow_of S', + msg=_('workflow isn\'t a workflow of this type'))] class State(EntityType): @@ -48,18 +50,22 @@ __permissions__ = META_ETYPE_PERMS name = String(required=True, indexed=True, internationalizable=True, - maxsize=256) + maxsize=256, + constraints=[RQLUniqueConstraint('S name N, S state_of WF, Y state_of WF, Y name N', 'Y', + _('workflow already have a state of that name'))]) description = RichString(fulltextindexed=True, default_format='text/rest', description=_('semantic description of this state')) # 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')], + constraints=[RQLConstraint('S state_of WF, O transition_of WF', + msg=_('state and transition don\'t belong the the same workflow'))], description=_('allowed transitions from this state')) state_of = SubjectRelation('Workflow', cardinality='1*', composite='object', description=_('workflow to which this state belongs'), - constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N')]) + constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N', 'Y', + _('workflow already have a state of that name'))]) class BaseTransition(EntityType): @@ -67,7 +73,9 @@ __permissions__ = META_ETYPE_PERMS name = String(required=True, indexed=True, internationalizable=True, - maxsize=256) + maxsize=256, + constraints=[RQLUniqueConstraint('S name N, S transition_of WF, Y transition_of WF, Y name N', 'Y', + _('workflow already have a transition of that name'))]) type = String(vocabulary=(_('normal'), _('auto')), default='normal') description = RichString(fulltextindexed=True, description=_('semantic description of this transition')) @@ -83,7 +91,8 @@ 'allowed to pass this transition')) transition_of = SubjectRelation('Workflow', cardinality='1*', composite='object', description=_('workflow to which this transition belongs'), - constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N')]) + constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N', 'Y', + _('workflow already have a transition of that name'))]) class Transition(BaseTransition): @@ -94,7 +103,8 @@ destination_state = SubjectRelation( 'State', cardinality='1*', - constraints=[RQLConstraint('S transition_of WF, O state_of WF')], + constraints=[RQLConstraint('S transition_of WF, O state_of WF', + msg=_('state and transition don\'t belong the the same workflow'))], description=_('destination state for this transition')) @@ -103,7 +113,9 @@ __specializes_schema__ = True subworkflow = SubjectRelation('Workflow', cardinality='1*', - constraints=[RQLConstraint('S transition_of WF, WF workflow_of ET, O workflow_of ET')]) + constraints=[RQLConstraint('S transition_of WF, WF workflow_of ET, O workflow_of ET', + msg=_('subworkflow isn\'t a workflow for the same types as the transition\'s workflow'))] + ) # XXX use exit_of and inline it subworkflow_exit = SubjectRelation('SubWorkflowExitPoint', cardinality='*1', composite='subject') @@ -113,11 +125,13 @@ """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')], + constraints=[RQLConstraint('T subworkflow_exit S, T subworkflow WF, O state_of WF', + msg=_('exit state must a subworkflow state'))], description=_('subworkflow state')) destination_state = SubjectRelation( 'State', cardinality='?*', - constraints=[RQLConstraint('T subworkflow_exit S, T transition_of WF, O state_of WF')], + constraints=[RQLConstraint('T subworkflow_exit S, T transition_of WF, O state_of WF', + msg=_('destination state must be in the same workflow as our parent transition'))], description=_('destination state. No destination state means that transition ' 'should go back to the state from which we\'ve entered the ' 'subworkflow.')) @@ -214,7 +228,8 @@ __permissions__ = META_RTYPE_PERMS cardinality = '?*' - constraints = [RQLConstraint('S is ET, O workflow_of ET')] + constraints = [RQLConstraint('S is ET, O workflow_of ET', + msg=_('workflow isn\'t a workflow for this type'))] object = 'Workflow' @@ -243,5 +258,6 @@ inlined = False cardinality = '1*' - constraints = [RQLConstraint('S is ET, O state_of WF, WF workflow_of ET')] + constraints = [RQLConstraint('S is ET, O state_of WF, WF workflow_of ET', + msg=_('state doesn\'t apply to this entity\'s type'))] object = 'State' diff -r 92ead039d3d0 -r 94cc7cad3d2d server/__init__.py --- a/server/__init__.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/__init__.py Thu Dec 03 17:17:43 2009 +0100 @@ -143,14 +143,6 @@ #skip_entities=[str(e) for e in schema.entities() # if not repo.system_source.support_entity(str(e))]) sqlexec(schemasql, execute, pbtitle=_title) - # install additional driver specific sql files - for fpath in glob(join(CW_SOFTWARE_ROOT, 'schemas', '*.sql.%s' % driver)): - print '-> installing', fpath - sqlexec(open(fpath).read(), execute, False, delimiter=';;') - for directory in reversed(config.cubes_path()): - for fpath in glob(join(directory, 'schema', '*.sql.%s' % driver)): - print '-> installing', fpath - sqlexec(open(fpath).read(), execute, False, delimiter=';;') sqlcursor.close() sqlcnx.commit() sqlcnx.close() @@ -183,6 +175,10 @@ assert len(repo.sources) == 1, repo.sources handler = config.migration_handler(schema, interactive=False, cnx=cnx, repo=repo) + # install additional driver specific sql files + handler.install_custom_sql_scripts(join(CW_SOFTWARE_ROOT, 'schemas'), driver) + for directory in reversed(config.cubes_path()): + handler.install_custom_sql_scripts(join(directory, 'schema'), driver) initialize_schema(config, schema, handler) # yoo ! cnx.commit() diff -r 92ead039d3d0 -r 94cc7cad3d2d server/hook.py --- a/server/hook.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/hook.py Thu Dec 03 17:17:43 2009 +0100 @@ -369,6 +369,9 @@ operation list """ + def postcommit_event(self): + """the observed connections pool has committed""" + @property @deprecated('[3.6] use self.session.user') def user(self): diff -r 92ead039d3d0 -r 94cc7cad3d2d server/migractions.py --- a/server/migractions.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/migractions.py Thu Dec 03 17:17:43 2009 +0100 @@ -24,6 +24,8 @@ import shutil import os.path as osp from datetime import datetime +from glob import glob +from warnings import warn from logilab.common.deprecation import deprecated from logilab.common.decorators import cached, clear_cache @@ -104,12 +106,12 @@ if migrscript.endswith('.sql'): if self.execscript_confirm(migrscript): sqlexec(open(migrscript).read(), self.session.system_sql) - elif migrscript.endswith('.py'): + elif migrscript.endswith('.py') or migrscript.endswith('.txt'): return super(ServerMigrationHelper, self).cmd_process_script( migrscript, funcname, *args, **kwargs) else: print - print ('-> ignoring %s, only .py and .sql scripts are considered' % + print ('-> ignoring %s, only .py .sql and .txt scripts are considered' % migrscript) print self.commit() @@ -308,6 +310,21 @@ 'after_add_entity', '') self.cmd_reactivate_verification_hooks() + def install_custom_sql_scripts(self, directory, driver): + self.session.set_pool() # ensure pool is set + for fpath in glob(osp.join(directory, '*.sql.%s' % driver)): + newname = osp.basename(fpath).replace('.sql.%s' % driver, + '.%s.sql' % driver) + warn('[3.5.6] rename %s into %s' % (fpath, newname), + DeprecationWarning) + print '-> installing', fpath + sqlexec(open(fpath).read(), self.session.system_sql, False, + delimiter=';;') + for fpath in glob(osp.join(directory, '*.%s.sql' % driver)): + print '-> installing', fpath + sqlexec(open(fpath).read(), self.session.system_sql, False, + delimiter=';;') + # schema synchronization internals ######################################## def _synchronize_permissions(self, erschema, teid): @@ -544,8 +561,11 @@ self.fs_schema = self._create_context()['fsschema'] = newcubes_schema new = set() # execute pre-create files + driver = self.repo.system_source.dbdriver for pack in reversed(newcubes): - self.exec_event_script('precreate', self.config.cube_dir(pack)) + cubedir = self.config.cube_dir(pack) + self.install_custom_sql_scripts(osp.join(cubedir, 'schema'), driver) + self.exec_event_script('precreate', cubedir) # add new entity and relation types for rschema in newcubes_schema.relations(): if not rschema in self.repo.schema: diff -r 92ead039d3d0 -r 94cc7cad3d2d server/pool.py --- a/server/pool.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/pool.py Thu Dec 03 17:17:43 2009 +0100 @@ -123,6 +123,7 @@ self.source_cnxs[source.uri] = (source, cnx) self._cursors.pop(source.uri, None) + from cubicweb.server.hook import (Operation, LateOperation, SingleOperation, SingleLastOperation) from logilab.common.deprecation import class_moved, class_renamed diff -r 92ead039d3d0 -r 94cc7cad3d2d server/schemaserial.py diff -r 92ead039d3d0 -r 94cc7cad3d2d server/serverconfig.py --- a/server/serverconfig.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/serverconfig.py Thu Dec 03 17:17:43 2009 +0100 @@ -10,7 +10,7 @@ import os from os.path import join, exists -from logilab.common.configuration import Method, Configuration, \ +from logilab.common.configuration import REQUIRED, Method, Configuration, \ ini_format_section from logilab.common.decorators import wproperty, cached, clear_cache @@ -28,12 +28,22 @@ 'inputlevel': 0, }), ('password', {'type' : 'password', + 'default': REQUIRED, 'help': "cubicweb manager account's password", 'inputlevel': 0, }), ) -def generate_sources_file(sourcesfile, sourcescfg, keys=None): +class SourceConfiguration(Configuration): + def __init__(self, appid, options): + self.appid = appid # has to be done before super call + super(SourceConfiguration, self).__init__(options=options) + + # make Method('default_instance_id') usable in db option defs (in native.py) + def default_instance_id(self): + return self.appid + +def generate_sources_file(appid, sourcesfile, sourcescfg, keys=None): """serialize repository'sources configuration into a INI like file the `keys` parameter may be used to sort sections @@ -53,7 +63,7 @@ options = USER_OPTIONS else: options = SOURCE_TYPES[sconfig['adapter']].options - _sconfig = Configuration(options=options) + _sconfig = SourceConfiguration(appid, options=options) for attr, val in sconfig.items(): if attr == 'uri': continue @@ -236,7 +246,8 @@ if exists(sourcesfile): import shutil shutil.copy(sourcesfile, sourcesfile + '.bak') - generate_sources_file(sourcesfile, sourcescfg, ['admin', 'system']) + generate_sources_file(self.appid, sourcesfile, sourcescfg, + ['admin', 'system']) restrict_perms_to_user(sourcesfile) def pyro_enabled(self): diff -r 92ead039d3d0 -r 94cc7cad3d2d server/serverctl.py --- a/server/serverctl.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/serverctl.py Thu Dec 03 17:17:43 2009 +0100 @@ -18,7 +18,8 @@ from cubicweb.toolsutils import Command, CommandHandler, underline_title from cubicweb.server import SOURCE_TYPES from cubicweb.server.utils import ask_source_config -from cubicweb.server.serverconfig import USER_OPTIONS, ServerConfiguration +from cubicweb.server.serverconfig import (USER_OPTIONS, ServerConfiguration, + SourceConfiguration) # utility functions ########################################################### @@ -113,6 +114,7 @@ config._cubes = None login, pwd = manager_userpasswd() + # repository specific command handlers ######################################## class RepositoryCreateHandler(CommandHandler): @@ -135,8 +137,8 @@ sourcesfile = config.sources_file() # XXX hack to make Method('default_instance_id') usable in db option # defs (in native.py) - Configuration.default_instance_id = staticmethod(lambda: config.appid) - sconfig = Configuration(options=SOURCE_TYPES['native'].options) + sconfig = SourceConfiguration(config.appid, + options=SOURCE_TYPES['native'].options) sconfig.adapter = 'native' sconfig.input_config(inputlevel=inputlevel) sourcescfg = {'system': sconfig} @@ -238,6 +240,7 @@ # repository specific commands ################################################ + class CreateInstanceDBCommand(Command): """Create the system database of an instance (run after 'create'). @@ -329,7 +332,7 @@ cmd_run('db-init', config.appid) else: print ('-> nevermind, you can do it later with ' - '"cubicweb-ctl db-init %s".' % self.config.appid) + '"cubicweb-ctl db-init %s".' % config.appid) class InitInstanceCommand(Command): @@ -356,8 +359,20 @@ def run(self, args): print '\n'+underline_title('Initializing the system database') from cubicweb.server import init_repository + from logilab.common.db import get_connection appid = pop_arg(args, msg='No instance specified !') config = ServerConfiguration.config_for(appid) + try: + system = config.sources()['system'] + get_connection( + system['db-driver'], database=system['db-name'], + host=system.get('db-host'), port=system.get('db-port'), + user=system.get('db-user'), password=system.get('db-password')) + except Exception, ex: + raise ConfigurationError( + 'You seem to have provided wrong connection information in '\ + 'the %s file. Resolve this first (error: %s).' + % (config.sources_file(), str(ex).strip())) init_repository(config, drop=self.config.drop) @@ -402,6 +417,7 @@ cnx.commit() print '-> rights granted to %s on instance %s.' % (appid, user) + class ResetAdminPasswordCommand(Command): """Reset the administrator password. diff -r 92ead039d3d0 -r 94cc7cad3d2d server/session.py --- a/server/session.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/session.py Thu Dec 03 17:17:43 2009 +0100 @@ -476,6 +476,15 @@ self.rollback(reset_pool) raise self.pool.commit() + self.commit_state = trstate = 'postcommit' + while self.pending_operations: + operation = self.pending_operations.pop(0) + operation.processed = trstate + try: + operation.handle_event('%s_event' % trstate) + except: + self.exception('error while %sing', trstate) + self.debug('%s session %s done', trstate, self.id) finally: self._touch() self.commit_state = None diff -r 92ead039d3d0 -r 94cc7cad3d2d server/sources/native.py --- a/server/sources/native.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/sources/native.py Thu Dec 03 17:17:43 2009 +0100 @@ -337,7 +337,7 @@ def manual_insert(self, results, table, session): """insert given result into a temporary table on the system source""" if server.DEBUG & server.DBG_RQL: - print ' manual insertion of', res, 'into', table + print ' manual insertion of', results, 'into', table if not results: return query_args = ['%%(%s)s' % i for i in xrange(len(results[0]))] diff -r 92ead039d3d0 -r 94cc7cad3d2d server/sources/rql2sql.py --- a/server/sources/rql2sql.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/sources/rql2sql.py Thu Dec 03 17:17:43 2009 +0100 @@ -693,26 +693,11 @@ lhsvar, _, rhsvar, rhsconst = relation_info(relation) # we are sure here to have a lhsvar assert lhsvar is not None - if isinstance(relation.parent, Not): + if isinstance(relation.parent, Not) \ + and len(lhsvar.stinfo['relations']) > 1 \ + and (rhsvar is None or rhsvar._q_invariant): self._state.done.add(relation.parent) - if rhsvar is not None and not rhsvar._q_invariant: - # if the lhs variable is only linked to this relation, this mean we - # only want the relation to NOT exists - self._state.push_scope() - lhssql = self._inlined_var_sql(lhsvar, relation.r_type) - rhssql = rhsvar.accept(self) - restrictions, tables = self._state.pop_scope() - restrictions.append('%s=%s' % (lhssql, rhssql)) - if not tables: - sql = 'NOT EXISTS(SELECT 1 WHERE %s)' % ( - ' AND '.join(restrictions)) - else: - sql = 'NOT EXISTS(SELECT 1 FROM %s WHERE %s)' % ( - ', '.join(tables), ' AND '.join(restrictions)) - else: - lhssql = self._inlined_var_sql(lhsvar, relation.r_type) - sql = '%s IS NULL' % self._inlined_var_sql(lhsvar, relation.r_type) - return sql + return '%s IS NULL' % self._inlined_var_sql(lhsvar, relation.r_type) lhssql = self._inlined_var_sql(lhsvar, relation.r_type) if rhsconst is not None: return '%s=%s' % (lhssql, rhsconst.accept(self)) diff -r 92ead039d3d0 -r 94cc7cad3d2d server/test/data/schema.py --- a/server/test/data/schema.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/test/data/schema.py Thu Dec 03 17:17:43 2009 +0100 @@ -172,7 +172,7 @@ name = 'ecrit_par' subject = 'Note' object ='Personne' - constraints = [RQLConstraint('E concerns P, X version_of P')] + constraints = [RQLConstraint('E concerns P, S version_of P')] cardinality = '?*' class ecrit_par_2(RelationDefinition): diff -r 92ead039d3d0 -r 94cc7cad3d2d server/test/unittest_multisources.py diff -r 92ead039d3d0 -r 94cc7cad3d2d server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/test/unittest_rql2sql.py Thu Dec 03 17:17:43 2009 +0100 @@ -1044,7 +1044,12 @@ UNION ALL SELECT _S.cw_in_state FROM cw_Note AS _S -WHERE _S.cw_eid=0 AND _S.cw_in_state IS NOT NULL''') +WHERE _S.cw_eid=0 AND _S.cw_in_state IS NOT NULL'''), + + ('Any X WHERE NOT Y for_user X, X eid 123', + '''SELECT 123 +WHERE NOT EXISTS(SELECT 1 FROM cw_CWProperty AS _Y WHERE _Y.cw_for_user=123) +'''), ] @@ -1553,7 +1558,16 @@ self.o = SQLGenerator(schema, dbms_helper) def _norm_sql(self, sql): - return sql.strip().replace(' ILIKE ', ' LIKE ').replace('TRUE', '1').replace('FALSE', '0') + sql = sql.strip().replace(' ILIKE ', ' LIKE ').replace('TRUE', '1').replace('FALSE', '0') + newsql = [] + latest = None + for line in sql.splitlines(False): + firstword = line.split(None, 1)[0] + if firstword == 'WHERE' and latest == 'SELECT': + newsql.append('FROM (SELECT 1) AS _T') + newsql.append(line) + latest = firstword + return '\n'.join(newsql) def test_from_clause_needed(self): queries = [("Any 1 WHERE EXISTS(T is CWGroup, T name 'managers')", diff -r 92ead039d3d0 -r 94cc7cad3d2d server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Mon Nov 23 14:13:53 2009 +0100 +++ b/server/test/unittest_schemaserial.py Thu Dec 03 17:17:43 2009 +0100 @@ -55,13 +55,13 @@ {'rt': 'relation_type', 'description': u'', 'composite': u'object', 'oe': 'CWRType', 'ordernum': 1, 'cardinality': u'1*', 'se': 'CWRelation'}), ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWRelation', - {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWRelation', 'value': u'O final FALSE'}), + {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWRelation', 'value': u';O;O final FALSE\n'}), ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s', {'rt': 'relation_type', 'description': u'', 'composite': u'object', 'oe': 'CWRType', 'ordernum': 1, 'cardinality': u'1*', 'se': 'CWAttribute'}), ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWRelation', - {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWAttribute', 'value': u'O final TRUE'}), + {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWAttribute', 'value': u';O;O final TRUE\n'}), ]) def test_rschema2rql2(self): @@ -143,35 +143,35 @@ def test_eperms2rql1(self): self.assertListEquals([rql for rql, kwargs in erperms2rql(schema.eschema('CWEType'), self.GROUP_MAPPING)], - ['SET X read_permission Y WHERE X is CWEType, X name "CWEType", Y eid 2', - 'SET X read_permission Y WHERE X is CWEType, X name "CWEType", Y eid 0', - 'SET X read_permission Y WHERE X is CWEType, X name "CWEType", Y eid 1', - 'SET X add_permission Y WHERE X is CWEType, X name "CWEType", Y eid 0', - 'SET X update_permission Y WHERE X is CWEType, X name "CWEType", Y eid 0', - 'SET X update_permission Y WHERE X is CWEType, X name "CWEType", Y eid 3', - 'SET X delete_permission Y WHERE X is CWEType, X name "CWEType", Y eid 0', + ['SET X read_permission Y WHERE X is CWEType, X name %(name)s, Y eid %(g)s', + 'SET X read_permission Y WHERE X is CWEType, X name %(name)s, Y eid %(g)s', + 'SET X read_permission Y WHERE X is CWEType, X name %(name)s, Y eid %(g)s', + 'SET X add_permission Y WHERE X is CWEType, X name %(name)s, Y eid %(g)s', + 'SET X update_permission Y WHERE X is CWEType, X name %(name)s, Y eid %(g)s', + 'SET X update_permission Y WHERE X is CWEType, X name %(name)s, Y eid %(g)s', + 'SET X delete_permission Y WHERE X is CWEType, X name %(name)s, Y eid %(g)s', ]) def test_rperms2rql2(self): self.assertListEquals([rql for rql, kwargs in erperms2rql(schema.rschema('read_permission'), self.GROUP_MAPPING)], - ['SET X read_permission Y WHERE X is CWRType, X name "read_permission", Y eid 2', - 'SET X read_permission Y WHERE X is CWRType, X name "read_permission", Y eid 0', - 'SET X read_permission Y WHERE X is CWRType, X name "read_permission", Y eid 1', - 'SET X add_permission Y WHERE X is CWRType, X name "read_permission", Y eid 0', - 'SET X delete_permission Y WHERE X is CWRType, X name "read_permission", Y eid 0', + ['SET X read_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X read_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X read_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X add_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X delete_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', ]) def test_rperms2rql3(self): self.assertListEquals([rql for rql, kwargs in erperms2rql(schema.rschema('name'), self.GROUP_MAPPING)], - ['SET X read_permission Y WHERE X is CWRType, X name "name", Y eid 2', - 'SET X read_permission Y WHERE X is CWRType, X name "name", Y eid 0', - 'SET X read_permission Y WHERE X is CWRType, X name "name", Y eid 1', - 'SET X add_permission Y WHERE X is CWRType, X name "name", Y eid 2', - 'SET X add_permission Y WHERE X is CWRType, X name "name", Y eid 0', - 'SET X add_permission Y WHERE X is CWRType, X name "name", Y eid 1', - 'SET X delete_permission Y WHERE X is CWRType, X name "name", Y eid 2', - 'SET X delete_permission Y WHERE X is CWRType, X name "name", Y eid 0', - 'SET X delete_permission Y WHERE X is CWRType, X name "name", Y eid 1', + ['SET X read_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X read_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X read_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X add_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X add_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X add_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X delete_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X delete_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', + 'SET X delete_permission Y WHERE X is CWRType, X name %(name)s, Y eid %(g)s', ]) #def test_perms2rql(self): diff -r 92ead039d3d0 -r 94cc7cad3d2d test/unittest_schema.py --- a/test/unittest_schema.py Mon Nov 23 14:13:53 2009 +0100 +++ b/test/unittest_schema.py Thu Dec 03 17:17:43 2009 +0100 @@ -18,9 +18,11 @@ from yams.buildobjs import RelationDefinition, EntityType, RelationType from yams.reader import PyFileReader -from cubicweb.schema import CubicWebSchema, CubicWebEntitySchema, \ - RQLConstraint, CubicWebSchemaLoader, RQLExpression, ERQLExpression, RRQLExpression, \ - normalize_expression, order_eschemas +from cubicweb.schema import ( + CubicWebSchema, CubicWebEntitySchema, CubicWebSchemaLoader, + RQLConstraint, RQLUniqueConstraint, RQLVocabularyConstraint, + RQLExpression, ERQLExpression, RRQLExpression, + normalize_expression, order_eschemas) from cubicweb.devtools import TestServerConfiguration as TestConfiguration DATADIR = join(dirname(__file__), 'data') @@ -83,6 +85,18 @@ class CubicWebSchemaTC(TestCase): + def test_rql_constraints_inheritance(self): + # isinstance(cstr, RQLVocabularyConstraint) + # -> expected to return RQLVocabularyConstraint and RQLConstraint + # instances but not RQLUniqueConstraint + # + # isinstance(cstr, RQLConstraint) + # -> expected to return RQLConstraint instances but not + # RRQLVocabularyConstraint and QLUniqueConstraint + self.failIf(issubclass(RQLUniqueConstraint, RQLVocabularyConstraint)) + self.failIf(issubclass(RQLUniqueConstraint, RQLConstraint)) + self.failUnless(issubclass(RQLConstraint, RQLVocabularyConstraint)) + def test_normalize(self): """test that entities, relations and attributes name are normalized """ diff -r 92ead039d3d0 -r 94cc7cad3d2d utils.py --- a/utils.py Mon Nov 23 14:13:53 2009 +0100 +++ b/utils.py Thu Dec 03 17:17:43 2009 +0100 @@ -410,6 +410,8 @@ return obj.strftime('%Y/%m/%d') elif isinstance(obj, pydatetime.time): return obj.strftime('%H:%M:%S') + elif isinstance(obj, pydatetime.timedelta): + return '%10d.%s' % (obj.days, obj.seconds) elif isinstance(obj, decimal.Decimal): return float(obj) try: diff -r 92ead039d3d0 -r 94cc7cad3d2d view.py --- a/view.py Mon Nov 23 14:13:53 2009 +0100 +++ b/view.py Thu Dec 03 17:17:43 2009 +0100 @@ -179,6 +179,7 @@ if rset is None: raise NotImplementedError, self wrap = self.templatable and len(rset) > 1 and self.add_div_section + # XXX propagate self.extra_kwars? for i in xrange(len(rset)): if wrap: self.w(u'
') @@ -200,7 +201,7 @@ return True def is_primary(self): - return self.__regid__ == 'primary' + return self.extra_kwargs.get('is_primary', self.__regid__ == 'primary') def url(self): """return the url associated with this view. Should not be @@ -323,7 +324,10 @@ else: w(u'%s ' % label) if table: - w(u'%s' % value) + if not (show_label and label): + w(u'%s' % value) + else: + w(u'%s' % value) else: w(u'%s
' % value) diff -r 92ead039d3d0 -r 94cc7cad3d2d web/data/cubicweb.edition.js --- a/web/data/cubicweb.edition.js Mon Nov 23 14:13:53 2009 +0100 +++ b/web/data/cubicweb.edition.js Thu Dec 03 17:17:43 2009 +0100 @@ -270,11 +270,14 @@ /* * removes the part of the form used to edit an inlined entity */ -function removeInlineForm(peid, rtype, eid) { +function removeInlineForm(peid, rtype, eid, showaddnewlink) { jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() { $(this).remove(); updateInlinedEntitiesCounters(rtype); }); + if (showaddnewlink) { + toggleVisibility(showaddnewlink); + } } /* diff -r 92ead039d3d0 -r 94cc7cad3d2d web/form.py --- a/web/form.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/form.py Thu Dec 03 17:17:43 2009 +0100 @@ -36,6 +36,163 @@ return False +<<<<<<< /home/syt/src/fcubicweb/cubicweb/web/form.py +======= +# XXX should disappear +class FormMixIn(object): + """abstract form mix-in + XXX: you should inherit from this FIRST (obscure pb with super call) + """ + force_session_key = None + + def session_key(self): + """return the key that may be used to store / retreive data about a + previous post which failed because of a validation error + """ + if self.force_session_key is None: + return '%s#%s' % (self.req.url(), self.domid) + return self.force_session_key + + def restore_previous_post(self, sessionkey): + # get validation session data which may have been previously set. + # deleting validation errors here breaks form reloading (errors are + # no more available), they have to be deleted by application's publish + # method on successful commit + forminfo = self.req.get_session_data(sessionkey, pop=True) + if forminfo: + # XXX remove req.data assigment once cw.web.widget is killed + self.req.data['formvalues'] = self._form_previous_values = forminfo['values'] + self.req.data['formerrors'] = self._form_valerror = forminfo['errors'] + self.req.data['displayederrors'] = self.form_displayed_errors = set() + # if some validation error occured on entity creation, we have to + # get the original variable name from its attributed eid + foreid = self.form_valerror.entity + for var, eid in forminfo['eidmap'].items(): + if foreid == eid: + self.form_valerror.eid = var + break + else: + self.form_valerror.eid = foreid + else: + self._form_previous_values = {} + self._form_valerror = None + + @property + def form_previous_values(self): + if self.parent_form is None: + return self._form_previous_values + return self.parent_form.form_previous_values + + @property + def form_valerror(self): + if self.parent_form is None: + return self._form_valerror + return self.parent_form.form_valerror + + # XXX deprecated with new form system. Should disappear + + domid = 'entityForm' + category = 'form' + controller = 'edit' + http_cache_manager = httpcache.NoHTTPCacheManager + add_to_breadcrumbs = False + + def html_headers(self): + """return a list of html headers (eg something to be inserted between + and of the returned page + + by default forms are neither indexed nor followed + """ + return [NOINDEX, NOFOLLOW] + + def linkable(self): + """override since forms are usually linked by an action, + so we don't want them to be listed by appli.possible_views + """ + return False + + + def button(self, label, klass='validateButton', tabindex=None, **kwargs): + if tabindex is None: + tabindex = self.req.next_tabindex() + return tags.input(value=label, klass=klass, **kwargs) + + def action_button(self, label, onclick=None, __action=None, **kwargs): + if onclick is None: + onclick = "postForm('__action_%s', \'%s\', \'%s\')" % ( + __action, label, self.domid) + return self.button(label, onclick=onclick, **kwargs) + + def button_ok(self, label=None, type='submit', name='defaultsubmit', + **kwargs): + label = self.req._(label or stdmsgs.BUTTON_OK).capitalize() + return self.button(label, name=name, type=type, **kwargs) + + def button_apply(self, label=None, type='button', **kwargs): + label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize() + return self.action_button(label, __action='apply', type=type, **kwargs) + + def button_delete(self, label=None, type='button', **kwargs): + label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize() + return self.action_button(label, __action='delete', type=type, **kwargs) + + def button_cancel(self, label=None, type='button', **kwargs): + label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize() + return self.action_button(label, __action='cancel', type=type, **kwargs) + + def button_reset(self, label=None, type='reset', name='__action_cancel', + **kwargs): + label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize() + return self.button(label, type=type, **kwargs) + + def need_multipart(self, entity, categories=('primary', 'secondary')): + """return a boolean indicating if form's enctype should be multipart + """ + for rschema, _, x in entity.relations_by_category(categories): + if entity.get_widget(rschema, x).need_multipart: + return True + # let's find if any of our inlined entities needs multipart + for rschema, targettypes, x in entity.relations_by_category('inlineview'): + assert len(targettypes) == 1, \ + "I'm not able to deal with several targets and inlineview" + ttype = targettypes[0] + inlined_entity = self.vreg.etype_class(ttype)(self.req, None, None) + for irschema, _, x in inlined_entity.relations_by_category(categories): + if inlined_entity.get_widget(irschema, x).need_multipart: + return True + return False + + def error_message(self): + """return formatted error message + + This method should be called once inlined field errors has been consumed + """ + errex = self.req.data.get('formerrors') or self.form_valerror + # get extra errors + if errex is not None: + errormsg = self.req._('please correct the following errors:') + displayed = self.req.data.get('displayederrors') or self.form_displayed_errors + errors = sorted((field, err) for field, err in errex.errors.items() + if not field in displayed) + if errors: + if len(errors) > 1: + templstr = '
  • %s
  • \n' + else: + templstr = ' %s\n' + for field, err in errors: + if field is None: + errormsg += templstr % err + else: + errormsg += templstr % '%s: %s' % (self.req._(field), err) + if len(errors) > 1: + errormsg = '
      %s
    ' % errormsg + return u'
    %s
    ' % errormsg + return u'' + + +############################################################################### + +>>>>>>> /tmp/form.py~other.xdns1y class metafieldsform(type): """metaclass for FieldsForm to retrieve fields defined as class attributes and put them into a single ordered list: '_fields_'. @@ -65,6 +222,7 @@ __registry__ = 'forms' parent_form = None + force_session_key = None def __init__(self, req, rset, **kwargs): super(Form, self).__init__(req, rset=rset, **kwargs) @@ -77,6 +235,18 @@ return self return self.parent_form.root_form + @property + def form_previous_values(self): + if self.parent_form is None: + return self._form_previous_values + return self.parent_form.form_previous_values + + @property + def form_valerror(self): + if self.parent_form is None: + return self._form_valerror + return self.parent_form.form_valerror + @iclassmethod def _fieldsattr(cls_or_self): if isinstance(cls_or_self, type): @@ -127,7 +297,9 @@ """return the key that may be used to store / retreive data about a previous post which failed because of a validation error """ - return '%s#%s' % (self._cw.url(), self.domid) + if self.force_session_key is None: + return '%s#%s' % (self.req.url(), self.domid) + return self.force_session_key def restore_previous_post(self, sessionkey): # get validation session data which may have been previously set. @@ -136,9 +308,9 @@ # method on successful commit forminfo = self._cw.get_session_data(sessionkey, pop=True) if forminfo: - # XXX remove req.data assigment once cw.web.widget is killed - self._cw.data['formvalues'] = self.form_previous_values = forminfo['values'] - self._cw.data['formerrors'] = self.form_valerror = forminfo['errors'] + # XXX remove _cw.data assigment once cw.web.widget is killed + self._cw.data['formvalues'] = self._form_previous_values = forminfo['values'] + self._cw.data['formerrors'] = self._form_valerror = forminfo['errors'] self._cw.data['displayederrors'] = self.form_displayed_errors = set() # if some validation error occured on entity creation, we have to # get the original variable name from its attributed eid @@ -150,5 +322,5 @@ else: self.form_valerror.eid = foreid else: - self.form_previous_values = {} - self.form_valerror = None + self._form_previous_values = {} + self._form_valerror = None diff -r 92ead039d3d0 -r 94cc7cad3d2d web/formwidgets.py --- a/web/formwidgets.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/formwidgets.py Thu Dec 03 17:17:43 2009 +0100 @@ -412,6 +412,8 @@ init_ajax_attributes(attrs, self.wdgtype, self.loadtype) # XXX entity form specific attrs['cubicweb:dataurl'] = self._get_url(form.edited_entity, field) + if not values: + values = ('',) return name, values, attrs def _get_url(self, entity, field): diff -r 92ead039d3d0 -r 94cc7cad3d2d web/test/unittest_form.py --- a/web/test/unittest_form.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/test/unittest_form.py Thu Dec 03 17:17:43 2009 +0100 @@ -95,7 +95,7 @@ self.req.form['__linkto'] = 'in_group:%s:subject' % geid form = self.vreg['forms'].select('edition', self.req, entity=e) form.content_type = 'text/html' - pageinfo = self._check_html(form.form_render(), form, template=None) + pageinfo = self._check_html(form.render(), form, template=None) inputs = pageinfo.find_tag('select', False) self.failUnless(any(attrs for t, attrs in inputs if attrs.get('name') == 'in_group:A')) inputs = pageinfo.find_tag('input', False) @@ -126,14 +126,14 @@ creation_date = DateTimeField(widget=DateTimePicker) form = CustomChangeStateForm(self.req, redirect_path='perdu.com', entity=self.entity) - form.form_render(state=123, trcomment=u'', - trcomment_format=u'text/plain') + form.render(formvalues=dict(state=123, trcomment=u'', + trcomment_format=u'text/plain')) def test_change_state_form(self): form = ChangeStateForm(self.req, redirect_path='perdu.com', entity=self.entity) - form.form_render(state=123, trcomment=u'', - trcomment_format=u'text/plain') + form.render(formvalues=dict(state=123, trcomment=u'', + trcomment_format=u'text/plain')) # fields tests ############################################################ diff -r 92ead039d3d0 -r 94cc7cad3d2d web/test/unittest_views_editforms.py --- a/web/test/unittest_views_editforms.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/test/unittest_views_editforms.py Thu Dec 03 17:17:43 2009 +0100 @@ -5,7 +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 logilab.common.testlib import unittest_main +from logilab.common.testlib import unittest_main, mock_object from logilab.common.compat import any from cubicweb.devtools.testlib import CubicWebTC @@ -155,13 +155,16 @@ geid = self.execute('CWGroup X LIMIT 1')[0][0] rset = self.execute('CWUser X LIMIT 1') self.view('inline-edition', rset, row=0, col=0, rtype='in_group', - peid=geid, role='object', template=None, i18nctx='').source + peid=geid, role='object', template=None, i18nctx='', + pform=MOCKPFORM).source def test_automatic_inline_creation_formview(self): geid = self.execute('CWGroup X LIMIT 1')[0][0] self.view('inline-creation', None, etype='CWUser', rtype='in_group', - peid=geid, template=None, i18nctx='', role='object').source + peid=geid, template=None, i18nctx='', role='object', + pform=MOCKPFORM).source +MOCKPFORM = mock_object(form_previous_values={}, form_valerror=None) if __name__ == '__main__': unittest_main() diff -r 92ead039d3d0 -r 94cc7cad3d2d web/test/unittest_viewselector.py --- a/web/test/unittest_viewselector.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/test/unittest_viewselector.py Thu Dec 03 17:17:43 2009 +0100 @@ -25,9 +25,9 @@ SITEACTIONS = [actions.SiteConfigurationAction, actions.ManageAction, schema.ViewSchemaAction, - actions.SiteInfoAction, - ] -FOOTERACTIONS = [wdoc.ChangeLogAction, + actions.SiteInfoAction] +FOOTERACTIONS = [wdoc.HelpAction, + wdoc.ChangeLogAction, wdoc.AboutAction, actions.PoweredByAction] @@ -218,7 +218,6 @@ ('text', baseviews.TextView), ('treeview', treeview.TreeView), ('vcard', vcard.VCardCWUserView), - ('wfhistory', workflow.WFHistoryView), ('xbel', xbel.XbelView), ('xml', xmlrss.XMLView), ]) diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/boxes.py --- a/web/views/boxes.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/boxes.py Thu Dec 03 17:17:43 2009 +0100 @@ -69,7 +69,8 @@ else: menu = defaultmenu action.fill_menu(self, menu) - if box.is_empty() and not other_menu.is_empty(): + # if we've nothing but actions in the other_menu, add them directly into the box + if box.is_empty() and len(self._menus_by_id) == 1 and not other_menu.is_empty(): box.items = other_menu.items other_menu.items = [] else: # ensure 'more actions' menu appears last diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/cwproperties.py --- a/web/views/cwproperties.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/cwproperties.py Thu Dec 03 17:17:43 2009 +0100 @@ -202,7 +202,7 @@ self.form_row(form, key, splitlabel) renderer = self._cw.vreg['formrenderers'].select('cwproperties', self._cw, display_progress_div=False) - return form.form_render(renderer=renderer) + return form.render(renderer=renderer) def form_row(self, form, key, splitlabel): entity = self.entity_for_key(key) diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/editcontroller.py --- a/web/views/editcontroller.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/editcontroller.py Thu Dec 03 17:17:43 2009 +0100 @@ -70,6 +70,9 @@ req = self._cw self.errors = [] self.relations_rql = [] + # so we're able to know the main entity from the repository side + if '__maineid' in form: + req.set_shared_data('__maineid', form['__maineid'], querydata=True) # no specific action, generic edition self._to_create = req.data['eidmap'] = {} self._pending_relations = [] diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/editforms.py --- a/web/views/editforms.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/editforms.py Thu Dec 03 17:17:43 2009 +0100 @@ -28,6 +28,7 @@ from cubicweb.web.formwidgets import Button, SubmitButton, ResetButton from cubicweb.web.views import forms +_pvdc = uicfg.primaryview_display_ctrl def relation_id(eid, rtype, role, reid): """return an identifier for a relation between two entities""" @@ -96,7 +97,7 @@ w(u'
  • %s
  • ' % tags.a(entity.view('textoutofcontext'), href=entity.absolute_url())) w(u'\n') - w(form.form_render()) + w(form.render()) class ClickAndEditFormView(FormViewMixIn, EntityView): @@ -149,8 +150,7 @@ self.relation_form(lzone, value, form, self._build_renderer(entity, rtype, role)) else: - if rvid is None: - rvid = self._compute_best_vid(entity.e_schema, rschema, role) + rvid = self._compute_best_vid(entity.e_schema, rschema, role) rset = entity.related(rtype, role) if rset: value = self._cw.view(rvid, rset) @@ -202,7 +202,7 @@ u'onmouseover="removeElementClass(jQuery(\'#%s\'), \'hidden\')">' % (divid, divid, divid)) w(u'
    %s
    ' % (divid, value)) - w(form.form_render(renderer=renderer)) + w(form.render(renderer=renderer)) w(u'') def _compute_best_vid(self, eschema, rschema, role): + dispctrl = _pvdc.etype_get(eschema, rschema, role) + if dispctrl.get('rvid'): + return dispctrl['rvid'] if eschema.cardinality(rschema, role) in '+*': return self._many_rvid return self._one_rvid @@ -254,6 +257,8 @@ __slots__ = ('event_args',) def form_render(self, **_args): return u'' + def render(self, **_args): + return u'' def append_field(self, *args): pass @@ -269,7 +274,7 @@ eschema = entity.e_schema rtype = str(rschema) # XXX check autoform_section. what if 'generic'? - dispctrl = uicfg.primaryview_display_ctrl.etype_get(eschema, rtype, role) + dispctrl = _pvdc.etype_get(eschema, rtype, role) vid = dispctrl.get('vid', 'reledit') if vid != 'reledit': # reledit explicitly disabled return False @@ -310,7 +315,7 @@ entity=entity, submitmsg=self.submited_message()) self.init_form(form, entity) - self.w(form.form_render(formvid=u'edition')) + self.w(form.render(rendervalues=dict(formvid=u'edition'))) def init_form(self, form, entity): """customize your form before rendering here""" @@ -447,7 +452,7 @@ form = self._cw.vreg['forms'].select(self.__regid__, self._cw, rset=self.cw_rset, copy_nav_params=True) - self.w(form.form_render()) + self.w(form.render()) class InlineEntityEditionFormView(FormViewMixIn, EntityView): @@ -479,9 +484,13 @@ form = self.vreg['forms'].select('edition', self._cw, entity=entity, form_renderer_id='inline', - mainform=False, copy_nav_params=False, + copy_nav_params=False, + mainform=False, + parent_form=self.pform, **self.extra_kwargs) - form.parent_form = self.pform + if self.pform is None: + form.restore_previous_post(form.session_key()) + #assert form.parent_form self.add_hiddens(form, entity) return form @@ -500,16 +509,24 @@ """fetch and render the form""" entity = self._entity() divid = '%s-%s-%s' % (self.peid, self.rtype, entity.eid) - title = self.req.pgettext(i18nctx, 'This %s' % entity.e_schema) - removejs = self.removejs % (self.peid, self.rtype, entity.eid) + title = self.form_title(entity, i18nctx) + removejs = self.removejs and self.removejs % ( + self.peid, self.rtype, entity.eid) countkey = '%s_count' % self.rtype try: self._cw.data[countkey] += 1 - except: + except KeyError: self._cw.data[countkey] = 1 - self.w(self.form.form_render( + self.w(self.form.form.render( divid=divid, title=title, removejs=removejs, i18nctx=i18nctx, counter=self.req.data[countkey], **kwargs)) + self.w(self.form.render( + rendervalues=dict(divid=divid, title=title, removejs=removejs, + i18nctx=i18nctx, counter=self._cw.data[countkey]), + formvalues=kwargs)) + + def form_title(self, entity, i18nctx): + return self.req.pgettext(i18nctx, 'This %s' % entity.e_schema) def add_hiddens(self, form, entity): """to ease overriding (see cubes.vcsfile.views.forms for instance)""" @@ -540,7 +557,20 @@ __select__ = (match_kwargs('peid', 'rtype') & specified_etype_implements('Any')) _select_attrs = InlineEntityEditionFormView._select_attrs + ('etype',) - removejs = "removeInlineForm('%s', '%s', '%s')" + + @property + def removejs(self): + entity = self._entity() + card = entity.e_schema.role_rproperty(neg_role(self.role), self.rtype, 'cardinality') + card = card[self.role == 'object'] + # when one is adding an inline entity for a relation of a single card, + # the 'add a new xxx' link disappears. If the user then cancel the addition, + # we have to make this link appears back. This is done by giving add new link + # id to removeInlineForm. + if card not in '?1': + return "removeInlineForm('%s', '%s', '%s')" + divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid) + return "removeInlineForm('%%s', '%%s', '%%s', '%s')" % divid @cached def _entity(self): diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/forms.py --- a/web/views/forms.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/forms.py Thu Dec 03 17:17:43 2009 +0100 @@ -10,6 +10,7 @@ from warnings import warn from logilab.common.compat import any +from logilab.common.deprecation import deprecated from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param @@ -89,6 +90,8 @@ if mainform: self.form_add_hidden('__errorurl', self.session_key()) self.form_add_hidden('__domid', self.domid) + self.restore_previous_post(self.session_key()) + # XXX why do we need two different variables (mainform and copy_nav_params ?) if self.copy_nav_params: for param in NAV_FORM_PARAMETERS: @@ -125,15 +128,14 @@ if self.needs_css: self._cw.add_css(self.needs_css) - def form_render(self, **values): + def render(self, formvalues=None, rendervalues=None, renderer=None): """render this form, using the renderer given in args or the default FormRenderer() """ - self.build_context(values) - renderer = values.pop('renderer', None) + self.build_context(formvalues or {}) if renderer is None: renderer = self.form_default_renderer() - return renderer.render(self, values) + return renderer.render(self, rendervalues or {}) def form_default_renderer(self): return self._cw.vreg['formrenderers'].select(self.form_renderer_id, @@ -146,8 +148,8 @@ containing field 'name' (qualified), 'id', 'value' (for display, always a string). - rendervalues is an optional dictionary containing extra kwargs given to - form_render() + rendervalues is an optional dictionary containing extra form values + given to render() """ if self.context is not None: return # already built @@ -249,6 +251,17 @@ """ return self.form_valerror and field.name in self.form_valerror.errors + @deprecated('use .render(formvalues, rendervalues)') + def form_render(self, **values): + """render this form, using the renderer given in args or the default + FormRenderer() + """ + self.build_context(values) + renderer = values.pop('renderer', None) + if renderer is None: + renderer = self.form_default_renderer() + return renderer.render(self, values) + class EntityFieldsForm(FieldsForm): __regid__ = 'base' @@ -279,6 +292,19 @@ if msg: self.form_add_hidden('__message', msg) + def session_key(self): + """return the key that may be used to store / retreive data about a + previous post which failed because of a validation error + """ + try: + return self.force_session_key + except AttributeError: + # XXX if this is a json request, suppose we should redirect to the + # entity primary view + if self.req.json_request: + return '%s#%s' % (self.edited_entity.absolute_url(), self.domid) + return '%s#%s' % (self.req.url(), self.domid) + def _field_has_error(self, field): """return true if the field has some error in given validation exception """ @@ -401,6 +427,8 @@ return eid_param(field.id, self.edited_entity.eid) return field.id + # XXX all this vocabulary handling should be on the field, no? + def form_field_vocabulary(self, field, limit=None): """return vocabulary for the given field""" role, rtype = field.role, field.name diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/igeocodable.py --- a/web/views/igeocodable.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/igeocodable.py Thu Dec 03 17:17:43 2009 +0100 @@ -76,7 +76,7 @@ self._cw.demote_to_html() # remove entities that don't define latitude and longitude self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude) - self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s' % gmap_key, + self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s' % gmap_key, localfile=False) self._cw.add_js( ('cubicweb.widgets.js', 'cubicweb.gmap.js', 'gmap.utility.labeledmarker.js') ) rql = self.cw_rset.printable_rql() diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/management.py --- a/web/views/management.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/management.py Thu Dec 03 17:17:43 2009 +0100 @@ -115,7 +115,7 @@ __redirectpath=entity.rest_path()) field = guess_field(entity.e_schema, self._cw.schema.rschema('owned_by')) form.append_field(field) - self.w(form.form_render(display_progress_div=False)) + self.w(form.render(rendervalues=dict(display_progress_div=False))) def owned_by_information(self, entity): ownersrset = entity.related('owned_by') @@ -185,7 +185,7 @@ form.append_field(field) renderer = self._cw.vreg['formrenderers'].select( 'htable', self._cw, rset=None, display_progress_div=False) - self.w(form.form_render(renderer=renderer)) + self.w(form.render(renderer=renderer)) class ErrorView(AnyRsetView): @@ -248,7 +248,7 @@ form.form_add_hidden('__bugreporting', '1') form.form_buttons = [wdgs.SubmitButton(MAIL_SUBMIT_MSGID)] form.action = req.build_url('reportbug') - w(form.form_render()) + w(form.render()) def exc_message(ex, encoding): diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/massmailing.py --- a/web/views/massmailing.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/massmailing.py Thu Dec 03 17:17:43 2009 +0100 @@ -127,4 +127,4 @@ from_addr = '%s <%s>' % (req.user.dc_title(), req.user.get_email()) form = self._cw.vreg['forms'].select('massmailing', self._cw, rset=self.cw_rset, action='sendmail', domid='sendmail') - self.w(form.form_render(sender=from_addr)) + self.w(form.render(formvalues=dict(sender=from_addr))) diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/primary.py --- a/web/views/primary.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/primary.py Thu Dec 03 17:17:43 2009 +0100 @@ -13,6 +13,7 @@ from logilab.mtconverter import xml_escape from cubicweb import Unauthorized +from cubicweb.selectors import match_kwargs from cubicweb.view import EntityView from cubicweb.schema import display_name from cubicweb.web import uicfg @@ -50,7 +51,10 @@ #self.render_entity_toolbox(entity) # entity's attributes and relations, excluding meta data # if the entity isn't meta itself - boxes = self._prepare_side_boxes(entity) + if self.is_primary(): + boxes = self._prepare_side_boxes(entity) + else: + boxes = None if boxes or hasattr(self, 'render_side_related'): self.w(u'
    ') self.render_entity_summary(entity) @@ -88,7 +92,12 @@ """default implementation return dc_title""" title = xml_escape(entity.dc_title()) if title: - self.w(u'

    %s

    ' % title) + if self.is_primary(): + self.w(u'

    %s

    ' % title) + else: + atitle = self.req._('follow this link for more information on this %s') % entity.dc_type() + self.w(u'

    %s

    ' + % (entity.absolute_url(), atitle, title)) def render_entity_toolbox(self, entity): self.content_navigation_components('ctxtoolbar') @@ -123,15 +132,26 @@ value = None if self.skip_none and (value is None or value == ''): continue - self._render_attribute(rschema, value, role=role, table=True) + try: + self._render_attribute(dispctrl, rschema, value, + role=role, table=True) + except TypeError: + warn('[3.6] _render_attribute prototype has changed, ' + 'please update %s' % self.__class___, DeprecationWarning) + self._render_attribute(rschema, value, role=role, table=True) self.w(u'
    ') def render_entity_relations(self, entity, siderelations=None): for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'): rset = self._relation_rset(entity, rschema, role, dispctrl) if rset: - self._render_relation(rset, dispctrl, 'autolimited', - self.show_rel_label) + try: + self._render_relation(dispctrl, rset, 'autolimited') + except TypeError: + warn('[3.6] _render_relation prototype has changed, ' + 'please update %s' % self.__class__, DeprecationWarning) + self._render_relation(rset, dispctrl, 'autolimited', + self.show_rel_label) def render_side_boxes(self, boxes): """display side related relations: @@ -139,7 +159,13 @@ """ for box in boxes: if isinstance(box, tuple): - label, rset, vid = box + try: + label, rset, vid, dispctrl = box + except ValueError: + warn('box views should now be defined as a 4-uple (label, rset, vid, dispctrl), ' + 'please update %s' % self.__class__.__name__, + DeprecationWarning) + label, rset, vid = box self.w(u'') @@ -159,11 +185,19 @@ continue label = display_name(self._cw, rschema.type, role) vid = dispctrl.get('vid', 'sidebox') - sideboxes.append( (label, rset, vid) ) + sideboxes.append( (label, rset, vid, dispctrl) ) sideboxes += self._cw.vreg['boxes'].poss_visible_objects( self._cw, rset=self.cw_rset, row=self.cw_row, view=self, context='incontext') - return sideboxes + # XXX since we've two sorted list, it may be worth using bisect + def get_order(x): + if isinstance(x, tuple): + # x is a view box (label, rset, vid, dispctrl) + # default to 1000 so view boxes occurs after component boxes + return x[-1].get('order', 1000) + # x is a component box + return x.propval('order') + return sorted(sideboxes, key=get_order) def _section_def(self, entity, where): rdefs = [] @@ -193,20 +227,25 @@ rset = dispctrl['filter'](rset) return rset - def _render_relation(self, rset, dispctrl, defaultvid, showlabel): + def _render_relation(self, dispctrl, rset, defaultvid): self.w(u'
    ') - if showlabel: - self.w(u'

    %s

    ' % self._cw._(dispctrl['label']), + if dispctrl.get('showlabel', self.show_rel_label): + self.w(u'

    %s

    ' % self._cw._(dispctrl['label'])) + self.wview(dispctrl.get('vid', defaultvid), rset, initargs={'dispctrl': dispctrl}) self.w(u'
    ') - def _render_attribute(self, rschema, value, role='subject', table=False): + def _render_attribute(self, dispctrl, rschema, value, + role='subject', table=False): if rschema.final: - show_label = self.show_attr_label + showlabel = dispctrl.get('showlabel', self.show_attr_label) else: - show_label = self.show_rel_label - label = display_name(self._cw, rschema.type, role) - self.field(label, value, show_label=show_label, tr=False, table=table) + showlabel = dispctrl.get('showlabel', self.show_rel_label) + if dispctrl.get('label'): + label = self._cw._(dispctrl.get('label')) + else: + label = display_name(self.req, rschema.type, role) + self.field(label, value, show_label=showlabel, tr=False, table=table) class RelatedView(EntityView): @@ -239,6 +278,21 @@ self._cw._('see them all'))) self.w(u'') + +class URLAttributeView(EntityView): + """use this view for attributes whose value is an url and that you want + to display as clickable link + """ + id = 'urlattr' + __select__ = EntityView.__select__ & match_kwargs('rtype') + + def cell_call(self, row, col, rtype, **kwargs): + entity = self.rset.get_entity(row, col) + url = entity.printable_value(rtype) + if url: + self.w(u'%s' % (url, url)) + + ## default primary ui configuration ########################################### _pvs = uicfg.primaryview_section diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/sparql.py --- a/web/views/sparql.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/sparql.py Thu Dec 03 17:17:43 2009 +0100 @@ -39,7 +39,7 @@ __regid__ = 'sparql' def call(self): form = self._cw.vreg.select('forms', 'sparql', self._cw) - self.w(form.form_render()) + self.w(form.render()) sparql = self._cw.form.get('sparql') vid = self._cw.form.get('resultvid', 'table') if sparql: diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/tabs.py --- a/web/views/tabs.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/tabs.py Thu Dec 03 17:17:43 2009 +0100 @@ -179,7 +179,7 @@ self.w(u'') -class TabedPrimaryView(TabsMixin, primary.PrimaryView): +class TabbedPrimaryView(TabsMixin, primary.PrimaryView): __abstract__ = True # don't register tabs = ['main_tab'] @@ -191,7 +191,7 @@ # XXX uncomment this in 3.6 #self.render_entity_toolbox(entity) self.render_tabs(self.tabs, self.default_tab, entity) - +TabedPrimaryView = TabbedPrimaryView # XXX deprecate that typo! class PrimaryTab(primary.PrimaryView): __regid__ = 'main_tab' diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/timetable.py --- a/web/views/timetable.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/timetable.py Thu Dec 03 17:17:43 2009 +0100 @@ -10,7 +10,7 @@ from cubicweb.interfaces import ITimetableViews from cubicweb.selectors import implements -from cubicweb.utils import date_range +from cubicweb.utils import date_range, todatetime from cubicweb.view import AnyRsetView @@ -22,6 +22,7 @@ self.lines = 1 MIN_COLS = 3 # minimum number of task columns for a single user +ALL_USERS = object() class TimeTableView(AnyRsetView): __regid__ = 'timetable' @@ -54,6 +55,7 @@ elif task.stop: the_dates.append(task.stop) for d in the_dates: + d = todatetime(d) d_users = dates.setdefault(d, {}) u_tasks = d_users.setdefault(user, set()) u_tasks.add( task ) @@ -137,7 +139,7 @@ for user, width in zip(users, widths): self.w(u'' % max(MIN_COLS, width)) if user is ALL_USERS: - self.w('*') + self.w(u'*') else: user.view('oneline', w=self.w) self.w(u'') diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/treeview.py --- a/web/views/treeview.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/treeview.py Thu Dec 03 17:17:43 2009 +0100 @@ -119,7 +119,7 @@ (each item should be expandable if it's not a tree leaf) """ __regid__ = 'treeitemview' - __select__ = EntityView.__select__ & implements(ITree) + __select__ = implements(ITree) default_branch_state_is_open = False def open_state(self, eeid, treeid): diff -r 92ead039d3d0 -r 94cc7cad3d2d web/views/workflow.py --- a/web/views/workflow.py Mon Nov 23 14:13:53 2009 +0100 +++ b/web/views/workflow.py Thu Dec 03 17:17:43 2009 +0100 @@ -17,7 +17,7 @@ from cubicweb import Unauthorized, view from cubicweb.selectors import (implements, has_related_entities, one_line_rset, relation_possible, match_form_params, - entity_implements) + entity_implements, score_entity) from cubicweb.interfaces import IWorkflowable from cubicweb.view import EntityView from cubicweb.schema import display_name @@ -64,37 +64,40 @@ def cell_call(self, row, col): entity = self.cw_rset.get_entity(row, col) transition = self._cw.entity_from_eid(self._cw.form['treid']) - dest = transition.destination() - _ = self._cw._ - # specify both rset/row/col and entity in case implements selector (and - # not entity_implements) is used on custom form - form = self._cw.vreg['forms'].select( - 'changestate', self._cw, rset=self.cw_rset, row=row, col=col, - entity=entity, transition=transition, - redirect_path=self.redirectpath(entity)) + form = self.get_form(entity, transition) self.w(form.error_message()) - self.w(u'

    %s %s

    \n' % (_(transition.name), + self.w(u'

    %s %s

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

    %s

    \n' % msg) + self.w(form.render(formvalues=dict(wf_info_for=entity.eid, + by_transition=transition.eid))) + + def redirectpath(self, entity): + return entity.rest_path() + + def get_form(self, entity, transition, **kwargs): + # XXX used to specify both rset/row/col and entity in case implements + # selector (and not entity_implements) is used on custom form + form = self._cw.vreg['forms'].select( + 'changestate', self._cw, entity=entity, transition=transition, + redirect_path=self.redirectpath(entity), **kwargs) trinfo = self._cw.vreg['etypes'].etype_class('TrInfo')(self._cw) trinfo.eid = self._cw.varmaker.next() subform = self._cw.vreg['forms'].select('edition', self._cw, entity=trinfo, mainform=False) subform.field_by_name('by_transition').widget = fwdgs.HiddenInput() 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() + return form class WFHistoryView(EntityView): __regid__ = 'wfhistory' - __select__ = relation_possible('wf_info_for', role='object') + __select__ = relation_possible('wf_info_for', role='object') & \ + score_entity(lambda x: x.workflow_history) + title = _('Workflow history') def cell_call(self, row, col, view=None): @@ -247,7 +250,7 @@ def object_allowed_transition_vocabulary(self, rtype, limit=None): if not self.edited_entity.has_eid(): return self.workflow_states_for_relation('allowed_transition') - return self.subject_relation_vocabulary(rtype, limit) + return self.object_relation_vocabulary(rtype, limit) class StateEditionForm(autoform.AutomaticEntityForm): @@ -306,7 +309,6 @@ for state in self.entity.reverse_state_of: state.complete() yield state.eid, state - for transition in self.entity.reverse_transition_of: transition.complete() yield transition.eid, transition