--- a/.hgtags Tue Sep 14 08:48:44 2010 +0200
+++ b/.hgtags Thu Sep 16 18:56:35 2010 +0200
@@ -151,3 +151,5 @@
12038ca95f0fff2205f7ee029f5602d192118aec cubicweb-debian-version-3.9.5-1
d37428222a6325583be958d7c7fe7c595115663d cubicweb-version-3.9.6
7d2cab567735a17cab391c1a7f1bbe39118308a2 cubicweb-debian-version-3.9.6-1
+de588e756f4fbe9c53c72159c6b96580a36d3fa6 cubicweb-version-3.9.7
+1c01f9dffd64d507863c9f8f68e3585b7aa24374 cubicweb-debian-version-3.9.7-1
--- a/__pkginfo__.py Tue Sep 14 08:48:44 2010 +0200
+++ b/__pkginfo__.py Thu Sep 16 18:56:35 2010 +0200
@@ -22,7 +22,7 @@
modname = distname = "cubicweb"
-numversion = (3, 9, 6)
+numversion = (3, 9, 7)
version = '.'.join(str(num) for num in numversion)
description = "a repository of entities / relations for knowledge management"
@@ -57,7 +57,7 @@
}
__recommends__ = {
- 'Pyro': '>= 3.9.1',
+ 'Pyro': '>= 3.9.1, < 4.0.0',
'PIL': '', # for captcha
'pycrypto': '', # for crypto extensions
'fyzz': '>= 0.1.0', # for sparql
--- a/dbapi.py Tue Sep 14 08:48:44 2010 +0200
+++ b/dbapi.py Thu Sep 16 18:56:35 2010 +0200
@@ -457,6 +457,12 @@
time() - tstart, clock() - cstart))
return rset
+def check_not_closed(func):
+ def decorator(self, *args, **kwargs):
+ if self._closed is not None:
+ raise ProgrammingError('Closed connection')
+ return func(self, *args, **kwargs)
+ return decorator
class Connection(object):
"""DB-API 2.0 compatible Connection object for CubicWeb
@@ -497,59 +503,15 @@
self.rollback()
return False #propagate the exception
- def _txid(self, cursor=None): # XXX could now handle various isolation level!
- # return a dict as bw compat trick
- return {'txid': currentThread().getName()}
-
- def request(self):
- return DBAPIRequest(self.vreg, DBAPISession(self))
-
- def check(self):
- """raise `BadConnectionId` if the connection is no more valid, else
- return its latest activity timestamp.
- """
- if self._closed is not None:
- raise ProgrammingError('Closed connection')
- return self._repo.check_session(self.sessionid)
-
- def set_session_props(self, **props):
- """raise `BadConnectionId` if the connection is no more valid"""
- if self._closed is not None:
- raise ProgrammingError('Closed connection')
- self._repo.set_session_props(self.sessionid, props)
-
- def get_shared_data(self, key, default=None, pop=False, txdata=False):
- """return value associated to key in the session's data dictionary or
- session's transaction's data if `txdata` is true.
-
- If pop is True, value will be removed from the dictionnary.
+ def __del__(self):
+ """close the remote connection if necessary"""
+ if self._closed is None and self._close_on_del:
+ try:
+ self.close()
+ except:
+ pass
- If key isn't defined in the dictionnary, value specified by the
- `default` argument will be returned.
- """
- if self._closed is not None:
- raise ProgrammingError('Closed connection')
- return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
-
- def set_shared_data(self, key, value, txdata=False):
- """set value associated to `key` in shared data
-
- if `txdata` is true, the value will be added to the repository session's
- transaction's data which are cleared on commit/rollback of the current
- transaction.
- """
- if self._closed is not None:
- raise ProgrammingError('Closed connection')
- return self._repo.set_shared_data(self.sessionid, key, value, txdata)
-
- def get_schema(self):
- """Return the schema currently used by the repository.
-
- This is NOT part of the DB-API.
- """
- if self._closed is not None:
- raise ProgrammingError('Closed connection')
- return self._repo.get_schema()
+ # connection initialization methods ########################################
def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
config = self.vreg.config
@@ -605,20 +567,18 @@
if sitetitle is not None:
self.vreg['propertydefs']['ui.site-title'] = {'default': sitetitle}
+ @check_not_closed
def source_defs(self):
"""Return the definition of sources used by the repository.
This is NOT part of the DB-API.
"""
- if self._closed is not None:
- raise ProgrammingError('Closed connection')
return self._repo.source_defs()
+ @check_not_closed
def user(self, req=None, props=None):
"""return the User object associated to this connection"""
# cnx validity is checked by the call to .user_info
- if self._closed is not None:
- raise ProgrammingError('Closed connection')
eid, login, groups, properties = self._repo.user_info(self.sessionid,
props)
if req is None:
@@ -634,19 +594,103 @@
user.cw_attr_cache['login'] = login # cache login
return user
- def __del__(self):
- """close the remote connection if necessary"""
- if self._closed is None and self._close_on_del:
- try:
- self.close()
- except:
- pass
+ @check_not_closed
+ def check(self):
+ """raise `BadConnectionId` if the connection is no more valid, else
+ return its latest activity timestamp.
+ """
+ self._repo.check_session(self.sessionid)
+
+ def _txid(self, cursor=None): # XXX could now handle various isolation level!
+ # return a dict as bw compat trick
+ return {'txid': currentThread().getName()}
+
+ def request(self):
+ return DBAPIRequest(self.vreg, DBAPISession(self))
+
+ # session data methods #####################################################
+
+ @check_not_closed
+ def set_session_props(self, **props):
+ """raise `BadConnectionId` if the connection is no more valid"""
+ self._repo.set_session_props(self.sessionid, props)
+
+ @check_not_closed
+ def get_shared_data(self, key, default=None, pop=False, txdata=False):
+ """return value associated to key in the session's data dictionary or
+ session's transaction's data if `txdata` is true.
+
+ If pop is True, value will be removed from the dictionnary.
+ If key isn't defined in the dictionnary, value specified by the
+ `default` argument will be returned.
+ """
+ return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
+
+ @check_not_closed
+ def set_shared_data(self, key, value, txdata=False):
+ """set value associated to `key` in shared data
+
+ if `txdata` is true, the value will be added to the repository
+ session's query data which are cleared on commit/rollback of the current
+ transaction.
+ """
+ return self._repo.set_shared_data(self.sessionid, key, value, txdata)
+
+ # meta-data accessors ######################################################
+
+ @check_not_closed
+ def get_schema(self):
+ """Return the schema currently used by the repository."""
+ return self._repo.get_schema()
+
+ @check_not_closed
+ def get_option_value(self, option):
+ """return the value for `option` in the repository configuration."""
+ return self._repo.get_option_value(option)
+
+ @check_not_closed
def describe(self, eid):
- if self._closed is not None:
- raise ProgrammingError('Closed connection')
return self._repo.describe(self.sessionid, eid, **self._txid())
+ # db-api like interface ####################################################
+
+ @check_not_closed
+ def commit(self):
+ """Commit pending transaction for this connection to the repository.
+
+ may raises `Unauthorized` or `ValidationError` if we attempted to do
+ something we're not allowed to for security or integrity reason.
+
+ If the transaction is undoable, a transaction id will be returned.
+ """
+ return self._repo.commit(self.sessionid, **self._txid())
+
+ @check_not_closed
+ def rollback(self):
+ """This method is optional since not all databases provide transaction
+ support.
+
+ In case a database does provide transactions this method causes the the
+ database to roll back to the start of any pending transaction. Closing
+ a connection without committing the changes first will cause an implicit
+ rollback to be performed.
+ """
+ self._repo.rollback(self.sessionid, **self._txid())
+
+ @check_not_closed
+ def cursor(self, req=None):
+ """Return a new Cursor Object using the connection.
+
+ On pyro connection, you should get cursor after calling if
+ load_appobjects method if desired (which you should call if you intend
+ to use ORM abilities).
+ """
+ if req is None:
+ req = self.request()
+ return self.cursor_class(self, self._repo, req=req)
+
+ @check_not_closed
def close(self):
"""Close the connection now (rather than whenever __del__ is called).
@@ -656,52 +700,13 @@
connection. Note that closing a connection without committing the
changes first will cause an implicit rollback to be performed.
"""
- if self._closed:
- raise ProgrammingError('Connection is already closed')
self._repo.close(self.sessionid, **self._txid())
del self._repo # necessary for proper garbage collection
self._closed = 1
- def commit(self):
- """Commit pending transaction for this connection to the repository.
-
- may raises `Unauthorized` or `ValidationError` if we attempted to do
- something we're not allowed to for security or integrity reason.
-
- If the transaction is undoable, a transaction id will be returned.
- """
- if not self._closed is None:
- raise ProgrammingError('Connection is already closed')
- return self._repo.commit(self.sessionid, **self._txid())
-
- def rollback(self):
- """This method is optional since not all databases provide transaction
- support.
-
- In case a database does provide transactions this method causes the the
- database to roll back to the start of any pending transaction. Closing
- a connection without committing the changes first will cause an implicit
- rollback to be performed.
- """
- if not self._closed is None:
- raise ProgrammingError('Connection is already closed')
- self._repo.rollback(self.sessionid, **self._txid())
-
- def cursor(self, req=None):
- """Return a new Cursor Object using the connection.
-
- On pyro connection, you should get cursor after calling if
- load_appobjects method if desired (which you should call if you intend
- to use ORM abilities).
- """
- if self._closed is not None:
- raise ProgrammingError('Can\'t get cursor on closed connection')
- if req is None:
- req = self.request()
- return self.cursor_class(self, self._repo, req=req)
-
# undo support ############################################################
+ @check_not_closed
def undoable_transactions(self, ueid=None, req=None, **actionfilters):
"""Return a list of undoable transaction objects by the connection's
user, ordered by descendant transaction time.
@@ -734,6 +739,7 @@
txinfo.req = req
return txinfos
+ @check_not_closed
def transaction_info(self, txuuid, req=None):
"""Return transaction object for the given uid.
@@ -748,6 +754,7 @@
txinfo.req = req
return txinfo
+ @check_not_closed
def transaction_actions(self, txuuid, public=True):
"""Return an ordered list of action effectued during that transaction.
@@ -761,6 +768,7 @@
return self._repo.transaction_actions(self.sessionid, txuuid, public,
**self._txid())
+ @check_not_closed
def undo_transaction(self, txuuid):
"""Undo the given transaction. Return potential restoration errors.
--- a/debian/changelog Tue Sep 14 08:48:44 2010 +0200
+++ b/debian/changelog Thu Sep 16 18:56:35 2010 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.9.7-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Thu, 16 Sep 2010 15:41:51 +0200
+
cubicweb (3.9.6-1) unstable; urgency=low
* new upstream release
--- a/doc/book/README Tue Sep 14 08:48:44 2010 +0200
+++ b/doc/book/README Thu Sep 16 18:56:35 2010 +0200
@@ -46,9 +46,26 @@
.. [foot note] the foot note content
+Cross references
+================
-XXX
-* lien vers cw.cwconfig.CW_CUBES_PATH par ex.
+To arbitrary section
+--------------------
+
+:ref:`identifier` ou :ref:`label <identifier>`
+
+Label required of referencing node which as no title, else the node's title will be used.
-automodule, autofunction, automethod, autofunction
+To API objects
+--------------
+See the autodoc sphinx extension documentation. Quick overview:
+
+* ref to a class: :class:`cubicweb.devtools.testlib.AutomaticWebTest`
+
+* if you can to see only the class name in the generated documentation, add a ~:
+ :class:`~cubicweb.devtools.testlib.AutomaticWebTest`
+
+* you can also use :mod: (module), :exc: (exception), :func: (function), :meth: (method)...
+
+* syntax explained above to specify label explicitly may also be used
--- a/doc/book/en/devrepo/migration.rst Tue Sep 14 08:48:44 2010 +0200
+++ b/doc/book/en/devrepo/migration.rst Thu Sep 16 18:56:35 2010 +0200
@@ -91,6 +91,24 @@
* `session`, repository session object
+New cube dependencies
+---------------------
+
+If your code depends on some new cubes, you have to add them in a migration
+script by using:
+
+* `add_cube(cube, update_database=True)`, add a cube.
+* `add_cubes(cubes, update_database=True)`, add a list of cubes.
+
+The `update_database` parameter is telling if the database schema
+should be updated or if only the relevant persistent property should be
+inserted (for the case where a new cube has been extracted from an
+existing one, so the new cube schema is actually already in there).
+
+If some of the added cubes are already used by an instance, they'll simply be
+silently skipped.
+
+
Schema migration
----------------
The following functions for schema migration are available in `repository`
--- a/doc/book/en/devrepo/testing.rst Tue Sep 14 08:48:44 2010 +0200
+++ b/doc/book/en/devrepo/testing.rst Thu Sep 16 18:56:35 2010 +0200
@@ -184,15 +184,70 @@
mail = MAILBOX[1]
self.assertEquals(mail.subject, '[data] yes')
+Visible actions tests
+`````````````````````
+
+It is easy to write unit tests to test actions which are visible to
+user or to a category of users. Let's take an example in the
+`conference cube`_.
+
+.. _`conference cube`: http://www.cubicweb.org/project/cubicweb-conference
+.. sourcecode:: python
+
+ class ConferenceActionsTC(CubicWebTC):
+
+ def setup_database(self):
+ self.conf = self.create_entity('Conference',
+ title=u'my conf',
+ url_id=u'conf',
+ start_on=date(2010, 1, 27),
+ end_on = date(2010, 1, 29),
+ call_open=True,
+ reverse_is_chair_at=chair,
+ reverse_is_reviewer_at=reviewer)
+
+ def test_admin(self):
+ req = self.request()
+ rset = req.execute('Any C WHERE C is Conference')
+ self.assertListEquals(self.pactions(req, rset),
+ [('workflow', workflow.WorkflowActions),
+ ('edit', confactions.ModifyAction),
+ ('managepermission', actions.ManagePermissionsAction),
+ ('addrelated', actions.AddRelatedActions),
+ ('delete', actions.DeleteAction),
+ ('generate_badge_action', badges.GenerateBadgeAction),
+ ('addtalkinconf', confactions.AddTalkInConferenceAction)
+ ])
+ self.assertListEquals(self.action_submenu(req, rset, 'addrelated'),
+ [(u'add Track in_conf Conference object',
+ u'http://testing.fr/cubicweb/add/Track'
+ u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'
+ u'__redirectpath=conference%%2Fconf&'
+ u'__redirectvid=' % {'conf': self.conf.eid}),
+ ])
+
+You just have to execute a rql query corresponding to the view you want to test,
+and to compare the result of
+:meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions
+that must be visible in the interface. This is a list of tuples. The first
+element is the action's `__regid__`, the second the action's class.
+
+To test actions in submenu, you just have to test the result of
+:meth:`~cubicweb.devtools.testlib.CubicWebTC.action_submenu` method. The last
+parameter of the method is the action's category. The result is a list of
+tuples. The first element is the action's title, and the second element the
+action's url.
+
+
.. _automatic_views_tests:
Automatic views testing
-----------------------
-This is done automatically with the AutomaticWebTest class. At cube
-creation time, the mycube/test/test_mycube.py file contains such a
-test. The code here has to be uncommented to be usable, without
-further modification.
+This is done automatically with the :class:`cubicweb.devtools.testlib.AutomaticWebTest`
+class. At cube creation time, the mycube/test/test_mycube.py file
+contains such a test. The code here has to be uncommented to be
+usable, without further modification.
The ``auto_populate`` method uses a smart algorithm to create
pseudo-random data in the database, thus enabling the views to be
@@ -212,6 +267,11 @@
auto_populate cannot guess by itself; these must yield resultsets
against which views may be selected.
+.. warning::
+
+ Take care to not let the imported `AutomaticWebTest` in your test module
+ namespace, else both your subclass *and* this parent class will be run.
+
Testing on a real-life database
-------------------------------
--- a/doc/book/en/devweb/views/reledit.rst Tue Sep 14 08:48:44 2010 +0200
+++ b/doc/book/en/devweb/views/reledit.rst Thu Sep 16 18:56:35 2010 +0200
@@ -70,12 +70,23 @@
controlled using the reledit_ctrl rtag, defined in
:mod:`cubicweb.web.uicfg`.
-This rtag provides three control variables:
+This rtag provides four control variables:
-* ``default_value``
-* ``reload``, to specificy if edition of the relation entails a full page
- reload, which defaults to False
-* ``noedit``, to explicitly inhibit edition
+* ``default_value``: alternative default value
+ The default value is what is shown when there is no value.
+* ``reload``: boolean, eid (to reload to) or function taking subject
+ and returning bool/eid This is useful when editing a relation (or
+ attribute) that impacts the url or another parts of the current
+ displayed page. Defaults to false.
+* ``rvid``: alternative view id (as str) for relation or composite
+ edition Default is 'incontext' or 'csv' depending on the
+ cardinality. They can also be statically changed by subclassing
+ ClickAndEditFormView and redefining _one_rvid (resp. _many_rvid).
+* ``edit_target``: 'rtype' (to edit the relation) or 'related' (to
+ edit the related entity) This controls whether to edit the relation
+ or the target entity of the relation. Currently only one-to-one
+ relations support target entity edition. By default, the 'related'
+ option is taken whenever the relation is composite and one-to-one.
Let's see how to use these controls.
@@ -86,15 +97,13 @@
reledit_ctrl.tag_attribute(('Company', 'name'),
{'reload': lambda x:x.eid,
'default_value': xml_escape(u'<logilab tastes better>')})
- reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'noedit': True})
+ reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'edit_target': 'related'})
The `default_value` needs to be an xml escaped unicode string.
-The `noedit` attribute is convenient to programmatically disable some
-relation edition on views that apply it systematically (the prime
-example being the primary view). Here we use it to forbid changing the
-`boss` relation from a `Person` side (as it could have unwanted
-effects).
+The `edit_target` tag on the `boss` relation being set to `related` will
+ensure edition of the `Person` entity instead (using a standard
+automatic form) of the association of Company and Person.
Finally, the `reload` key accepts either a boolean, an eid or an
unicode string representing an url. If an eid is provided, it will be
--- a/entity.py Tue Sep 14 08:48:44 2010 +0200
+++ b/entity.py Thu Sep 16 18:56:35 2010 +0200
@@ -222,9 +222,10 @@
destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
selection, orderby, restrictions,
user, ordermethod, visited=visited)
- orderterm = getattr(cls, ordermethod)(attr, var)
- if orderterm:
- orderby.append(orderterm)
+ if ordermethod is not None:
+ orderterm = getattr(cls, ordermethod)(attr, var)
+ if orderterm:
+ orderby.append(orderterm)
return selection, orderby, restrictions
@classmethod
--- a/etwist/service.py Tue Sep 14 08:48:44 2010 +0200
+++ b/etwist/service.py Thu Sep 16 18:56:35 2010 +0200
@@ -72,8 +72,9 @@
# create the site
config = cwcfg.config_for(self.instance)
config.init_log(force=True)
+ config.debugmode = False
logger.info('starting cubicweb instance %s ', self.instance)
- root_resource = CubicWebRootResource(config, False)
+ root_resource = CubicWebRootResource(config)
website = server.Site(root_resource)
# serve it via standard HTTP on port set in the configuration
port = config['port'] or 8080
--- a/ext/rest.py Tue Sep 14 08:48:44 2010 +0200
+++ b/ext/rest.py Thu Sep 16 18:56:35 2010 +0200
@@ -242,8 +242,14 @@
data = data.translate(ESC_CAR_TABLE)
settings = {'input_encoding': encoding, 'output_encoding': 'unicode',
'warning_stream': StringIO(),
+ 'traceback': True, # don't sys.exit
+ 'stylesheet': None, # don't try to embed stylesheet (may cause
+ # obscure bug due to docutils computing
+ # relative path according to the directory
+ # used *at import time*
# dunno what's the max, severe is 4, and we never want a crash
- # (though try/except may be a better option...)
+ # (though try/except may be a better option...). May be the
+ # above traceback option will avoid this?
'halt_level': 10,
}
if context:
--- a/hooks/syncschema.py Tue Sep 14 08:48:44 2010 +0200
+++ b/hooks/syncschema.py Thu Sep 16 18:56:35 2010 +0200
@@ -34,8 +34,8 @@
from cubicweb import ValidationError
from cubicweb.selectors import is_instance
-from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS,
- ETYPE_NAME_MAP, display_name)
+from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
+ CONSTRAINTS, ETYPE_NAME_MAP, display_name)
from cubicweb.server import hook, schemaserial as ss
from cubicweb.server.sqlutils import SQL_PREFIX
@@ -52,16 +52,9 @@
}
# core entity and relation types which can't be removed
-CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
- 'CWConstraint', 'CWAttribute', 'CWRelation']
-CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
- 'login', 'upassword', 'name',
- 'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
- 'relation_type', 'from_entity', 'to_entity',
- 'constrainted_by',
- 'read_permission', 'add_permission',
- 'delete_permission', 'updated_permission',
- ]
+CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
+ ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group'))
+
def get_constraints(session, entity):
constraints = []
@@ -873,7 +866,7 @@
def __call__(self):
# final entities can't be deleted, don't care about that
name = self.entity.name
- if name in CORE_ETYPES:
+ if name in CORE_TYPES:
raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
# delete every entities of this type
if not name in ETYPE_NAME_MAP:
@@ -939,7 +932,7 @@
def __call__(self):
name = self.entity.name
- if name in CORE_RTYPES:
+ if name in CORE_TYPES:
raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
# delete relation definitions using this relation type
self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
--- a/i18n/en.po Tue Sep 14 08:48:44 2010 +0200
+++ b/i18n/en.po Thu Sep 16 18:56:35 2010 +0200
@@ -5,7 +5,7 @@
msgstr ""
"Project-Id-Version: 2.0\n"
"POT-Creation-Date: 2006-01-12 17:35+CET\n"
-"PO-Revision-Date: 2010-05-16 18:58+0200\n"
+"PO-Revision-Date: 2010-09-15 14:55+0200\n"
"Last-Translator: Sylvain Thenault <sylvain.thenault@logilab.fr>\n"
"Language-Team: English <devel@logilab.fr.org>\n"
"Language: en\n"
@@ -317,6 +317,12 @@
msgid "CWRelation_plural"
msgstr "Relations"
+msgid "CWUniqueTogetherConstraint"
+msgstr ""
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr ""
+
msgid "CWUser"
msgstr "User"
@@ -496,6 +502,9 @@
msgid "New CWRelation"
msgstr "New relation"
+msgid "New CWUniqueTogetherConstraint"
+msgstr ""
+
msgid "New CWUser"
msgstr "New user"
@@ -686,6 +695,9 @@
msgid "This CWRelation"
msgstr "This relation"
+msgid "This CWUniqueTogetherConstraint"
+msgstr ""
+
msgid "This CWUser"
msgstr "This user"
@@ -880,6 +892,9 @@
msgid "add CWRelation relation_type CWRType object"
msgstr "relation definition"
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr ""
+
msgid "add CWUser in_group CWGroup object"
msgstr "user"
@@ -1436,6 +1451,20 @@
msgid "constraint factory"
msgstr ""
+msgid "constraint_of"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr ""
+
+msgid "constraint_of_object"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr ""
+
msgid "constraints"
msgstr ""
@@ -1559,6 +1588,11 @@
msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
msgstr "creating relation %(linkto)s"
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr ""
+
msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
msgstr "creating a new user in group %(linkto)s"
@@ -1800,6 +1834,9 @@
msgid "define how we get out from a sub-workflow"
msgstr ""
+msgid "defines a sql-level multicolumn unique index"
+msgstr ""
+
msgid ""
"defines what's the property is applied for. You must select this first to be "
"able to set value"
@@ -3116,9 +3153,27 @@
msgid "relation_type_object"
msgstr "relation definitions"
+msgid "relations"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr ""
+
msgid "relations deleted"
msgstr ""
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr ""
+
msgid "relative url of the bookmarked page"
msgstr ""
@@ -3898,6 +3953,10 @@
msgid "view_index"
msgstr "index"
+#, python-format
+msgid "violates unique_together constraints (%s)"
+msgstr "violates unique_together constraints (%s)"
+
msgid "visible"
msgstr ""
--- a/i18n/es.po Tue Sep 14 08:48:44 2010 +0200
+++ b/i18n/es.po Thu Sep 16 18:56:35 2010 +0200
@@ -326,6 +326,12 @@
msgid "CWRelation_plural"
msgstr "Relaciones"
+msgid "CWUniqueTogetherConstraint"
+msgstr ""
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr ""
+
msgid "CWUser"
msgstr "Usuario"
@@ -517,6 +523,9 @@
msgid "New CWRelation"
msgstr "Nueva definición de relación final"
+msgid "New CWUniqueTogetherConstraint"
+msgstr ""
+
msgid "New CWUser"
msgstr "Agregar usuario"
@@ -707,6 +716,9 @@
msgid "This CWRelation"
msgstr "Esta definición de relación no final"
+msgid "This CWUniqueTogetherConstraint"
+msgstr ""
+
msgid "This CWUser"
msgstr "Este usuario"
@@ -922,6 +934,9 @@
msgid "add CWRelation relation_type CWRType object"
msgstr "Definición de relación"
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr ""
+
msgid "add CWUser in_group CWGroup object"
msgstr "Usuario"
@@ -1487,6 +1502,20 @@
msgid "constraint factory"
msgstr "Fábrica de restricciones"
+msgid "constraint_of"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr ""
+
+msgid "constraint_of_object"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr ""
+
msgid "constraints"
msgstr "Restricciones"
@@ -1619,6 +1648,11 @@
msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
msgstr "Creación de la relación %(linkto)s"
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr ""
+
msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
msgstr "Creación de un usuario para agregar al grupo %(linkto)s"
@@ -1873,6 +1907,9 @@
msgid "define how we get out from a sub-workflow"
msgstr "Define como salir de un sub-Workflow"
+msgid "defines a sql-level multicolumn unique index"
+msgstr ""
+
msgid ""
"defines what's the property is applied for. You must select this first to be "
"able to set value"
@@ -3231,9 +3268,27 @@
msgid "relation_type_object"
msgstr "Definición de Relaciones"
+msgid "relations"
+msgstr ""
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr ""
+
msgid "relations deleted"
msgstr "Relaciones Eliminadas"
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr ""
+
msgid "relative url of the bookmarked page"
msgstr "Url relativa de la página"
@@ -4035,6 +4090,10 @@
msgid "view_index"
msgstr "Inicio"
+#, python-format
+msgid "violates unique_together constraints (%s)"
+msgstr ""
+
msgid "visible"
msgstr "Visible"
--- a/i18n/fr.po Tue Sep 14 08:48:44 2010 +0200
+++ b/i18n/fr.po Thu Sep 16 18:56:35 2010 +0200
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2010-09-06 21:44+0200\n"
+"PO-Revision-Date: 2010-09-15 15:12+0200\n"
"Last-Translator: Logilab Team <contact@logilab.fr>\n"
"Language-Team: fr <contact@logilab.fr>\n"
"Language: \n"
@@ -37,7 +37,7 @@
msgstr " de l'état %(fromstate)s vers l'état %(tostate)s\n"
msgid " :"
-msgstr ""
+msgstr " :"
#, python-format
msgid "%(attr)s set to %(newvalue)s"
@@ -324,6 +324,12 @@
msgid "CWRelation_plural"
msgstr "Relations"
+msgid "CWUniqueTogetherConstraint"
+msgstr "Contrainte unique_together"
+
+msgid "CWUniqueTogetherConstraint_plural"
+msgstr "Contraintes unique_together"
+
msgid "CWUser"
msgstr "Utilisateur"
@@ -515,6 +521,9 @@
msgid "New CWRelation"
msgstr "Nouvelle définition de relation non finale"
+msgid "New CWUniqueTogetherConstraint"
+msgstr "Nouvelle contrainte unique_together"
+
msgid "New CWUser"
msgstr "Nouvel utilisateur"
@@ -705,6 +714,9 @@
msgid "This CWRelation"
msgstr "Cette définition de relation"
+msgid "This CWUniqueTogetherConstraint"
+msgstr "Cette contrainte unique_together"
+
msgid "This CWUser"
msgstr "Cet utilisateur"
@@ -920,6 +932,9 @@
msgid "add CWRelation relation_type CWRType object"
msgstr "définition de relation"
+msgid "add CWUniqueTogetherConstraint constraint_of CWEType object"
+msgstr "contrainte unique_together"
+
msgid "add CWUser in_group CWGroup object"
msgstr "utilisateur"
@@ -1487,6 +1502,20 @@
msgid "constraint factory"
msgstr "fabrique de contraintes"
+msgid "constraint_of"
+msgstr "contrainte de"
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "constraint_of"
+msgstr "contrainte de"
+
+msgid "constraint_of_object"
+msgstr "contraint par"
+
+msgctxt "CWEType"
+msgid "constraint_of_object"
+msgstr "contraint par"
+
msgid "constraints"
msgstr "contraintes"
@@ -1546,7 +1575,9 @@
msgstr "contexte où ce composant doit être affiché"
msgid "context where this facet should be displayed, leave empty for both"
-msgstr "contexte où cette facette doit être affichée. Laissez ce champ vide pour l'avoir dans les deux."
+msgstr ""
+"contexte où cette facette doit être affichée. Laissez ce champ vide pour "
+"l'avoir dans les deux."
msgid "control subject entity's relations order"
msgstr "contrôle l'ordre des relations de l'entité sujet"
@@ -1619,6 +1650,11 @@
msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)"
msgstr "création relation %(linkto)s"
+msgid ""
+"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint "
+"constraint_of CWEType %(linkto)s)"
+msgstr "création d'une contrainte unique_together sur %(linkto)s"
+
msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)"
msgstr "création d'un utilisateur à rajouter au groupe %(linkto)s"
@@ -1869,6 +1905,9 @@
msgid "define how we get out from a sub-workflow"
msgstr "définit comment sortir d'un sous-workflow"
+msgid "defines a sql-level multicolumn unique index"
+msgstr "définit un index SQL unique sur plusieurs colonnes"
+
msgid ""
"defines what's the property is applied for. You must select this first to be "
"able to set value"
@@ -3229,9 +3268,27 @@
msgid "relation_type_object"
msgstr "définition"
+msgid "relations"
+msgstr "relations"
+
+msgctxt "CWUniqueTogetherConstraint"
+msgid "relations"
+msgstr "relations"
+
msgid "relations deleted"
msgstr "relations supprimées"
+msgid "relations_object"
+msgstr "relations de"
+
+msgctxt "CWAttribute"
+msgid "relations_object"
+msgstr "contraint par"
+
+msgctxt "CWRelation"
+msgid "relations_object"
+msgstr "contraint par"
+
msgid "relative url of the bookmarked page"
msgstr "url relative de la page"
@@ -4032,6 +4089,10 @@
msgid "view_index"
msgstr "accueil"
+#, python-format
+msgid "violates unique_together constraints (%s)"
+msgstr "violation de contrainte unique_together (%s)"
+
msgid "visible"
msgstr "visible"
--- a/rtags.py Tue Sep 14 08:48:44 2010 +0200
+++ b/rtags.py Thu Sep 16 18:56:35 2010 +0200
@@ -34,8 +34,6 @@
* ``tag_subject_of`` tag a relation in the subject's context
* ``tag_object_of`` tag a relation in the object's context
* ``tag_attribute`` shortcut for tag_subject_of
-
-
"""
__docformat__ = "restructuredtext en"
@@ -212,4 +210,27 @@
_allowed_values = frozenset((True, False))
+class NoTargetRelationTagsDict(RelationTagsDict):
+
+ @property
+ def name(self):
+ return self.__class__.name
+
+ def tag_subject_of(self, key, tag):
+ subj, rtype, obj = key
+ if obj != '*':
+ self.warning('using explict target type in %s.tag_subject_of() '
+ 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
+ self.name, subj, rtype, subj, rtype, obj)
+ super(NoTargetRelationTagsDict, self).tag_subject_of((subj, rtype, '*'), tag)
+
+ def tag_object_of(self, key, tag):
+ subj, rtype, obj = key
+ if subj != '*':
+ self.warning('using explict subject type in %s.tag_object_of() '
+ 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
+ self.name, rtype, obj, subj, rtype, obj)
+ super(NoTargetRelationTagsDict, self).tag_object_of(('*', rtype, obj), tag)
+
+
set_log_methods(RelationTags, logging.getLogger('cubicweb.rtags'))
--- a/schema.py Tue Sep 14 08:48:44 2010 +0200
+++ b/schema.py Thu Sep 16 18:56:35 2010 +0200
@@ -62,6 +62,8 @@
'relation_type', 'from_entity', 'to_entity',
'constrained_by', 'cstrtype',
'constraint_of', 'relations',
+ 'read_permission', 'add_permission',
+ 'delete_permission', 'update_permission',
))
WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow',
@@ -613,6 +615,7 @@
class BaseRQLConstraint(BaseConstraint):
"""base class for rql constraints
"""
+ distinct_query = None
def __init__(self, restriction, mainvars=None):
self.restriction = normalize_expression(restriction)
@@ -652,8 +655,12 @@
pass # this is a vocabulary constraint, not enforce XXX why?
def __str__(self):
- return '%s(Any %s WHERE %s)' % (self.__class__.__name__, self.mainvars,
- self.restriction)
+ if self.distinct_query:
+ selop = 'Any'
+ else:
+ selop = 'DISTINCT Any'
+ return '%s(%s %s WHERE %s)' % (self.__class__.__name__, selop,
+ self.mainvars, self.restriction)
def __repr__(self):
return '<%s @%#x>' % (self.__str__(), id(self))
@@ -745,13 +752,14 @@
class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint):
"""the unique rql constraint check that the result of the query isn't
- greater than one
- """
- distinct_query = True
+ greater than one.
- # 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)
+ You *must* specify mainvars when instantiating the constraint since there is
+ no way to guess it correctly (e.g. if using S,O or U the constraint will
+ always be satisfied because we've to use a DISTINCT query).
+ """
+ # XXX turns mainvars into a required argument in __init__
+ distinct_query = True
def match_condition(self, session, eidfrom, eidto):
return len(self.exec_query(session, eidfrom, eidto)) <= 1
--- a/server/hook.py Tue Sep 14 08:48:44 2010 +0200
+++ b/server/hook.py Thu Sep 16 18:56:35 2010 +0200
@@ -248,7 +248,7 @@
from itertools import chain
from logilab.common.decorators import classproperty
-from logilab.common.deprecation import deprecated
+from logilab.common.deprecation import deprecated, class_renamed
from logilab.common.logging_ext import set_log_methods
from cubicweb import RegistryNotFound
@@ -480,15 +480,19 @@
set_log_methods(Hook, getLogger('cubicweb.hook'))
-# base classes for relation propagation ########################################
+# abtract hooks for relation propagation #######################################
+# See example usage in hooks of the nosylist cube
-class PropagateSubjectRelationHook(Hook):
+class PropagateRelationHook(Hook):
"""propagate some `main_rtype` relation on entities linked as object of
`subject_relations` or as subject of `object_relations` (the watched
relations).
This hook ensure that when one of the watched relation is added, the
`main_rtype` relation is added to the target entity of the relation.
+
+ You usually want to use the :class:`match_rtype_sets` selector on concret
+ classes.
"""
events = ('after_add_relation',)
@@ -514,56 +518,77 @@
{'x': meid, 'e': seid})
-class PropagateSubjectRelationAddHook(Hook):
- """propagate to entities at the end of watched relations when a `main_rtype`
- relation is added
+class PropagateRelationAddHook(Hook):
+ """Propagate to entities at the end of watched relations when a `main_rtype`
+ relation is added.
+
+ `subject_relations` and `object_relations` attributes should be specified on
+ subclasses and are usually shared references with attributes of the same
+ name on :class:`PropagateRelationHook`.
+
+ Because of those shared references, you can use `skip_subject_relations` and
+ `skip_object_relations` attributes when you don't want to propagate to
+ entities linked through some particular relations.
"""
events = ('after_add_relation',)
- # to set in concrete class
+ # to set in concrete class (mandatory)
subject_relations = None
object_relations = None
+ # to set in concrete class (optionaly)
+ skip_subject_relations = ()
+ skip_object_relations = ()
def __call__(self):
eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
execute = self._cw.execute
for rel in self.subject_relations:
- if rel in eschema.subjrels:
+ if rel in eschema.subjrels and not rel in self.skip_subject_relations:
execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
'X %s R, NOT R %s P' % (self.rtype, rel, self.rtype),
{'x': self.eidfrom, 'p': self.eidto})
for rel in self.object_relations:
- if rel in eschema.objrels:
+ if rel in eschema.objrels and not rel in self.skip_object_relations:
execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
'R %s X, NOT R %s P' % (self.rtype, rel, self.rtype),
{'x': self.eidfrom, 'p': self.eidto})
-class PropagateSubjectRelationDelHook(Hook):
- """propagate to entities at the end of watched relations when a `main_rtype`
- relation is deleted
+class PropagateRelationDelHook(PropagateRelationAddHook):
+ """Propagate to entities at the end of watched relations when a `main_rtype`
+ relation is deleted.
+
+ This is the opposite of the :class:`PropagateRelationAddHook`, see its
+ documentation for how to use this class.
"""
events = ('after_delete_relation',)
- # to set in concrete class
- subject_relations = None
- object_relations = None
-
def __call__(self):
eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
execute = self._cw.execute
for rel in self.subject_relations:
- if rel in eschema.subjrels:
+ if rel in eschema.subjrels and not rel in self.skip_subject_relations:
execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
'X %s R' % (self.rtype, rel),
{'x': self.eidfrom, 'p': self.eidto})
for rel in self.object_relations:
- if rel in eschema.objrels:
+ if rel in eschema.objrels and not rel in self.skip_object_relations:
execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
'R %s X' % (self.rtype, rel),
{'x': self.eidfrom, 'p': self.eidto})
+PropagateSubjectRelationHook = class_renamed(
+ 'PropagateSubjectRelationHook', PropagateRelationHook,
+ '[3.9] PropagateSubjectRelationHook has been renamed to PropagateRelationHook')
+PropagateSubjectRelationAddHook = class_renamed(
+ 'PropagateSubjectRelationAddHook', PropagateRelationAddHook,
+ '[3.9] PropagateSubjectRelationAddHook has been renamed to PropagateRelationAddHook')
+PropagateSubjectRelationDelHook = class_renamed(
+ 'PropagateSubjectRelationDelHook', PropagateRelationDelHook,
+ '[3.9] PropagateSubjectRelationDelHook has been renamed to PropagateRelationDelHook')
+
+
# abstract classes for operation ###############################################
class Operation(object):
--- a/server/repository.py Tue Sep 14 08:48:44 2010 +0200
+++ b/server/repository.py Thu Sep 16 18:56:35 2010 +0200
@@ -413,6 +413,11 @@
# public (dbapi) interface ################################################
def stats(self): # XXX restrict to managers session?
+ """Return a dictionary containing some statistics about the repository
+ resources usage.
+
+ This is a public method, not requiring a session id.
+ """
results = {}
querier = self.querier
source = self.system_source
@@ -435,8 +440,9 @@
return results
def get_schema(self):
- """return the instance schema. This is a public method, not
- requiring a session id
+ """Return the instance schema.
+
+ This is a public method, not requiring a session id.
"""
try:
# necessary to support pickling used by pyro
@@ -446,8 +452,9 @@
self.schema.__hashmode__ = None
def get_cubes(self):
- """return the list of cubes used by this instance. This is a
- public method, not requiring a session id.
+ """Return the list of cubes used by this instance.
+
+ This is a public method, not requiring a session id.
"""
versions = self.get_versions(not (self.config.creating
or self.config.repairing
@@ -457,11 +464,20 @@
cubes.remove('cubicweb')
return cubes
+ def get_option_value(self, option):
+ """Return the value for `option` in the configuration.
+
+ This is a public method, not requiring a session id.
+ """
+ # XXX we may want to check we don't give sensible information
+ return self.config[option]
+
@cached
def get_versions(self, checkversions=False):
- """return the a dictionary containing cubes used by this instance
- as key with their version as value, including cubicweb version. This is a
- public method, not requiring a session id.
+ """Return the a dictionary containing cubes used by this instance
+ as key with their version as value, including cubicweb version.
+
+ This is a public method, not requiring a session id.
"""
from logilab.common.changelog import Version
vcconf = {}
@@ -491,6 +507,11 @@
@cached
def source_defs(self):
+ """Return the a dictionary containing source uris as value and a
+ dictionary describing each source as value.
+
+ This is a public method, not requiring a session id.
+ """
sources = self.config.sources().copy()
# remove manager information
sources.pop('admin', None)
@@ -502,7 +523,10 @@
return sources
def properties(self):
- """return a result set containing system wide properties"""
+ """Return a result set containing system wide properties.
+
+ This is a public method, not requiring a session id.
+ """
session = self.internal_session()
try:
# don't use session.execute, we don't want rset.req set
--- a/server/sources/native.py Tue Sep 14 08:48:44 2010 +0200
+++ b/server/sources/native.py Thu Sep 16 18:56:35 2010 +0200
@@ -677,6 +677,11 @@
etype = elements[0]
rtypes = elements[1:]
raise UniqueTogetherError(etype, rtypes)
+ mo = re.search('columns (.*) are not unique', arg)
+ if mo is not None: # sqlite in use
+ rtypes = [c.strip().lstrip('cw_') for c in mo.group(1).split(',')]
+ etype = '???'
+ raise UniqueTogetherError(etype, rtypes)
raise
return cursor
--- a/server/test/data/schema.py Tue Sep 14 08:48:44 2010 +0200
+++ b/server/test/data/schema.py Thu Sep 16 18:56:35 2010 +0200
@@ -20,7 +20,8 @@
SubjectRelation, RichString, String, Int, Float,
Boolean, Datetime)
from yams.constraints import SizeConstraint
-from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
+from cubicweb.schema import (WorkflowableEntityType,
+ RQLConstraint, RQLUniqueConstraint,
ERQLExpression, RRQLExpression)
class Affaire(WorkflowableEntityType):
@@ -94,7 +95,10 @@
migrated_from = SubjectRelation('Note')
attachment = SubjectRelation('File')
- inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*')
+ inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*',
+ constraints=[RQLUniqueConstraint('S type T, S inline1 A1, A1 todo_by C, '
+ 'Y type T, Y inline1 A2, A2 todo_by C',
+ 'S,Y')])
todo_by = SubjectRelation('CWUser')
class Personne(EntityType):
--- a/server/test/unittest_repository.py Tue Sep 14 08:48:44 2010 +0200
+++ b/server/test/unittest_repository.py Thu Sep 16 18:56:35 2010 +0200
@@ -627,6 +627,18 @@
self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
('after_add_relation', eidn, 'ecrit_par', eidp)])
+ def test_unique_contraint(self):
+ req = self.request()
+ toto = req.create_entity('Personne', nom=u'toto')
+ a01 = req.create_entity('Affaire', ref=u'A01', todo_by=toto)
+ req.cnx.commit()
+ req = self.request()
+ req.create_entity('Note', type=u'todo', inline1=a01)
+ req.cnx.commit()
+ req = self.request()
+ req.create_entity('Note', type=u'todo', inline1=a01)
+ ex = self.assertRaises(ValidationError, req.cnx.commit)
+ self.assertEquals(ex.errors, {'inline1-subject': u'RQLUniqueConstraint S type T, S inline1 A1, A1 todo_by C, Y type T, Y inline1 A2, A2 todo_by C failed'})
if __name__ == '__main__':
unittest_main()
--- a/web/application.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/application.py Thu Sep 16 18:56:35 2010 +0200
@@ -420,11 +420,12 @@
self.validation_error_handler(req, ex)
except (Unauthorized, BadRQLQuery, RequestError), ex:
self.error_handler(req, ex, tb=False)
- except Exception, ex:
+ except BaseException, ex:
self.error_handler(req, ex, tb=True)
except:
self.critical('Catch all triggered!!!')
self.exception('this is what happened')
+ result = 'oops'
finally:
if req.cnx and not commited:
try:
--- a/web/component.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/component.py Thu Sep 16 18:56:35 2010 +0200
@@ -27,6 +27,7 @@
from cubicweb import Unauthorized, role, tags
from cubicweb.uilib import js, domid
+from cubicweb.utils import json_dumps
from cubicweb.view import ReloadableMixIn, Component
from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset,
non_final_entity, partial_relation_possible,
@@ -106,17 +107,23 @@
if view is not None and hasattr(view, 'page_navigation_url'):
url = view.page_navigation_url(self, path, params)
elif path == 'json':
- rql = params.pop('rql', self.cw_rset.printable_rql())
- # latest 'true' used for 'swap' mode
- url = 'javascript: %s' % (js.replacePageChunk(
- params.get('divid', 'pageContent'), rql,
- params.pop('vid', None), params))
+ url = self.ajax_page_url(**params)
else:
url = self._cw.build_url(path, **params)
return url
+ def ajax_page_url(self, **params):
+ divid = params.setdefault('divid', 'pageContent')
+ params['rql'] = self.cw_rset.printable_rql()
+ return "javascript: $(%s).loadxhtml('json', %s, 'get', 'swap')" % (
+ json_dumps('#'+divid), js.ajaxFuncArgs('view', params))
+
def page_link(self, path, params, start, stop, content):
url = xml_escape(self.page_url(path, params, start, stop))
+ # XXX hack to avoid opening a new page containing the evaluation of the
+ # js expression on ajax call
+ if url.startswith('javascript:'):
+ url += '; noop();'
if start == self.starting_from:
return self.selected_page_link_templ % (url, content, content)
return self.page_link_templ % (url, content, content)
--- a/web/data/cubicweb.css Tue Sep 14 08:48:44 2010 +0200
+++ b/web/data/cubicweb.css Thu Sep 16 18:56:35 2010 +0200
@@ -917,6 +917,14 @@
}
/********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+ float: right;
+}
+
+/********************************/
/* rest related classes */
/********************************/
@@ -928,6 +936,18 @@
margin-right: 1.5em;
}
+/******************************/
+/* reledit */
+/******************************/
+
+.releditField {
+ display: inline;
+}
+
+.releditForm {
+ display:none;
+}
+
/********************************/
/* overwite other css here */
/********************************/
@@ -955,4 +975,3 @@
margin-left: 0.5em;
vertical-align: bottom;
}
-
--- a/web/data/cubicweb.edition.js Tue Sep 14 08:48:44 2010 +0200
+++ b/web/data/cubicweb.edition.js Thu Sep 16 18:56:35 2010 +0200
@@ -317,7 +317,7 @@
form.insertBefore(insertBefore).slideDown('fast');
updateInlinedEntitiesCounters(rtype, role);
reorderTabindex(null, $(insertBefore).closest('form')[0]);
- jQuery(CubicWeb).trigger('inlinedform-added', form);
+ jQuery(cw).trigger('inlinedform-added', form);
// if the inlined form contains a file input, we must force
// the form enctype to multipart/form-data
if (form.find('input:file').length) {
--- a/web/data/cubicweb.old.css Tue Sep 14 08:48:44 2010 +0200
+++ b/web/data/cubicweb.old.css Thu Sep 16 18:56:35 2010 +0200
@@ -901,3 +901,24 @@
border-color:#edecd2 #cfceb7 #cfceb7 #edecd2;
background: #fffff8 url("button.png") bottom left repeat-x;
}
+
+/********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+ float: right;
+}
+
+
+/******************************/
+/* reledit */
+/******************************/
+
+.releditField {
+ display: inline;
+}
+
+.releditForm {
+ display:none;
+}
--- a/web/data/cubicweb.reledit.js Tue Sep 14 08:48:44 2010 +0200
+++ b/web/data/cubicweb.reledit.js Thu Sep 16 18:56:35 2010 +0200
@@ -53,6 +53,7 @@
}
}
jQuery('#'+params.divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, params, 'post');
+ jQuery(cw).trigger('reledit-reloaded', params);
},
/* called by reledit forms to submit changes
--- a/web/facet.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/facet.py Thu Sep 16 18:56:35 2010 +0200
@@ -515,6 +515,7 @@
def title(self):
return display_name(self._cw, self.rtype, form=self.role)
+ @property
def rql_sort(self):
"""return true if we can handle sorting in the rql query. E.g. if
sortfunc is set or if we have not to transform the returned value (eg no
--- a/web/formwidgets.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/formwidgets.py Thu Sep 16 18:56:35 2010 +0200
@@ -653,10 +653,16 @@
timestr = req.form.get(field.input_name(form, 'time')).strip() or None
if datestr is None:
return None
- date = todatetime(req.parse_datetime(datestr, 'Date'))
+ try:
+ date = todatetime(req.parse_datetime(datestr, 'Date'))
+ except ValueError, exc:
+ raise ProcessFormError(unicode(exc))
if timestr is None:
return date
- time = req.parse_datetime(timestr, 'Time')
+ try:
+ time = req.parse_datetime(timestr, 'Time')
+ except ValueError, exc:
+ raise ProcessFormError(unicode(exc))
return date.replace(hour=time.hour, minute=time.minute, second=time.second)
--- a/web/request.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/request.py Thu Sep 16 18:56:35 2010 +0200
@@ -99,9 +99,6 @@
# search state: 'normal' or 'linksearch' (eg searching for an object
# to create a relation with another)
self.search_state = ('normal',)
- # tabindex generator
- self.tabindexgen = count(1)
- self.next_tabindex = self.tabindexgen.next
# page id, set by htmlheader template
self.pageid = None
self._set_pageid()
@@ -131,6 +128,13 @@
"""
return self.set_varmaker()
+ def _get_tabindex_func(self):
+ nextfunc = self.get_page_data('nexttabfunc')
+ if nextfunc is None:
+ nextfunc = count(1).next
+ self.set_page_data('nexttabfunc', nextfunc)
+ return nextfunc
+
def set_varmaker(self):
varmaker = self.get_page_data('rql_varmaker')
if varmaker is None:
@@ -143,6 +147,8 @@
or an anonymous connection is open
"""
super(CubicWebRequestBase, self).set_session(session, user)
+ # tabindex generator
+ self.next_tabindex = self._get_tabindex_func()
# set request language
vreg = self.vreg
if self.user:
--- a/web/test/data/schema.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/test/data/schema.py Thu Sep 16 18:56:35 2010 +0200
@@ -75,3 +75,19 @@
cp = String(maxsize=12)
ville= String(maxsize=32)
+# enough relations to cover most reledit use cases
+class Project(EntityType):
+ title = String(maxsize=32, required=True, fulltextindexed=True)
+ long_desc = SubjectRelation('Blog', composite='subject', cardinality='?*')
+ manager = SubjectRelation('Personne', cardinality='?*')
+
+class composite_card11_2ttypes(RelationDefinition):
+ subject = 'Project'
+ object = ('File', 'Blog')
+ composite = 'subject'
+ cardinality = '?*'
+
+class Ticket(EntityType):
+ title = String(maxsize=32, required=True, fulltextindexed=True)
+ concerns = SubjectRelation('Project', composite='object')
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_reledit.py Thu Sep 16 18:56:35 2010 +0200
@@ -0,0 +1,223 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""
+mainly regression-preventing tests for reledit/doreledit views
+"""
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.web.uicfg import reledit_ctrl
+
+class ReleditMixinTC(object):
+
+ def setup_database(self):
+ self.req = self.request()
+ self.proj = self.req.create_entity('Project', title=u'cubicweb-world-domination')
+ self.tick = self.req.create_entity('Ticket', title=u'write the code')
+ self.toto = self.req.create_entity('Personne', nom=u'Toto')
+
+class ClickAndEditFormTC(ReleditMixinTC, CubicWebTC):
+
+ def test_default_config(self):
+ reledit = {'title': """<div id="title-subject-917-reledit" onmouseout="jQuery('#title-subject-917').addClass('hidden')" onmouseover="jQuery('#title-subject-917').removeClass('hidden')" class="releditField"><div id="title-subject-917-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-917" class="editableField hidden"><div id="title-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'title', 'subject', 'title-subject-917', false, '', '&lt;title not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+ 'long_desc': """<div id="long_desc-subject-917-reledit" onmouseout="jQuery('#long_desc-subject-917').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-917').removeClass('hidden')" class="releditField"><div id="long_desc-subject-917-value" class="editableFieldValue"><long_desc not specified></div><div id="long_desc-subject-917" class="editableField hidden"><div id="long_desc-subject-917-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm('edition', 917, 'long_desc', 'subject', 'long_desc-subject-917', false, 'incontext', '&lt;long_desc not specified&gt;');" title="click to add a value"><img title="click to add a value" src="data/plus.png" alt="click to add a value"/></div></div></div>""",
+ 'manager': """<div id="manager-subject-917-reledit" onmouseout="jQuery('#manager-subject-917').addClass('hidden')" onmouseover="jQuery('#manager-subject-917').removeClass('hidden')" class="releditField"><div id="manager-subject-917-value" class="editableFieldValue"><manager not specified></div><div id="manager-subject-917" class="editableField hidden"><div id="manager-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'manager', 'subject', 'manager-subject-917', false, 'incontext', '&lt;manager not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+ 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""",
+ 'concerns': """<concerns_object not specified>"""}
+
+ for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+ if rschema not in reledit:
+ continue
+ rtype = rschema.type
+ self.assertTextEquals(reledit[rtype], self.proj.view('reledit', rtype=rtype, role=role), rtype)
+
+ def test_default_forms(self):
+ doreledit = {'title': """<div id="title-subject-917-reledit" onmouseout="jQuery('#title-subject-917').addClass('hidden')" onmouseover="jQuery('#title-subject-917').removeClass('hidden')" class="releditField"><div id="title-subject-917-value" class="editableFieldValue">cubicweb-world-domination</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="title-subject-917-form" onsubmit="return freezeFormButtons('title-subject-917-form');" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="base" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&vid=blop#title-subject-917-form" />
+<input name="__domid" type="hidden" value="title-subject-917-form" />
+<input name="__type:917" type="hidden" value="Project" />
+<input name="eid" type="hidden" value="917" />
+<input name="__maineid" type="hidden" value="917" />
+<input name="__reledit|default_value" type="hidden" value="&lt;title not specified&gt;" />
+<input name="__reledit|vid" type="hidden" value="" />
+<input name="__reledit|rtype" type="hidden" value="title" />
+<input name="__reledit|divid" type="hidden" value="title-subject-917" />
+<input name="__reledit|formid" type="hidden" value="base" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="917" />
+<input name="_cw_edited_fields:917" type="hidden" value="title-subject,__type" />
+<fieldset class="default">
+<table class="">
+<tr class="title_subject_row">
+<td
+>
+<input id="title-subject:917" maxlength="32" name="title-subject:917" size="32" tabindex="1" type="text" value="cubicweb-world-domination" />
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="2" type="submit" value="button_ok"><img alt="OK_ICON" src="http://crater:8080/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('title-subject-917')" tabindex="3" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://crater:8080/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="title-subject-917" class="editableField hidden"><div id="title-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'title', 'subject', 'title-subject-917', false, '', '&lt;title not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+
+ 'long_desc': """<div id="long_desc-subject-917-reledit" onmouseout="jQuery('#long_desc-subject-917').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-917').removeClass('hidden')" class="releditField"><div id="long_desc-subject-917-value" class="editableFieldValue"><long_desc not specified></div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="long_desc-subject-917-form" onsubmit="return freezeFormButtons('long_desc-subject-917-form');" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="edition" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&vid=blop#long_desc-subject-917-form" />
+<input name="__domid" type="hidden" value="long_desc-subject-917-form" />
+<input name="__type:A" type="hidden" value="Blog" />
+<input name="eid" type="hidden" value="A" />
+<input name="__maineid" type="hidden" value="A" />
+<input name="__linkto" type="hidden" value="long_desc:917:object" />
+<input name="__message" type="hidden" value="entity linked" />
+<input name="__reledit|default_value" type="hidden" value="&lt;long_desc not specified&gt;" />
+<input name="__reledit|vid" type="hidden" value="incontext" />
+<input name="__reledit|rtype" type="hidden" value="long_desc" />
+<input name="__reledit|divid" type="hidden" value="long_desc-subject-917" />
+<input name="__reledit|formid" type="hidden" value="edition" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="917" />
+<input name="_cw_edited_fields:A" type="hidden" value="title-subject,rss_url-subject,__type,description-subject" />
+<fieldset class="default">
+<table class="attributeForm">
+<tr class="title_subject_row">
+<th class="labelCol"><label class="required" for="title-subject:A">title</label></th>
+<td
+>
+<input id="title-subject:A" maxlength="50" name="title-subject:A" size="45" tabindex="4" type="text" value="" />
+</td></tr>
+<tr class="description_subject_row">
+<th class="labelCol"><label for="description-subject:A">description</label></th>
+<td
+>
+<input name="description_format-subject:A" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:A" name="description-subject:A" onkeyup="autogrow(this)" rows="2" tabindex="5"></textarea>
+</td></tr>
+<tr class="rss_url_subject_row">
+<th class="labelCol"><label for="rss_url-subject:A">rss_url</label></th>
+<td
+>
+<input id="rss_url-subject:A" maxlength="128" name="rss_url-subject:A" size="45" tabindex="6" type="text" value="" />
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="7" type="submit" value="button_ok"><img alt="OK_ICON" src="http://crater:8080/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('long_desc-subject-917')" tabindex="8" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://crater:8080/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="long_desc-subject-917" class="editableField hidden"><div id="long_desc-subject-917-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm('edition', 917, 'long_desc', 'subject', 'long_desc-subject-917', false, 'incontext', '&lt;long_desc not specified&gt;');" title="click to add a value"><img title="click to add a value" src="data/plus.png" alt="click to add a value"/></div></div></div>""",
+
+ 'manager': """<div id="manager-subject-917-reledit" onmouseout="jQuery('#manager-subject-917').addClass('hidden')" onmouseover="jQuery('#manager-subject-917').removeClass('hidden')" class="releditField"><div id="manager-subject-917-value" class="editableFieldValue"><manager not specified></div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="manager-subject-917-form" onsubmit="return freezeFormButtons('manager-subject-917-form');" class="releditForm" cubicweb:target="eformframe">
+<fieldset>
+<input name="__form_id" type="hidden" value="base" />
+<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&vid=blop#manager-subject-917-form" />
+<input name="__domid" type="hidden" value="manager-subject-917-form" />
+<input name="__type:917" type="hidden" value="Project" />
+<input name="eid" type="hidden" value="917" />
+<input name="__maineid" type="hidden" value="917" />
+<input name="__linkto" type="hidden" value="long_desc:917:object" />
+<input name="__message" type="hidden" value="entity linked" />
+<input name="__reledit|default_value" type="hidden" value="&lt;manager not specified&gt;" />
+<input name="__reledit|vid" type="hidden" value="incontext" />
+<input name="__reledit|rtype" type="hidden" value="manager" />
+<input name="__reledit|divid" type="hidden" value="manager-subject-917" />
+<input name="__reledit|formid" type="hidden" value="base" />
+<input name="__reledit|reload" type="hidden" value="false" />
+<input name="__reledit|role" type="hidden" value="subject" />
+<input name="__reledit|eid" type="hidden" value="917" />
+<input name="_cw_edited_fields:917" type="hidden" value="manager-subject,__type" />
+<fieldset class="default">
+<table class="">
+<tr class="manager_subject_row">
+<td
+>
+<select id="manager-subject:917" name="manager-subject:917" size="1" tabindex="9">
+<option value="__cubicweb_internal_field__"></option>
+<option value="919">Toto</option>
+</select>
+</td></tr>
+</table></fieldset>
+<table class="buttonbar">
+<tr>
+
+<td><button class="validateButton" tabindex="10" type="submit" value="button_ok"><img alt="OK_ICON" src="http://crater:8080/data/ok.png" />button_ok</button></td>
+
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('manager-subject-917')" tabindex="11" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://crater:8080/data/cancel.png" />button_cancel</button></td>
+
+</tr></table>
+</fieldset>
+</form><div id="manager-subject-917" class="editableField hidden"><div id="manager-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'manager', 'subject', 'manager-subject-917', false, 'incontext', '&lt;manager not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+ 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""",
+ 'concerns': """<concerns_object not specified>"""
+ }
+ for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+ if rschema not in doreledit:
+ continue
+ rtype = rschema.type
+ self.assertTextEquals(doreledit[rtype],
+ self.proj.view('doreledit', rtype=rtype, role=role,
+ formid='edition' if rtype == 'long_desc' else 'base'),
+ rtype)
+
+class ClickAndEditFormUICFGTC(ReleditMixinTC, CubicWebTC):
+
+ def setup_database(self):
+ super(ClickAndEditFormUICFGTC, self).setup_database()
+ self.tick.set_relations(concerns=self.proj)
+ self.proj.set_relations(manager=self.toto)
+
+ def test_with_uicfg(self):
+ old_rctl = reledit_ctrl._tagdefs.copy()
+ reledit_ctrl.tag_attribute(('Project', 'title'),
+ {'default_value': '<title is required>', 'reload': True})
+ reledit_ctrl.tag_subject_of(('Project', 'long_desc', '*'),
+ {'reload': True, 'edit_target': 'rtype',
+ 'default_value': u'<long_desc is required>'})
+ reledit_ctrl.tag_subject_of(('Project', 'manager', '*'),
+ {'edit_target': 'related'})
+ reledit_ctrl.tag_subject_of(('Project', 'composite_card11_2ttypes', '*'),
+ {'edit_target': 'related'})
+ reledit_ctrl.tag_object_of(('Ticket', 'concerns', 'Project'),
+ {'edit_target': 'rtype'})
+ reledit = {
+ 'title': """<div id="title-subject-917-reledit" onmouseout="jQuery('#title-subject-917').addClass('hidden')" onmouseover="jQuery('#title-subject-917').removeClass('hidden')" class="releditField"><div id="title-subject-917-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-917" class="editableField hidden"><div id="title-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'title', 'subject', 'title-subject-917', true, '', '<title is required>');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+ 'long_desc': """<div id="long_desc-subject-917-reledit" onmouseout="jQuery('#long_desc-subject-917').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-917').removeClass('hidden')" class="releditField"><div id="long_desc-subject-917-value" class="editableFieldValue"><long_desc is required></div><div id="long_desc-subject-917" class="editableField hidden"><div id="long_desc-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'long_desc', 'subject', 'long_desc-subject-917', true, 'incontext', '<long_desc is required>');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
+ 'manager': """<div id="manager-subject-917-reledit" onmouseout="jQuery('#manager-subject-917').addClass('hidden')" onmouseover="jQuery('#manager-subject-917').removeClass('hidden')" class="releditField"><div id="manager-subject-917-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/personne/919" title="">Toto</a></div><div id="manager-subject-917" class="editableField hidden"><div id="manager-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('edition', 917, 'manager', 'subject', 'manager-subject-917', false, 'incontext', '&lt;manager not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div><div id="manager-subject-917-delete" class="editableField" onclick="cw.reledit.loadInlineEditionForm('deleteconf', 917, 'manager', 'subject', 'manager-subject-917', false, 'incontext', '&lt;manager not specified&gt;');" title="click to delete this value"><img title="click to delete this value" src="data/cancel.png" alt="click to delete this value"/></div></div></div>""",
+ 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""",
+ 'concerns': """<div id="concerns-object-917-reledit" onmouseout="jQuery('#concerns-object-917').addClass('hidden')" onmouseover="jQuery('#concerns-object-917').removeClass('hidden')" class="releditField"><div id="concerns-object-917-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/ticket/918" title="">write the code</a></div><div id="concerns-object-917" class="editableField hidden"><div id="concerns-object-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'concerns', 'object', 'concerns-object-917', false, 'csv', '&lt;concerns_object not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>"""
+ }
+ for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
+ if rschema not in reledit:
+ continue
+ rtype = rschema.type
+ self.assertTextEquals(reledit[rtype],
+ self.proj.view('reledit', rtype=rtype, role=role),
+ rtype)
+ reledit_ctrl.clear()
+ reledit_ctrl._tagdefs.update(old_rctl)
--- a/web/uicfg.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/uicfg.py Thu Sep 16 18:56:35 2010 +0200
@@ -53,7 +53,8 @@
from cubicweb import neg_role
from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet,
- RelationTagsDict, register_rtag, _ensure_str_key)
+ RelationTagsDict, NoTargetRelationTagsDict,
+ register_rtag, _ensure_str_key)
from cubicweb.schema import META_RTYPES
@@ -83,27 +84,11 @@
'sideboxes', 'hidden')))
-class DisplayCtrlRelationTags(RelationTagsDict):
+class DisplayCtrlRelationTags(NoTargetRelationTagsDict):
def __init__(self, *args, **kwargs):
super(DisplayCtrlRelationTags, self).__init__(*args, **kwargs)
self.counter = 0
- def tag_subject_of(self, key, tag):
- subj, rtype, obj = key
- if obj != '*':
- self.warning('using explict target type in display_ctrl.tag_subject_of() '
- 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
- subj, rtype, subj, rtype, obj)
- super(DisplayCtrlRelationTags, self).tag_subject_of((subj, rtype, '*'), tag)
-
- def tag_object_of(self, key, tag):
- subj, rtype, obj = key
- if subj != '*':
- self.warning('using explict subject type in display_ctrl.tag_object_of() '
- 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
- rtype, obj, subj, rtype, obj)
- super(DisplayCtrlRelationTags, self).tag_object_of(('*', rtype, obj), tag)
-
def init_primaryview_display_ctrl(rtag, sschema, rschema, oschema, role):
if role == 'subject':
oschema = '*'
@@ -378,7 +363,7 @@
autoform_field = RelationTags('autoform_field')
# relations'field explicit kwargs (given to field's __init__)
-autoform_field_kwargs = RelationTagsDict()
+autoform_field_kwargs = RelationTagsDict('autoform_field_kwargs')
# set of tags of the form <action>_on_new on relations. <action> is a
@@ -386,31 +371,49 @@
# permissions checking is by-passed and supposed to be ok
autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides')
-class _ReleditTags(RelationTagsDict):
- _keys = frozenset('reload default_value noedit'.split())
-
- def tag_subject_of(self, key, *args, **kwargs):
- subj, rtype, obj = key
- if obj != '*':
- self.warning('using explict target type in display_ctrl.tag_subject_of() '
- 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
- subj, rtype, subj, rtype, obj)
- super(_ReleditTags, self).tag_subject_of(key, *args, **kwargs)
-
- def tag_object_of(self, key, *args, **kwargs):
- subj, rtype, obj = key
- if subj != '*':
- self.warning('using explict subject type in display_ctrl.tag_object_of() '
- 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
- rtype, obj, subj, rtype, obj)
- super(_ReleditTags, self).tag_object_of(key, *args, **kwargs)
+class ReleditTags(NoTargetRelationTagsDict):
+ """
+ default_value: alternative default value
+ The default value is what is shown when there is no value.
+ reload: boolean, eid (to reload to) or function taking subject and returning bool/eid
+ This is useful when editing a relation (or attribute) that impacts the url
+ or another parts of the current displayed page. Defaults to False.
+ rvid: alternative view id (as str) for relation or composite edition
+ Default is 'incontext' or 'csv' depending on the cardinality. They can also be
+ statically changed by subclassing ClickAndEditFormView and redefining _one_rvid
+ (resp. _many_rvid).
+ edit_target: 'rtype' (to edit the relation) or 'related' (to edit the related entity)
+ This controls whether to edit the relation or the target entity of the relation.
+ Currently only one-to-one relations support target entity edition. By default,
+ the 'related' option is taken whenever the relation is composite and one-to-one.
+ """
+ _keys = frozenset('default_value reload rvid edit_target'.split())
def tag_relation(self, key, tag):
for tagkey in tag.iterkeys():
assert tagkey in self._keys, 'tag %r not in accepted tags: %r' % (tag, self._keys)
- return super(_ReleditTags, self).tag_relation(key, tag)
+ return super(ReleditTags, self).tag_relation(key, tag)
-reledit_ctrl = _ReleditTags('reledit')
+def init_reledit_ctrl(rtag, sschema, rschema, oschema, role):
+ if rschema.final:
+ return
+ composite = rschema.rdef(sschema, oschema).composite == role
+ if role == 'subject':
+ oschema = '*'
+ else:
+ sschema = '*'
+ values = rtag.get(sschema, rschema, oschema, role)
+ edittarget = values.get('edit_target')
+ if edittarget not in (None, 'rtype', 'related'):
+ rtag.warning('reledit: wrong value for edit_target on relation %s: %s',
+ rschema, edittarget)
+ edittarget = None
+ if not edittarget:
+ edittarget = 'related' if composite else 'rtype'
+ rtag.tag_relation((sschema, rschema, oschema, role),
+ {'edit_target': edittarget})
+
+reledit_ctrl = ReleditTags('reledit', init_reledit_ctrl)
# boxes.EditBox configuration #################################################
--- a/web/views/__init__.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/__init__.py Thu Sep 16 18:56:35 2010 +0200
@@ -84,7 +84,7 @@
return VID_BY_MIMETYPE[mimetype]
nb_rows = len(rset)
# empty resultset
- if nb_rows == 0 :
+ if nb_rows == 0:
return 'noresult'
# entity result set
if not schema.eschema(rset.description[0][0]).final:
--- a/web/views/basecontrollers.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/basecontrollers.py Thu Sep 16 18:56:35 2010 +0200
@@ -260,9 +260,8 @@
def optional_kwargs(extraargs):
if extraargs is None:
return {}
- else: # we receive unicode keys which is not supported by the **syntax
- return dict((str(key), value)
- for key, value in extraargs.items())
+ # we receive unicode keys which is not supported by the **syntax
+ return dict((str(key), value) for key, value in extraargs.iteritems())
class JSonController(Controller):
__regid__ = 'json'
@@ -334,6 +333,9 @@
def _exec(self, rql, args=None, rocheck=True):
"""json mode: execute RQL and return resultset as json"""
+ rql = rql.strip()
+ if rql.startswith('rql:'):
+ rql = rql[4:]
if rocheck:
self._cw.ensure_ro_rql(rql)
try:
@@ -344,7 +346,7 @@
return None
def _call_view(self, view, paginate=False, **kwargs):
- divid = self._cw.form.get('divid', 'pageContent')
+ divid = self._cw.form.get('divid')
# we need to call pagination before with the stream set
try:
stream = view.set_stream()
@@ -352,23 +354,26 @@
stream = UStringIO()
kwargs['w'] = stream.write
assert not paginate
+ if divid == 'pageContent':
+ # ensure divid isn't reused by the view (e.g. table view)
+ del self._cw.form['divid']
+ # mimick main template behaviour
+ stream.write(u'<div id="pageContent">')
+ vtitle = self._cw.form.get('vtitle')
+ if vtitle:
+ stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+ paginate = True
if paginate:
- if divid == 'pageContent':
- # mimick main template behaviour
- stream.write(u'<div id="pageContent">')
- vtitle = self._cw.form.get('vtitle')
- if vtitle:
- stream.write(u'<div class="vtitle">%s</div>\n' % vtitle)
view.paginate()
- if divid == 'pageContent':
- stream.write(u'<div id="contentmain">')
+ if divid == 'pageContent':
+ stream.write(u'<div id="contentmain">')
view.render(**kwargs)
extresources = self._cw.html_headers.getvalue(skiphead=True)
if extresources:
stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
stream.write(extresources)
stream.write(u'</div>\n')
- if paginate and divid == 'pageContent':
+ if divid == 'pageContent':
stream.write(u'</div></div>')
return stream.getvalue()
@@ -390,7 +395,7 @@
vid = req.form.get('fallbackvid', 'noresult')
view = self._cw.vreg['views'].select(vid, req, rset=rset)
self.validate_cache(view)
- return self._call_view(view, paginate=req.form.get('paginate'))
+ return self._call_view(view, paginate=req.form.pop('paginate', False))
@xhtmlize
def js_prop_widget(self, propkey, varname, tabindex=None):
@@ -411,11 +416,6 @@
rset = self._exec(rql)
else:
rset = None
- if extraargs is None:
- extraargs = {}
- else: # we receive unicode keys which is not supported by the **syntax
- extraargs = dict((str(key), value)
- for key, value in extraargs.items())
# XXX while it sounds good, addition of the try/except below cause pb:
# when filtering using facets return an empty rset, the edition box
# isn't anymore selectable, as expected. The pb is that with the
@@ -425,21 +425,22 @@
# error is expected and should'nt be reported.
#try:
comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
- **extraargs)
+ **optional_kwargs(extraargs))
#except NoSelectableObject:
# raise RemoteCallFailed('unselectable')
return self._call_view(comp, **extraargs)
@xhtmlize
- def js_render(self, registry, oid, eid=None, selectargs=None, renderargs=None):
+ def js_render(self, registry, oid, eid=None,
+ selectargs=None, renderargs=None):
if eid is not None:
rset = self._cw.eid_rset(eid)
elif self._cw.form.get('rql'):
rset = self._cw.execute(self._cw.form['rql'])
else:
rset = None
- selectargs = optional_kwargs(selectargs)
- view = self._cw.vreg[registry].select(oid, self._cw, rset=rset, **selectargs)
+ view = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
+ **optional_kwargs(selectargs))
return self._call_view(view, **optional_kwargs(renderargs))
@check_pageid
--- a/web/views/forms.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/forms.py Thu Sep 16 18:56:35 2010 +0200
@@ -188,7 +188,7 @@
if self.formvalues is not None:
return # already built
self.formvalues = formvalues or {}
- # use a copy in case fields are modified while context is build (eg
+ # use a copy in case fields are modified while context is built (eg
# __linkto handling for instance)
for field in self.fields[:]:
for field in field.actual_fields(self):
--- a/web/views/primary.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/primary.py Thu Sep 16 18:56:35 2010 +0200
@@ -24,7 +24,7 @@
from logilab.mtconverter import xml_escape
-from cubicweb import Unauthorized
+from cubicweb import Unauthorized, NoSelectableObject
from cubicweb.utils import support_args
from cubicweb.selectors import match_kwargs
from cubicweb.view import EntityView
@@ -161,13 +161,19 @@
def render_entity_relations(self, entity):
for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'):
if rschema.final or dispctrl.get('rtypevid'):
+ vid = dispctrl.get('vid', 'reledit')
+ try:
+ rview = self._cw.vreg['views'].select(
+ vid, self._cw, rset=entity.cw_rset, row=entity.cw_row,
+ col=entity.cw_col, dispctrl=dispctrl, rtype=rschema, role=role)
+ except NoSelectableObject:
+ continue
self.w(u'<div class="section">')
label = self._rel_label(entity, rschema, role, dispctrl)
if label:
self.w(u'<h4>%s</h4>' % label)
- vid = dispctrl.get('vid', 'reledit')
- entity.view(vid, rtype=rschema.type, role=role, w=self.w,
- initargs={'dispctrl': dispctrl})
+ rview.render(row=entity.cw_row, col=entity.cw_col, w=self.w,
+ rtype=rschema.type, role=role)
self.w(u'</div>')
continue
rset = self._relation_rset(entity, rschema, role, dispctrl)
@@ -248,10 +254,7 @@
if section == where:
matchtschemas.append(tschema)
if matchtschemas:
- # XXX pick the latest dispctrl
- dispctrl = self.display_ctrl.etype_get(eschema, rschema, role,
- matchtschemas[-1])
-
+ dispctrl = self.display_ctrl.etype_get(eschema, rschema, role, '*')
rdefs.append( (rschema, matchtschemas, role, dispctrl) )
return sorted(rdefs, key=lambda x: x[-1]['order'])
@@ -338,6 +341,28 @@
if url:
self.w(u'<a href="%s">%s</a>' % (url, url))
+class AttributeView(EntityView):
+ """use this view on an entity as an alternative to more sophisticated
+ views such as reledit.
+
+ Ex. usage:
+
+ uicfg.primaryview_display_ctrl.tag_attribute(('Foo', 'bar'), {'vid': 'attribute'})
+ """
+ __regid__ = 'attribute'
+ __select__ = EntityView.__select__ & match_kwargs('rtype')
+
+ def cell_call(self, row, col, rtype, **kwargs):
+ entity = self.cw_rset.get_entity(row, col)
+ if self._cw.vreg.schema.rschema(rtype).final:
+ self.w(entity.printable_value(rtype))
+ else:
+ dispctrl = uicfg.primaryview_display_ctrl.etype_get(
+ entity.e_schema, rtype, kwargs['role'], '*')
+ rset = entity.related(rtype, role)
+ if rset:
+ self.wview('autolimited', rset, initargs={'dispctrl': dispctrl})
+
## default primary ui configuration ###########################################
--- a/web/views/reledit.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/reledit.py Thu Sep 16 18:56:35 2010 +0200
@@ -21,6 +21,7 @@
import copy
from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import deprecated
from cubicweb import neg_role
from cubicweb.schema import display_name
@@ -39,8 +40,8 @@
return u''
def append_field(self, *args):
pass
- def field_by_name(self, rtype, role, eschema=None):
- return None
+
+rctrl = uicfg.reledit_ctrl
class ClickAndEditFormView(EntityView):
__regid__ = 'doreledit'
@@ -60,8 +61,11 @@
_editzonemsg = _('click to edit this field')
# default relation vids according to cardinality
+ # can be changed per rtype using reledit_ctrl rtag
_one_rvid = 'incontext'
_many_rvid = 'csv'
+ # renderer
+ _form_renderer_id = 'base'
def cell_call(self, row, col, rtype=None, role='subject',
reload=False, # controls reloading the whole page after change
@@ -69,89 +73,98 @@
# function taking the subject entity & returning a boolean or an eid
rvid=None, # vid to be applied to other side of rtype (non final relations only)
default_value=None,
- formid=None
+ formid='base'
):
"""display field to edit entity's `rtype` relation on click"""
assert rtype
assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
- if self.__regid__ == 'doreledit':
- assert formid
- self._cw.add_js('cubicweb.reledit.js')
- if formid:
- self._cw.add_js('cubicweb.edition.js')
self._cw.add_css('cubicweb.form.css')
+ self._cw.add_js('cubicweb.reledit.js', 'cubicweb.edition.js')
entity = self.cw_rset.get_entity(row, col)
rschema = self._cw.vreg.schema[rtype]
+ self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
reload = self._compute_reload(entity, rschema, role, reload)
default_value = self._compute_default_value(entity, rschema, role, default_value)
divid = self._build_divid(rtype, role, entity.eid)
if rschema.final:
- self._handle_attributes(entity, rschema, role, divid, reload, default_value)
+ self._handle_attribute(entity, rschema, role, divid, reload, default_value)
else:
- self._handle_relations(entity, rschema, role, divid, reload, default_value, formid)
+ if self._is_composite():
+ self._handle_composite(entity, rschema, role, divid, reload, default_value, formid)
+ else:
+ self._handle_relation(entity, rschema, role, divid, reload, default_value, formid)
- def _handle_attributes(self, entity, rschema, role, divid, reload, default_value):
+ def _handle_attribute(self, entity, rschema, role, divid, reload, default_value):
rtype = rschema.type
value = entity.printable_value(rtype)
- form, renderer = self._build_form(entity, rtype, role, divid, 'base',
- default_value, reload)
- if not self._should_edit_attribute(entity, rschema, form):
+ if not self._should_edit_attribute(entity, rschema):
self.w(value)
return
+
+ display_label, related_entity = self._prepare_form(entity, rtype, role)
+ form, renderer = self._build_form(entity, rtype, role, divid, 'base', default_value,
+ reload, display_label, related_entity)
value = value or default_value
- field = form.field_by_name(rtype, role, entity.e_schema)
- form.append_field(field)
self.view_form(divid, value, form, renderer)
- def _handle_relations(self, entity, rschema, role, divid, reload, default_value, formid):
- rtype = rschema.type
- rvid = self._compute_best_vid(entity.e_schema, rschema, role)
- related_rset = entity.related(rtype, role)
+ def _compute_formid_value(self, entity, rschema, role, default_value, rvid, formid):
+ related_rset = entity.related(rschema.type, role)
if related_rset:
value = self._cw.view(rvid, related_rset)
else:
value = default_value
- ttypes = self._compute_ttypes(rschema, role)
+ if not self._should_edit_relation(entity, rschema, role):
+ return None, value
+ return formid, value
+
+ def _handle_relation(self, entity, rschema, role, divid, reload, default_value, formid):
+ rvid = self._compute_best_vid(entity.e_schema, rschema, role)
+ formid, value = self._compute_formid_value(entity, rschema, role, default_value, rvid, formid)
+ if formid is None:
+ return self.w(value)
- if not self._should_edit_relation(entity, rschema, role):
- self.w(value)
- return
- # this is for attribute-like composites (1 target type, 1 related entity at most)
+ rtype = rschema.type
+ display_label, related_entity = self._prepare_form(entity, rtype, role)
+ form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, reload,
+ display_label, related_entity, dict(vid=rvid))
+ self.view_form(divid, value, form, renderer)
+
+ def _handle_composite(self, entity, rschema, role, divid, reload, default_value, formid):
+ # this is for attribute-like composites (1 target type, 1 related entity at most, for now)
+ ttypes = self._compute_ttypes(rschema, role)
+ related_rset = entity.related(rschema.type, role)
add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes)
edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes)
delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role)
- # compute formid
- if len(ttypes) > 1: # redundant safety belt
- formid = 'base'
- else:
- afs = uicfg.autoform_section.etype_get(entity.e_schema, rschema, role, ttypes[0])
- # is there an afs spec that says we should edit
- # the rschema as an attribute ?
- if afs and 'main_attributes' in afs:
- formid = 'base'
+
+ rvid = self._compute_best_vid(entity.e_schema, rschema, role)
+ formid, value = self._compute_formid_value(entity, rschema, role, default_value, rvid, formid)
+ if formid is None or not (edit_related or add_related):
+ # till we learn to handle cases where not (edit_related or add_related)
+ self.w(value)
+ return
- form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value,
- reload, dict(vid=rvid),
- edit_related, add_related and ttypes[0])
- if formid == 'base':
- field = form.field_by_name(rtype, role, entity.e_schema)
- form.append_field(field)
- self.view_form(divid, value, form, renderer, edit_related,
- delete_related, add_related)
-
+ rtype = rschema.type
+ ttype = ttypes[0]
+ _fdata = self._prepare_composite_form(entity, rtype, role, edit_related, add_related and ttype)
+ display_label, related_entity = _fdata
+ form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, reload,
+ display_label, related_entity, dict(vid=rvid))
+ self.view_form(divid, value, form, renderer,
+ edit_related, add_related, delete_related)
def _compute_best_vid(self, eschema, rschema, role):
+ rvid = self._one_rvid
if eschema.rdef(rschema, role).role_cardinality(role) in '+*':
- return self._many_rvid
- return self._one_rvid
+ rvid = self._many_rvid
+ return self._rules.get('rvid', rvid)
def _compute_ttypes(self, rschema, role):
dual_role = neg_role(role)
return getattr(rschema, '%ss' % dual_role)()
def _compute_reload(self, entity, rschema, role, reload):
- rule = uicfg.reledit_ctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
- ctrl_reload = rule.get('reload', reload)
+ ctrl_reload = self._rules.get('reload', reload)
if callable(ctrl_reload):
ctrl_reload = ctrl_reload(entity)
if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False
@@ -160,8 +173,7 @@
def _compute_default_value(self, entity, rschema, role, default_value):
etype = entity.e_schema.type
- rule = uicfg.reledit_ctrl.etype_get(etype, rschema.type, role, '*')
- ctrl_default = rule.get('default_value', default_value)
+ ctrl_default = self._rules.get('default_value', default_value)
if ctrl_default:
return ctrl_default
if default_value is None:
@@ -169,47 +181,42 @@
display_name(self._cw, rschema.type, role))
return default_value
- def _is_composite(self, eschema, rschema, role):
- return eschema.rdef(rschema, role).composite == role
+ def _is_composite(self):
+ return self._rules.get('edit_target') == 'related'
def _may_add_related(self, related_rset, entity, rschema, role, ttypes):
""" ok for attribute-like composite entities """
- if self._is_composite(entity.e_schema, rschema, role):
- if len(ttypes) > 1: # wrong cardinality: do not handle
- return False
- rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role)
- card = rdef.role_cardinality(role)
- if related_rset and card in '?1':
- return False
- if role == 'subject':
- kwargs = {'fromeid': entity.eid}
- else:
- kwargs = {'toeid': entity.eid}
- if rdef.has_perm(self._cw, 'add', **kwargs):
- return True
- return False
+ if len(ttypes) > 1: # many etypes: learn how to do it
+ return False
+ rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role)
+ card = rdef.role_cardinality(role)
+ if related_rset or card not in '?1':
+ return False
+ if role == 'subject':
+ kwargs = {'fromeid': entity.eid}
+ else:
+ kwargs = {'toeid': entity.eid}
+ return rdef.has_perm(self._cw, 'add', **kwargs)
def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes):
""" controls the edition of the related entity """
- if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1':
- return False
- if len(related_rset.rows) != 1:
+ if len(ttypes) > 1 or len(related_rset.rows) != 1:
return False
- if len(ttypes) > 1:
- return False
- if not self._is_composite(entity.e_schema, rschema, role):
+ if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1':
return False
return related_rset.get_entity(0, 0).cw_has_perm('update')
def _may_delete_related(self, related_rset, entity, rschema, role):
- # we assume may_edit_related
- kwargs = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
- if not rschema.has_perm(self._cw, 'delete', **kwargs):
+ # we assume may_edit_related, only 1 related entity
+ if not related_rset:
return False
- for related_entity in related_rset.entities():
- if not related_entity.cw_has_perm('delete'):
- return False
- return True
+ rentity = related_rset.get_entity(0, 0)
+ if role == 'subject':
+ kwargs = {'fromeid': entity.eid, 'toeid': rentity.eid}
+ else:
+ kwargs = {'fromeid': rentity.eid, 'toeid': entity.eid}
+ # NOTE: should be sufficient given a well built schema/security
+ return rschema.has_perm(self._cw, 'delete', **kwargs)
def _build_edit_zone(self):
return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))}
@@ -234,32 +241,43 @@
event_args.update(extradata)
return event_args
- def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
- extradata=None, edit_related=False, add_related=False, **formargs):
- event_args = self._build_args(entity, rtype, role, formid, default_value,
- reload, extradata)
- cancelclick = self._cancelclick % divid
+ def _prepare_form(self, entity, _rtype, role):
+ display_label = False
+ related_entity = entity
+ return display_label, related_entity
+
+ def _prepare_composite_form(self, entity, rtype, role, edit_related, add_related):
if edit_related and not add_related:
- display_fields = None
display_label = True
related_entity = entity.related(rtype, role).get_entity(0, 0)
- self._cw.form['eid'] = related_entity.eid
elif add_related:
- display_fields = None
display_label = True
_new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw)
_new_entity.eid = self._cw.varmaker.next()
related_entity = _new_entity
+ # XXX see forms.py ~ 276 and entities.linked_to method
+ # is there another way ?
self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role))
- else: # base case: edition/attribute relation
- display_fields = [(rtype, role)]
- display_label = False
- related_entity = entity
+ return display_label, related_entity
+
+ def _build_renderer(self, related_entity, display_label):
+ return self._cw.vreg['formrenderers'].select(
+ self._form_renderer_id, self._cw, entity=related_entity,
+ display_label=display_label,
+ table_class='attributeForm' if display_label else '',
+ display_help=False, button_bar_class='buttonbar',
+ display_progress_div=False)
+
+ def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
+ display_label, related_entity, extradata=None, **formargs):
+ event_args = self._build_args(entity, rtype, role, formid, default_value,
+ reload, extradata)
+ cancelclick = self._cancelclick % divid
form = self._cw.vreg['forms'].select(
- formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, domid='%s-form' % divid,
- display_fields=display_fields, formtype='inlined',
- action=self._cw.build_url('validateform?__onsuccess=window.parent.cw.reledit.onSuccess'),
- cwtarget='eformframe', cssstyle='display: none',
+ formid, self._cw, rset=related_entity.as_rset(), entity=related_entity,
+ domid='%s-form' % divid, formtype='inlined',
+ action=self._cw.build_url('validateform', __onsuccess='window.parent.cw.reledit.onSuccess'),
+ cwtarget='eformframe', cssclass='releditForm',
**formargs)
# pass reledit arguments
for pname, pvalue in event_args.iteritems():
@@ -279,53 +297,37 @@
form.form_buttons = [SubmitButton(),
Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)]
form.event_args = event_args
- renderer = self._cw.vreg['formrenderers'].select(
- 'base', self._cw, entity=related_entity, display_label=display_label,
- display_help=False, table_class='',
- button_bar_class='buttonbar', display_progress_div=False)
- return form, renderer
+ if formid == 'base':
+ field = form.field_by_name(rtype, role, entity.e_schema)
+ form.append_field(field)
+ return form, self._build_renderer(related_entity, display_label)
- def _should_edit_attribute(self, entity, rschema, form):
- # examine rtags
- noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rschema.type, 'subject').get('noedit', False)
- if noedit:
- return False
+ def _should_edit_attribute(self, entity, rschema):
rdef = entity.e_schema.rdef(rschema)
- afs = uicfg.autoform_section.etype_get(entity.__regid__, rschema, 'subject', rdef.object)
- if 'main_hidden' in afs:
- return False
# check permissions
if not entity.cw_has_perm('update'):
return False
rdef = entity.e_schema.rdef(rschema)
- if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
- return False
- # XXX ?
- try:
- form.field_by_name(str(rschema), 'subject', entity.e_schema)
- except FieldNotFound:
- return False
- return True
+ return rdef.has_perm(self._cw, 'update', eid=entity.eid)
+
+ should_edit_attributes = deprecated('[3.9] should_edit_attributes is deprecated,'
+ ' use _should_edit_attribute instead',
+ _should_edit_attribute)
def _should_edit_relation(self, entity, rschema, role):
- # examine rtags
- rtype = rschema.type
- noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rtype, role).get('noedit', False)
- if noedit:
- return False
- rdef = entity.e_schema.rdef(rschema, role)
- afs = uicfg.autoform_section.etype_get(
- entity.__regid__, rschema, role, rdef.object)
- if 'main_hidden' in afs:
- return False
- perm_args = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
+ eeid = entity.eid
+ perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid}
return rschema.has_perm(self._cw, 'add', **perm_args)
- def view_form(self, divid, value, form=None, renderer=None,
- edit_related=False, delete_related=False, add_related=False):
+ should_edit_relations = deprecated('[3.9] should_edit_relations is deprecated,'
+ ' use _should_edit_relation instead',
+ _should_edit_relation)
+
+ def _open_form_wrapper(self, divid, value, form, renderer,
+ _edit_related, _add_related, _delete_related):
w = self.w
- w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s">' %
- {'id': divid,
+ w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s" class="%(css)s">' %
+ {'id': divid, 'css': 'releditField',
'out': "jQuery('#%s').addClass('hidden')" % divid,
'over': "jQuery('#%s').removeClass('hidden')" % divid})
w(u'<div id="%s-value" class="editableFieldValue">' % divid)
@@ -333,33 +335,54 @@
w(u'</div>')
w(form.render(renderer=renderer))
w(u'<div id="%s" class="editableField hidden">' % divid)
- args = form.event_args.copy()
- if not add_related: # excludes edition
- args['formid'] = 'edition'
+
+ def _edit_action(self, divid, args, edit_related, add_related, _delete_related):
+ if not add_related: # currently, excludes edition
+ w = self.w
+ args['formid'] = 'edition' if edit_related else 'base'
w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' %
(divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg)))
w(self._build_edit_zone())
w(u'</div>')
- else:
- args['formid'] = 'edition'
+
+ def _add_action(self, divid, args, _edit_related, add_related, _delete_related):
+ if add_related:
+ w = self.w
+ args['formid'] = 'edition' if add_related else 'base'
w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' %
(divid, xml_escape(self._onclick % args), self._cw._(self._addmsg)))
w(self._build_add_zone())
w(u'</div>')
+
+ def _del_action(self, divid, args, _edit_related, _add_related, delete_related):
if delete_related:
+ w = self.w
args['formid'] = 'deleteconf'
w(u'<div id="%s-delete" class="editableField" onclick="%s" title="%s">' %
(divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg)))
w(self._build_delete_zone())
w(u'</div>')
- w(u'</div>')
- w(u'</div>')
+
+ def _close_form_wrapper(self):
+ self.w(u'</div>')
+ self.w(u'</div>')
+
+ def view_form(self, divid, value, form=None, renderer=None,
+ edit_related=False, add_related=False, delete_related=False):
+ self._open_form_wrapper(divid, value, form, renderer,
+ edit_related, add_related, delete_related)
+ args = form.event_args.copy()
+ self._edit_action(divid, args, edit_related, add_related, delete_related)
+ self._add_action(divid, args, edit_related, add_related, delete_related)
+ self._del_action(divid, args, edit_related, add_related, delete_related)
+ self._close_form_wrapper()
+
class AutoClickAndEditFormView(ClickAndEditFormView):
__regid__ = 'reledit'
def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
- extradata=None, edit_related=False, add_related=False, **formargs):
+ display_label, related_entity, extradata=None, **formargs):
event_args = self._build_args(entity, rtype, role, 'base', default_value,
reload, extradata)
form = _DummyForm()
--- a/web/views/tableview.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/tableview.py Thu Sep 16 18:56:35 2010 +0200
@@ -140,11 +140,11 @@
if mainindex is None:
displayfilter, displayactions = False, False
else:
- if displayfilter is None and 'displayfilter' in req.form:
+ if displayfilter is None and req.form.get('displayfilter'):
displayfilter = True
if req.form['displayfilter'] == 'shown':
hidden = False
- if displayactions is None and 'displayactions' in req.form:
+ if displayactions is None and req.form.get('displayactions'):
displayactions = True
displaycols = self.displaycols(displaycols, headers)
fromformfilter = 'fromformfilter' in req.form
@@ -187,14 +187,9 @@
def page_navigation_url(self, navcomp, path, params):
if hasattr(self, 'divid'):
- divid = self.divid
- else:
- divid = params.get('divid', 'pageContent'),
- rql = params.pop('rql', self.cw_rset.printable_rql())
- # latest 'true' used for 'swap' mode
- return 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
- json_dumps(divid), json_dumps(rql), json_dumps(self.__regid__),
- json_dumps(params))
+ params['divid'] = self.divid
+ params['vid'] = self.__regid__
+ return navcomp.ajax_page_url(**params)
def show_hide_actions(self, divid, currentlydisplayed=False):
showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
@@ -324,7 +319,8 @@
title = None
def call(self, title=None, subvid=None, headers=None, divid=None,
- paginate=False, displaycols=None, displayactions=None, mainindex=None):
+ paginate=False, displaycols=None, displayactions=None,
+ mainindex=None):
"""Dumps a table displaying a composite query"""
try:
actrql = self._cw.form['actualrql']
@@ -347,7 +343,8 @@
mainindex = self.main_var_index()
if mainindex is not None:
actions = self.form_filter(divid, displaycols, displayactions,
- displayfilter=True, paginate=paginate, hidden=True)
+ displayfilter=True, paginate=paginate,
+ hidden=True)
else:
actions = ()
if not subvid and 'subvid' in self._cw.form:
--- a/web/views/urlpublishing.py Tue Sep 14 08:48:44 2010 +0200
+++ b/web/views/urlpublishing.py Thu Sep 16 18:56:35 2010 +0200
@@ -176,18 +176,27 @@
else:
attrname = cls._rest_attr_info()[0]
value = req.url_unquote(parts.pop(0))
- rset = self.attr_rset(req, etype, attrname, value)
- else:
- rset = self.cls_rset(req, cls)
+ return self.handle_etype_attr(req, cls, attrname, value)
+ return self.handle_etype(req, cls)
+
+ def set_vid_for_rset(self, req, cls, rset):# cls is there to ease overriding
if rset.rowcount == 0:
raise NotFound()
+ # we've to set a default vid here, since vid_from_rset may try to use a
+ # table view if fetch_rql include some non final relation
+ if rset.rowcount == 1:
+ req.form.setdefault('vid', 'primary')
+ else: # rset.rowcount >= 1
+ req.form.setdefault('vid', 'sameetypelist')
+
+ def handle_etype(self, req, cls):
+ rset = req.execute(cls.fetch_rql(req.user))
+ self.set_vid_for_rset(req, cls, rset)
return None, rset
- def cls_rset(self, req, cls):
- return req.execute(cls.fetch_rql(req.user))
-
- def attr_rset(self, req, etype, attrname, value):
- rql = u'Any X WHERE X is %s, X %s %%(x)s' % (etype, attrname)
+ def handle_etype_attr(self, req, cls, attrname, value):
+ rql = cls.fetch_rql(req.user, ['X %s %%(x)s' % (attrname)],
+ mainvar='X', ordermethod=None)
if attrname == 'eid':
try:
rset = req.execute(rql, {'x': typed_eid(value)})
@@ -196,7 +205,8 @@
raise PathDontMatch()
else:
rset = req.execute(rql, {'x': value})
- return rset
+ self.set_vid_for_rset(req, cls, rset)
+ return None, rset
class URLRewriteEvaluator(URLPathEvaluator):