--- 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$
--- 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
--- 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'
--- 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
--- 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):
--- 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,
--- 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:
--- 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:
--- 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 <sylvain.thenault@logilab.fr> Thu, 03 Dec 2009 15:48:38 +0100
+
+cubicweb (3.5.9-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Nicolas Chauvat <nicolas.chauvat@logilab.fr> Sun, 29 Nov 2009 23:28:47 +0100
+
+cubicweb (3.5.8-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Nicolas Chauvat <nicolas.chauvat@logilab.fr> Sun, 29 Nov 2009 22:43:11 +0100
+
+cubicweb (3.5.7-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Nicolas Chauvat <nicolas.chauvat@logilab.fr> Sat, 28 Nov 2009 11:50:08 +0100
+
+cubicweb (3.5.6-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Adrien Di Mascio <Adrien.DiMascio@logilab.fr> Mon, 23 Nov 2009 18:55:01 +0100
+
cubicweb (3.5.5-1) unstable; urgency=low
* new upstream release
--- 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. <contact@logilab.fr>
Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>,
Julien Jehannet <julien.jehannet@logilab.fr>,
+ Adrien Di Mascio <Adrien.DiMascio@logilab.fr>,
Aurélien Campéas <aurelien.campeas@logilab.fr>,
Nicolas Chauvat <nicolas.chauvat@logilab.fr>
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
--- 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
--- 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
--- 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()
--- 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.
--- 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--- 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
--- 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
--- 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_'<relation> 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)' % (
--- 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))
--- 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"
--- 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"
--- 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é"
--- /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'))
+
--- 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):
--- 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
--- 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
--- /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 ;;
--- /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;;
--- 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 ;;
--- 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;;
--- 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'
--- 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()
--- 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):
--- 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:
--- 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
--- 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):
--- 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.
--- 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
--- 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]))]
--- 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))
--- 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):
--- 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')",
--- 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):
--- 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
"""
--- 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:
--- 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'<div class="section">')
@@ -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'<span>%s</span> ' % label)
if table:
- w(u'<td>%s</td></tr>' % value)
+ if not (show_label and label):
+ w(u'<td colspan="2">%s</td></tr>' % value)
+ else:
+ w(u'<td>%s</td></tr>' % value)
else:
w(u'<span>%s</span></div>' % value)
--- 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);
+ }
}
/*
--- 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
+ <head> and </head> 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 = '<li>%s</li>\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 = '<ul>%s</ul>' % errormsg
+ return u'<div class="errorMessage">%s</div>' % 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
--- 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):
--- 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 ############################################################
--- 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()
--- 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),
])
--- 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
--- 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)
--- 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 = []
--- 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'<li>%s</li>' % tags.a(entity.view('textoutofcontext'),
href=entity.absolute_url()))
w(u'</ul>\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'<div id="%s-value" class="editableFieldValue">%s</div>' % (divid, value))
- w(form.form_render(renderer=renderer))
+ w(form.render(renderer=renderer))
w(u'<div id="%s" class="editableField hidden" onclick="%s" title="%s">' % (
divid, xml_escape(self._onclick % form.event_args),
self.req._(self._landingzonemsg)))
@@ -211,6 +211,9 @@
w(u'</div>')
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):
--- 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
--- 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()
--- 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):
--- 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)))
--- 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'<table width="100%"><tr><td style="width: 75%">')
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'<h1>%s</h1>' % title)
+ if self.is_primary():
+ self.w(u'<h1>%s</h1>' % title)
+ else:
+ atitle = self.req._('follow this link for more information on this %s') % entity.dc_type()
+ self.w(u'<h4><a href="%s" title="%s">%s</a></h4>'
+ % (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'</table>')
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'<div class="sideBox">')
self.wview(vid, rset, title=label)
self.w(u'</div>')
@@ -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'<div class="section">')
- if showlabel:
- self.w(u'<h4>%s</h4>' % self._cw._(dispctrl['label']),
+ if dispctrl.get('showlabel', self.show_rel_label):
+ self.w(u'<h4>%s</h4>' % self._cw._(dispctrl['label']))
+ self.wview(dispctrl.get('vid', defaultvid), rset,
initargs={'dispctrl': dispctrl})
self.w(u'</div>')
- 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'</div>')
+
+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'<a href="%s">%s</a>' % (url, url))
+
+
## default primary ui configuration ###########################################
_pvs = uicfg.primaryview_section
--- 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:
--- 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'</div>')
-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'
--- 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'<th colspan="%s">' % max(MIN_COLS, width))
if user is ALL_USERS:
- self.w('*')
+ self.w(u'*')
else:
user.view('oneline', w=self.w)
self.w(u'</th>')
--- 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):
--- 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'<h4>%s %s</h4>\n' % (_(transition.name),
+ self.w(u'<h4>%s %s</h4>\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'<p>%s</p>\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