# HG changeset patch # User Sylvain Thénault # Date 1284383721 -7200 # Node ID a176e68b7d0de2ef70b16466e56ab729f4eca38d # Parent 30de0be8f89561f342b72757396fdd63c78ba2d7# Parent 1f4beef3962d3ac568a86e9eed8ec4aceb378b21 backport stable diff -r 30de0be8f895 -r a176e68b7d0d .hgtags --- a/.hgtags Tue Sep 07 17:34:42 2010 +0200 +++ b/.hgtags Mon Sep 13 15:15:21 2010 +0200 @@ -149,3 +149,5 @@ 8d32d82134dc1d8eb0ce230191f34fd49084a168 cubicweb-debian-version-3.9.4-1 0a1fce8ddc672ca9ee7328ed4f88c1aa6e48d286 cubicweb-version-3.9.5 12038ca95f0fff2205f7ee029f5602d192118aec cubicweb-debian-version-3.9.5-1 +d37428222a6325583be958d7c7fe7c595115663d cubicweb-version-3.9.6 +7d2cab567735a17cab391c1a7f1bbe39118308a2 cubicweb-debian-version-3.9.6-1 diff -r 30de0be8f895 -r a176e68b7d0d __pkginfo__.py --- a/__pkginfo__.py Tue Sep 07 17:34:42 2010 +0200 +++ b/__pkginfo__.py Mon Sep 13 15:15:21 2010 +0200 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 9, 5) +numversion = (3, 9, 6) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -43,7 +43,7 @@ 'logilab-common': '>= 0.51.0', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.26.2', - 'yams': '>= 0.29.1', + 'yams': '>= 0.30.0', 'docutils': '>= 0.6', #gettext # for xgettext, msgcat, etc... # web dependancies @@ -52,7 +52,7 @@ 'Twisted': '', # XXX graphviz # server dependencies - 'logilab-database': '>= 1.1.0', + 'logilab-database': '>= 1.2.0', 'pysqlite': '>= 2.5.5', # XXX install pysqlite2 } diff -r 30de0be8f895 -r a176e68b7d0d _exceptions.py --- a/_exceptions.py Tue Sep 07 17:34:42 2010 +0200 +++ b/_exceptions.py Mon Sep 13 15:15:21 2010 +0200 @@ -80,6 +80,8 @@ class MultiSourcesError(RepositoryError, InternalError): """usually due to bad multisources configuration or rql query""" +class UniqueTogetherError(RepositoryError): + """raised when a unique_together constraint caused an IntegrityError""" # security exceptions ######################################################### diff -r 30de0be8f895 -r a176e68b7d0d cwconfig.py --- a/cwconfig.py Tue Sep 07 17:34:42 2010 +0200 +++ b/cwconfig.py Mon Sep 13 15:15:21 2010 +0200 @@ -51,7 +51,7 @@ CW_INSTANCES_DATA_DIR = /var/lib/cubicweb/instances/ CW_RUNTIME_DIR = /var/run/cubicweb/ - * 'user': :: +* 'user': :: CW_INSTANCES_DIR = ~/etc/cubicweb.d/ CW_INSTANCES_DATA_DIR = ~/etc/cubicweb.d/ diff -r 30de0be8f895 -r a176e68b7d0d debian/changelog --- a/debian/changelog Tue Sep 07 17:34:42 2010 +0200 +++ b/debian/changelog Mon Sep 13 15:15:21 2010 +0200 @@ -1,3 +1,9 @@ +cubicweb (3.9.6-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Mon, 13 Sep 2010 10:50:15 +0200 + cubicweb (3.9.5-1) unstable; urgency=low * new upstream release diff -r 30de0be8f895 -r a176e68b7d0d debian/control --- a/debian/control Tue Sep 07 17:34:42 2010 +0200 +++ b/debian/control Mon Sep 13 15:15:21 2010 +0200 @@ -33,7 +33,7 @@ Conflicts: cubicweb-multisources Replaces: cubicweb-multisources Provides: cubicweb-multisources -Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.1.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2 +Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.2.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2 Recommends: pyro, cubicweb-documentation (= ${source:Version}) Description: server part of the CubicWeb framework CubicWeb is a semantic web application framework. @@ -97,7 +97,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.51.0), python-yams (>= 0.29.1), python-rql (>= 0.26.3), python-lxml +Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.51.0), python-yams (>= 0.30.0), python-rql (>= 0.26.3), python-lxml Recommends: python-simpletal (>= 4.0), python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core diff -r 30de0be8f895 -r a176e68b7d0d doc/book/en/admin/setup.rst --- a/doc/book/en/admin/setup.rst Tue Sep 07 17:34:42 2010 +0200 +++ b/doc/book/en/admin/setup.rst Mon Sep 13 15:15:21 2010 +0200 @@ -59,6 +59,19 @@ .. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/ +.. _PipInstallation: + +Installation with pip +````````````````````` + +|cubicweb| and its cubes have been pip_ installable since version 3.8. Search +for them on pypi_:: + + pip install cubicweb + +.. _pip: http://pypi.python.org/pypi/pip +.. _pypi: http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb + .. _SourceInstallation: Install from source @@ -70,12 +83,9 @@ .. _`ftp site`: http://ftp.logilab.org/pub/cubicweb/ -Make sure you have installed the dependencies (see appendixes for the list). +Make sure you also have all the :ref:`InstallDependencies`. -|cubicweb| should soon be pip_ installable, stay tuned (expected in 3.8). - -.. _pip: http://pypi.python.org/pypi/pip - +.. _MercurialInstallation: Install from version control system ``````````````````````````````````` @@ -94,7 +104,7 @@ Do not forget to update the forest itself (using `cd path/to/forest ; hg up`). -Make sure you have installed the dependencies (see appendixes for the list). +Make sure you also have all the :ref:`InstallDependencies`. .. _WindowsInstallation: @@ -102,6 +112,10 @@ Windows installation ```````````````````` +Your best option is probably the :ref:`PipInstallation`. If it does not work or +if you want more control over the process, continue with the following +instructions. + Base elements ~~~~~~~~~~~~~ @@ -110,14 +124,15 @@ done. We assume everything goes into `C:\\` in this document. Adjusting the installation drive should be straightforward. -You should start by downloading and installing the Python(x,y) distribution. It -contains python 2.5 plus numerous useful third-party modules and applications:: +You should start by downloading and installing Python version >= 2.5 and < 3. - http://www.pythonxy.com/download_fr.php +An alternative option would be installing the Python(x,y) +distribution. Python(x,y) is not a requirement, but it makes things easier for +Windows user by wrapping in a single installer python 2.5 plus numerous useful +third-party modules and applications (including Eclipse + pydev, which is an +arguably good IDE for Python under Windows). Download it from this page:: -At the time of this writting, one gets version 2.1.15. Among the many things -provided, one finds Eclipse + pydev (an arguably good IDE for python under -windows). + http://code.google.com/p/pythonxy/wiki/Downloads Then you must grab Twisted. There is a windows installer directly available from this page:: @@ -166,11 +181,15 @@ http://www.graphviz.org/Download_windows.php -Simplejson will be provided within the forest, but a win32 compiled version will -run much faster:: +Simplejson is needed when installing with Python 2.5, but included in the +standard library for Python >= 2.6. It will be provided within the forest, but a +win32 compiled version will run much faster:: http://www.osuch.org/python-simplejson%3Awin32 +Make sure you also have all the :ref:`InstallDependencies` that are not specific +to Windows. + Tools ~~~~~ @@ -189,10 +208,14 @@ http://www.vectrace.com/mercurialeclipse/ -Setting up the sources -~~~~~~~~~~~~~~~~~~~~~~ +Getting the sources +~~~~~~~~~~~~~~~~~~~ -You need to enable the mercurial forest extension. To do this, edit the file:: +You can either download the latest release (see :ref:`SourceInstallation`) or +get the development version using Mercurial (see +:ref:`MercurialInstallation` and below). + +To enable the Mercurial forest extension on Windows, edit the file:: C:\Program Files\TortoiseHg\Mercurial.ini @@ -250,14 +273,14 @@ This currently assumes that the instances configurations is located at C:\\etc\\cubicweb.d. -For a cube 'my_cube', you will then find -C:\\etc\\cubicweb.d\\my_cube\\win32svc.py that has to be used thusly:: +For a cube 'my_instance', you will then find +C:\\etc\\cubicweb.d\\my_instance\\win32svc.py that has to be used thusly:: win32svc install This should just register your instance as a windows service. A simple:: - net start cubicweb-my_cube + net start cubicweb-my_instance should start the service. @@ -282,7 +305,12 @@ Whatever the backend used, database connection information are stored in the instance's :file:`sources` file. Currently cubicweb has been tested using -Postgresql (recommanded), MySQL, SQLServer and SQLite. +Postgresql (recommended), MySQL, SQLServer and SQLite. + +Other possible sources of data include CubicWeb, Subversion, LDAP and Mercurial, +but at least one relational database is required for CubicWeb to work. SQLite is +not fit for production use, but it works for testing and ships with Python, +which saves installation time when you want to get started quickly. .. _PostgresqlConfiguration: @@ -394,7 +422,7 @@ max_allowed_packet = 128M .. Note:: - It is unclear whether mysql supports indexed string of arbitrary lenght or + It is unclear whether mysql supports indexed string of arbitrary length or not. @@ -403,9 +431,10 @@ SQLServer configuration ``````````````````````` -As of this writing, sqlserver support is in progress. You should be able to -connect, create a database and go quite far, but some of the generated SQL is -still currently not accepted by the backend. +As of this writing, support for SQLServer 2005 is functional but incomplete. You +should be able to connect, create a database and go quite far, but some of the +SQL generated from RQL queries is still currently not accepted by the +backend. Porting to SQLServer 2008 is also an item on the backlog. The `source` configuration file may look like this (specific parts only are shown):: @@ -440,14 +469,13 @@ ------------------ If you want to use Pyro to access your instance remotly, or to have multi-source -or distributed configuration, it is required to have a name server Pyro running -on your network. By by default it is detected by a broadcast request, but you can +or distributed configuration, it is required to have a Pyro name server running +on your network. By default it is detected by a broadcast request, but you can specify a location in the instance's configuration file. To do so, you need to : -* launch the server manually before starting cubicweb as a server with `pyro-nsd - start` +* launch the pyro name server with `pyro-nsd start` before starting cubicweb * under debian, edit the file :file:`/etc/default/pyro-nsd` so that the name server pyro will be launched automatically when the machine fire up diff -r 30de0be8f895 -r a176e68b7d0d doc/book/en/annexes/depends.rst --- a/doc/book/en/annexes/depends.rst Tue Sep 07 17:34:42 2010 +0200 +++ b/doc/book/en/annexes/depends.rst Mon Sep 13 15:15:21 2010 +0200 @@ -1,9 +1,9 @@ .. -*- coding: utf-8 -*- -.. _dependencies: +.. _InstallDependencies: -Dependencies -============ +Installation dependencies +========================= When you run CubicWeb from source, either by downloading the tarball or cloning the mercurial forest, here is the list of tools and libraries you need @@ -55,9 +55,7 @@ * plpythonu extension * tsearch2 extension (for postgres < 8.3, in postgres-contrib) -Other optional packages : - -: +Other optional packages: * fyzz - http://www.logilab.org/project/fyzz - http://pypi.python.org/pypi/fyzz - included in the forest, *to activate Sparql querying* diff -r 30de0be8f895 -r a176e68b7d0d doc/book/en/tutorials/tools/windmill.rst --- a/doc/book/en/tutorials/tools/windmill.rst Tue Sep 07 17:34:42 2010 +0200 +++ b/doc/book/en/tutorials/tools/windmill.rst Mon Sep 13 15:15:21 2010 +0200 @@ -29,7 +29,7 @@ However, the Windmill project doesn't release frequently. Our recommandation is to used the last snapshot of the Git repository: -.. sourcecode:: shell +.. sourcecode:: bash git clone git://github.com/windmill/windmill.git HEAD cd windmill diff -r 30de0be8f895 -r a176e68b7d0d hooks/syncschema.py --- a/hooks/syncschema.py Tue Sep 07 17:34:42 2010 +0200 +++ b/hooks/syncschema.py Mon Sep 13 15:15:21 2010 +0200 @@ -703,6 +703,45 @@ syssource.update_rdef_unique(session, rdef) self.unique_changed = True +class CWUniqueTogetherConstraintAddOp(MemSchemaOperation): + entity = None # make pylint happy + def precommit_event(self): + session = self.session + prefix = SQL_PREFIX + table = '%s%s' % (prefix, self.entity.constraint_of[0].name) + cols = ['%s%s' % (prefix, r.rtype.name) + for r in self.entity.relations] + dbhelper= session.pool.source('system').dbhelper + sql = dbhelper.sql_create_multicol_unique_index(table, cols) + session.system_sql(sql) + + # XXX revertprecommit_event + + def postcommit_event(self): + eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid) + attrs = [r.rtype.name for r in self.entity.relations] + eschema._unique_together.append(attrs) + +class CWUniqueTogetherConstraintDelOp(MemSchemaOperation): + entity = oldcstr = None # for pylint + cols = [] # for pylint + def precommit_event(self): + session = self.session + prefix = SQL_PREFIX + table = '%s%s' % (prefix, self.entity.type) + dbhelper= session.pool.source('system').dbhelper + cols = ['%s%s' % (prefix, c) for c in self.cols] + sql = dbhelper.sql_drop_multicol_unique_index(table, cols) + session.system_sql(sql) + + # XXX revertprecommit_event + + def postcommit_event(self): + eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid) + cols = set(self.cols) + unique_together = [ut for ut in eschema._unique_together + if set(ut) != cols] + eschema._unique_together = unique_together # operations for in-memory schema synchronization ############################# @@ -1049,17 +1088,19 @@ class AfterAddConstrainedByHook(SyncSchemaHook): - __regid__ = 'syncdelconstrainedby' + __regid__ = 'syncaddconstrainedby' __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by') events = ('after_add_relation',) def __call__(self): if self._cw.added_in_transaction(self.eidfrom): + # used by get_constraints() which is called in CWAttributeAddOp self._cw.transaction_data.setdefault(self.eidfrom, []).append(self.eidto) -class BeforeDeleteConstrainedByHook(AfterAddConstrainedByHook): +class BeforeDeleteConstrainedByHook(SyncSchemaHook): __regid__ = 'syncdelconstrainedby' + __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by') events = ('before_delete_relation',) def __call__(self): @@ -1075,6 +1116,32 @@ else: CWConstraintDelOp(self._cw, rdef=rdef, oldcstr=cstr) +# unique_together constraints +# XXX: use setoperations and before_add_relation here (on constraint_of and relations) +class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook): + __regid__ = 'syncadd_cwuniquetogether_constraint' + __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint') + events = ('after_add_entity', 'after_update_entity') + + def __call__(self): + CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity) + + +class BeforeDeleteConstraintOfHook(SyncSchemaHook): + __regid__ = 'syncdelconstraintof' + __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constraint_of') + events = ('before_delete_relation',) + + def __call__(self): + if self._cw.deleted_in_transaction(self.eidto): + return + schema = self._cw.vreg.schema + cstr = self._cw.entity_from_eid(self.eidfrom) + entity = schema.schema_by_eid(self.eidto) + cols = [r.rtype.name + for r in cstr.relations] + CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, oldcstr=cstr, cols=cols) + # permissions synchronization hooks ############################################ diff -r 30de0be8f895 -r a176e68b7d0d i18n/en.po --- a/i18n/en.po Tue Sep 07 17:34:42 2010 +0200 +++ b/i18n/en.po Mon Sep 13 15:15:21 2010 +0200 @@ -2016,9 +2016,6 @@ msgid "editable-table" msgstr "" -msgid "edition" -msgstr "" - msgid "eid" msgstr "" @@ -2717,6 +2714,9 @@ msgid "missing parameters for entity %s" msgstr "" +msgid "modification" +msgstr "" + msgid "modification_date" msgstr "modification date" @@ -2967,9 +2967,6 @@ msgid "permissions for this entity" msgstr "" -msgid "personnal informations" -msgstr "" - msgid "pick existing bookmarks" msgstr "" @@ -3029,6 +3026,9 @@ msgid "primary_email_object" msgstr "primary email of" +msgid "profile" +msgstr "" + msgid "progress" msgstr "" diff -r 30de0be8f895 -r a176e68b7d0d i18n/es.po --- a/i18n/es.po Tue Sep 07 17:34:42 2010 +0200 +++ b/i18n/es.po Mon Sep 13 15:15:21 2010 +0200 @@ -2100,9 +2100,6 @@ msgid "editable-table" msgstr "Tabla modificable" -msgid "edition" -msgstr "Edición" - msgid "eid" msgstr "eid" @@ -2831,6 +2828,9 @@ msgid "missing parameters for entity %s" msgstr "Parámetros faltantes a la entidad %s" +msgid "modification" +msgstr "" + msgid "modification_date" msgstr "Fecha de modificación" @@ -3082,9 +3082,6 @@ msgid "permissions for this entity" msgstr "Permisos para esta entidad" -msgid "personnal informations" -msgstr "Información personal" - msgid "pick existing bookmarks" msgstr "Seleccionar favoritos existentes" @@ -3144,6 +3141,9 @@ msgid "primary_email_object" msgstr "Dirección principal de correo electrónico de" +msgid "profile" +msgstr "" + msgid "progress" msgstr "Progreso" @@ -4136,5 +4136,11 @@ msgid "you should probably delete that property" msgstr "Debería probablamente suprimir esta propriedad" +#~ msgid "edition" +#~ msgstr "Edición" + #~ msgid "graphical workflow for %s" #~ msgstr "Gráfica del workflow por %s" + +#~ msgid "personnal informations" +#~ msgstr "Información personal" diff -r 30de0be8f895 -r a176e68b7d0d i18n/fr.po --- a/i18n/fr.po Tue Sep 07 17:34:42 2010 +0200 +++ b/i18n/fr.po Mon Sep 13 15:15:21 2010 +0200 @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: cubicweb 2.46.0\n" -"PO-Revision-Date: 2010-05-16 18:59+0200\n" +"PO-Revision-Date: 2010-09-06 21:44+0200\n" "Last-Translator: Logilab Team \n" "Language-Team: fr \n" "Language: \n" @@ -380,7 +380,7 @@ #, python-format msgid "Data connection graph for %s" -msgstr "" +msgstr "Graphique de connection des données pour %s" msgid "Date" msgstr "Date" @@ -447,7 +447,7 @@ msgstr "Information sur le ramasse-miette" msgid "Got rhythm?" -msgstr "" +msgstr "T'as le rythme ?" msgid "Help" msgstr "Aide" @@ -1546,7 +1546,7 @@ msgstr "contexte où ce composant doit être affiché" msgid "context where this facet should be displayed, leave empty for both" -msgstr "" +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" @@ -2096,9 +2096,6 @@ msgid "editable-table" msgstr "table éditable" -msgid "edition" -msgstr "édition" - msgid "eid" msgstr "eid" @@ -2827,6 +2824,9 @@ msgid "missing parameters for entity %s" msgstr "paramètres manquants pour l'entité %s" +msgid "modification" +msgstr "modification" + msgid "modification_date" msgstr "date de modification" @@ -3080,9 +3080,6 @@ msgid "permissions for this entity" msgstr "permissions pour cette entité" -msgid "personnal informations" -msgstr "informations personnelles" - msgid "pick existing bookmarks" msgstr "récupérer des signets existants" @@ -3142,6 +3139,9 @@ msgid "primary_email_object" msgstr "adresse principale de" +msgid "profile" +msgstr "profil" + msgid "progress" msgstr "avancement" @@ -4134,5 +4134,14 @@ msgid "you should probably delete that property" msgstr "vous devriez probablement supprimer cette propriété" +#~ msgid "edition" +#~ msgstr "édition" + #~ msgid "graphical workflow for %s" #~ msgstr "graphique du workflow pour %s" + +#~ msgid "personnal informations" +#~ msgstr "informations personnelles" + +#~ msgid "yams type, rdf type or mime type of the object" +#~ msgstr "type yams, vocabulaire rdf ou type mime de l'objet" diff -r 30de0be8f895 -r a176e68b7d0d migration.py --- a/migration.py Tue Sep 07 17:34:42 2010 +0200 +++ b/migration.py Mon Sep 13 15:15:21 2010 +0200 @@ -122,6 +122,7 @@ 'config': self.config, 'interactive_mode': interactive, } + self._context_stack = [] def __getattribute__(self, name): try: @@ -199,7 +200,8 @@ if not ask_confirm or self.confirm(msg): return meth(*args, **kwargs) - def confirm(self, question, shell=True, abort=True, retry=False, default='y'): + def confirm(self, question, shell=True, abort=True, retry=False, pdb=False, + default='y'): """ask for confirmation and return true on positive answer if `retry` is true the r[etry] answer may return 2 @@ -207,6 +209,8 @@ possibleanswers = ['y', 'n'] if abort: possibleanswers.append('abort') + if pdb: + possibleanswers.append('pdb') if shell: possibleanswers.append('shell') if retry: @@ -221,9 +225,13 @@ return 2 if answer == 'abort': raise SystemExit(1) - if shell and answer == 'shell': + if answer == 'shell': self.interactive_shell() - return self.confirm(question) + return self.confirm(question, shell, abort, retry, pdb, default) + if answer == 'pdb': + import pdb + pdb.set_trace() + return self.confirm(question, shell, abort, retry, pdb, default) return True def interactive_shell(self): @@ -277,6 +285,11 @@ context[attr[4:]] = getattr(self, attr) return context + def update_context(self, key, value): + for context in self._context_stack: + context[key] = value + self.__context[key] = value + def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs): """execute a migration script in interactive mode @@ -320,6 +333,7 @@ if not self.execscript_confirm(migrscript): return scriptlocals = self._create_context().copy() + self._context_stack.append(scriptlocals) if script_mode == 'python': if funcname is None: pyname = '__main__' @@ -345,6 +359,7 @@ import doctest doctest.testfile(migrscript, module_relative=False, optionflags=doctest.ELLIPSIS, globs=scriptlocals) + del self._context_stack[-1] def cmd_option_renamed(self, oldname, newname): """a configuration option has been renamed""" diff -r 30de0be8f895 -r a176e68b7d0d misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Tue Sep 07 17:34:42 2010 +0200 +++ b/misc/migration/bootstrapmigration_repository.py Mon Sep 13 15:15:21 2010 +0200 @@ -35,6 +35,9 @@ ss.execschemarql(rql, rdef, ss.rdef2rql(rdef, CSTRMAP, groupmap=None)) commit(ask_confirm=False) +if applcubicwebversion < (3, 9, 6) and cubicwebversion >= (3, 9, 6): + add_entity_type('CWUniqueTogetherConstraint') + if applcubicwebversion == (3, 6, 0) and cubicwebversion >= (3, 6, 0): CSTRMAP = dict(rql('Any T, X WHERE X is CWConstraintType, X name T', ask_confirm=False)) diff -r 30de0be8f895 -r a176e68b7d0d misc/scripts/detect_cycle.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/scripts/detect_cycle.py Mon Sep 13 15:15:21 2010 +0200 @@ -0,0 +1,15 @@ + +try: + rtype, = __args__ +except ValueError: + print 'USAGE: cubicweb-ctl shell detect_cycle.py -- ' + print + +graph = {} +for fromeid, toeid in rql('Any X,Y WHERE X %s Y' % rtype): + graph.setdefault(fromeid, []).append(toeid) + +from logilab.common.graph import get_cycles + +for cycle in get_cycles(graph): + print 'cycle', '->'.join(str(n) for n in cycle) diff -r 30de0be8f895 -r a176e68b7d0d rset.py --- a/rset.py Tue Sep 07 17:34:42 2010 +0200 +++ b/rset.py Mon Sep 13 15:15:21 2010 +0200 @@ -569,7 +569,8 @@ if i == col: continue coletype = self.description[row][i] - # None description possible on column resulting from an outer join + # None description possible on column resulting from an + # outer join if coletype is None or eschema(coletype).final: continue try: @@ -588,11 +589,20 @@ @cached def related_entity(self, row, col): - """try to get the related entity to extract format information if any""" + """given an cell of the result set, try to return a (entity, relation + name) tuple to which this cell is linked. + + This is especially useful when the cell is an attribute of an entity, + to get the entity to which this attribute belongs to. + """ rqlst = self.syntax_tree() + # UNION query, we've first to find a 'pivot' column to use to get the + # actual query from which the row is coming etype, locate_query_col = self._locate_query_params(rqlst, row, col) - # UNION query, find the subquery from which this entity has been found + # now find the query from which this entity has been found. Returned + # select node may be a subquery with different column indexes. select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0] + # then get the index of root query's col in the subquery col = rqlst.subquery_selection_index(select, col) if col is None: # XXX unexpected, should fix subquery_selection_index ? diff -r 30de0be8f895 -r a176e68b7d0d schema.py --- a/schema.py Tue Sep 07 17:34:42 2010 +0200 +++ b/schema.py Mon Sep 13 15:15:21 2010 +0200 @@ -571,13 +571,7 @@ rdef.name = rdef.name.lower() rdef.subject = bw_normalize_etype(rdef.subject) rdef.object = bw_normalize_etype(rdef.object) - try: - rdefs = super(CubicWebSchema, self).add_relation_def(rdef) - except BadSchemaDefinition: - reversed_etype_map = dict( (v, k) for k, v in ETYPE_NAME_MAP.iteritems() ) - if rdef.subject in reversed_etype_map or rdef.object in reversed_etype_map: - return - raise + rdefs = super(CubicWebSchema, self).add_relation_def(rdef) if rdefs: try: self._eid_index[rdef.eid] = rdefs diff -r 30de0be8f895 -r a176e68b7d0d schemas/bootstrap.py --- a/schemas/bootstrap.py Tue Sep 07 17:34:42 2010 +0200 +++ b/schemas/bootstrap.py Mon Sep 13 15:15:21 2010 +0200 @@ -154,6 +154,17 @@ value = String(description=_('depends on the constraint type')) +class CWUniqueTogetherConstraint(EntityType): + """defines a sql-level multicolumn unique index""" + __permissions__ = PUB_SYSTEM_ENTITY_PERMS + constraint_of = SubjectRelation('CWEType', cardinality='1*', composite='object', + inlined=True) + relations = SubjectRelation(('CWAttribute', 'CWRelation'), cardinality='+*', + constraints=[RQLConstraint( + 'O from_entity X, S constraint_of X, O relation_type T, ' + 'T final TRUE OR (T final FALSE AND T inlined TRUE)')]) + + class CWConstraintType(EntityType): """define a schema constraint type""" __permissions__ = PUB_SYSTEM_ENTITY_PERMS diff -r 30de0be8f895 -r a176e68b7d0d server/checkintegrity.py --- a/server/checkintegrity.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/checkintegrity.py Mon Sep 13 15:15:21 2010 +0200 @@ -240,7 +240,12 @@ table, column, column, eid) session.system_sql(sql) continue - cursor = session.system_sql('SELECT eid_from FROM %s_relation;' % rschema) + try: + cursor = session.system_sql('SELECT eid_from FROM %s_relation;' % rschema) + except Exception, ex: + # usually because table doesn't exist + print 'ERROR', ex + continue for row in cursor.fetchall(): eid = row[0] if not has_eid(session, cursor, eid, eids): diff -r 30de0be8f895 -r a176e68b7d0d server/migractions.py --- a/server/migractions.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/migractions.py Mon Sep 13 15:15:21 2010 +0200 @@ -50,12 +50,15 @@ from yams.schema2sql import eschema2sql, rschema2sql from cubicweb import AuthenticationError, ExecutionError +from cubicweb.selectors import is_instance from cubicweb.schema import (ETYPE_NAME_MAP, META_RTYPES, VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES, CubicWebRelationSchema, order_eschemas) +from cubicweb.cwvreg import CW_EVENT_MANAGER from cubicweb.dbapi import get_repository, repo_connect from cubicweb.migration import MigrationHelper, yes from cubicweb.server.session import hooks_control +from cubicweb.server import hook try: from cubicweb.server import SOURCE_TYPES, schemaserial as ss from cubicweb.server.utils import manager_userpasswd, ask_source_config @@ -63,6 +66,20 @@ except ImportError: # LAX pass +class ClearGroupMap(hook.Hook): + __regid__ = 'cw.migration.clear_group_mapping' + __select__ = hook.Hook.__select__ & is_instance('CWGroup') + events = ('after_add_entity', 'after_update_entity',) + def __call__(self): + clear_cache(self.mih, 'group_mapping') + self.mih._synchronized.clear() + + @classmethod + def mih_register(cls, repo): + # may be already registered in tests (e.g. unittest_migractions at + # least) + if not cls.__regid__ in repo.vreg['after_add_entity_hooks']: + repo.vreg.register(ClearGroupMap) class ServerMigrationHelper(MigrationHelper): """specific migration helper for server side migration scripts, @@ -83,8 +100,17 @@ self.repo_connect() # no config on shell to a remote instance if config is not None and (cnx or connect): + repo = self.repo self.session.data['rebuild-infered'] = False - self.repo.hm.call_hooks('server_maintenance', repo=self.repo) + # register a hook to clear our group_mapping cache and the + # self._synchronized set when some group is added or updated + ClearGroupMap.mih = self + ClearGroupMap.mih_register(repo) + CW_EVENT_MANAGER.bind('after-registry-reload', + ClearGroupMap.mih_register, repo) + # notify we're starting maintenance (called instead of server_start + # which is called on regular start + repo.hm.call_hooks('server_maintenance', repo=repo) if not schema and not getattr(config, 'quick_start', False): schema = config.load_schema(expand_cubes=True) self.fs_schema = schema @@ -272,7 +298,7 @@ if self.session: self.session.set_pool() - def rqlexecall(self, rqliter, ask_confirm=True): + def rqlexecall(self, rqliter, ask_confirm=False): for rql, kwargs in rqliter: self.rqlexec(rql, kwargs, ask_confirm=ask_confirm) @@ -451,6 +477,7 @@ * description * internationalizable, fulltextindexed, indexed, meta * relations from/to this entity + * __unique_together__ * permissions if `syncperms` """ etype = str(etype) @@ -498,6 +525,44 @@ continue self._synchronize_rdef_schema(subj, rschema, obj, syncprops=syncprops, syncperms=syncperms) + if syncprops: # need to process __unique_together__ after rdefs were processed + repo_unique_together = set([frozenset(ut) + for ut in repoeschema._unique_together]) + unique_together = set([frozenset(ut) + for ut in eschema._unique_together]) + for ut in repo_unique_together - unique_together: + restrictions = ', '.join(['C relations R%(i)d, ' + 'R%(i)d relation_type T%(i)d, ' + 'R%(i)d from_entity X, ' + 'T%(i)d name %%(T%(i)d)s' % {'i': i, + 'col':col} + for (i, col) in enumerate(ut)]) + substs = {'etype': etype} + for i, col in enumerate(ut): + substs['T%d'%i] = col + self.rqlexec('DELETE CWUniqueTogetherConstraint C ' + 'WHERE C constraint_of E, ' + ' E name %%(etype)s,' + ' %s' % restrictions, + substs) + for ut in unique_together - repo_unique_together: + relations = ', '.join(['C relations R%d' % i + for (i, col) in enumerate(ut)]) + restrictions = ', '.join(['R%(i)d relation_type T%(i)d, ' + 'R%(i)d from_entity E, ' + 'T%(i)d name %%(T%(i)d)s' % {'i': i, + 'col':col} + for (i, col) in enumerate(ut)]) + substs = {'etype': etype} + for i, col in enumerate(ut): + substs['T%d'%i] = col + self.rqlexec('INSERT CWUniqueTogetherConstraint C:' + ' C constraint_of E, ' + ' %s ' + 'WHERE ' + ' E name %%(etype)s,' + ' %s' % (relations, restrictions), + substs) def _synchronize_rdef_schema(self, subjtype, rtype, objtype, syncperms=True, syncprops=True): @@ -592,7 +657,8 @@ newcubes_schema = self.config.load_schema(construction_mode='non-strict') # XXX we have to replace fs_schema, used in cmd_add_relation_type # etc. and fsschema of migration script contexts - self.fs_schema = self._create_context()['fsschema'] = newcubes_schema + self.fs_schema = newcubes_schema + self.update_context('fsschema', self.fs_schema) new = set() # execute pre-create files driver = self.repo.system_source.dbdriver @@ -710,13 +776,8 @@ targeted type is known """ instschema = self.repo.schema - assert not etype in instschema - # # XXX (syt) plz explain: if we're adding an entity type, it should - # # not be there... - # eschema = instschema[etype] - # if eschema.final: - # instschema.del_entity_type(etype) - # else: + assert not etype in instschema, \ + '%s already defined in the instance schema' % etype eschema = self.fs_schema.eschema(etype) confirm = self.verbosity >= 2 groupmap = self.group_mapping() @@ -885,21 +946,49 @@ self.sqlexec('UPDATE %s_relation SET eid_to=%s WHERE eid_to=%s' % (rtype, new.eid, oldeid), ask_confirm=False) # delete relations using SQL to avoid relations content removal - # triggered by schema synchronization hooks. Should add deleted eids - # into pending eids else we may get some validation error on commit - # since integrity hooks may think some required relation is - # missing... - pending = self.session.transaction_data.setdefault('pendingeids', set()) + # triggered by schema synchronization hooks. + session = self.session for rdeftype in ('CWRelation', 'CWAttribute'): + thispending = set() for eid, in self.sqlexec('SELECT cw_eid FROM cw_%s ' 'WHERE cw_from_entity=%%(eid)s OR ' ' cw_to_entity=%%(eid)s' % rdeftype, {'eid': oldeid}, ask_confirm=False): - pending.add(eid) + # we should add deleted eids into pending eids else we may + # get some validation error on commit since integrity hooks + # may think some required relation is missing... This also ensure + # repository caches are properly cleanup + hook.set_operation(session, 'pendingeids', eid, + hook.CleanupDeletedEidsCacheOp) + # and don't forget to remove record from system tables + self.repo.system_source.delete_info( + session, session.entity_from_eid(eid, rdeftype), + 'system', None) + thispending.add(eid) self.sqlexec('DELETE FROM cw_%s ' 'WHERE cw_from_entity=%%(eid)s OR ' 'cw_to_entity=%%(eid)s' % rdeftype, {'eid': oldeid}, ask_confirm=False) + # now we have to manually cleanup relations pointing to deleted + # entities + thiseids = ','.join(str(eid) for eid in thispending) + for rschema, ttypes, role in schema[rdeftype].relation_definitions(): + if rschema.type in VIRTUAL_RTYPES: + continue + sqls = [] + if role == 'object': + if rschema.inlined: + for eschema in ttypes: + sqls.append('DELETE FROM cw_%s WHERE cw_%s IN(%%s)' + % (eschema, rschema)) + else: + sqls.append('DELETE FROM %s_relation WHERE eid_to IN(%%s)' + % rschema) + elif not rschema.inlined: + sqls.append('DELETE FROM %s_relation WHERE eid_from IN(%%s)' + % rschema) + for sql in sqls: + self.sqlexec(sql % thiseids, ask_confirm=False) # remove the old type: use rql to propagate deletion self.rqlexec('DELETE CWEType ET WHERE ET name %(on)s', {'on': oldname}, ask_confirm=False) @@ -1299,7 +1388,7 @@ cu = self.session.system_sql(sql, args) except: ex = sys.exc_info()[1] - if self.confirm('Error: %s\nabort?' % ex): + if self.confirm('Error: %s\nabort?' % ex, pdb=True): raise return try: @@ -1309,7 +1398,7 @@ return def rqlexec(self, rql, kwargs=None, cachekey=None, build_descr=True, - ask_confirm=True): + ask_confirm=False): """rql action""" if cachekey is not None: warn('[3.8] cachekey is deprecated, you can safely remove this argument', @@ -1327,7 +1416,7 @@ try: res = execute(rql, kwargs, build_descr=build_descr) except Exception, ex: - if self.confirm('Error: %s\nabort?' % ex): + if self.confirm('Error: %s\nabort?' % ex, pdb=True): raise return res diff -r 30de0be8f895 -r a176e68b7d0d server/repository.py --- a/server/repository.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/repository.py Mon Sep 13 15:15:21 2010 +0200 @@ -50,14 +50,13 @@ UnknownEid, AuthenticationError, ExecutionError, ETypeNotSupportedBySources, MultiSourcesError, BadConnectionId, Unauthorized, ValidationError, - RepositoryError, typed_eid, onevent) + RepositoryError, UniqueTogetherError, typed_eid, onevent) from cubicweb import cwvreg, schema, server from cubicweb.server import utils, hook, pool, querier, sources from cubicweb.server.session import Session, InternalSession, InternalManager, \ security_enabled from cubicweb.server.ssplanner import EditedEntity - def del_existing_rel_if_needed(session, eidfrom, rtype, eidto): """delete existing relation when adding a new one if card is 1 or ? @@ -82,14 +81,14 @@ # not expected for this). So: don't do it, we pretend to ensure repository # consistency. # - # XXX we don't want read permissions to be applied but we want delete - # permission to be checked - rschema = session.repo.schema.rschema(rtype) - if card[0] in '1?': - if not rschema.inlined: # inlined relations will be implicitly deleted - with security_enabled(session, read=False): - session.execute('DELETE X %s Y WHERE X eid %%(x)s, ' - 'NOT Y eid %%(y)s' % rtype, + # notes: + # * inlined relations will be implicitly deleted for the subject entity + # * we don't want read permissions to be applied but we want delete + # permission to be checked + if card[0] in '1?' and not session.repo.schema.rschema(rtype).inlined: + with security_enabled(session, read=False): + session.execute('DELETE X %s Y WHERE X eid %%(x)s, ' + 'NOT Y eid %%(y)s' % rtype, {'x': eidfrom, 'y': eidto}) if card[1] in '1?': with security_enabled(session, read=False): @@ -1070,10 +1069,16 @@ edited.set_defaults() if session.is_hook_category_activated('integrity'): edited.check(creation=True) - source.add_entity(session, entity) + try: + source.add_entity(session, entity) + except UniqueTogetherError, exc: + etype, rtypes = exc.args + problems = {} + for col in rtypes: + problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes)) + raise ValidationError(entity.eid, problems) + self.add_info(session, entity, source, extid, complete=False) edited.saved = True - self.add_info(session, entity, source, extid, complete=False) - entity._cw_is_saved = True # entity has an eid and is saved # prefill entity relation caches for rschema in eschema.subject_relations(): rtype = str(rschema) @@ -1089,9 +1094,10 @@ if rtype in schema.VIRTUAL_RTYPES: continue entity.cw_set_relation_cache(rtype, 'object', session.empty_rset()) - # set inline relation cache before call to after_add_entity + # set inlined relation cache before call to after_add_entity for attr, value in relations: session.update_rel_cache_add(entity.eid, attr, value) + del_existing_rel_if_needed(session, entity.eid, attr, value) # trigger after_add_entity after after_add_relation if source.should_call_hooks: self.hm.call_hooks('after_add_entity', session, entity=entity) @@ -1142,15 +1148,22 @@ relations.append((attr, edited[attr], previous_value)) if source.should_call_hooks: # call hooks for inlined relations - for attr, value, _ in relations: + for attr, value, _t in relations: hm.call_hooks('before_add_relation', session, eidfrom=entity.eid, rtype=attr, eidto=value) if not only_inline_rels: hm.call_hooks('before_update_entity', session, entity=entity) if session.is_hook_category_activated('integrity'): edited.check() - source.update_entity(session, entity) - edited.saved = True + try: + source.update_entity(session, entity) + edited.saved = True + except UniqueTogetherError, exc: + etype, rtypes = exc.args + problems = {} + for col in rtypes: + problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes)) + raise ValidationError(entity.eid, problems) self.system_source.update_info(session, entity, need_fti_update) if source.should_call_hooks: if not only_inline_rels: diff -r 30de0be8f895 -r a176e68b7d0d server/schemaserial.py --- a/server/schemaserial.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/schemaserial.py Mon Sep 13 15:15:21 2010 +0200 @@ -24,7 +24,7 @@ from logilab.common.shellutils import ProgressBar -from yams import schema as schemamod, buildobjs as ybo +from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo from cubicweb import CW_SOFTWARE_ROOT, typed_eid from cubicweb.schema import (CONSTRAINTS, ETYPE_NAME_MAP, @@ -87,7 +87,7 @@ """ repo = session.repo dbhelper = repo.system_source.dbhelper - # 3.6 migration + # XXX bw compat (3.6 migration) sqlcu = session.pool['system'] sqlcu.execute("SELECT * FROM cw_CWRType WHERE cw_name='symetric'") if sqlcu.fetchall(): @@ -95,8 +95,10 @@ dbhelper.TYPE_MAPPING['Boolean'], True) sqlcu.execute(sql) sqlcu.execute("UPDATE cw_CWRType SET cw_name='symmetric' WHERE cw_name='symetric'") - sidx = {} - permsdict = deserialize_ertype_permissions(session) + session.commit(False) + ertidx = {} + copiedeids = set() + permsidx = deserialize_ertype_permissions(session) schema.reading_from_database = True for eid, etype, desc in session.execute( 'Any X, N, D WHERE X is CWEType, X name N, X description D', @@ -106,7 +108,7 @@ # just set the eid eschema = schema.eschema(etype) eschema.eid = eid - sidx[eid] = eschema + ertidx[eid] = etype continue if etype in ETYPE_NAME_MAP: needcopy = False @@ -115,7 +117,8 @@ sqlexec = session.system_sql if sqlexec('SELECT 1 FROM %(p)sCWEType WHERE %(p)sname=%%(n)s' % {'p': sqlutils.SQL_PREFIX}, {'n': netype}).fetchone(): - # the new type already exists, we should merge + # the new type already exists, we should copy (eg make existing + # instances of the old type instances of the new type) assert etype.lower() != netype.lower() needcopy = True else: @@ -139,16 +142,16 @@ repo.clear_caches(tocleanup) session.commit(False) if needcopy: - from logilab.common.testlib import mock_object - sidx[eid] = mock_object(type=netype) + ertidx[eid] = netype + copiedeids.add(eid) # copy / CWEType entity removal expected to be done through # rename_entity_type in a migration script continue etype = netype - etype = ybo.EntityType(name=etype, description=desc, eid=eid) - eschema = schema.add_entity_type(etype) - sidx[eid] = eschema - set_perms(eschema, permsdict) + ertidx[eid] = etype + eschema = schema.add_entity_type( + ybo.EntityType(name=etype, description=desc, eid=eid)) + set_perms(eschema, permsidx) for etype, stype in session.execute( 'Any XN, ETN WHERE X is CWEType, X name XN, X specializes ET, ET name ETN', build_descr=False): @@ -159,43 +162,81 @@ for eid, rtype, desc, sym, il, ftc in session.execute( 'Any X,N,D,S,I,FTC WHERE X is CWRType, X name N, X description D, ' 'X symmetric S, X inlined I, X fulltext_container FTC', build_descr=False): - rtype = ybo.RelationType(name=rtype, description=desc, - symmetric=bool(sym), inlined=bool(il), - fulltext_container=ftc, eid=eid) - rschema = schema.add_relation_type(rtype) - sidx[eid] = rschema - cstrsdict = deserialize_rdef_constraints(session) + ertidx[eid] = rtype + rschema = schema.add_relation_type( + ybo.RelationType(name=rtype, description=desc, + symmetric=bool(sym), inlined=bool(il), + fulltext_container=ftc, eid=eid)) + cstrsidx = deserialize_rdef_constraints(session) + pendingrdefs = [] + # closure to factorize common code of attribute/relation rdef addition + def _add_rdef(rdefeid, seid, reid, oeid, **kwargs): + rdef = ybo.RelationDefinition(ertidx[seid], ertidx[reid], ertidx[oeid], + constraints=cstrsidx.get(rdefeid, ()), + eid=rdefeid, **kwargs) + if seid in copiedeids or oeid in copiedeids: + # delay addition of this rdef. We'll insert them later if needed. We + # have to do this because: + # + # * on etype renaming, we want relation of the old entity type being + # redirected to the new type during migration + # + # * in the case of a copy, we've to take care that rdef already + # existing in the schema are not overwritten by a redirected one, + # since we want correct eid on them (redirected rdef will be + # removed in rename_entity_type) + pendingrdefs.append(rdef) + else: + # add_relation_def return a RelationDefinitionSchema if it has been + # actually added (can be None on duplicated relation definitions, + # e.g. if the relation type is marked as beeing symmetric) + rdefs = schema.add_relation_def(rdef) + if rdefs is not None: + ertidx[rdefeid] = rdefs + set_perms(rdefs, permsidx) + for values in session.execute( 'Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT WHERE X is CWAttribute,' 'X relation_type RT, X cardinality CARD, X ordernum ORD, X indexed IDX,' 'X description DESC, X internationalizable I18N, X defaultval DFLT,' 'X fulltextindexed FTIDX, X from_entity SE, X to_entity OE', build_descr=False): - rdefeid, seid, reid, teid, card, ord, desc, idx, ftidx, i18n, default = values - rdef = ybo.RelationDefinition(sidx[seid].type, sidx[reid].type, sidx[teid].type, - cardinality=card, - constraints=cstrsdict.get(rdefeid, ()), - order=ord, description=desc, - indexed=idx, fulltextindexed=ftidx, - internationalizable=i18n, - default=default, eid=rdefeid) - rdefs = schema.add_relation_def(rdef) - # rdefs can be None on duplicated relation definitions (e.g. symmetrics) - if rdefs is not None: - set_perms(rdefs, permsdict) + rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values + _add_rdef(rdefeid, seid, reid, oeid, + cardinality=card, description=desc, order=ord, + indexed=idx, fulltextindexed=ftidx, internationalizable=i18n, + default=default) for values in session.execute( 'Any X,SE,RT,OE,CARD,ORD,DESC,C WHERE X is CWRelation, X relation_type RT,' 'X cardinality CARD, X ordernum ORD, X description DESC, ' 'X from_entity SE, X to_entity OE, X composite C', build_descr=False): - rdefeid, seid, reid, teid, card, ord, desc, c = values - rdef = ybo.RelationDefinition(sidx[seid].type, sidx[reid].type, sidx[teid].type, - constraints=cstrsdict.get(rdefeid, ()), - cardinality=card, order=ord, description=desc, - composite=c, eid=rdefeid) - rdefs = schema.add_relation_def(rdef) - # rdefs can be None on duplicated relation definitions (e.g. symmetrics) + rdefeid, seid, reid, oeid, card, ord, desc, comp = values + _add_rdef(rdefeid, seid, reid, oeid, + cardinality=card, description=desc, order=ord, + composite=comp) + for rdef in pendingrdefs: + try: + rdefs = schema.add_relation_def(rdef) + except BadSchemaDefinition: + continue if rdefs is not None: - set_perms(rdefs, permsdict) + set_perms(rdefs, permsidx) + unique_togethers = {} + try: + rset = session.execute( + 'Any X,E,R WHERE ' + 'X is CWUniqueTogetherConstraint, ' + 'X constraint_of E, X relations R', build_descr=False) + except Exception: + session.rollback() # first migration introducing CWUniqueTogetherConstraint cw 3.9.6 + else: + for values in rset: + uniquecstreid, eeid, releid = values + eschema = schema.schema_by_eid(eeid) + relations = unique_togethers.setdefault(uniquecstreid, (eschema, [])) + relations[1].append(ertidx[releid].rtype.type) + for eschema, unique_together in unique_togethers.itervalues(): + eschema._unique_together.append(tuple(sorted(unique_together))) schema.infer_specialization_rules() session.commit() schema.reading_from_database = False @@ -232,7 +273,7 @@ res.setdefault(rdefeid, []).append(cstr) return res -def set_perms(erschema, permsdict): +def set_perms(erschema, permsidx): """set permissions on the given erschema according to the permission definition dictionary as built by deserialize_ertype_permissions for a given erschema's eid @@ -240,7 +281,7 @@ # reset erschema permissions here to avoid getting yams default anyway erschema.permissions = dict((action, ()) for action in erschema.ACTIONS) try: - thispermsdict = permsdict[erschema.eid] + thispermsdict = permsidx[erschema.eid] except KeyError: return for action, somethings in thispermsdict.iteritems(): @@ -309,6 +350,10 @@ rdef2rql(rdef, cstrtypemap, groupmap)) if pb is not None: pb.update() + # serialize unique_together constraints + for eschema in eschemas: + for unique_together in eschema._unique_together: + execschemarql(execute, eschema, [uniquetogether2rql(eschema, unique_together)]) for rql, kwargs in specialize2rql(schema): execute(rql, kwargs, build_descr=False) if pb is not None: @@ -366,6 +411,31 @@ values = {'x': eschema.eid, 'et': specialized_type.eid} yield 'SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', values +def uniquetogether2rql(eschema, unique_together): + relations = [] + restrictions = [] + substs = {} + for i, name in enumerate(unique_together): + rschema = eschema.rdef(name) + var = 'R%d' % i + rtype = 'T%d' % i + substs[rtype] = rschema.rtype.type + relations.append('C relations %s' % var) + restrictions.append('%(var)s from_entity X, ' + '%(var)s relation_type %(rtype)s, ' + '%(rtype)s name %%(%(rtype)s)s' \ + % {'var': var, + 'rtype':rtype}) + relations = ', '.join(relations) + restrictions = ', '.join(restrictions) + rql = ('INSERT CWUniqueTogetherConstraint C: ' + ' C constraint_of X, %s ' + 'WHERE ' + ' X eid %%(x)s, %s' ) + + return rql % (relations, restrictions), substs + + def _ervalues(erschema): try: type_ = unicode(erschema.type) diff -r 30de0be8f895 -r a176e68b7d0d server/serverctl.py --- a/server/serverctl.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/serverctl.py Mon Sep 13 15:15:21 2010 +0200 @@ -814,6 +814,7 @@ ) def run(self, args): + from cubicweb.server.checkintegrity import check appid = args[0] config = ServerConfiguration.config_for(appid) config.repairing = self.config.force diff -r 30de0be8f895 -r a176e68b7d0d server/sources/native.py --- a/server/sources/native.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/sources/native.py Mon Sep 13 15:15:21 2010 +0200 @@ -34,6 +34,7 @@ from base64 import b64decode, b64encode from contextlib import contextmanager from os.path import abspath +import re from logilab.common.compat import any from logilab.common.cache import Cache @@ -44,7 +45,7 @@ from yams import schema2sql as y2sql -from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary +from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary, UniqueTogetherError from cubicweb import transaction as tx, server, neg_role from cubicweb.schema import VIRTUAL_RTYPES from cubicweb.cwconfig import CubicWebNoAppConfiguration @@ -211,7 +212,7 @@ 'default': 'postgres', # XXX use choice type 'help': 'database driver (postgres, mysql, sqlite, sqlserver2005)', - 'group': 'native-source', 'level': 1, + 'group': 'native-source', 'level': 0, }), ('db-host', {'type' : 'string', @@ -666,6 +667,16 @@ self.critical('transaction has been rollbacked') except: pass + if ex.__class__.__name__ == 'IntegrityError': + # need string comparison because of various backends + for arg in ex.args: + mo = re.search('unique_cw_[^ ]+_idx', arg) + if mo is not None: + index_name = mo.group(0) + elements = index_name.rstrip('_idx').split('_cw_')[1:] + etype = elements[0] + rtypes = elements[1:] + raise UniqueTogetherError(etype, rtypes) raise return cursor diff -r 30de0be8f895 -r a176e68b7d0d server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/data/migratedapp/schema.py Mon Sep 13 15:15:21 2010 +0200 @@ -101,6 +101,7 @@ class Personne(EntityType): + __unique_together__ = [('nom', 'prenom', 'datenaiss')] nom = String(fulltextindexed=True, required=True, maxsize=64) prenom = String(fulltextindexed=True, maxsize=64) civility = String(maxsize=1, default='M', fulltextindexed=True) @@ -126,7 +127,6 @@ 'delete': ('managers', 'owners'), 'add': ('managers', 'users',) } - nom = String(maxsize=64, fulltextindexed=True) web = String(maxsize=128) tel = Int() diff -r 30de0be8f895 -r a176e68b7d0d server/test/data/schema.py --- a/server/test/data/schema.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/data/schema.py Mon Sep 13 15:15:21 2010 +0200 @@ -98,6 +98,7 @@ todo_by = SubjectRelation('CWUser') class Personne(EntityType): + __unique_together__ = [('nom', 'prenom', 'inline2')] nom = String(fulltextindexed=True, required=True, maxsize=64) prenom = String(fulltextindexed=True, maxsize=64) sexe = String(maxsize=1, default='M', fulltextindexed=True) diff -r 30de0be8f895 -r a176e68b7d0d server/test/unittest_fti.py --- a/server/test/unittest_fti.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/unittest_fti.py Mon Sep 13 15:15:21 2010 +0200 @@ -1,5 +1,7 @@ from __future__ import with_statement +import socket + from cubicweb.devtools import ApptestConfiguration from cubicweb.devtools.testlib import CubicWebTC from cubicweb.selectors import is_instance @@ -8,6 +10,11 @@ class PostgresFTITC(CubicWebTC): config = ApptestConfiguration('data', sourcefile='sources_fti') + def setUp(self): + if not socket.gethostname().endswith('.logilab.fr'): + self.skip('XXX require logilab configuration') + super(PostgresFTITC, self).setUp() + def test_occurence_count(self): req = self.request() c1 = req.create_entity('Card', title=u'c1', diff -r 30de0be8f895 -r a176e68b7d0d server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/unittest_migractions.py Mon Sep 13 15:15:21 2010 +0200 @@ -309,6 +309,7 @@ migrschema['titre'].rdefs[('Personne', 'String')].description = 'title for this person' delete_concerne_rqlexpr = self._rrqlexpr_rset('delete', 'concerne') add_concerne_rqlexpr = self._rrqlexpr_rset('add', 'concerne') + self.mh.cmd_sync_schema_props_perms(commit=False) self.assertEquals(cursor.execute('Any D WHERE X name "Personne", X description D')[0][0], @@ -380,8 +381,15 @@ # finally self.assertEquals(cursor.execute('Any COUNT(X) WHERE X is RQLExpression')[0][0], nbrqlexpr_start + 1 + 2 + 2) - - self.mh.rollback() + self.mh.commit() + # unique_together test + self.assertEqual(len(self.schema.eschema('Personne')._unique_together), 1) + self.assertUnorderedIterableEquals(self.schema.eschema('Personne')._unique_together[0], + ('nom', 'prenom', 'datenaiss')) + rset = cursor.execute('Any C WHERE C is CWUniqueTogetherConstraint') + self.assertEquals(len(rset), 1) + relations = [r.rtype.name for r in rset.get_entity(0,0).relations] + self.assertUnorderedIterableEquals(relations, ('nom', 'prenom', 'datenaiss')) def _erqlexpr_rset(self, action, ertype): rql = 'RQLExpression X WHERE ET is CWEType, ET %s_permission X, ET name %%(name)s' % action diff -r 30de0be8f895 -r a176e68b7d0d server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/unittest_msplanner.py Mon Sep 13 15:15:21 2010 +0200 @@ -60,6 +60,7 @@ {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, {'X': 'CWEType'}, {'X': 'CWGroup'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, {'X': 'CWRType'}, {'X': 'CWRelation'}, {'X': 'CWUser'}, + {'X': 'CWUniqueTogetherConstraint'}, {'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailAddress'}, {'X': 'EmailPart'}, {'X': 'EmailThread'}, {'X': 'ExternalUri'}, {'X': 'File'}, @@ -893,13 +894,14 @@ [{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])], [self.cards, self.system], {}, {'X': 'table0.C0'}, []), ('FetchStep', - [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', + [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', [{'X': 'BaseTransition'}, {'X': 'Bookmark'}, {'X': 'CWAttribute'}, {'X': 'CWCache'}, {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, {'X': 'CWEType'}, {'X': 'CWGroup'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, {'X': 'CWRType'}, {'X': 'CWRelation'}, + {'X': 'CWUniqueTogetherConstraint'}, {'X': 'Comment'}, {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailAddress'}, {'X': 'EmailPart'}, {'X': 'EmailThread'}, @@ -954,14 +956,16 @@ [self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), # extra UnionFetchStep could be avoided but has no cost, so don't care ('UnionFetchStep', - [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', + [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', [{'X': 'BaseTransition', 'ET': 'CWEType'}, {'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'}, {'X': 'CWCache', 'ET': 'CWEType'}, {'X': 'CWConstraint', 'ET': 'CWEType'}, {'X': 'CWConstraintType', 'ET': 'CWEType'}, {'X': 'CWEType', 'ET': 'CWEType'}, {'X': 'CWGroup', 'ET': 'CWEType'}, {'X': 'CWPermission', 'ET': 'CWEType'}, {'X': 'CWProperty', 'ET': 'CWEType'}, {'X': 'CWRType', 'ET': 'CWEType'}, - {'X': 'CWRelation', 'ET': 'CWEType'}, {'X': 'Comment', 'ET': 'CWEType'}, + {'X': 'CWRelation', 'ET': 'CWEType'}, + {'X': 'CWUniqueTogetherConstraint', 'ET': 'CWEType'}, + {'X': 'Comment', 'ET': 'CWEType'}, {'X': 'Division', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'}, {'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'}, {'X': 'EmailThread', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ET': 'CWEType'}, @@ -2253,7 +2257,7 @@ None, {'X': 'table0.C0'}, []), ('UnionStep', None, None, [('OneFetchStep', - [(u'Any X WHERE X owned_by U, U login "anon", U is CWUser, X is IN(Affaire, BaseTransition, Basket, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', + [(u'Any X WHERE X owned_by U, U login "anon", U is CWUser, X is IN(Affaire, BaseTransition, Basket, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, CWUser, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', [{'U': 'CWUser', 'X': 'Affaire'}, {'U': 'CWUser', 'X': 'BaseTransition'}, {'U': 'CWUser', 'X': 'Basket'}, @@ -2268,6 +2272,7 @@ {'U': 'CWUser', 'X': 'CWProperty'}, {'U': 'CWUser', 'X': 'CWRType'}, {'U': 'CWUser', 'X': 'CWRelation'}, + {'U': 'CWUser', 'X': 'CWUniqueTogetherConstraint'}, {'U': 'CWUser', 'X': 'CWUser'}, {'U': 'CWUser', 'X': 'Division'}, {'U': 'CWUser', 'X': 'Email'}, diff -r 30de0be8f895 -r a176e68b7d0d server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/unittest_multisources.py Mon Sep 13 15:15:21 2010 +0200 @@ -151,7 +151,7 @@ self.assertEquals(len(rset), 5, zip(rset.rows, rset.description)) rset = cu.execute('Any X ORDERBY FTIRANK(X) WHERE X has_text "card"') self.assertEquals(len(rset), 5, zip(rset.rows, rset.description)) - Connection_close(cnx) + Connection_close(cnx.cnx) # cnx is a TestCaseConnectionProxy def test_synchronization(self): cu = cnx2.cursor() diff -r 30de0be8f895 -r a176e68b7d0d server/test/unittest_querier.py --- a/server/test/unittest_querier.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/unittest_querier.py Mon Sep 13 15:15:21 2010 +0200 @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""unit tests for modules cubicweb.server.querier and cubicweb.server.querier_steps +"""unit tests for modules cubicweb.server.querier and cubicweb.server.ssplanner """ from datetime import date, datetime @@ -130,7 +130,7 @@ 'X': 'Affaire', 'ET': 'CWEType', 'ETN': 'String'}]) rql, solutions = partrqls[1] - self.assertEquals(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)') + self.assertEquals(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUniqueTogetherConstraint, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)') self.assertListEquals(sorted(solutions), sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'}, @@ -143,15 +143,16 @@ {'X': 'CWEType', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'CWGroup', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'CWRelation', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'CWPermission', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'CWProperty', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'CWRType', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'CWUniqueTogetherConstraint', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'CWUser', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Email', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'EmailAddress', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'EmailThread', 'ETN': 'String', 'ET': 'CWEType'}, - {'X': 'CWRelation', 'ETN': 'String', 'ET': 'CWEType'}, - {'X': 'CWPermission', 'ETN': 'String', 'ET': 'CWEType'}, - {'X': 'CWProperty', 'ETN': 'String', 'ET': 'CWEType'}, - {'X': 'CWRType', 'ETN': 'String', 'ET': 'CWEType'}, - {'X': 'CWUser', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'File', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Folder', 'ETN': 'String', 'ET': 'CWEType'}, @@ -493,14 +494,14 @@ [[u'description_format', 12], [u'description', 13], [u'name', 14], - [u'created_by', 37], - [u'creation_date', 37], - [u'cwuri', 37], - [u'in_basket', 37], - [u'is', 37], - [u'is_instance_of', 37], - [u'modification_date', 37], - [u'owned_by', 37]]) + [u'created_by', 38], + [u'creation_date', 38], + [u'cwuri', 38], + [u'in_basket', 38], + [u'is', 38], + [u'is_instance_of', 38], + [u'modification_date', 38], + [u'owned_by', 38]]) def test_select_aggregat_having_dumb(self): # dumb but should not raise an error @@ -691,19 +692,15 @@ self.assertEqual(len(rset.rows), 1, rset.rows) def test_select_ordered_distinct_1(self): - self.execute("INSERT Affaire X: X sujet 'cool', X ref '1'") - self.execute("INSERT Affaire X: X sujet 'cool', X ref '2'") - rset = self.execute('DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R') - self.assertEqual(rset.rows, [['cool']]) + self.assertRaises(BadRQLQuery, + self.execute, 'DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R') def test_select_ordered_distinct_2(self): self.execute("INSERT Affaire X: X sujet 'minor'") - self.execute("INSERT Affaire X: X sujet 'important'") - self.execute("INSERT Affaire X: X sujet 'normal'") self.execute("INSERT Affaire X: X sujet 'zou'") self.execute("INSERT Affaire X: X sujet 'abcd'") rset = self.execute('DISTINCT Any S ORDERBY S WHERE A is Affaire, A sujet S') - self.assertEqual(rset.rows, [['abcd'], ['important'], ['minor'], ['normal'], ['zou']]) + self.assertEqual(rset.rows, [['abcd'], ['minor'], ['zou']]) def test_select_ordered_distinct_3(self): rset = self.execute('DISTINCT Any N ORDERBY GROUP_SORT_VALUE(N) WHERE X is CWGroup, X name N') @@ -1085,14 +1082,14 @@ self.commit() # fill the cache self.execute("Any X WHERE X eid %(x)s", {'x': eid}) - self.execute("Any X WHERE X eid %s" %eid) + self.execute("Any X WHERE X eid %s" % eid) self.execute("Folder X WHERE X eid %(x)s", {'x': eid}) - self.execute("Folder X WHERE X eid %s" %eid) - self.execute("DELETE Folder T WHERE T eid %s"%eid) + self.execute("Folder X WHERE X eid %s" % eid) + self.execute("DELETE Folder T WHERE T eid %s" % eid) self.commit() rset = self.execute("Any X WHERE X eid %(x)s", {'x': eid}) self.assertEquals(rset.rows, []) - rset = self.execute("Any X WHERE X eid %s" %eid) + rset = self.execute("Any X WHERE X eid %s" % eid) self.assertEquals(rset.rows, []) rset = self.execute("Folder X WHERE X eid %(x)s", {'x': eid}) self.assertEquals(rset.rows, []) diff -r 30de0be8f895 -r a176e68b7d0d server/test/unittest_repository.py --- a/server/test/unittest_repository.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/unittest_repository.py Mon Sep 13 15:15:21 2010 +0200 @@ -74,9 +74,27 @@ (u'Int',), (u'Interval',), (u'Password',), (u'String',), (u'Time',)]) + sql = ("SELECT etype.cw_eid, etype.cw_name, cstr.cw_eid, rel.eid_to " + "FROM cw_CWUniqueTogetherConstraint as cstr, " + " relations_relation as rel, " + " cw_CWEType as etype " + "WHERE cstr.cw_eid = rel.eid_from " + " AND cstr.cw_constraint_of = etype.cw_eid " + " AND etype.cw_name = 'Personne' " + ";") + cu = self.session.system_sql(sql) + rows = cu.fetchall() + self.assertEquals(len(rows), 3) + self.test_unique_together() finally: self.repo.set_schema(origshema) + def test_unique_together(self): + person = self.repo.schema.eschema('Personne') + self.assertEquals(len(person._unique_together), 1) + self.assertUnorderedIterableEquals(person._unique_together[0], + ('nom', 'prenom', 'inline2')) + def test_schema_has_owner(self): repo = self.repo cnxid = repo.connect(self.admlogin, password=self.admpassword) @@ -385,6 +403,13 @@ self.assertEquals(len(rset), 1) self.assertEquals(rset.rows[0][0], p2.eid) + def test_delete_if_object_inlined_singlecard(self): + req = self.request() + c = req.create_entity('Card', title=u'Carte') + req.create_entity('Personne', nom=u'Vincent', fiche=c) + req.create_entity('Personne', nom=u'Florent', fiche=c) + self.commit() + self.assertEquals(len(c.reverse_fiche), 1) def test_set_attributes_in_before_update(self): # local hook diff -r 30de0be8f895 -r a176e68b7d0d server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Tue Sep 07 17:34:42 2010 +0200 +++ b/server/test/unittest_rql2sql.py Mon Sep 13 15:15:21 2010 +0200 @@ -429,11 +429,6 @@ GROUP BY _X.cw_data_name,_X.cw_data_format ORDER BY 1,2,_X.cw_data_format'''), - ('DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R', - '''SELECT T1.C0 FROM (SELECT DISTINCT _A.cw_sujet AS C0, _A.cw_ref AS C1 -FROM cw_Affaire AS _A -ORDER BY 2) AS T1'''), - ('DISTINCT Any MAX(X)+MIN(LENGTH(D)), N GROUPBY N ORDERBY 2, DF WHERE X data_name N, X data D, X data_format DF;', '''SELECT T1.C0,T1.C1 FROM (SELECT DISTINCT (MAX(_X.cw_eid) + MIN(LENGTH(_X.cw_data))) AS C0, _X.cw_data_name AS C1, _X.cw_data_format AS C2 FROM cw_File AS _X diff -r 30de0be8f895 -r a176e68b7d0d test/unittest_cwctl.py --- a/test/unittest_cwctl.py Tue Sep 07 17:34:42 2010 +0200 +++ b/test/unittest_cwctl.py Mon Sep 13 15:15:21 2010 +0200 @@ -39,7 +39,7 @@ def test_list(self): from cubicweb.cwctl import ListCommand - ListCommand().run([]) + ListCommand(None).run([]) class CubicWebShellTC(CubicWebTC): diff -r 30de0be8f895 -r a176e68b7d0d test/unittest_rset.py --- a/test/unittest_rset.py Tue Sep 07 17:34:42 2010 +0200 +++ b/test/unittest_rset.py Mon Sep 13 15:15:21 2010 +0200 @@ -368,6 +368,14 @@ 'WITH B,T BEING (Any B,T WHERE B is Bookmark, B title T)') rset.related_entity(0, 2) + def test_related_entity_subquery_outerjoin(self): + rset = self.execute('Any X,S,L WHERE X in_state S ' + 'WITH X, L BEING (Any X,MAX(L) GROUPBY X ' + 'WHERE X is CWUser, T? wf_info_for X, T creation_date L)') + self.assertEquals(len(rset), 2) + rset.related_entity(0, 1) + rset.related_entity(0, 2) + def test_entities(self): rset = self.execute('Any U,G WHERE U in_group G') # make sure we have at least one element diff -r 30de0be8f895 -r a176e68b7d0d utils.py --- a/utils.py Tue Sep 07 17:34:42 2010 +0200 +++ b/utils.py Mon Sep 13 15:15:21 2010 +0200 @@ -370,6 +370,7 @@ else: from logilab.common.date import ustrftime + class CubicWebJsonEncoder(json.JSONEncoder): """define a json encoder to be able to encode yams std types""" @@ -410,7 +411,7 @@ _THIS_MOD_NS = globals() for funcname in ('date_range', 'todate', 'todatetime', 'datetime2ticks', 'days_in_month', 'days_in_year', 'previous_month', - 'next_month', 'first_day', 'last_day', 'ustrftime', + 'next_month', 'first_day', 'last_day', 'strptime'): msg = '[3.6] %s has been moved to logilab.common.date' % funcname _THIS_MOD_NS[funcname] = deprecated(msg)(getattr(date, funcname)) diff -r 30de0be8f895 -r a176e68b7d0d web/data/cubicweb.ajax.box.js --- a/web/data/cubicweb.ajax.box.js Tue Sep 07 17:34:42 2010 +0200 +++ b/web/data/cubicweb.ajax.box.js Mon Sep 13 15:15:21 2010 +0200 @@ -11,71 +11,71 @@ var holderid = cw.utils.domid(boxid) + eid + 'Holder'; var value = $('#' + holderid + 'Input').val(); if (separator) { - value = $.map(value.split(separator), jQuery.trim); + value = $.map(value.split(separator), jQuery.trim); } var d = loadRemote('json', ajaxFuncArgs(fname, null, eid, value)); d.addCallback(function() { - $('#' + holderid).empty(); - var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid); - $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams); - if (msg) { - document.location.hash = '#header'; - updateMessage(msg); - } - }); + $('#' + holderid).empty(); + var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid); + $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams); + if (msg) { + document.location.hash = '#header'; + updateMessage(msg); + } + }); } function ajaxBoxRemoveLinkedEntity(boxid, eid, relatedeid, delfname, msg) { var d = loadRemote('json', ajaxFuncArgs(delfname, null, eid, relatedeid)); d.addCallback(function() { - var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid); - $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams); - if (msg) { - document.location.hash = '#header'; - updateMessage(msg); - } + var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid); + $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams); + if (msg) { + document.location.hash = '#header'; + updateMessage(msg); + } }); } function ajaxBoxShowSelector(boxid, eid, - unrelfname, - addfname, msg, - oklabel, cancellabel, - separator) { + unrelfname, + addfname, msg, + oklabel, cancellabel, + separator) { var holderid = cw.utils.domid(boxid) + eid + 'Holder'; var holder = $('#' + holderid); if (holder.children().length) { - holder.empty(); + holder.empty(); } else { - var inputid = holderid + 'Input'; - var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid)); - deferred.addCallback(function (unrelated) { - var input = INPUT({'type': 'text', 'id': inputid, 'size': 20}); - holder.append(input).show(); - $input = $(input); - $input.keypress(function (event) { - if (event.keyCode == KEYS.KEY_ENTER) { - // XXX not very user friendly: we should test that the suggestions - // aren't visible anymore - ajaxBoxValidateSelectorInput(boxid, eid, separator, addfname, msg); - } - }); - var buttons = DIV({'class' : "sgformbuttons"}, - A({'href' : "javascript: noop();", - 'onclick' : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput', - boxid, eid, separator, addfname, msg)}, - oklabel), - ' / ', - A({'href' : "javascript: noop();", - 'onclick' : '$("#' + holderid + '").empty()'}, - cancellabel)); - holder.append(buttons); - $input.autocomplete(unrelated, { - multiple: separator, - max: 15 - }); - $input.focus(); - }); + var inputid = holderid + 'Input'; + var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid)); + deferred.addCallback(function (unrelated) { + var input = INPUT({'type': 'text', 'id': inputid, 'size': 20}); + holder.append(input).show(); + $input = $(input); + $input.keypress(function (event) { + if (event.keyCode == KEYS.KEY_ENTER) { + // XXX not very user friendly: we should test that the suggestions + // aren't visible anymore + ajaxBoxValidateSelectorInput(boxid, eid, separator, addfname, msg); + } + }); + var buttons = DIV({'class' : "sgformbuttons"}, + A({'href' : "javascript: noop();", + 'onclick' : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput', + boxid, eid, separator, addfname, msg)}, + oklabel), + ' / ', + A({'href' : "javascript: noop();", + 'onclick' : '$("#' + holderid + '").empty()'}, + cancellabel)); + holder.append(buttons); + $input.autocomplete(unrelated, { + multiple: separator, + max: 15 + }); + $input.focus(); + }); } } diff -r 30de0be8f895 -r a176e68b7d0d web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Tue Sep 07 17:34:42 2010 +0200 +++ b/web/data/cubicweb.ajax.js Mon Sep 13 15:15:21 2010 +0200 @@ -94,28 +94,28 @@ function _loadAjaxHtmlHead($node, $head, tag, srcattr) { var jqtagfilter = tag + '[' + srcattr + ']'; if (cw['loaded_'+srcattr] === undefined) { - cw['loaded_'+srcattr] = []; - var loaded = cw['loaded_'+srcattr]; - jQuery('head ' + jqtagfilter).each(function(i) { - loaded.push(this.getAttribute(srcattr)); - }); + cw['loaded_'+srcattr] = []; + var loaded = cw['loaded_'+srcattr]; + jQuery('head ' + jqtagfilter).each(function(i) { + loaded.push(this.getAttribute(srcattr)); + }); } else { - var loaded = cw['loaded_'+srcattr]; + var loaded = cw['loaded_'+srcattr]; } $node.find(tag).each(function(i) { - var url = this.getAttribute(srcattr); + var url = this.getAttribute(srcattr); if (url) { if (jQuery.inArray(url, loaded) == -1) { - // take care to