--- a/.hgtags Fri Aug 03 13:29:37 2012 +0200
+++ b/.hgtags Fri Sep 07 14:01:59 2012 +0200
@@ -256,5 +256,11 @@
20ee573bd2379a00f29ff27bb88a8a3344d4cdfe cubicweb-debian-version-3.14.7-1
15fe07ff687238f8cc09d8e563a72981484085b3 cubicweb-version-3.14.8
81394043ad226942ac0019b8e1d4f7058d67a49f cubicweb-debian-version-3.14.8-1
+9337812cef6b949eee89161190e0c3d68d7f32ea cubicweb-version-3.14.9
+68c762adf2d5a2c338910ef1091df554370586f0 cubicweb-debian-version-3.14.9-1
783a5df54dc742e63c8a720b1582ff08366733bd cubicweb-version-3.15.1
fe5e60862b64f1beed2ccdf3a9c96502dfcd811b cubicweb-debian-version-3.15.1-1
+2afc157ea9b2b92eccb0f2d704094e22ce8b5a05 cubicweb-version-3.15.2
+9aa5553b26520ceb68539e7a32721b5cd5393e16 cubicweb-debian-version-3.15.2-1
+0e012eb80990ca6f91aa9a8ad3324fbcf51435b1 cubicweb-version-3.15.3
+7ad423a5b6a883dbdf00e6c87a5f8ab121041640 cubicweb-debian-version-3.15.3-1
--- a/__init__.py Fri Aug 03 13:29:37 2012 +0200
+++ b/__init__.py Fri Sep 07 14:01:59 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/__pkginfo__.py Fri Aug 03 13:29:37 2012 +0200
+++ b/__pkginfo__.py Fri Sep 07 14:01:59 2012 +0200
@@ -22,7 +22,7 @@
modname = distname = "cubicweb"
-numversion = (3, 15, 1)
+numversion = (3, 15, 4)
version = '.'.join(str(num) for num in numversion)
description = "a repository of entities / relations for knowledge management"
--- a/cwconfig.py Fri Aug 03 13:29:37 2012 +0200
+++ b/cwconfig.py Fri Sep 07 14:01:59 2012 +0200
@@ -171,6 +171,7 @@
import sys
import os
+import stat
import logging
import logging.config
from smtplib import SMTP
@@ -306,7 +307,10 @@
_forced_mode = os.environ.get('CW_MODE')
assert _forced_mode in (None, 'system', 'user')
-CWDEV = exists(join(CW_SOFTWARE_ROOT, '.hg'))
+# CWDEV tells whether directories such as i18n/, web/data/, etc. (ie containing
+# some other resources than python libraries) are located with the python code
+# or as a 'shared' cube
+CWDEV = exists(join(CW_SOFTWARE_ROOT, 'i18n'))
try:
_INSTALL_PREFIX = os.environ['CW_INSTALL_PREFIX']
@@ -1073,7 +1077,12 @@
If not, try to fix this, letting exception propagate when not possible.
"""
if not exists(path):
- os.makedirs(path)
+ self.info('creating %s directory', path)
+ try:
+ os.makedirs(path)
+ except OSError, ex:
+ self.warning('error while creating %s directory: %s', path, ex)
+ return
if self['uid']:
try:
uid = int(self['uid'])
@@ -1087,10 +1096,20 @@
return
fstat = os.stat(path)
if fstat.st_uid != uid:
- os.chown(path, uid, os.getgid())
- import stat
+ self.info('giving ownership of %s directory to %s', path, self['uid'])
+ try:
+ os.chown(path, uid, os.getgid())
+ except OSError, ex:
+ self.warning('error while giving ownership of %s directory to %s: %s',
+ path, self['uid'], ex)
if not (fstat.st_mode & stat.S_IWUSR):
- os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+ self.info('forcing write permission on directory %s', path)
+ try:
+ os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+ except OSError, ex:
+ self.warning('error while forcing write permission on directory %s: %s',
+ path, ex)
+ return
@cached
def instance_md5_version(self):
--- a/debian/changelog Fri Aug 03 13:29:37 2012 +0200
+++ b/debian/changelog Fri Sep 07 14:01:59 2012 +0200
@@ -1,6 +1,24 @@
+cubicweb (3.15.4-1) unstable; urgency=low
+
+ * New upstream release
+
+ -- Julien Cristau <jcristau@debian.org> Fri, 31 Aug 2012 16:43:11 +0200
+
+cubicweb (3.15.3-1) unstable; urgency=low
+
+ * New upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr> Tue, 21 Aug 2012 14:19:31 +0200
+
+cubicweb (3.15.2-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Fri, 20 Jul 2012 15:17:17 +0200
+
cubicweb (3.15.1-1) quantal; urgency=low
- * new upstream release
+ * new upstream release
-- David Douard <david.douard@logilab.fr> Mon, 11 Jun 2012 09:45:24 +0200
@@ -10,6 +28,12 @@
-- Sylvain Thénault <sylvain.thenault@logilab.fr> Thu, 12 Apr 2012 13:52:05 +0200
+cubicweb (3.14.9-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr> Tue, 31 Jul 2012 16:16:28 +0200
+
cubicweb (3.14.8-1) unstable; urgency=low
* new upstream release
--- a/debian/control Fri Aug 03 13:29:37 2012 +0200
+++ b/debian/control Fri Sep 07 14:01:59 2012 +0200
@@ -7,18 +7,25 @@
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>,
Aurélien Campéas <aurelien.campeas@logilab.fr>,
Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 7), python (>= 2.5), python-central (>= 0.5), python-sphinx, python-logilab-common, python-unittest2
-# for the documentation:
-# python-sphinx, python-logilab-common, python-unittest2, logilab-doctools, logilab-xml
+Build-Depends:
+ debhelper (>= 7),
+ python (>= 2.5),
+ python-central (>= 0.5),
+ python-sphinx,
+ python-logilab-common,
+ python-unittest2,
+ python-logilab-mtconverter,
+ python-rql,
+ python-yams,
+ python-lxml,
Standards-Version: 3.9.1
Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.5, << 3.0
+XS-Python-Version: >= 2.5
Package: cubicweb
Architecture: all
XB-Python-Version: ${python:Versions}
Depends: ${misc:Depends}, ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version})
-XB-Recommends: (postgresql, postgresql-plpython) | mysql | sqlite3
Recommends: postgresql | mysql | sqlite3
Description: the complete CubicWeb framework
CubicWeb is a semantic web application framework.
--- a/debian/rules Fri Aug 03 13:29:37 2012 +0200
+++ b/debian/rules Fri Sep 07 14:01:59 2012 +0200
@@ -11,10 +11,14 @@
build-stamp:
dh_testdir
NO_SETUPTOOLS=1 python setup.py build
+ # cubicweb.foo needs to be importable by sphinx, so create a cubicweb symlink to the source dir
+ mkdir -p debian/pythonpath
+ ln -sf $(CURDIR) debian/pythonpath/cubicweb
# documentation build is now made optional since it can break for old
# distributions and we don't want to block a new release of Cubicweb
# because of documentation issues.
- -PYTHONPATH=$(CURDIR)/.. $(MAKE) -C doc/book/en all
+ -PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc/book/en all
+ rm -rf debian/pythonpath
touch build-stamp
clean:
--- a/devtools/testlib.py Fri Aug 03 13:29:37 2012 +0200
+++ b/devtools/testlib.py Fri Sep 07 14:01:59 2012 +0200
@@ -817,9 +817,8 @@
"""
req = req or rset and rset.req or self.request()
req.form['vid'] = vid
- kwargs['rset'] = rset
viewsreg = self.vreg['views']
- view = viewsreg.select(vid, req, **kwargs)
+ view = viewsreg.select(vid, req, rset=rset, **kwargs)
# set explicit test description
if rset is not None:
self.set_description("testing vid=%s defined in %s with (%s)" % (
@@ -831,10 +830,8 @@
viewfunc = view.render
else:
kwargs['view'] = view
- templateview = viewsreg.select(template, req, **kwargs)
viewfunc = lambda **k: viewsreg.main_template(req, template,
- **kwargs)
- kwargs.pop('rset')
+ rset=rset, **kwargs)
return self._test_view(viewfunc, view, template, kwargs)
--- a/doc/book/en/admin/instance-config.rst Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/admin/instance-config.rst Fri Sep 07 14:01:59 2012 +0200
@@ -17,7 +17,7 @@
each option name is prefixed with its own section and followed by its
default value if necessary, e.g. "`<section>.<option>` [value]."
-.. _`Configuring the Web server`:
+.. _`WebServerConfig`:
Configuring the Web server
--------------------------
--- a/doc/book/en/admin/ldap.rst Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/admin/ldap.rst Fri Sep 07 14:01:59 2012 +0200
@@ -29,6 +29,15 @@
The base functionality for this is in
:file:`cubicweb/server/sources/ldapuser.py`.
+External dependencies
+---------------------
+
+You'll need the following packages to make CubicWeb interact with your LDAP /
+Active Directory server:
+
+* python-ldap
+* ldaputils if using `ldapfeed` source
+
Configurations options
----------------------
--- a/doc/book/en/annexes/depends.rst Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/annexes/depends.rst Fri Sep 07 14:01:59 2012 +0200
@@ -45,6 +45,9 @@
* indexer - http://www.logilab.org/project/indexer -
http://pypi.python.org/pypi/indexer - included in the forest
+* passlib - https://code.google.com/p/passlib/ -
+ http://pypi.python.org/pypi/passlib
+
To use network communication between cubicweb instances / clients:
* Pyro - http://www.xs4all.nl/~irmen/pyro3/ - http://pypi.python.org/pypi/Pyro
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devrepo/fti.rst Fri Sep 07 14:01:59 2012 +0200
@@ -0,0 +1,159 @@
+.. _fti:
+
+Full Text Indexing in CubicWeb
+------------------------------
+
+When an attribute is tagged as *fulltext-indexable* in the datamodel,
+CubicWeb will automatically trigger hooks to update the internal
+fulltext index (i.e the ``appears`` SQL table) each time this attribute
+is modified.
+
+CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
+fulltext on demand:
+
+.. sourcecode:: bash
+
+ cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
+
+You can also rebuild the fulltext index for a given set of entity types:
+
+.. sourcecode:: bash
+
+ cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
+
+In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
+will be rebuilt.
+
+
+Standard FTI process
+~~~~~~~~~~~~~~~~~~~~
+
+Considering an entity type ``ET``, the default *fti* process is to :
+
+1. fetch all entities of type ``ET``
+
+2. for each entity, adapt it to ``IFTIndexable`` (see
+ :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
+
+3. call
+ :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
+ the adapter which is supposed to return a dictionary *weight* ->
+ *list of words* as expected by
+ :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
+ tokenization of each attribute value is done by
+ :meth:`~logilab.database.fti.tokenize`.
+
+
+See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
+
+
+Yams and ``fultext_container``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible in the datamodel to indicate that fulltext-indexed
+attributes defined for an entity type will be used to index not the
+entity itself but a related entity. This is especially useful for
+composite entities. Let's take a look at (a simplified version of)
+the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
+
+.. sourcecode:: python
+
+ class CWUser(WorkflowableEntityType):
+ login = String(required=True, unique=True, maxsize=64)
+ upassword = Password(required=True)
+
+ class EmailAddress(EntityType):
+ address = String(required=True, fulltextindexed=True,
+ indexed=True, unique=True, maxsize=128)
+
+
+ class use_email_relation(RelationDefinition):
+ name = 'use_email'
+ subject = 'CWUser'
+ object = 'EmailAddress'
+ cardinality = '*?'
+ composite = 'subject'
+
+
+The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
+and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
+in your application, if you use fulltext search to look for an email address, CubicWeb
+will return the ``EmailAddress`` itself. But the objects we'd like to index
+are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
+
+The simplest way to achieve that is to tag the ``use_email`` relation in
+the datamodel:
+
+.. sourcecode:: python
+
+ class use_email(RelationType):
+ fulltext_container = 'subject'
+
+
+Customizing how entities are fetched during ``db-rebuild-fti``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``db-rebuild-fti`` will call the
+:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
+method on your entity type.
+
+.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
+
+Now, suppose you've got a _huge_ table to index, you probably don't want to
+get all entities at once. So here's a simple customized example that will
+process block of 10000 entities:
+
+.. sourcecode:: python
+
+
+ class MyEntityClass(AnyEntity):
+ __regid__ = 'MyEntityClass'
+
+ @classmethod
+ def cw_fti_index_rql_queries(cls, req):
+ # get the default RQL method and insert LIMIT / OFFSET instructions
+ base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
+ selected, restrictions = base_rql.split(' WHERE ')
+ rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
+ selected, restrictions)
+ # count how many entities you'll have to index
+ count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
+ # iterate by blocks of 10000 entities
+ chunksize = 10000
+ for offset in xrange(0, count, chunksize):
+ print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
+ yield rql_template % {'limit': chunksize, 'offset': offset}
+
+Since you have access to ``req``, you can more or less fetch whatever you want.
+
+
+Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also customize the FTI process by providing your own ``get_words()``
+implementation:
+
+.. sourcecode:: python
+
+ from cubicweb.entities.adapters import IFTIndexableAdapter
+
+ class SearchIndexAdapter(IFTIndexableAdapter):
+ __regid__ = 'IFTIndexable'
+ __select__ = is_instance('MyEntityClass')
+
+ def fti_containers(self, _done=None):
+ """this should yield any entity that must be considered to
+ fulltext-index self.entity
+
+ CubicWeb's default implementation will look for yams'
+ ``fulltex_container`` property.
+ """
+ yield self.entity
+ yield self.entity.some_related_entity
+
+
+ def get_words(self):
+ # implement any logic here
+ # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
+ # for the actual signification of 'C'
+ return {'C': ['any', 'word', 'I', 'want']}
--- a/doc/book/en/devrepo/index.rst Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/devrepo/index.rst Fri Sep 07 14:01:59 2012 +0200
@@ -21,3 +21,5 @@
testing.rst
migration.rst
profiling.rst
+ fti.rst
+
--- a/doc/book/en/devweb/ajax.rst Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/devweb/ajax.rst Fri Sep 07 14:01:59 2012 +0200
@@ -7,6 +7,6 @@
You can, for instance, register some python functions that will become
callable from javascript through ajax calls. All the ajax URLs are handled
-by the ``AjaxController`` controller.
+by the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
.. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/views/index.rst Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/devweb/views/index.rst Fri Sep 07 14:01:59 2012 +0200
@@ -22,6 +22,7 @@
breadcrumbs
idownloadable
wdoc
+
.. editforms
.. embedding
--- a/doc/book/en/devweb/views/views.rst Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/devweb/views/views.rst Fri Sep 07 14:01:59 2012 +0200
@@ -32,8 +32,8 @@
Basic class for views
~~~~~~~~~~~~~~~~~~~~~
-Class `View` (`cubicweb.view`)
-```````````````````````````````
+Class :class:`~cubicweb.view.View`
+``````````````````````````````````
.. autoclass:: cubicweb.view.View
--- a/entities/adapters.py Fri Aug 03 13:29:37 2012 +0200
+++ b/entities/adapters.py Fri Sep 07 14:01:59 2012 +0200
@@ -87,10 +87,20 @@
class IFTIndexableAdapter(view.EntityAdapter):
+ """standard adapter to handle fulltext indexing
+
+ .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers
+ .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.get_words
+ """
__regid__ = 'IFTIndexable'
__select__ = is_instance('Any')
def fti_containers(self, _done=None):
+ """return the list of entities to index when handling ``self.entity``
+
+ The actual list of entities depends on ``fulltext_container`` usage
+ in the datamodel definition
+ """
if _done is None:
_done = set()
entity = self.entity
--- a/entity.py Fri Aug 03 13:29:37 2012 +0200
+++ b/entity.py Fri Sep 07 14:01:59 2012 +0200
@@ -1153,6 +1153,9 @@
# insert security RQL expressions granting the permission to 'add' the
# relation into the rql syntax tree, if necessary
rqlexprs = rdef.get_rqlexprs('add')
+ if not self.has_eid():
+ rqlexprs = [rqlexpr for rqlexpr in rqlexprs
+ if searchedvar.name in rqlexpr.mainvars]
if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
# compute a varmap suitable to RQLRewriter.rewrite argument
varmap = dict((v, v) for v in (searchedvar.name, evar.name)
--- a/hooks/integrity.py Fri Aug 03 13:29:37 2012 +0200
+++ b/hooks/integrity.py Fri Sep 07 14:01:59 2012 +0200
@@ -301,11 +301,10 @@
def precommit_event(self):
session = self.session
pendingeids = session.transaction_data.get('pendingeids', ())
- neweids = session.transaction_data.get('neweids', ())
eids_by_etype_rtype = {}
for eid, rtype in self.get_data():
- # don't do anything if the entity is being created or deleted
- if not (eid in pendingeids or eid in neweids):
+ # don't do anything if the entity is being deleted
+ if eid not in pendingeids:
etype = session.describe(eid)[0]
key = (etype, rtype)
if key not in eids_by_etype_rtype:
--- a/hooks/syncschema.py Fri Aug 03 13:29:37 2012 +0200
+++ b/hooks/syncschema.py Fri Sep 07 14:01:59 2012 +0200
@@ -755,7 +755,13 @@
cols = ['%s%s' % (prefix, c) for c in self.cols]
sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols)
for sql in sqls:
- session.system_sql(sql)
+ try:
+ session.system_sql(sql)
+ except Exception, exc: # should be ProgrammingError
+ if sql.startswith('DROP'):
+ self.error('execute of `%s` failed (cause: %s)', sql, exc)
+ continue
+ raise
# XXX revertprecommit_event
--- a/i18n/de.po Fri Aug 03 13:29:37 2012 +0200
+++ b/i18n/de.po Fri Sep 07 14:01:59 2012 +0200
@@ -3280,6 +3280,9 @@
msgid "new"
msgstr "neu"
+msgid "next page"
+msgstr ""
+
msgid "next_results"
msgstr "weitere Ergebnisse"
@@ -3481,6 +3484,9 @@
msgid "preferences"
msgstr "Einstellungen"
+msgid "previous page"
+msgstr ""
+
msgid "previous_results"
msgstr "vorige Ergebnisse"
@@ -4618,3 +4624,10 @@
#, python-format
msgid "you should un-inline relation %s which is supported and may be crossed "
msgstr ""
+
+#~ msgid ""
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
+#~ msgstr ""
+#~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, "
+#~ "diese Relation existiert nicht mehr in dem Schema."
--- a/i18n/en.po Fri Aug 03 13:29:37 2012 +0200
+++ b/i18n/en.po Fri Sep 07 14:01:59 2012 +0200
@@ -3196,6 +3196,9 @@
msgid "new"
msgstr ""
+msgid "next page"
+msgstr ""
+
msgid "next_results"
msgstr "next results"
@@ -3396,6 +3399,9 @@
msgid "preferences"
msgstr ""
+msgid "previous page"
+msgstr ""
+
msgid "previous_results"
msgstr "previous results"
--- a/i18n/es.po Fri Aug 03 13:29:37 2012 +0200
+++ b/i18n/es.po Fri Sep 07 14:01:59 2012 +0200
@@ -3321,6 +3321,9 @@
msgid "new"
msgstr "Nuevo"
+msgid "next page"
+msgstr ""
+
msgid "next_results"
msgstr "Siguientes resultados"
@@ -3522,6 +3525,9 @@
msgid "preferences"
msgstr "Preferencias"
+msgid "previous page"
+msgstr ""
+
msgid "previous_results"
msgstr "Resultados Anteriores"
@@ -4669,3 +4675,10 @@
msgstr ""
"usted debe quitar la puesta en línea de la relación %s que es aceptada y "
"puede ser cruzada"
+
+#~ msgid ""
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
+#~ msgstr ""
+#~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
+#~ "relación ya no existe en el esquema."
--- a/i18n/fr.po Fri Aug 03 13:29:37 2012 +0200
+++ b/i18n/fr.po Fri Sep 07 14:01:59 2012 +0200
@@ -2765,7 +2765,7 @@
msgstr "contient le texte"
msgid "header-center"
-msgstr ""
+msgstr "en-tête (centre)"
msgid "header-left"
msgstr "en-tête (gauche)"
@@ -3328,6 +3328,9 @@
msgid "new"
msgstr "nouveau"
+msgid "next page"
+msgstr "page suivante"
+
msgid "next_results"
msgstr "résultats suivants"
@@ -3531,6 +3534,9 @@
msgid "preferences"
msgstr "préférences"
+msgid "previous page"
+msgstr "page précédente"
+
msgid "previous_results"
msgstr "résultats précédents"
@@ -4122,10 +4128,14 @@
msgstr "la valeur \"%s\" est déjà utilisée, veuillez utiliser une autre valeur"
msgid "there is no next page"
-msgstr "il n'y a pas de page suivante"
+msgstr "Il n'y a pas de page suivante"
msgid "there is no previous page"
-msgstr "il n'y a pas de page précédente"
+msgstr "Il n'y a pas de page précédente"
+
+#, python-format
+msgid "there is no transaction #%s"
+msgstr "Il n'y a pas de transaction #%s"
#, python-format
msgid "there is no transaction #%s"
@@ -4343,7 +4353,7 @@
msgstr "valeur non autorisée"
msgid "undefined user"
-msgstr ""
+msgstr "utilisateur inconnu"
msgid "undo"
msgstr "annuler"
@@ -4679,3 +4689,24 @@
msgstr ""
"vous devriez enlevé la mise en ligne de la relation %s qui est supportée et "
"peut-être croisée"
+
+#~ msgid "Action"
+#~ msgstr "Action"
+
+#~ msgid "day"
+#~ msgstr "jour"
+
+#~ msgid "log out first"
+#~ msgstr "déconnecter vous d'abord"
+
+#~ msgid "month"
+#~ msgstr "mois"
+
+#~ msgid "today"
+#~ msgstr "aujourd'hui"
+
+#~ msgid "undo last change"
+#~ msgstr "annuler dernier changement"
+
+#~ msgid "week"
+#~ msgstr "semaine"
--- a/migration.py Fri Aug 03 13:29:37 2012 +0200
+++ b/migration.py Fri Sep 07 14:01:59 2012 +0200
@@ -514,7 +514,9 @@
elif op == None:
continue
else:
- print 'unable to handle this case', oper, version, op, ver
+ print ('unable to handle %s in %s, set to `%s %s` '
+ 'but currently up to `%s %s`' %
+ (cube, source, oper, version, op, ver))
# "solve" constraint satisfaction problem
if cube not in self.cubes:
self.errors.append( ('add', cube, version, source) )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.4_Any.py Fri Sep 07 14:01:59 2012 +0200
@@ -0,0 +1,11 @@
+from logilab.common.shellutils import generate_password
+from cubicweb.server.utils import crypt_password
+
+for user in rql('CWUser U WHERE U cw_source S, S name "system", U upassword P, U login L').entities():
+ salt = user.upassword.getvalue()
+ if crypt_password('', salt) == salt:
+ passwd = generate_password()
+ print 'setting random password for user %s' % user.login
+ user.set_attributes(upassword=passwd)
+
+commit()
--- a/misc/scripts/ldapuser2ldapfeed.py Fri Aug 03 13:29:37 2012 +0200
+++ b/misc/scripts/ldapuser2ldapfeed.py Fri Sep 07 14:01:59 2012 +0200
@@ -3,6 +3,8 @@
Once this script is run, execute c-c db-check to cleanup relation tables.
"""
import sys
+from collections import defaultdict
+from logilab.common.shellutils import generate_password
try:
source_name, = __args__
@@ -33,44 +35,65 @@
print '******************** backport entity content ***************************'
-todelete = {}
+todelete = defaultdict(list)
+extids = set()
+duplicates = []
for entity in rql('Any X WHERE X cw_source S, S eid %(s)s', {'s': source.eid}).entities():
- etype = entity.__regid__
- if not source.support_entity(etype):
- print "source doesn't support %s, delete %s" % (etype, entity.eid)
- else:
- try:
- entity.complete()
- except Exception:
- print '%s %s much probably deleted, delete it (extid %s)' % (
- etype, entity.eid, entity.cw_metainformation()['extid'])
- else:
- print 'get back', etype, entity.eid
- entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
- if not entity.creation_date:
- entity.cw_edited['creation_date'] = datetime.now()
- if not entity.modification_date:
- entity.cw_edited['modification_date'] = datetime.now()
- if not entity.upassword:
- entity.cw_edited['upassword'] = u''
- if not entity.cwuri:
- entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
- source.urls[0], entity.cw_metainformation()['extid'])
- print entity.cw_edited
- system_source.add_entity(session, entity)
- sql("UPDATE entities SET source='system' "
- "WHERE eid=%(eid)s", {'eid': entity.eid})
- continue
- todelete.setdefault(etype, []).append(entity)
+ etype = entity.__regid__
+ if not source.support_entity(etype):
+ print "source doesn't support %s, delete %s" % (etype, entity.eid)
+ todelete[etype].append(entity)
+ continue
+ try:
+ entity.complete()
+ except Exception:
+ print '%s %s much probably deleted, delete it (extid %s)' % (
+ etype, entity.eid, entity.cw_metainformation()['extid'])
+ todelete[etype].append(entity)
+ continue
+ print 'get back', etype, entity.eid
+ entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
+ if not entity.creation_date:
+ entity.cw_edited['creation_date'] = datetime.now()
+ if not entity.modification_date:
+ entity.cw_edited['modification_date'] = datetime.now()
+ if not entity.upassword:
+ entity.cw_edited['upassword'] = generate_password()
+ extid = entity.cw_metainformation()['extid']
+ if not entity.cwuri:
+ entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
+ source.urls[0], extid.decode('utf-8', 'ignore'))
+ print entity.cw_edited
+ if extid in extids:
+ duplicates.append(extid)
+ continue
+ extids.add(extid)
+ system_source.add_entity(session, entity)
+ sql("UPDATE entities SET source='system' "
+ "WHERE eid=%(eid)s", {'eid': entity.eid})
# only cleanup entities table, remaining stuff should be cleaned by a c-c
# db-check to be run after this script
-for entities in todelete.values():
+if duplicates:
+ print 'found %s duplicate entries' % len(duplicates)
+ from pprint import pprint
+ pprint(duplicates)
+
+print len(todelete), 'entities will be deleted'
+for etype, entities in todelete.values():
+ print 'deleting', etype, [e.login for e in entities]
system_source.delete_info_multi(session, entities, source_name)
+
source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0)
source_ent.cw_set(type=u"ldapfeed", parser=u"ldapfeed")
-commit()
+if raw_input('Commit ?') in 'yY':
+ print 'committing'
+ commit()
+else:
+ rollback()
+ print 'rollbacked'
+
--- a/predicates.py Fri Aug 03 13:29:37 2012 +0200
+++ b/predicates.py Fri Sep 07 14:01:59 2012 +0200
@@ -352,12 +352,12 @@
"""
def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
- **kwargs):
- if not rset and not kwargs.get('entity'):
+ entity=None, **kwargs):
+ if not rset and entity is None:
return 0
score = 0
- if kwargs.get('entity'):
- score = self.score_entity(kwargs['entity'])
+ if entity is not None:
+ score = self.score_entity(entity)
elif row is None:
col = col or 0
if accept_none is None:
@@ -558,7 +558,7 @@
@objectify_predicate
def nonempty_rset(cls, req, rset=None, **kwargs):
"""Return 1 for result set containing one ore more rows."""
- if rset is not None and rset.rowcount:
+ if rset:
return 1
return 0
@@ -567,7 +567,7 @@
@objectify_predicate
def empty_rset(cls, req, rset=None, **kwargs):
"""Return 1 for result set which doesn't contain any row."""
- if rset is not None and rset.rowcount == 0:
+ if rset is not None and len(rset) == 0:
return 1
return 0
@@ -580,7 +580,7 @@
"""
if rset is None and 'entity' in kwargs:
return 1
- if rset is not None and (row is not None or rset.rowcount == 1):
+ if rset is not None and (row is not None or len(rset) == 1):
return 1
return 0
@@ -608,7 +608,7 @@
return self.operator(num, self.expected)
def __call__(self, cls, req, rset=None, **kwargs):
- return int(rset is not None and self.match_expected(rset.rowcount))
+ return int(rset is not None and self.match_expected(len(rset)))
class multi_columns_rset(multi_lines_rset):
@@ -648,7 +648,7 @@
page_size = req.property_value('navigation.page-size')
else:
page_size = int(page_size)
- if rset.rowcount <= (page_size*self.nbpages):
+ if len(rset) <= (page_size*self.nbpages):
return 0
return self.nbpages
@@ -1080,9 +1080,9 @@
# don't use EntityPredicate.__call__ but this optimized implementation to
# avoid considering each entity when it's not necessary
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- if kwargs.get('entity'):
- return self.score_entity(kwargs['entity'])
+ def __call__(self, cls, req, rset=None, row=None, col=0, entity=None, **kwargs):
+ if entity is not None:
+ return self.score_entity(entity)
if rset is None:
return 0
if row is None:
--- a/server/checkintegrity.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/checkintegrity.py Fri Sep 07 14:01:59 2012 +0200
@@ -317,7 +317,7 @@
print 'Checking mandatory relations'
msg = '%s #%s is missing mandatory %s relation %s (autofix will delete the entity)'
for rschema in schema.relations():
- if rschema.final or rschema.type in PURE_VIRTUAL_RTYPES:
+ if rschema.final or rschema in PURE_VIRTUAL_RTYPES or rschema in ('is', 'is_instance_of'):
continue
smandatory = set()
omandatory = set()
--- a/server/hook.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/hook.py Fri Sep 07 14:01:59 2012 +0200
@@ -236,8 +236,8 @@
or rollback() will restore the hooks.
-Hooks specific predicate
-~~~~~~~~~~~~~~~~~~~~~~~~
+Hooks specific predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~
.. autoclass:: cubicweb.server.hook.match_rtype
.. autoclass:: cubicweb.server.hook.match_rtype_sets
@@ -473,16 +473,18 @@
argument. The goal of this predicate is that it keeps reference to original sets,
so modification to thoses sets are considered by the predicate. For instance
- MYSET = set()
+ .. sourcecode:: python
+
+ MYSET = set()
- class Hook1(Hook):
- __regid__ = 'hook1'
- __select__ = Hook.__select__ & match_rtype_sets(MYSET)
- ...
+ class Hook1(Hook):
+ __regid__ = 'hook1'
+ __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+ ...
- class Hook2(Hook):
- __regid__ = 'hook2'
- __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+ class Hook2(Hook):
+ __regid__ = 'hook2'
+ __select__ = Hook.__select__ & match_rtype_sets(MYSET)
Client code can now change `MYSET`, this will changes the selection criteria
of :class:`Hook1` and :class:`Hook1`.
--- a/server/ldaputils.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/ldaputils.py Fri Sep 07 14:01:59 2012 +0200
@@ -250,10 +250,11 @@
except ldap.LDAPError: # Invalid protocol version, fall back safely
conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION2)
# Deny auto-chasing of referrals to be safe, we handle them instead
- #try:
- # connection.set_option(ldap.OPT_REFERRALS, 0)
- #except ldap.LDAPError: # Cannot set referrals, so do nothing
- # pass
+ # Required for AD
+ try:
+ conn.set_option(ldap.OPT_REFERRALS, 0)
+ except ldap.LDAPError: # Cannot set referrals, so do nothing
+ pass
#conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
#conn.timeout = op_timeout
# Now bind with the credentials given. Let exceptions propagate out.
--- a/server/migractions.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/migractions.py Fri Sep 07 14:01:59 2012 +0200
@@ -1526,9 +1526,9 @@
def cmd_reactivate_verification_hooks(self):
self.session.enable_hook_categories('integrity')
- @deprecated("[3.15] use session.rename_relation_type(oldname, newname)")
+ @deprecated("[3.15] use rename_relation_type(oldname, newname)")
def cmd_rename_relation(self, oldname, newname, commit=True):
- self.session.rename_relation_type(oldname, newname, commit)
+ self.cmd_rename_relation_type(oldname, newname, commit)
class ForRqlIterator:
--- a/server/session.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/session.py Fri Sep 07 14:01:59 2012 +0200
@@ -106,7 +106,8 @@
self.free_cnxset = free_cnxset
def __enter__(self):
- pass
+ # ensure session has a cnxset
+ self.session.set_cnxset()
def __exit__(self, exctype, exc, traceback):
if exctype:
--- a/server/sources/datafeed.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/sources/datafeed.py Fri Sep 07 14:01:59 2012 +0200
@@ -152,21 +152,24 @@
def update_latest_retrieval(self, session):
self.latest_retrieval = datetime.utcnow()
+ session.set_cnxset()
session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
{'x': self.eid, 'date': self.latest_retrieval})
+ session.commit()
def acquire_synchronization_lock(self, session):
# XXX race condition until WHERE of SET queries is executed using
# 'SELECT FOR UPDATE'
now = datetime.utcnow()
+ session.set_cnxset()
if not session.execute(
'SET X in_synchronization %(now)s WHERE X eid %(x)s, '
'X in_synchronization NULL OR X in_synchronization < %(maxdt)s',
{'x': self.eid, 'now': now, 'maxdt': now - self.max_lock_lifetime}):
self.error('concurrent synchronization detected, skip pull')
- session.commit(free_cnxset=False)
+ session.commit()
return False
- session.commit(free_cnxset=False)
+ session.commit()
return True
def release_synchronization_lock(self, session):
@@ -205,7 +208,9 @@
importlog.record_info('added %s entities' % len(stats['created']))
if stats.get('updated'):
importlog.record_info('updated %s entities' % len(stats['updated']))
+ session.set_cnxset()
importlog.write_log(session, end_timestamp=self.latest_retrieval)
+ session.commit()
return stats
def process_urls(self, parser, urls, raise_on_error=False):
@@ -376,8 +381,10 @@
byetype.setdefault(etype, []).append(str(eid))
for etype, eids in byetype.iteritems():
self.warning('delete %s %s entities', len(eids), etype)
+ session.set_cnxset()
session.execute('DELETE %s X WHERE X eid IN (%s)'
% (etype, ','.join(eids)))
+ session.commit()
def update_if_necessary(self, entity, attrs):
entity.complete(tuple(attrs))
--- a/server/sources/native.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/sources/native.py Fri Sep 07 14:01:59 2012 +0200
@@ -613,12 +613,10 @@
etype = entities[0].__regid__
for attr, storage in self._storages.get(etype, {}).items():
for entity in entities:
- try:
+ if event == 'deleted':
+ storage.entity_deleted(entity, attr)
+ else:
edited = entity.cw_edited
- except AttributeError:
- assert event == 'deleted'
- getattr(storage, 'entity_deleted')(entity, attr)
- else:
if attr in edited:
handler = getattr(storage, 'entity_%s' % event)
to_restore = handler(entity, attr)
@@ -1597,9 +1595,10 @@
pass
class LoginPasswordAuthentifier(BaseAuthentifier):
- passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
- auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
- _sols = ({'X': 'CWUser', 'P': 'Password'},)
+ passwd_rql = 'Any P WHERE X is CWUser, X login %(login)s, X upassword P'
+ auth_rql = ('Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
+ 'X cw_source S, S name "system"')
+ _sols = ({'X': 'CWUser', 'P': 'Password', 'S': 'CWSource'},)
def set_schema(self, schema):
"""set the instance'schema"""
--- a/server/test/unittest_ldapuser.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/test/unittest_ldapuser.py Fri Sep 07 14:01:59 2012 +0200
@@ -32,7 +32,6 @@
from cubicweb.devtools.httptest import get_available_port
from cubicweb.devtools import get_test_db_handler
-from cubicweb.server.session import security_enabled
from cubicweb.server.sources.ldapuser import GlobTrFunc, UnknownEid, RQL2LDAPFilter
CONFIG = u'user-base-dn=ou=People,dc=cubicweb,dc=test'
@@ -110,10 +109,9 @@
def _pull(self):
with self.session.repo.internal_session() as isession:
- with security_enabled(isession, read=False, write=False):
- lfsource = isession.repo.sources_by_uri['ldapuser']
- stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
- isession.commit()
+ lfsource = isession.repo.sources_by_uri['ldapuser']
+ stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+ isession.commit()
def test_delete(self):
""" delete syt, pull, check deactivation, repull,
@@ -138,6 +136,14 @@
self.assertEqual(self.execute('Any N WHERE U login "syt", '
'U in_state S, S name N').rows[0][0],
'deactivated')
+ # test reactivating the user isn't enough to authenticate, as the native source
+ # refuse to authenticate user from other sources
+ os.system(deletecmd)
+ self._pull()
+ user = self.execute('CWUser U WHERE U login "syt"').get_entity(0, 0)
+ user.cw_adapt_to('IWorkflowable').fire_transition('activate')
+ self.commit()
+ self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
class LDAPFeedSourceTC(LDAPTestBase):
test_db_id = 'ldap-feed'
--- a/server/test/unittest_repository.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/test/unittest_repository.py Fri Sep 07 14:01:59 2012 +0200
@@ -113,6 +113,8 @@
self.assertRaises(AuthenticationError,
self.repo.connect, self.admlogin, password='nimportnawak')
self.assertRaises(AuthenticationError,
+ self.repo.connect, self.admlogin, password='')
+ self.assertRaises(AuthenticationError,
self.repo.connect, self.admlogin, password=None)
self.assertRaises(AuthenticationError,
self.repo.connect, None, password=None)
--- a/server/test/unittest_security.py Fri Aug 03 13:29:37 2012 +0200
+++ b/server/test/unittest_security.py Fri Sep 07 14:01:59 2012 +0200
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""functional tests for server'security"""
+from __future__ import with_statement
import sys
--- a/sobjects/ldapparser.py Fri Aug 03 13:29:37 2012 +0200
+++ b/sobjects/ldapparser.py Fri Sep 07 14:01:59 2012 +0200
@@ -20,6 +20,7 @@
unlike ldapuser source, this source is copy based and will import ldap content
(beside passwords for authentication) into the system source.
"""
+from __future__ import with_statement
from logilab.common.decorators import cached
from logilab.common.shellutils import generate_password
@@ -71,7 +72,9 @@
session.commit(free_cnxset=False)
def update_if_necessary(self, entity, attrs):
- entity.complete(tuple(attrs))
+ # disable read security to allow password selection
+ with entity._cw.security_enabled(read=False):
+ entity.complete(tuple(attrs))
if entity.__regid__ == 'CWUser':
wf = entity.cw_adapt_to('IWorkflowable')
if wf.state == 'deactivated':
--- a/test/unittest_entity.py Fri Aug 03 13:29:37 2012 +0200
+++ b/test/unittest_entity.py Fri Sep 07 14:01:59 2012 +0200
@@ -18,6 +18,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unit tests for cubicweb.web.views.entities module"""
+from __future__ import with_statement
+
from datetime import datetime
from logilab.common import tempattr
@@ -28,7 +30,7 @@
from cubicweb.mttransforms import HAS_TAL
from cubicweb.entities import fetch_config
from cubicweb.uilib import soup2xhtml
-from cubicweb.schema import RQLVocabularyConstraint
+from cubicweb.schema import RQLVocabularyConstraint, RRQLExpression
class EntityTC(CubicWebTC):
@@ -361,6 +363,18 @@
'NOT (S connait AD, AD nom "toto"), AD is Personne, '
'EXISTS(S travaille AE, AE nom "tutu")')
+ def test_unrelated_rql_security_rel_perms(self):
+ '''check `connait` add permission has no effect for a new entity on the
+ unrelated rql'''
+ rdef = self.schema['Personne'].rdef('connait')
+ perm_rrqle = RRQLExpression('U has_update_permission S')
+ with self.temporary_permissions((rdef, {'add': (perm_rrqle,)})):
+ person = self.vreg['etypes'].etype_class('Personne')(self.request())
+ rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
+ self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
+ 'O is Personne, O nom AA, O prenom AB, '
+ 'O modification_date AC')
+
def test_unrelated_rql_constraints_edition_subject(self):
person = self.request().create_entity('Personne', nom=u'sylvain')
rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
--- a/web/application.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/application.py Fri Sep 07 14:01:59 2012 +0200
@@ -381,6 +381,9 @@
content = self.loggedout_content(req)
# let the explicitly reset http credential
raise AuthenticationError()
+ except Redirect, ex:
+ # authentication needs redirection (eg openid)
+ content = self.redirect_handler(req, ex)
# Wrong, absent or Reseted credential
except AuthenticationError:
# If there is an https url configured and
@@ -445,17 +448,9 @@
result = ex.content
req.status_out = ex.status
except Redirect, ex:
- # handle redirect
- # - comply to ex status
- # - set header field
- #
- # Redirect Maybe be is raised by edit controller when
- # everything went fine, so try to commit
- self.debug('redirecting to %s', str(ex.location))
- req.headers_out.setHeader('location', str(ex.location))
- assert 300<= ex.status < 400
- req.status_out = ex.status
- result = ''
+ # Redirect may be raised by edit controller when everything went
+ # fine, so attempt to commit
+ result = self.redirect_handler(req, ex)
if req.cnx:
txuuid = req.cnx.commit()
commited = True
@@ -501,7 +496,20 @@
self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart)
return result
- ### Error handler
+ # Error handlers
+
+ def redirect_handler(self, req, ex):
+ """handle redirect
+ - comply to ex status
+ - set header field
+ - return empty content
+ """
+ self.debug('redirecting to %s', str(ex.location))
+ req.headers_out.setHeader('location', str(ex.location))
+ assert 300 <= ex.status < 400
+ req.status_out = ex.status
+ return ''
+
def validation_error_handler(self, req, ex):
ex.errors = dict((k, v) for k, v in ex.errors.items())
if '__errorurl' in req.form:
@@ -551,8 +559,6 @@
req.build_url('undo', txuuid=txuuid), req._('undo'))
req.append_to_redirect_message(msg)
-
-
def ajax_error_handler(self, req, ex):
req.set_header('content-type', 'application/json')
status = ex.status
--- a/web/form.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/form.py Fri Sep 07 14:01:59 2012 +0200
@@ -88,23 +88,19 @@
def __init__(self, req, rset=None, row=None, col=None,
submitmsg=None, mainform=True, **kwargs):
- super(Form, self).__init__(req, rset=rset, row=row, col=col)
+ # process kwargs first so we can properly pass them to Form and match
+ # order expectation (ie cw_extra_kwargs populated almost first)
+ hiddens, extrakw = self._process_kwargs(kwargs)
+ # now call ancestor init
+ super(Form, self).__init__(req, rset=rset, row=row, col=col, **extrakw)
+ # then continue with further specific initialization
self.fields = list(self.__class__._fields_)
+ for key, val in hiddens:
+ self.add_hidden(key, val)
if mainform:
formid = kwargs.pop('formvid', self.__regid__)
self.add_hidden(u'__form_id', formid)
self._posting = self._cw.form.get('__form_id') == formid
- for key, val in kwargs.iteritems():
- if key in controller.NAV_FORM_PARAMETERS:
- self.add_hidden(key, val)
- elif key == 'redirect_path':
- self.add_hidden(u'__redirectpath', val)
- elif hasattr(self.__class__, key) and not key[0] == '_':
- setattr(self, key, val)
- else:
- self.cw_extra_kwargs[key] = val
- # skip other parameters, usually given for selection
- # (else write a custom class to handle them)
if mainform:
self.add_hidden(u'__errorurl', self.session_key())
self.add_hidden(u'__domid', self.domid)
@@ -119,6 +115,22 @@
if submitmsg is not None:
self.set_message(submitmsg)
+ def _process_kwargs(self, kwargs):
+ hiddens = []
+ extrakw = {}
+ # search for navigation parameters and customization of existing
+ # attributes; remaining stuff goes in extrakwargs
+ for key, val in kwargs.iteritems():
+ if key in controller.NAV_FORM_PARAMETERS:
+ hiddens.append( (key, val) )
+ elif key == 'redirect_path':
+ hiddens.append( (u'__redirectpath', val) )
+ elif hasattr(self.__class__, key) and not key[0] == '_':
+ setattr(self, key, val)
+ else:
+ extrakw[key] = val
+ return hiddens, extrakw
+
def set_message(self, submitmsg):
"""sets a submitmsg if exists, using _cwmsgid mechanism """
cwmsgid = self._cw.set_redirect_message(submitmsg)
--- a/web/formfields.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/formfields.py Fri Sep 07 14:01:59 2012 +0200
@@ -313,6 +313,7 @@
def role_name(self):
"""return <field.name>-<field.role> if role is specified, else field.name"""
+ assert self.name, 'field without a name (give it to constructor for explicitly built fields)'
if self.role is not None:
return role_name(self.name, self.role)
return self.name
--- a/web/formwidgets.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/formwidgets.py Fri Sep 07 14:01:59 2012 +0200
@@ -672,10 +672,11 @@
"""
needs_js = ('jquery.ui.js', )
needs_css = ('jquery.ui.css',)
+ default_size = 10
def __init__(self, datestr=None, **kwargs):
super(JQueryDatePicker, self).__init__(**kwargs)
- self.datestr = datestr
+ self.value = datestr
def _render(self, form, field, renderer):
req = form._cw
@@ -689,44 +690,36 @@
'{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
' showOn: "button", buttonImageOnly: true})' % (
domid, req.uiprops['CALENDAR_ICON'], fmt))
- if self.datestr is None:
+ return self._render_input(form, field, domid)
+
+ def _render_input(self, form, field, domid):
+ if self.value is None:
value = self.values(form, field)[0]
else:
- value = self.datestr
- attrs = {}
- if self.settabindex:
- attrs['tabindex'] = req.next_tabindex()
- return tags.input(id=domid, name=domid, value=value,
- type='text', size='10', **attrs)
+ value = self.value
+ attrs = self.attributes(form, field)
+ attrs.setdefault('size', unicode(self.default_size))
+ return tags.input(name=domid, value=value, type='text', **attrs)
-class JQueryTimePicker(FieldWidget):
+class JQueryTimePicker(JQueryDatePicker):
"""Use jquery.timePicker to define a time picker. Will return the time as an
unicode string.
"""
needs_js = ('jquery.timePicker.js',)
needs_css = ('jquery.timepicker.css',)
+ default_size = 5
def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
- super(JQueryTimePicker, self).__init__(**kwargs)
- self.timestr = timestr
+ super(JQueryTimePicker, self).__init__(timestr, **kwargs)
self.timesteps = timesteps
self.separator = separator
def _render(self, form, field, renderer):
- req = form._cw
domid = field.dom_id(form, self.suffix)
- req.add_onload(u'cw.jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
- domid, self.timestr, self.timesteps, self.separator))
- if self.timestr is None:
- value = self.values(form, field)[0]
- else:
- value = self.timestr
- attrs = {}
- if self.settabindex:
- attrs['tabindex'] = req.next_tabindex()
- return tags.input(id=domid, name=domid, value=value,
- type='text', size='5')
+ form._cw.add_onload(u'cw.jqNode("%s").timePicker({step: %s, separator: "%s"})' % (
+ domid, self.timesteps, self.separator))
+ return self._render_input(form, field, domid)
class JQueryDateTimePicker(FieldWidget):
--- a/web/request.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/request.py Fri Sep 07 14:01:59 2012 +0200
@@ -170,8 +170,7 @@
@property
def authmode(self):
"""Authentification mode of the instance
-
- (see :ref:`Configuring the Web server`)"""
+ (see :ref:`WebServerConfig`)"""
return self.vreg.config['auth-mode']
# Various variable generator.
--- a/web/test/unittest_formfields.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/test/unittest_formfields.py Fri Sep 07 14:01:59 2012 +0200
@@ -147,7 +147,7 @@
def test_property_key_field(self):
from cubicweb.web.views.cwproperties import PropertyKeyField
req = self.request()
- field = PropertyKeyField()
+ field = PropertyKeyField(name='test')
e = self.vreg['etypes'].etype_class('CWProperty')(req)
renderer = self.vreg['formrenderers'].select('base', req)
form = EntityFieldsForm(req, entity=e)
--- a/web/test/unittest_views_basecontrollers.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/test/unittest_views_basecontrollers.py Fri Sep 07 14:01:59 2012 +0200
@@ -781,6 +781,14 @@
res, req = self.remote_call('foo')
self.assertEqual(res, '12')
+ def test_monkeypatch_jsoncontroller_stdfunc(self):
+ @monkeypatch(JSonController)
+ @jsonize
+ def js_reledit_form(self):
+ return 12
+ res, req = self.remote_call('reledit_form')
+ self.assertEqual(res, '12')
+
class UndoControllerTC(CubicWebTC):
@@ -843,16 +851,16 @@
def test_login_with_dest(self):
req = self.request()
- req.form = {'postlogin_path': '/elephants/babar'}
+ req.form = {'postlogin_path': 'elephants/babar'}
with self.assertRaises(Redirect) as cm:
self.ctrl_publish(req, ctrl='login')
- self.assertEqual('/elephants/babar', cm.exception.location)
+ self.assertEqual(req.build_url('elephants/babar'), cm.exception.location)
def test_login_no_dest(self):
req = self.request()
with self.assertRaises(Redirect) as cm:
self.ctrl_publish(req, ctrl='login')
- self.assertEqual('.', cm.exception.location)
+ self.assertEqual(req.base_url(), cm.exception.location)
if __name__ == '__main__':
unittest_main()
--- a/web/views/ajaxcontroller.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/ajaxcontroller.py Fri Sep 07 14:01:59 2012 +0200
@@ -63,6 +63,7 @@
__docformat__ = "restructuredtext en"
+from warnings import warn
from functools import partial
from logilab.common.date import strptime
@@ -114,22 +115,20 @@
fname = self._cw.form['fname']
except KeyError:
raise RemoteCallFailed('no method specified')
+ # 1/ check first for old-style (JSonController) ajax func for bw compat
try:
- func = self._cw.vreg['ajax-func'].select(fname, self._cw)
- except ObjectNotFound:
- # function not found in the registry, inspect JSonController for
- # backward compatibility
+ func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+ func = partial(func, self)
+ except AttributeError:
+ # 2/ check for new-style (AjaxController) ajax func
try:
- func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
- func = partial(func, self)
- except AttributeError:
+ func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+ except ObjectNotFound:
raise RemoteCallFailed('no %s method' % fname)
- else:
- self.warning('remote function %s found on JSonController, '
- 'use AjaxFunction / @ajaxfunc instead', fname)
- except NoSelectableObject:
- raise RemoteCallFailed('method %s not available in this context'
- % fname)
+ else:
+ warn('[3.15] remote function %s found on JSonController, '
+ 'use AjaxFunction / @ajaxfunc instead' % fname,
+ DeprecationWarning, stacklevel=2)
# no <arg> attribute means the callback takes no argument
args = self._cw.form.get('arg', ())
if not isinstance(args, (list, tuple)):
--- a/web/views/basecontrollers.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/basecontrollers.py Fri Sep 07 14:01:59 2012 +0200
@@ -90,8 +90,10 @@
def publish(self, rset=None):
"""log in the instance"""
- path = self._cw.form.get('postlogin_path', '.')
- raise Redirect(path)
+ path = self._cw.form.get('postlogin_path', '')
+ # redirect expect an url, not a path. Also path may contains a query
+ # string, hence should not be given to _cw.build_url()
+ raise Redirect(self._cw.base_url() + path)
class LogoutController(Controller):
--- a/web/views/basetemplates.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/basetemplates.py Fri Sep 07 14:01:59 2012 +0200
@@ -449,7 +449,7 @@
def form_action(self):
if self.action is None:
- # reuse existing redirection if it exist
+ # reuse existing redirection if it exists
target = self._cw.form.get('postlogin_path',
self._cw.relative_path())
url_args = {}
--- a/web/views/navigation.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/navigation.py Fri Sep 07 14:01:59 2012 +0200
@@ -364,11 +364,13 @@
@property
def prev_icon(self):
- return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_prev.png'))
+ return '<img src="%s" alt="%s" />' % (
+ xml_escape(self._cw.data_url('go_prev.png')), self._cw._('previous page'))
@property
def next_icon(self):
- return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_next.png'))
+ return '<img src="%s" alt="%s" />' % (
+ xml_escape(self._cw.data_url('go_next.png')), self._cw._('next page'))
def init_rendering(self):
adapter = self.entity.cw_adapt_to('IPrevNext')
--- a/web/views/tableview.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/tableview.py Fri Sep 07 14:01:59 2012 +0200
@@ -174,8 +174,8 @@
@cachedproperty
def initial_load(self):
- """We detect a bit heuristically if we are built for the first time.
- or from subsequent calls by the form filter or by the pagination hooks.
+ """We detect a bit heuristically if we are built for the first time or
+ from subsequent calls by the form filter or by the pagination hooks.
"""
form = self._cw.form
return 'fromformfilter' not in form and '__fromnavigation' not in form
--- a/web/views/tabs.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/tabs.py Fri Sep 07 14:01:59 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -90,6 +90,7 @@
class TabsMixin(LazyViewMixin):
"""a tab mixin to easily get jQuery based, lazy, ajax tabs"""
+ lazy = True
@property
def cookie_name(self):
@@ -122,7 +123,7 @@
vid = tabkwargs.get('vid', tabid)
domid = uilib.domid(tabid)
try:
- viewsvreg.select(vid, self._cw, **tabkwargs)
+ viewsvreg.select(vid, self._cw, tabid=domid, **tabkwargs)
except NoSelectableObject:
continue
selected_tabs.append((tabid, domid, tabkwargs))
@@ -157,17 +158,20 @@
w(u'</ul>')
for tabid, domid, tabkwargs in tabs:
w(u'<div id="%s">' % domid)
- tabkwargs.setdefault('tabid', domid)
- tabkwargs.setdefault('vid', tabid)
- tabkwargs.setdefault('rset', self.cw_rset)
- self.lazyview(**tabkwargs)
+ if self.lazy:
+ tabkwargs.setdefault('tabid', domid)
+ tabkwargs.setdefault('vid', tabid)
+ self.lazyview(**tabkwargs)
+ else:
+ self._cw.view(tabid, w=self.w, **tabkwargs)
w(u'</div>')
w(u'</div>')
# call the setTab() JS function *after* each tab is generated
# because the callback binding needs to be done before
# XXX make work history: true
- self._cw.add_onload(u"""
- jQuery('#entity-tabs-%(eeid)s').tabs(
+ if self.lazy:
+ self._cw.add_onload(u"""
+ jQuery('#entity-tabs-%(uid)s').tabs(
{ selected: %(tabindex)s,
select: function(event, ui) {
setTab(ui.panel.id, '%(cookiename)s');
@@ -175,9 +179,13 @@
});
setTab('%(domid)s', '%(cookiename)s');
""" % {'tabindex' : active_tab_idx,
- 'domid' : active_tab,
- 'eeid' : (entity and entity.eid or uid),
+ 'domid' : active_tab,
+ 'uid' : uid,
'cookiename' : self.cookie_name})
+ else:
+ self._cw.add_onload(
+ u"jQuery('#entity-tabs-%(uid)s').tabs({selected: %(tabindex)s});"
+ % {'tabindex': active_tab_idx, 'uid': uid})
class EntityRelationView(EntityView):
@@ -218,8 +226,7 @@
tabs = [_('main_tab')]
default_tab = 'main_tab'
- def cell_call(self, row, col):
- entity = self.cw_rset.complete_entity(row, col)
+ def render_entity(self, entity):
self.render_entity_toolbox(entity)
self.w(u'<div class="tabbedprimary"></div>')
self.render_entity_title(entity)
--- a/web/views/workflow.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/workflow.py Fri Sep 07 14:01:59 2012 +0200
@@ -315,7 +315,7 @@
wf = req.entity_from_eid(wfeid)
rschema = req.vreg.schema[field.name]
param = 'toeid' if field.role == 'subject' else 'fromeid'
- return sorted((e.view('combobox'), e.eid)
+ return sorted((e.view('combobox'), unicode(e.eid))
for e in getattr(wf, 'reverse_%s' % wfrelation)
if rschema.has_perm(req, 'add', **{param: e.eid}))
@@ -330,12 +330,14 @@
def transition_states_vocabulary(form, field):
entity = form.edited_entity
- if not entity.has_eid():
+ if entity.has_eid():
+ wfeid = entity.transition_of[0].eid
+ else:
eids = form.linked_to.get(('transition_of', 'subject'))
if not eids:
return []
- return _wf_items_for_relation(form._cw, eids[0], 'state_of', field)
- return field.relvoc_unrelated(form)
+ wfeid = eids[0]
+ return _wf_items_for_relation(form._cw, wfeid, 'state_of', field)
_afs.tag_subject_of(('*', 'destination_state', '*'), 'main', 'attributes')
_affk.tag_subject_of(('*', 'destination_state', '*'),
@@ -348,12 +350,14 @@
def state_transitions_vocabulary(form, field):
entity = form.edited_entity
- if not entity.has_eid():
+ if entity.has_eid():
+ wfeid = entity.state_of[0].eid
+ else :
eids = form.linked_to.get(('state_of', 'subject'))
- if eids:
- return _wf_items_for_relation(form._cw, eids[0], 'transition_of', field)
- return []
- return field.relvoc_unrelated(form)
+ if not eids:
+ return []
+ wfeid = eids[0]
+ return _wf_items_for_relation(form._cw, wfeid, 'transition_of', field)
_afs.tag_subject_of(('State', 'allowed_transition', '*'), 'main', 'attributes')
_affk.tag_subject_of(('State', 'allowed_transition', '*'),
--- a/web/webconfig.py Fri Aug 03 13:29:37 2012 +0200
+++ b/web/webconfig.py Fri Sep 07 14:01:59 2012 +0200
@@ -331,10 +331,9 @@
self.datadir_url = baseurl + data_relpath
def data_relpath(self):
- if (self.debugmode or self.mode == 'test'):
+ if self.mode == 'test':
return 'data/'
- else:
- return 'data/%s/' % self.instance_md5_version()
+ return 'data/%s/' % self.instance_md5_version()
def _build_ui_properties(self):
# self.datadir_url[:-1] to remove trailing /