# HG changeset patch # User Sylvain Thénault # Date 1297423067 -3600 # Node ID 8e59c2cdcc99c1b49c84f0fde614ce18946c8d6d # Parent 75a232b2e4776d8637556cd8dc10cb31d8dfe290# Parent a8fbcf9b65729ec0ee1d13d4aea8a82690cdca31 backport stable diff -r a8fbcf9b6572 -r 8e59c2cdcc99 __pkginfo__.py --- a/__pkginfo__.py Fri Feb 11 11:37:10 2011 +0100 +++ b/__pkginfo__.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# pylint: disable=W0622,C0103 + # pylint: disable=W0622,C0103 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 10, 8) +numversion = (3, 11, 0) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -43,7 +43,7 @@ 'logilab-common': '>= 0.54.0', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.28.0', - 'yams': '>= 0.30.1', + 'yams': '>= 0.30.4', 'docutils': '>= 0.6', #gettext # for xgettext, msgcat, etc... # web dependancies diff -r a8fbcf9b6572 -r 8e59c2cdcc99 dbapi.py --- a/dbapi.py Fri Feb 11 11:37:10 2011 +0100 +++ b/dbapi.py Fri Feb 11 12:17:47 2011 +0100 @@ -220,11 +220,10 @@ return False class DBAPISession(object): - def __init__(self, cnx, login=None, authinfo=None): + def __init__(self, cnx, login=None): self.cnx = cnx self.data = {} self.login = login - self.authinfo = authinfo # dbapi session identifier is the same as the first connection # identifier, but may later differ in case of auto-reconnection as done # by the web authentication manager (in cw.web.views.authentication) @@ -586,9 +585,8 @@ req = self.request() rset = req.eid_rset(eid, 'CWUser') if self.vreg is not None and 'etypes' in self.vreg: - user = self.vreg['etypes'].etype_class('CWUser')(req, rset, row=0, - groups=groups, - properties=properties) + user = self.vreg['etypes'].etype_class('CWUser')( + req, rset, row=0, groups=groups, properties=properties) else: from cubicweb.entity import Entity user = Entity(req, rset, row=0) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 debian/control --- a/debian/control Fri Feb 11 11:37:10 2011 +0100 +++ b/debian/control Fri Feb 11 12:17:47 2011 +0100 @@ -97,7 +97,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.54.0), python-yams (>= 0.30.1), python-rql (>= 0.28.0), python-lxml +Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.54.0), python-yams (>= 0.30.4), python-rql (>= 0.28.0), python-lxml Recommends: python-simpletal (>= 4.0), python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core diff -r a8fbcf9b6572 -r 8e59c2cdcc99 devtools/fake.py --- a/devtools/fake.py Fri Feb 11 11:37:10 2011 +0100 +++ b/devtools/fake.py Fri Feb 11 12:17:47 2011 +0100 @@ -185,8 +185,7 @@ def internal_session(self): return FakeSession(self) - def extid2eid(self, source, extid, etype, session, insert=True, - recreate=False): + def extid2eid(self, source, extid, etype, session, insert=True): try: return self.extids[extid] except KeyError: diff -r a8fbcf9b6572 -r 8e59c2cdcc99 devtools/fill.py --- a/devtools/fill.py Fri Feb 11 11:37:10 2011 +0100 +++ b/devtools/fill.py Fri Feb 11 12:17:47 2011 +0100 @@ -157,6 +157,11 @@ base = date(randint(2000, 2010), 1, 1) + timedelta(randint(1, 365)) return self._constrained_generate(entity, attrname, base, timedelta(days=1), index) + def generate_interval(self, entity, attrname, index): + """generates a random date (format is 'yyyy-mm-dd')""" + base = timedelta(randint(1, 365)) + return self._constrained_generate(entity, attrname, base, timedelta(days=1), index) + def generate_time(self, entity, attrname, index): """generates a random time (format is ' HH:MM')""" return time(11, index%60) #'11:%02d' % (index % 60) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 devtools/repotest.py --- a/devtools/repotest.py Fri Feb 11 11:37:10 2011 +0100 +++ b/devtools/repotest.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -277,7 +277,8 @@ class BasePlannerTC(BaseQuerierTC): - newsources = 0 + newsources = () + def setup(self): clear_cache(self.repo, 'rel_type_sources') clear_cache(self.repo, 'rel_type_sources') @@ -293,18 +294,21 @@ do_monkey_patch() self._dumb_sessions = [] # by hi-jacked parent setup self.repo.vreg.rqlhelper.backend = 'postgres' # so FTIRANK is considered + self.newsources = [] def add_source(self, sourcecls, uri): - self.sources.append(sourcecls(self.repo, {'uri': uri})) - self.repo.sources_by_uri[uri] = self.sources[-1] - setattr(self, uri, self.sources[-1]) - self.newsources += 1 + source = sourcecls(self.repo, {'uri': uri, 'type': 'whatever'}) + if not source.copy_based_source: + self.sources.append(source) + self.newsources.append(source) + self.repo.sources_by_uri[uri] = source + setattr(self, uri, source) def tearDown(self): - while self.newsources: - source = self.sources.pop(-1) + for source in self.newsources: + if not source.copy_based_source: + self.sources.remove(source) del self.repo.sources_by_uri[source.uri] - self.newsources -= 1 undo_monkey_patch() for session in self._dumb_sessions: session._threaddata.pool = None diff -r a8fbcf9b6572 -r 8e59c2cdcc99 devtools/testlib.py --- a/devtools/testlib.py Fri Feb 11 11:37:10 2011 +0100 +++ b/devtools/testlib.py Fri Feb 11 12:17:47 2011 +0100 @@ -259,8 +259,7 @@ cls.init_config(cls.config) cls.repo.hm.call_hooks('server_startup', repo=cls.repo) cls.vreg = cls.repo.vreg - cls.websession = DBAPISession(cls.cnx, cls.admlogin, - {'password': cls.admpassword}) + cls.websession = DBAPISession(cls.cnx, cls.admlogin) cls._orig_cnx = (cls.cnx, cls.websession) cls.config.repository = lambda x=None: cls.repo @@ -453,7 +452,8 @@ for a in self.vreg['views'].possible_views(req, rset=rset)) def pactions(self, req, rset, - skipcategories=('addrelated', 'siteactions', 'useractions', 'footer')): + skipcategories=('addrelated', 'siteactions', 'useractions', + 'footer', 'manage')): return [(a.__regid__, a.__class__) for a in self.vreg['actions'].poss_visible_objects(req, rset=rset) if a.category not in skipcategories] @@ -464,7 +464,8 @@ if a.category in categories] def pactionsdict(self, req, rset, - skipcategories=('addrelated', 'siteactions', 'useractions', 'footer')): + skipcategories=('addrelated', 'siteactions', 'useractions', + 'footer', 'manage')): res = {} for a in self.vreg['actions'].poss_visible_objects(req, rset=rset): if a.category not in skipcategories: diff -r a8fbcf9b6572 -r 8e59c2cdcc99 entities/adapters.py --- a/entities/adapters.py Fri Feb 11 11:37:10 2011 +0100 +++ b/entities/adapters.py Fri Feb 11 12:17:47 2011 +0100 @@ -183,7 +183,7 @@ """return actual data of the downloadable content""" raise NotImplementedError - +# XXX should propose to use two different relations for children/parent class ITreeAdapter(EntityAdapter): """This adapter has to be overriden to be configured using the tree_relation, child_role and parent_role class attributes to benefit from diff -r a8fbcf9b6572 -r 8e59c2cdcc99 entities/schemaobjs.py --- a/entities/schemaobjs.py Fri Feb 11 11:37:10 2011 +0100 +++ b/entities/schemaobjs.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -19,12 +19,7 @@ __docformat__ = "restructuredtext en" -import re -from socket import gethostname - from logilab.common.decorators import cached -from logilab.common.textutils import text_to_dict -from logilab.common.configuration import OptionError from yams.schema import role_name @@ -34,58 +29,6 @@ from cubicweb.entities import AnyEntity, fetch_config -class _CWSourceCfgMixIn(object): - @property - def dictconfig(self): - return self.config and text_to_dict(self.config) or {} - - def update_config(self, skip_unknown=False, **config): - from cubicweb.server import SOURCE_TYPES - from cubicweb.server.serverconfig import (SourceConfiguration, - generate_source_config) - cfg = self.dictconfig - cfg.update(config) - options = SOURCE_TYPES[self.type].options - sconfig = SourceConfiguration(self._cw.vreg.config, options=options) - for opt, val in cfg.iteritems(): - try: - sconfig.set_option(opt, val) - except OptionError: - if skip_unknown: - continue - raise - cfgstr = unicode(generate_source_config(sconfig), self._cw.encoding) - self.set_attributes(config=cfgstr) - - -class CWSource(_CWSourceCfgMixIn, AnyEntity): - __regid__ = 'CWSource' - fetch_attrs, fetch_order = fetch_config(['name', 'type']) - - @property - def host_config(self): - dictconfig = self.dictconfig - host = gethostname() - for hostcfg in self.host_configs: - if hostcfg.match(host): - self.info('matching host config %s for source %s', - hostcfg.match_host, self.name) - dictconfig.update(hostcfg.dictconfig) - return dictconfig - - @property - def host_configs(self): - return self.reverse_cw_host_config_of - - -class CWSourceHostConfig(_CWSourceCfgMixIn, AnyEntity): - __regid__ = 'CWSourceHostConfig' - fetch_attrs, fetch_order = fetch_config(['match_host', 'config']) - - def match(self, hostname): - return re.match(self.match_host, hostname) - - class CWEType(AnyEntity): __regid__ = 'CWEType' fetch_attrs, fetch_order = fetch_config(['name']) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 entities/sources.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/sources.py Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,133 @@ +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""data source related entities""" + +__docformat__ = "restructuredtext en" + +import re +from socket import gethostname + +from logilab.common.textutils import text_to_dict +from logilab.common.configuration import OptionError + +from cubicweb import ValidationError +from cubicweb.entities import AnyEntity, fetch_config + +class _CWSourceCfgMixIn(object): + @property + def dictconfig(self): + return self.config and text_to_dict(self.config) or {} + + def update_config(self, skip_unknown=False, **config): + from cubicweb.server import SOURCE_TYPES + from cubicweb.server.serverconfig import (SourceConfiguration, + generate_source_config) + cfg = self.dictconfig + cfg.update(config) + options = SOURCE_TYPES[self.type].options + sconfig = SourceConfiguration(self._cw.vreg.config, options=options) + for opt, val in cfg.iteritems(): + try: + sconfig.set_option(opt, val) + except OptionError: + if skip_unknown: + continue + raise + cfgstr = unicode(generate_source_config(sconfig), self._cw.encoding) + self.set_attributes(config=cfgstr) + + +class CWSource(_CWSourceCfgMixIn, AnyEntity): + __regid__ = 'CWSource' + fetch_attrs, fetch_order = fetch_config(['name', 'type']) + + @property + def host_config(self): + dictconfig = self.dictconfig + host = gethostname() + for hostcfg in self.host_configs: + if hostcfg.match(host): + self.info('matching host config %s for source %s', + hostcfg.match_host, self.name) + dictconfig.update(hostcfg.dictconfig) + return dictconfig + + @property + def host_configs(self): + return self.reverse_cw_host_config_of + + def init_mapping(self, mapping): + for key, options in mapping: + if isinstance(key, tuple): # relation definition + assert len(key) == 3 + restrictions = ['X relation_type RT, RT name %(rt)s'] + kwargs = {'rt': key[1]} + if key[0] != '*': + restrictions.append('X from_entity FT, FT name %(ft)s') + kwargs['ft'] = key[0] + if key[2] != '*': + restrictions.append('X to_entity TT, TT name %(tt)s') + kwargs['tt'] = key[2] + rql = 'Any X WHERE %s' % ','.join(restrictions) + schemarset = self._cw.execute(rql, kwargs) + elif key[0].isupper(): # entity type + schemarset = self._cw.execute('CWEType X WHERE X name %(et)s', + {'et': key}) + else: # relation type + schemarset = self._cw.execute('CWRType X WHERE X name %(rt)s', + {'rt': key}) + for schemaentity in schemarset.entities(): + self._cw.create_entity('CWSourceSchemaConfig', + cw_for_source=self, + cw_schema=schemaentity, + options=options) + + @property + def repo_source(self): + """repository only property, not available from the web side (eg + self._cw is expected to be a server session) + """ + return self._cw.repo.sources_by_eid[self.eid] + + +class CWSourceHostConfig(_CWSourceCfgMixIn, AnyEntity): + __regid__ = 'CWSourceHostConfig' + fetch_attrs, fetch_order = fetch_config(['match_host', 'config']) + + @property + def cwsource(self): + return self.cw_host_config_of[0] + + def match(self, hostname): + return re.match(self.match_host, hostname) + + +class CWSourceSchemaConfig(AnyEntity): + __regid__ = 'CWSourceSchemaConfig' + fetch_attrs, fetch_order = fetch_config(['cw_for_source', 'cw_schema', 'options']) + + def dc_title(self): + return self._cw._(self.__regid__) + ' #%s' % self.eid + + @property + def schema(self): + return self.cw_schema[0] + + @property + def cwsource(self): + return self.cw_for_source[0] diff -r a8fbcf9b6572 -r 8e59c2cdcc99 hooks/__init__.py --- a/hooks/__init__.py Fri Feb 11 11:37:10 2011 +0100 +++ b/hooks/__init__.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,17 +15,17 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""core hooks +"""core hooks registering some maintainance tasks as server startup time""" -""" __docformat__ = "restructuredtext en" from datetime import timedelta, datetime + from cubicweb.server import hook class ServerStartupHook(hook.Hook): """task to cleanup expirated auth cookie entities""" - __regid__ = 'cw_cleanup_transactions' + __regid__ = 'cw.start-looping-tasks' events = ('server_startup',) def __call__(self): @@ -47,3 +47,27 @@ finally: session.close() self.repo.looping_task(60*60*24, cleanup_old_transactions, self.repo) + def update_feeds(repo): + session = repo.internal_session() + try: + # don't iter on repo.sources which doesn't include copy based + # sources (the one we're looking for) + for source in repo.sources_by_eid.itervalues(): + if (not source.copy_based_source + or not repo.config.source_enabled(source) + or not source.config['synchronize']): + continue + try: + stats = source.pull_data(session) + if stats['created']: + source.info('added %s entities', len(stats['created'])) + if stats['updated']: + source.info('updated %s entities', len(stats['updated'])) + session.commit() + except Exception, exc: + session.exception('while trying to update feed %s', source) + session.rollback() + session.set_pool() + finally: + session.close() + self.repo.looping_task(60, update_feeds, self.repo) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 hooks/integrity.py --- a/hooks/integrity.py Fri Feb 11 11:37:10 2011 +0100 +++ b/hooks/integrity.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r a8fbcf9b6572 -r 8e59c2cdcc99 hooks/syncsources.py --- a/hooks/syncsources.py Fri Feb 11 11:37:10 2011 +0100 +++ b/hooks/syncsources.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,6 +1,27 @@ +# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""hooks for repository sources synchronization""" + +from yams.schema import role_name + from cubicweb import ValidationError from cubicweb.selectors import is_instance -from cubicweb.server import hook +from cubicweb.server import SOURCE_TYPES, hook class SourceHook(hook.Hook): __abstract__ = True @@ -8,7 +29,7 @@ class SourceAddedOp(hook.Operation): - def precommit_event(self): + def postcommit_event(self): self.session.repo.add_source(self.entity) class SourceAddedHook(SourceHook): @@ -16,6 +37,14 @@ __select__ = SourceHook.__select__ & is_instance('CWSource') events = ('after_add_entity',) def __call__(self): + try: + sourcecls = SOURCE_TYPES[self.entity.type] + except KeyError: + msg = self._cw._('unknown source type') + raise ValidationError(self.entity.eid, + {role_name('type', 'subject'): msg}) + sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config, + fail_if_unknown=not self._cw.vreg.config.repairing) SourceAddedOp(self._cw, entity=self.entity) @@ -31,3 +60,98 @@ if self.entity.name == 'system': raise ValidationError(self.entity.eid, {None: 'cant remove system source'}) SourceRemovedOp(self._cw, uri=self.entity.name) + + +class SourceUpdatedOp(hook.DataOperationMixIn, hook.Operation): + + def precommit_event(self): + self.__processed = [] + for source in self.get_data(): + conf = source.repo_source.check_config(source) + self.__processed.append( (source, conf) ) + + def postcommit_event(self): + for source, conf in self.__processed: + source.repo_source.update_config(source, conf) + +class SourceUpdatedHook(SourceHook): + __regid__ = 'cw.sources.configupdate' + __select__ = SourceHook.__select__ & is_instance('CWSource') + events = ('after_update_entity',) + def __call__(self): + if 'config' in self.entity.cw_edited: + SourceUpdatedOp.get_instance(self._cw).add_data(self.entity) + +class SourceHostConfigUpdatedHook(SourceHook): + __regid__ = 'cw.sources.hostconfigupdate' + __select__ = SourceHook.__select__ & is_instance('CWSourceHostConfig') + events = ('after_add_entity', 'after_update_entity', 'before_delete_entity',) + def __call__(self): + try: + SourceUpdatedOp.get_instance(self._cw).add_data(self.entity.cwsource) + except IndexError: + # XXX no source linked to the host config yet + pass + + +# source mapping synchronization. Expect cw_for_source/cw_schema are immutable +# relations (i.e. can't change from a source or schema to another). + +class SourceMappingDeleteHook(SourceHook): + """check cw_for_source and cw_schema are immutable relations + + XXX empty delete perms would be enough? + """ + __regid__ = 'cw.sources.delschemaconfig' + __select__ = SourceHook.__select__ & hook.match_rtype('cw_for_source', 'cw_schema') + events = ('before_add_relation',) + def __call__(self): + if not self._cw.added_in_transaction(self.eidfrom): + msg = self._cw._("can't change this relation") + raise ValidationError(self.eidfrom, {self.rtype: msg}) + + +class SourceMappingChangedOp(hook.DataOperationMixIn, hook.Operation): + def check_or_update(self, checkonly): + session = self.session + # take care, can't call get_data() twice + try: + data = self.__data + except AttributeError: + data = self.__data = self.get_data() + for schemacfg, source in data: + if source is None: + source = schemacfg.cwsource.repo_source + if session.added_in_transaction(schemacfg.eid): + if not session.deleted_in_transaction(schemacfg.eid): + source.add_schema_config(schemacfg, checkonly=checkonly) + elif session.deleted_in_transaction(schemacfg.eid): + source.delete_schema_config(schemacfg, checkonly=checkonly) + else: + source.update_schema_config(schemacfg, checkonly=checkonly) + + def precommit_event(self): + self.check_or_update(True) + + def postcommit_event(self): + self.check_or_update(False) + + +class SourceMappingChangedHook(SourceHook): + __regid__ = 'cw.sources.schemaconfig' + __select__ = SourceHook.__select__ & is_instance('CWSourceSchemaConfig') + events = ('after_add_entity', 'after_update_entity') + def __call__(self): + if self.event == 'after_add_entity' or ( + self.event == 'after_update_entity' and 'options' in self.entity.cw_edited): + SourceMappingChangedOp.get_instance(self._cw).add_data( + (self.entity, None) ) + +class SourceMappingDeleteHook(SourceHook): + __regid__ = 'cw.sources.delschemaconfig' + __select__ = SourceHook.__select__ & hook.match_rtype('cw_for_source') + events = ('before_delete_relation',) + def __call__(self): + SourceMappingChangedOp.get_instance(self._cw).add_data( + (self._cw.entity_from_eid(self.eidfrom), + self._cw.entity_from_eid(self.eidto)) ) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Fri Feb 11 11:37:10 2011 +0100 +++ b/hooks/test/unittest_hooks.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -18,12 +18,11 @@ # with CubicWeb. If not, see . """functional tests for core hooks -note: most schemahooks.py hooks are actually tested in unittest_migrations.py +Note: + syncschema.py hooks are mostly tested in server/test/unittest_migrations.py """ from __future__ import with_statement -from logilab.common.testlib import TestCase, unittest_main - from datetime import datetime from cubicweb import ValidationError, AuthenticationError, BadConnectionId @@ -31,38 +30,6 @@ class CoreHooksTC(CubicWebTC): - def test_delete_internal_entities(self): - self.assertRaises(ValidationError, self.execute, - 'DELETE CWEType X WHERE X name "CWEType"') - self.assertRaises(ValidationError, self.execute, - 'DELETE CWRType X WHERE X name "relation_type"') - self.assertRaises(ValidationError, self.execute, - 'DELETE CWGroup X WHERE X name "owners"') - - def test_delete_required_relations_subject(self): - self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y ' - 'WHERE Y name "users"') - self.commit() - self.execute('DELETE X in_group Y WHERE X login "toto", Y name "users"') - self.assertRaises(ValidationError, self.commit) - self.execute('DELETE X in_group Y WHERE X login "toto"') - self.execute('SET X in_group Y WHERE X login "toto", Y name "guests"') - self.commit() - - def test_delete_required_relations_object(self): - self.skipTest('no sample in the schema ! YAGNI ? Kermaat ?') - - def test_static_vocabulary_check(self): - self.assertRaises(ValidationError, - self.execute, - 'SET X composite "whatever" WHERE X from_entity FE, FE name "CWUser", X relation_type RT, RT name "in_group"') - - def test_missing_required_relations_subject_inline(self): - # missing in_group relation - self.execute('INSERT CWUser X: X login "toto", X upassword "hop"') - self.assertRaises(ValidationError, - self.commit) - def test_inlined(self): self.assertEqual(self.repo.schema['sender'].inlined, True) self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') @@ -73,54 +40,6 @@ rset = self.execute('Any S WHERE X sender S, X eid %s' % eeid) self.assertEqual(len(rset), 1) - def test_composite_1(self): - self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') - self.execute('INSERT EmailPart X: X content_format "text/plain", X ordernum 1, X content "this is a test"') - self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y, X parts P ' - 'WHERE Y is EmailAddress, P is EmailPart') - self.failUnless(self.execute('Email X WHERE X sender Y')) - self.commit() - self.execute('DELETE Email X') - rset = self.execute('Any X WHERE X is EmailPart') - self.assertEqual(len(rset), 1) - self.commit() - rset = self.execute('Any X WHERE X is EmailPart') - self.assertEqual(len(rset), 0) - - def test_composite_2(self): - self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') - self.execute('INSERT EmailPart X: X content_format "text/plain", X ordernum 1, X content "this is a test"') - self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y, X parts P ' - 'WHERE Y is EmailAddress, P is EmailPart') - self.commit() - self.execute('DELETE Email X') - self.execute('DELETE EmailPart X') - self.commit() - rset = self.execute('Any X WHERE X is EmailPart') - self.assertEqual(len(rset), 0) - - def test_composite_redirection(self): - self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') - self.execute('INSERT EmailPart X: X content_format "text/plain", X ordernum 1, X content "this is a test"') - self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y, X parts P ' - 'WHERE Y is EmailAddress, P is EmailPart') - self.execute('INSERT Email X: X messageid "<2345>", X subject "test2", X sender Y, X recipients Y ' - 'WHERE Y is EmailAddress') - self.commit() - self.execute('DELETE X parts Y WHERE X messageid "<1234>"') - self.execute('SET X parts Y WHERE X messageid "<2345>"') - self.commit() - rset = self.execute('Any X WHERE X is EmailPart') - self.assertEqual(len(rset), 1) - self.assertEqual(rset.get_entity(0, 0).reverse_parts[0].messageid, '<2345>') - - def test_unsatisfied_constraints(self): - releid = self.execute('SET U in_group G WHERE G name "owners", U login "admin"')[0][0] - with self.assertRaises(ValidationError) as cm: - self.commit() - self.assertEqual(cm.exception.errors, - {'in_group-object': u'RQLConstraint NOT O name "owners" failed'}) - def test_html_tidy_hook(self): req = self.request() entity = req.create_entity('Workflow', name=u'wf1', description_format=u'text/html', @@ -271,4 +190,5 @@ if __name__ == '__main__': + from logilab.common.testlib import unittest_main unittest_main() diff -r a8fbcf9b6572 -r 8e59c2cdcc99 hooks/test/unittest_integrity.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hooks/test/unittest_integrity.py Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""functional tests for integrity hooks""" + +from __future__ import with_statement + +from cubicweb import ValidationError +from cubicweb.devtools.testlib import CubicWebTC + +class CoreHooksTC(CubicWebTC): + + def test_delete_internal_entities(self): + self.assertRaises(ValidationError, self.execute, + 'DELETE CWEType X WHERE X name "CWEType"') + self.assertRaises(ValidationError, self.execute, + 'DELETE CWRType X WHERE X name "relation_type"') + self.assertRaises(ValidationError, self.execute, + 'DELETE CWGroup X WHERE X name "owners"') + + def test_delete_required_relations_subject(self): + self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y ' + 'WHERE Y name "users"') + self.commit() + self.execute('DELETE X in_group Y WHERE X login "toto", Y name "users"') + self.assertRaises(ValidationError, self.commit) + self.execute('DELETE X in_group Y WHERE X login "toto"') + self.execute('SET X in_group Y WHERE X login "toto", Y name "guests"') + self.commit() + + def test_delete_required_relations_object(self): + self.skipTest('no sample in the schema ! YAGNI ? Kermaat ?') + + def test_static_vocabulary_check(self): + self.assertRaises(ValidationError, + self.execute, + 'SET X composite "whatever" WHERE X from_entity FE, FE name "CWUser", X relation_type RT, RT name "in_group"') + + def test_missing_required_relations_subject_inline(self): + # missing in_group relation + self.execute('INSERT CWUser X: X login "toto", X upassword "hop"') + self.assertRaises(ValidationError, + self.commit) + + def test_composite_1(self): + self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') + self.execute('INSERT EmailPart X: X content_format "text/plain", X ordernum 1, X content "this is a test"') + self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y, X parts P ' + 'WHERE Y is EmailAddress, P is EmailPart') + self.failUnless(self.execute('Email X WHERE X sender Y')) + self.commit() + self.execute('DELETE Email X') + rset = self.execute('Any X WHERE X is EmailPart') + self.assertEqual(len(rset), 1) + self.commit() + rset = self.execute('Any X WHERE X is EmailPart') + self.assertEqual(len(rset), 0) + + def test_composite_2(self): + self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') + self.execute('INSERT EmailPart X: X content_format "text/plain", X ordernum 1, X content "this is a test"') + self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y, X parts P ' + 'WHERE Y is EmailAddress, P is EmailPart') + self.commit() + self.execute('DELETE Email X') + self.execute('DELETE EmailPart X') + self.commit() + rset = self.execute('Any X WHERE X is EmailPart') + self.assertEqual(len(rset), 0) + + def test_composite_redirection(self): + self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') + self.execute('INSERT EmailPart X: X content_format "text/plain", X ordernum 1, X content "this is a test"') + self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y, X parts P ' + 'WHERE Y is EmailAddress, P is EmailPart') + self.execute('INSERT Email X: X messageid "<2345>", X subject "test2", X sender Y, X recipients Y ' + 'WHERE Y is EmailAddress') + self.commit() + self.execute('DELETE X parts Y WHERE X messageid "<1234>"') + self.execute('SET X parts Y WHERE X messageid "<2345>"') + self.commit() + rset = self.execute('Any X WHERE X is EmailPart') + self.assertEqual(len(rset), 1) + self.assertEqual(rset.get_entity(0, 0).reverse_parts[0].messageid, '<2345>') + + def test_unsatisfied_constraints(self): + releid = self.execute('SET U in_group G WHERE G name "owners", U login "admin"')[0][0] + with self.assertRaises(ValidationError) as cm: + self.commit() + self.assertEqual(cm.exception.errors, + {'in_group-object': u'RQLConstraint NOT O name "owners" failed'}) + + def test_unique_constraint(self): + req = self.request() + entity = req.create_entity('CWGroup', name=u'trout') + self.commit() + self.assertRaises(ValidationError, req.create_entity, 'CWGroup', name=u'trout') + self.rollback() + req.execute('SET X name "trout" WHERE X eid %(x)s', {'x': entity.eid}) + self.commit() + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r a8fbcf9b6572 -r 8e59c2cdcc99 i18n/de.po --- a/i18n/de.po Fri Feb 11 11:37:10 2011 +0100 +++ b/i18n/de.po Fri Feb 11 12:17:47 2011 +0100 @@ -8,10 +8,10 @@ "PO-Revision-Date: 2010-09-15 14:55+0200\n" "Last-Translator: Dr. Leo \n" "Language-Team: English \n" -"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: de\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" @@ -27,8 +27,8 @@ "url: %(url)s\n" msgstr "" "\n" -"%(user)s hat den Zustand geändert von <%(previous_state)s> in <" -"%(current_state)s> für die Entität\n" +"%(user)s hat den Zustand geändert von <%(previous_state)s> in <%" +"(current_state)s> für die Entität\n" "'%(title)s'\n" "\n" "%(comment)s\n" @@ -392,8 +392,8 @@ "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist " "anymore." msgstr "" -"Kann die Relation %(rtype)s nicht wieder herstellen, die Entität %(role)s " -"%(eid)s existiert nicht mehr." +"Kann die Relation %(rtype)s nicht wieder herstellen, die Entität %(role)s %" +"(eid)s existiert nicht mehr." #, python-format msgid "" @@ -439,10 +439,10 @@ msgid "Download schema as OWL" msgstr "Herunterladen des Schemas im OWL-Format" +msgctxt "inlined:CWUser.use_email.subject" msgid "EmailAddress" msgstr "Email-Adresse" -msgctxt "inlined:CWUser.use_email.subject" msgid "EmailAddress" msgstr "Email-Adresse" @@ -1051,9 +1051,6 @@ msgid "add_permission" msgstr "Berechtigung hinzufügen" -msgid "add_permission_object" -msgstr "hat die Berechtigung zum Hinzufügen" - msgctxt "CWGroup" msgid "add_permission_object" msgstr "kann hinzufügen" @@ -1062,6 +1059,9 @@ msgid "add_permission_object" msgstr "benutzt, um die Hinzufüge-Berechtigung zu festzulegen für" +msgid "add_permission_object" +msgstr "hat die Berechtigung zum Hinzufügen" + msgid "add_relation" msgstr "hinzufügen" @@ -1071,11 +1071,11 @@ #, python-format msgid "" -"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #" -"%(eidto)s" +"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%" +"(eidto)s" msgstr "" -"Die Relation %(rtype)s von %(frometype)s #%(eidfrom)s zu %(toetype)s #" -"%(eidto)s wurde hinzugefügt." +"Die Relation %(rtype)s von %(frometype)s #%(eidfrom)s zu %(toetype)s #%" +"(eidto)s wurde hinzugefügt." msgid "addrelated" msgstr "hinzufügen" @@ -1107,9 +1107,6 @@ msgid "allowed_transition" msgstr "erlaubter Übergang" -msgid "allowed_transition_object" -msgstr "ausstehende Zustände" - msgctxt "BaseTransition" msgid "allowed_transition_object" msgstr "ausstehende Zustände" @@ -1122,6 +1119,9 @@ msgid "allowed_transition_object" msgstr "ausstehende Zustände" +msgid "allowed_transition_object" +msgstr "ausstehende Zustände" + msgid "am/pm calendar (month)" msgstr "am/pm Kalender (Monat)" @@ -1216,13 +1216,13 @@ msgid "bookmarked_by" msgstr "Lesezeichen angelegt durch" -msgid "bookmarked_by_object" -msgstr "hat Lesezeichen" - msgctxt "CWUser" msgid "bookmarked_by_object" msgstr "verwendet Lesezeichen" +msgid "bookmarked_by_object" +msgstr "hat Lesezeichen" + msgid "bookmarks" msgstr "Lesezeichen" @@ -1260,9 +1260,6 @@ msgid "by_transition" msgstr "je Übergang" -msgid "by_transition_object" -msgstr "Übergangsinformation" - msgctxt "BaseTransition" msgid "by_transition_object" msgstr "Übergangsinformation" @@ -1275,6 +1272,9 @@ msgid "by_transition_object" msgstr "Übergangsinformation" +msgid "by_transition_object" +msgstr "Übergangsinformation" + msgid "calendar" msgstr "Kalender anzeigen" @@ -1430,10 +1430,10 @@ msgid "condition" msgstr "Bedingung" +msgctxt "RQLExpression" msgid "condition_object" msgstr "Bedingung von" -msgctxt "RQLExpression" msgid "condition_object" msgstr "Bedingung von" @@ -1443,11 +1443,11 @@ msgid "config" msgstr "" -msgctxt "CWSource" +msgctxt "CWSourceHostConfig" msgid "config" msgstr "" -msgctxt "CWSourceHostConfig" +msgctxt "CWSource" msgid "config" msgstr "" @@ -1471,10 +1471,10 @@ msgid "constrained_by" msgstr "eingeschränkt durch" +msgctxt "CWConstraint" msgid "constrained_by_object" msgstr "Einschränkungen" -msgctxt "CWConstraint" msgid "constrained_by_object" msgstr "Einschränkungen" @@ -1488,10 +1488,10 @@ msgid "constraint_of" msgstr "" +msgctxt "CWEType" msgid "constraint_of_object" msgstr "" -msgctxt "CWEType" msgid "constraint_of_object" msgstr "" @@ -1583,8 +1583,8 @@ msgstr "Erstelle Relation %(linkto)s" msgid "" -"creating CWSourceHostConfig (CWSourceHostConfig cw_host_config_of CWSource " -"%(linkto)s)" +"creating CWSourceHostConfig (CWSourceHostConfig cw_host_config_of CWSource %" +"(linkto)s)" msgstr "" msgid "" @@ -1673,8 +1673,8 @@ msgstr "Erstelle Workflow-Übergang, der zum Zustand %(linkto)s führt." msgid "" -"creating WorkflowTransition (WorkflowTransition transition_of Workflow " -"%(linkto)s)" +"creating WorkflowTransition (WorkflowTransition transition_of Workflow %" +"(linkto)s)" msgstr "Erstelle Workflow-Übergang des Workflows %(linkto)s" msgid "creation" @@ -1696,13 +1696,13 @@ msgid "cstrtype" msgstr "Einschränkungstyp" -msgid "cstrtype_object" -msgstr "benutzt von" - msgctxt "CWConstraintType" msgid "cstrtype_object" msgstr "Einschränkungstyp von" +msgid "cstrtype_object" +msgstr "benutzt von" + msgid "csv entities export" msgstr "CSV-Export von Entitäten" @@ -1827,10 +1827,10 @@ msgid "cw_dont_cross" msgstr "" +msgctxt "CWRType" msgid "cw_dont_cross_object" msgstr "" -msgctxt "CWRType" msgid "cw_dont_cross_object" msgstr "" @@ -1841,10 +1841,10 @@ msgid "cw_host_config_of" msgstr "" +msgctxt "CWSource" msgid "cw_host_config_of_object" msgstr "" -msgctxt "CWSource" msgid "cw_host_config_of_object" msgstr "" @@ -1855,10 +1855,10 @@ msgid "cw_may_cross" msgstr "" +msgctxt "CWRType" msgid "cw_may_cross_object" msgstr "" -msgctxt "CWRType" msgid "cw_may_cross_object" msgstr "" @@ -1875,9 +1875,6 @@ msgid "cw_support" msgstr "" -msgid "cw_support_object" -msgstr "" - msgctxt "CWEType" msgid "cw_support_object" msgstr "" @@ -1886,6 +1883,9 @@ msgid "cw_support_object" msgstr "" +msgid "cw_support_object" +msgstr "" + msgid "cwetype-box" msgstr "Box-Ansicht" @@ -1922,6 +1922,9 @@ msgid "date" msgstr "Datum" +msgid "day" +msgstr "" + msgid "deactivate" msgstr "deaktivieren" @@ -1953,10 +1956,10 @@ msgid "default_workflow" msgstr "Standard-Workflow" +msgctxt "Workflow" msgid "default_workflow_object" msgstr "Standard-Workflow von" -msgctxt "Workflow" msgid "default_workflow_object" msgstr "Standard-Workflow von" @@ -2036,9 +2039,6 @@ msgid "delete_permission" msgstr "Lösch-Berechtigung" -msgid "delete_permission_object" -msgstr "hat Lösch-Berechtigung" - msgctxt "CWGroup" msgid "delete_permission_object" msgstr "hat Lösch-Berechtigung für" @@ -2047,14 +2047,17 @@ msgid "delete_permission_object" msgstr "hat die Berechtigung, zu löschen" +msgid "delete_permission_object" +msgstr "hat Lösch-Berechtigung" + #, python-format msgid "deleted %(etype)s #%(eid)s (%(title)s)" msgstr "Löschen der Entität %(etype)s #%(eid)s (%(title)s)" #, python-format msgid "" -"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #" -"%(eidto)s" +"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%" +"(eidto)s" msgstr "" "Relation %(rtype)s von %(frometype)s #%(eidfrom)s zu %(toetype)s #%(eidto)s " "gelöscht" @@ -2065,7 +2068,15 @@ msgid "description" msgstr "Beschreibung" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "description" +msgstr "Beschreibung" + +msgctxt "CWRelation" +msgid "description" +msgstr "Beschreibung" + +msgctxt "Workflow" msgid "description" msgstr "Beschreibung" @@ -2073,38 +2084,38 @@ msgid "description" msgstr "Beschreibung" -msgctxt "CWEType" -msgid "description" -msgstr "Beschreibung" - -msgctxt "CWRType" -msgid "description" -msgstr "Beschreibung" - -msgctxt "CWRelation" -msgid "description" -msgstr "Beschreibung" - -msgctxt "State" -msgid "description" -msgstr "Beschreibung" - msgctxt "Transition" msgid "description" msgstr "Beschreibung" -msgctxt "Workflow" -msgid "description" -msgstr "Beschreibung" - msgctxt "WorkflowTransition" msgid "description" msgstr "Beschreibung" +msgctxt "State" +msgid "description" +msgstr "Beschreibung" + +msgctxt "CWRType" +msgid "description" +msgstr "Beschreibung" + +msgctxt "BaseTransition" +msgid "description" +msgstr "Beschreibung" + msgid "description_format" msgstr "Format" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "description_format" +msgstr "Format" + +msgctxt "CWRelation" +msgid "description_format" +msgstr "Format" + +msgctxt "Workflow" msgid "description_format" msgstr "Format" @@ -2112,7 +2123,15 @@ msgid "description_format" msgstr "Format" -msgctxt "CWEType" +msgctxt "Transition" +msgid "description_format" +msgstr "Format" + +msgctxt "WorkflowTransition" +msgid "description_format" +msgstr "Format" + +msgctxt "State" msgid "description_format" msgstr "Format" @@ -2120,23 +2139,7 @@ msgid "description_format" msgstr "Format" -msgctxt "CWRelation" -msgid "description_format" -msgstr "Format" - -msgctxt "State" -msgid "description_format" -msgstr "Format" - -msgctxt "Transition" -msgid "description_format" -msgstr "Format" - -msgctxt "Workflow" -msgid "description_format" -msgstr "Format" - -msgctxt "WorkflowTransition" +msgctxt "BaseTransition" msgid "description_format" msgstr "Format" @@ -2159,18 +2162,18 @@ msgid "destination_state" msgstr "Zielzustand" +msgctxt "Transition" +msgid "destination_state" +msgstr "Zielzustand" + msgctxt "SubWorkflowExitPoint" msgid "destination_state" msgstr "Zielzustand" -msgctxt "Transition" -msgid "destination_state" -msgstr "Zielzustand" - +msgctxt "State" msgid "destination_state_object" msgstr "Ziel von" -msgctxt "State" msgid "destination_state_object" msgstr "Ziel von" @@ -2434,13 +2437,13 @@ msgid "for_user" msgstr "für Nutzer" -msgid "for_user_object" -msgstr "hat als Eigenschaft" - msgctxt "CWUser" msgid "for_user_object" msgstr "verwendet die Eigenschaften" +msgid "for_user_object" +msgstr "hat als Eigenschaft" + msgid "friday" msgstr "Freitag" @@ -2462,13 +2465,13 @@ msgid "from_entity" msgstr "Relation der Entität" -msgid "from_entity_object" -msgstr "der Entität" - msgctxt "CWEType" msgid "from_entity_object" msgstr "Subjektrelation" +msgid "from_entity_object" +msgstr "der Entität" + msgid "from_interval_start" msgstr "Von" @@ -2479,13 +2482,13 @@ msgid "from_state" msgstr "Anfangszustand" -msgid "from_state_object" -msgstr "Übergänge aus diesem Zustand" - msgctxt "State" msgid "from_state_object" msgstr "Anfangszustand von" +msgid "from_state_object" +msgstr "Übergänge aus diesem Zustand" + msgid "full text or RQL query" msgstr "Volltextsuche oder RQL-Anfrage" @@ -2531,16 +2534,16 @@ "graphical representation of the %(etype)s entity type from %(appid)s data " "model" msgstr "" -"graphische Darstellung des Datenmodells des Entitätstyps (etype)s von " -"%(appid)s" +"graphische Darstellung des Datenmodells des Entitätstyps (etype)s von %" +"(appid)s" #, python-format msgid "" "graphical representation of the %(rtype)s relation type from %(appid)s data " "model" msgstr "" -"graphische Darstellung des Datenmodells des Relationstyps %(rtype)s von " -"%(appid)s" +"graphische Darstellung des Datenmodells des Relationstyps %(rtype)s von %" +"(appid)s" msgid "group in which a user should be to be allowed to pass this transition" msgstr "" @@ -2570,6 +2573,9 @@ msgid "header-right" msgstr "" +msgid "help" +msgstr "" + msgid "hide filter form" msgstr "Filter verbergen" @@ -2658,13 +2664,13 @@ msgid "in_group" msgstr "gehört zu der Gruppe" -msgid "in_group_object" -msgstr "Mitglieder" - msgctxt "CWGroup" msgid "in_group_object" msgstr "enthält die Nutzer" +msgid "in_group_object" +msgstr "Mitglieder" + msgid "in_state" msgstr "Zustand" @@ -2718,10 +2724,10 @@ msgid "initial_state" msgstr "Anfangszustand" +msgctxt "State" msgid "initial_state_object" msgstr "Anfangszustand von" -msgctxt "State" msgid "initial_state_object" msgstr "Anfangszustand von" @@ -2990,6 +2996,9 @@ msgid "monday" msgstr "Montag" +msgid "month" +msgstr "" + msgid "more actions" msgstr "weitere Aktionen" @@ -3005,11 +3014,23 @@ msgid "name" msgstr "Name" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "name" +msgstr "Name" + +msgctxt "Transition" msgid "name" msgstr "Name" -msgctxt "CWCache" +msgctxt "CWSource" +msgid "name" +msgstr "" + +msgctxt "Workflow" +msgid "name" +msgstr "Name" + +msgctxt "CWGroup" msgid "name" msgstr "Name" @@ -3017,11 +3038,11 @@ msgid "name" msgstr "Name" -msgctxt "CWEType" +msgctxt "WorkflowTransition" msgid "name" msgstr "Name" -msgctxt "CWGroup" +msgctxt "State" msgid "name" msgstr "Name" @@ -3033,23 +3054,11 @@ msgid "name" msgstr "Name" -msgctxt "CWSource" -msgid "name" -msgstr "" - -msgctxt "State" +msgctxt "BaseTransition" msgid "name" msgstr "Name" -msgctxt "Transition" -msgid "name" -msgstr "Name" - -msgctxt "Workflow" -msgid "name" -msgstr "Name" - -msgctxt "WorkflowTransition" +msgctxt "CWCache" msgid "name" msgstr "Name" @@ -3267,13 +3276,13 @@ msgid "prefered_form" msgstr "bevorzugte form" -msgid "prefered_form_object" -msgstr "bevorzugte form vor" - msgctxt "EmailAddress" msgid "prefered_form_object" msgstr "bevorzugte form von" +msgid "prefered_form_object" +msgstr "bevorzugte form vor" + msgid "preferences" msgstr "Einstellungen" @@ -3290,13 +3299,13 @@ msgid "primary_email" msgstr "primäre E-Mail-Adresse" -msgid "primary_email_object" -msgstr "Objekt der primären E-Mail-Adresse" - msgctxt "EmailAddress" msgid "primary_email_object" msgstr "primäre E-Mail-Adresse von" +msgid "primary_email_object" +msgstr "Objekt der primären E-Mail-Adresse" + msgid "profile" msgstr "Profil" @@ -3321,11 +3330,11 @@ msgid "read_permission" msgstr "Leseberechtigung" -msgctxt "CWAttribute" +msgctxt "CWEType" msgid "read_permission" msgstr "Leseberechtigung" -msgctxt "CWEType" +msgctxt "CWAttribute" msgid "read_permission" msgstr "Leseberechtigung" @@ -3333,9 +3342,6 @@ msgid "read_permission" msgstr "Leseberechtigung" -msgid "read_permission_object" -msgstr "hat eine Leseberechtigung" - msgctxt "CWGroup" msgid "read_permission_object" msgstr "kann lesen" @@ -3344,6 +3350,9 @@ msgid "read_permission_object" msgstr "kann lesen" +msgid "read_permission_object" +msgstr "hat eine Leseberechtigung" + msgid "regexp matching host(s) to which this config applies" msgstr "" @@ -3380,13 +3389,13 @@ msgid "relation_type" msgstr "Relationstyp" -msgid "relation_type_object" -msgstr "Definition" - msgctxt "CWRType" msgid "relation_type_object" msgstr "definition" +msgid "relation_type_object" +msgstr "Definition" + msgid "relations" msgstr "Relationen" @@ -3397,12 +3406,12 @@ msgid "relations deleted" msgstr "Relationen entfernt" +msgctxt "CWRType" msgid "relations_object" msgstr "Relationen von" -msgctxt "CWRType" msgid "relations_object" -msgstr "" +msgstr "Relationen von" msgid "relative url of the bookmarked page" msgstr "URL relativ zu der Seite" @@ -3417,11 +3426,11 @@ msgid "require_group" msgstr "auf Gruppe beschränkt" -msgctxt "CWPermission" +msgctxt "Transition" msgid "require_group" msgstr "auf Gruppe beschränkt" -msgctxt "Transition" +msgctxt "CWPermission" msgid "require_group" msgstr "auf Gruppe beschränkt" @@ -3429,10 +3438,10 @@ msgid "require_group" msgstr "auf Gruppe beschränkt" +msgctxt "CWGroup" msgid "require_group_object" msgstr "hat die Rechte" -msgctxt "CWGroup" msgid "require_group_object" msgstr "hat die Rechte" @@ -3643,10 +3652,10 @@ msgid "specializes" msgstr "spezialisiert" +msgctxt "CWEType" msgid "specializes_object" msgstr "Vorgänger von" -msgctxt "CWEType" msgid "specializes_object" msgstr "Vorgänger von" @@ -3685,13 +3694,13 @@ msgid "state_of" msgstr "Zustand von" -msgid "state_of_object" -msgstr "hat als Zustand" - msgctxt "Workflow" msgid "state_of_object" msgstr "enthält die Zustände" +msgid "state_of_object" +msgstr "hat als Zustand" + msgid "status change" msgstr "Zustand ändern" @@ -3734,20 +3743,20 @@ msgid "subworkflow_exit" msgstr "Ende des Subworkflows" -msgid "subworkflow_exit_object" -msgstr "Endzustand" - msgctxt "SubWorkflowExitPoint" msgid "subworkflow_exit_object" msgstr "Endzustände" -msgid "subworkflow_object" -msgstr "verwendet vom Übergang" +msgid "subworkflow_exit_object" +msgstr "Endzustand" msgctxt "Workflow" msgid "subworkflow_object" msgstr "Subworkflow von" +msgid "subworkflow_object" +msgstr "verwendet vom Übergang" + msgid "subworkflow_state" msgstr "Zustand des Subworkflows" @@ -3755,10 +3764,10 @@ msgid "subworkflow_state" msgstr "Zustand" +msgctxt "State" msgid "subworkflow_state_object" msgstr "Endzustand von" -msgctxt "State" msgid "subworkflow_state_object" msgstr "Endzustand von" @@ -3873,10 +3882,10 @@ msgid "to_entity" msgstr "für die Entität" +msgctxt "CWEType" msgid "to_entity_object" msgstr "Objekt der Relation" -msgctxt "CWEType" msgid "to_entity_object" msgstr "Objekt der Relation" @@ -3890,13 +3899,16 @@ msgid "to_state" msgstr "Zielstatus" -msgid "to_state_object" -msgstr "Übergänge zu dem Zustand" - msgctxt "State" msgid "to_state_object" msgstr "Übergang zu diesem Zustand" +msgid "to_state_object" +msgstr "Übergänge zu dem Zustand" + +msgid "today" +msgstr "" + msgid "todo_by" msgstr "zu erledigen bis" @@ -3941,10 +3953,10 @@ msgid "transition_of" msgstr "Übergang des/der" +msgctxt "Workflow" msgid "transition_of_object" msgstr "hat als Übergang" -msgctxt "Workflow" msgid "transition_of_object" msgstr "hat als Übergang" @@ -3961,10 +3973,6 @@ msgid "type" msgstr "Typ" -msgctxt "CWSource" -msgid "type" -msgstr "" - msgctxt "Transition" msgid "type" msgstr "Typ" @@ -3973,6 +3981,10 @@ msgid "type" msgstr "Typ" +msgctxt "CWSource" +msgid "type" +msgstr "" + msgid "type here a sparql query" msgstr "Geben sie eine sparql-Anfrage ein" @@ -4053,17 +4065,14 @@ msgid "update_permission" msgstr "Änderungsrecht" +msgctxt "CWEType" +msgid "update_permission" +msgstr "Änderungsrecht" + msgctxt "CWAttribute" msgid "update_permission" msgstr "Änderungsrecht" -msgctxt "CWEType" -msgid "update_permission" -msgstr "Änderungsrecht" - -msgid "update_permission_object" -msgstr "hat die Änderungsberechtigung" - msgctxt "CWGroup" msgid "update_permission_object" msgstr "kann ändern" @@ -4072,6 +4081,9 @@ msgid "update_permission_object" msgstr "kann ändern" +msgid "update_permission_object" +msgstr "hat die Änderungsberechtigung" + msgid "update_relation" msgstr "aktualisieren" @@ -4108,13 +4120,13 @@ msgid "use_email" msgstr "verwendet die E-Mail-Adresse" -msgid "use_email_object" -msgstr "Adresse verwendet von" - msgctxt "EmailAddress" msgid "use_email_object" msgstr "verwendet von" +msgid "use_email_object" +msgstr "Adresse verwendet von" + msgid "use_template_format" msgstr "Benutzung des 'cubicweb template'-Formats" @@ -4304,10 +4316,10 @@ msgid "workflow_of" msgstr "Workflow von" +msgctxt "CWEType" msgid "workflow_of_object" msgstr "hat als Workflow" -msgctxt "CWEType" msgid "workflow_of_object" msgstr "hat als Workflow" diff -r a8fbcf9b6572 -r 8e59c2cdcc99 i18n/en.po --- a/i18n/en.po Fri Feb 11 11:37:10 2011 +0100 +++ b/i18n/en.po Fri Feb 11 12:17:47 2011 +0100 @@ -8,10 +8,10 @@ "PO-Revision-Date: 2010-09-15 14:55+0200\n" "Last-Translator: Sylvain Thenault \n" "Language-Team: English \n" -"Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: en\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" @@ -415,10 +415,10 @@ msgid "Download schema as OWL" msgstr "" +msgctxt "inlined:CWUser.use_email.subject" msgid "EmailAddress" msgstr "Email address" -msgctxt "inlined:CWUser.use_email.subject" msgid "EmailAddress" msgstr "Email address" @@ -1011,9 +1011,6 @@ msgid "add_permission" msgstr "add permission" -msgid "add_permission_object" -msgstr "has permission to add" - msgctxt "CWGroup" msgid "add_permission_object" msgstr "can add" @@ -1022,6 +1019,9 @@ msgid "add_permission_object" msgstr "used to define add permission on" +msgid "add_permission_object" +msgstr "has permission to add" + msgid "add_relation" msgstr "add" @@ -1031,8 +1031,8 @@ #, python-format msgid "" -"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #" -"%(eidto)s" +"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%" +"(eidto)s" msgstr "" msgid "addrelated" @@ -1065,9 +1065,6 @@ msgid "allowed_transition" msgstr "allowed transition" -msgid "allowed_transition_object" -msgstr "incoming states" - msgctxt "BaseTransition" msgid "allowed_transition_object" msgstr "incoming states" @@ -1080,6 +1077,9 @@ msgid "allowed_transition_object" msgstr "incoming states" +msgid "allowed_transition_object" +msgstr "incoming states" + msgid "am/pm calendar (month)" msgstr "" @@ -1171,13 +1171,13 @@ msgid "bookmarked_by" msgstr "bookmarked by" -msgid "bookmarked_by_object" -msgstr "has bookmarks" - msgctxt "CWUser" msgid "bookmarked_by_object" msgstr "uses bookmarks" +msgid "bookmarked_by_object" +msgstr "has bookmarks" + msgid "bookmarks" msgstr "" @@ -1215,9 +1215,6 @@ msgid "by_transition" msgstr "by transition" -msgid "by_transition_object" -msgstr "transition information" - msgctxt "BaseTransition" msgid "by_transition_object" msgstr "transition information" @@ -1230,6 +1227,9 @@ msgid "by_transition_object" msgstr "transition information" +msgid "by_transition_object" +msgstr "transition information" + msgid "calendar" msgstr "" @@ -1384,10 +1384,10 @@ msgid "condition" msgstr "condition" +msgctxt "RQLExpression" msgid "condition_object" msgstr "condition of" -msgctxt "RQLExpression" msgid "condition_object" msgstr "condition of" @@ -1397,11 +1397,11 @@ msgid "config" msgstr "" -msgctxt "CWSource" +msgctxt "CWSourceHostConfig" msgid "config" msgstr "" -msgctxt "CWSourceHostConfig" +msgctxt "CWSource" msgid "config" msgstr "" @@ -1425,10 +1425,10 @@ msgid "constrained_by" msgstr "constrained by" +msgctxt "CWConstraint" msgid "constrained_by_object" msgstr "constraints" -msgctxt "CWConstraint" msgid "constrained_by_object" msgstr "constraints" @@ -1442,10 +1442,10 @@ msgid "constraint_of" msgstr "constraint of" +msgctxt "CWEType" msgid "constraint_of_object" msgstr "constrained by" -msgctxt "CWEType" msgid "constraint_of_object" msgstr "constrained by" @@ -1536,8 +1536,8 @@ msgstr "creating relation %(linkto)s" msgid "" -"creating CWSourceHostConfig (CWSourceHostConfig cw_host_config_of CWSource " -"%(linkto)s)" +"creating CWSourceHostConfig (CWSourceHostConfig cw_host_config_of CWSource %" +"(linkto)s)" msgstr "creating host configuration for source %(linkto)s" msgid "" @@ -1626,8 +1626,8 @@ msgstr "creating workflow-transition leading to state %(linkto)s" msgid "" -"creating WorkflowTransition (WorkflowTransition transition_of Workflow " -"%(linkto)s)" +"creating WorkflowTransition (WorkflowTransition transition_of Workflow %" +"(linkto)s)" msgstr "creating workflow-transition of workflow %(linkto)s" msgid "creation" @@ -1649,13 +1649,13 @@ msgid "cstrtype" msgstr "constraint type" -msgid "cstrtype_object" -msgstr "used by" - msgctxt "CWConstraintType" msgid "cstrtype_object" msgstr "constraint type of" +msgid "cstrtype_object" +msgstr "used by" + msgid "csv entities export" msgstr "" @@ -1782,10 +1782,10 @@ msgid "cw_dont_cross" msgstr "" +msgctxt "CWRType" msgid "cw_dont_cross_object" msgstr "" -msgctxt "CWRType" msgid "cw_dont_cross_object" msgstr "" @@ -1796,10 +1796,10 @@ msgid "cw_host_config_of" msgstr "" +msgctxt "CWSource" msgid "cw_host_config_of_object" msgstr "" -msgctxt "CWSource" msgid "cw_host_config_of_object" msgstr "" @@ -1810,10 +1810,10 @@ msgid "cw_may_cross" msgstr "" +msgctxt "CWRType" msgid "cw_may_cross_object" msgstr "" -msgctxt "CWRType" msgid "cw_may_cross_object" msgstr "" @@ -1830,9 +1830,6 @@ msgid "cw_support" msgstr "" -msgid "cw_support_object" -msgstr "" - msgctxt "CWEType" msgid "cw_support_object" msgstr "" @@ -1841,6 +1838,9 @@ msgid "cw_support_object" msgstr "" +msgid "cw_support_object" +msgstr "" + msgid "cwetype-box" msgstr "\"box\" view" @@ -1877,6 +1877,9 @@ msgid "date" msgstr "" +msgid "day" +msgstr "" + msgid "deactivate" msgstr "" @@ -1908,10 +1911,10 @@ msgid "default_workflow" msgstr "default workflow" +msgctxt "Workflow" msgid "default_workflow_object" msgstr "default workflow of" -msgctxt "Workflow" msgid "default_workflow_object" msgstr "default workflow of" @@ -1987,9 +1990,6 @@ msgid "delete_permission" msgstr "delete_permission" -msgid "delete_permission_object" -msgstr "has permission to delete" - msgctxt "CWGroup" msgid "delete_permission_object" msgstr "has permission to delete" @@ -1998,14 +1998,17 @@ msgid "delete_permission_object" msgstr "has permission to delete" +msgid "delete_permission_object" +msgstr "has permission to delete" + #, python-format msgid "deleted %(etype)s #%(eid)s (%(title)s)" msgstr "" #, python-format msgid "" -"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #" -"%(eidto)s" +"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%" +"(eidto)s" msgstr "" msgid "depends on the constraint type" @@ -2014,7 +2017,15 @@ msgid "description" msgstr "description" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "description" +msgstr "description" + +msgctxt "CWRelation" +msgid "description" +msgstr "description" + +msgctxt "Workflow" msgid "description" msgstr "description" @@ -2022,38 +2033,38 @@ msgid "description" msgstr "description" -msgctxt "CWEType" -msgid "description" -msgstr "description" - -msgctxt "CWRType" -msgid "description" -msgstr "description" - -msgctxt "CWRelation" -msgid "description" -msgstr "description" - -msgctxt "State" -msgid "description" -msgstr "description" - msgctxt "Transition" msgid "description" msgstr "description" -msgctxt "Workflow" -msgid "description" -msgstr "description" - msgctxt "WorkflowTransition" msgid "description" msgstr "description" +msgctxt "State" +msgid "description" +msgstr "description" + +msgctxt "CWRType" +msgid "description" +msgstr "description" + +msgctxt "BaseTransition" +msgid "description" +msgstr "description" + msgid "description_format" msgstr "format" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "description_format" +msgstr "format" + +msgctxt "CWRelation" +msgid "description_format" +msgstr "format" + +msgctxt "Workflow" msgid "description_format" msgstr "format" @@ -2061,7 +2072,15 @@ msgid "description_format" msgstr "format" -msgctxt "CWEType" +msgctxt "Transition" +msgid "description_format" +msgstr "format" + +msgctxt "WorkflowTransition" +msgid "description_format" +msgstr "format" + +msgctxt "State" msgid "description_format" msgstr "format" @@ -2069,23 +2088,7 @@ msgid "description_format" msgstr "format" -msgctxt "CWRelation" -msgid "description_format" -msgstr "format" - -msgctxt "State" -msgid "description_format" -msgstr "format" - -msgctxt "Transition" -msgid "description_format" -msgstr "format" - -msgctxt "Workflow" -msgid "description_format" -msgstr "format" - -msgctxt "WorkflowTransition" +msgctxt "BaseTransition" msgid "description_format" msgstr "format" @@ -2106,18 +2109,18 @@ msgid "destination_state" msgstr "destination state" +msgctxt "Transition" +msgid "destination_state" +msgstr "destination state" + msgctxt "SubWorkflowExitPoint" msgid "destination_state" msgstr "destination state" -msgctxt "Transition" -msgid "destination_state" -msgstr "destination state" - +msgctxt "State" msgid "destination_state_object" msgstr "destination of" -msgctxt "State" msgid "destination_state_object" msgstr "destination of" @@ -2376,13 +2379,13 @@ msgid "for_user" msgstr "for user" -msgid "for_user_object" -msgstr "use properties" - msgctxt "CWUser" msgid "for_user_object" msgstr "property of" +msgid "for_user_object" +msgstr "use properties" + msgid "friday" msgstr "" @@ -2404,13 +2407,13 @@ msgid "from_entity" msgstr "from entity" -msgid "from_entity_object" -msgstr "subjet relation" - msgctxt "CWEType" msgid "from_entity_object" msgstr "subjec relation" +msgid "from_entity_object" +msgstr "subjet relation" + msgid "from_interval_start" msgstr "from" @@ -2421,10 +2424,10 @@ msgid "from_state" msgstr "from state" +msgctxt "State" msgid "from_state_object" msgstr "transitions from this state" -msgctxt "State" msgid "from_state_object" msgstr "transitions from this state" @@ -2505,6 +2508,9 @@ msgid "header-right" msgstr "header (right)" +msgid "help" +msgstr "" + msgid "hide filter form" msgstr "" @@ -2585,10 +2591,10 @@ msgid "in_group" msgstr "in group" +msgctxt "CWGroup" msgid "in_group_object" msgstr "contains" -msgctxt "CWGroup" msgid "in_group_object" msgstr "contains" @@ -2643,10 +2649,10 @@ msgid "initial_state" msgstr "initial state" +msgctxt "State" msgid "initial_state_object" msgstr "initial state of" -msgctxt "State" msgid "initial_state_object" msgstr "initial state of" @@ -2903,6 +2909,9 @@ msgid "monday" msgstr "" +msgid "month" +msgstr "" + msgid "more actions" msgstr "" @@ -2918,19 +2927,19 @@ msgid "name" msgstr "" -msgctxt "BaseTransition" +msgctxt "CWEType" msgid "name" -msgstr "name" - -msgctxt "CWCache" +msgstr "" + +msgctxt "Transition" msgid "name" -msgstr "name" - -msgctxt "CWConstraintType" +msgstr "" + +msgctxt "CWSource" msgid "name" msgstr "" -msgctxt "CWEType" +msgctxt "Workflow" msgid "name" msgstr "" @@ -2938,6 +2947,18 @@ msgid "name" msgstr "" +msgctxt "CWConstraintType" +msgid "name" +msgstr "" + +msgctxt "WorkflowTransition" +msgid "name" +msgstr "" + +msgctxt "State" +msgid "name" +msgstr "name" + msgctxt "CWPermission" msgid "name" msgstr "name" @@ -2946,25 +2967,13 @@ msgid "name" msgstr "name" -msgctxt "CWSource" -msgid "name" -msgstr "" - -msgctxt "State" +msgctxt "BaseTransition" msgid "name" msgstr "name" -msgctxt "Transition" -msgid "name" -msgstr "" - -msgctxt "Workflow" +msgctxt "CWCache" msgid "name" -msgstr "" - -msgctxt "WorkflowTransition" -msgid "name" -msgstr "" +msgstr "name" msgid "name of the cache" msgstr "" @@ -3177,10 +3186,10 @@ msgid "prefered_form" msgstr "prefered form" +msgctxt "EmailAddress" msgid "prefered_form_object" msgstr "prefered over" -msgctxt "EmailAddress" msgid "prefered_form_object" msgstr "prefered over" @@ -3200,10 +3209,10 @@ msgid "primary_email" msgstr "primary email" +msgctxt "EmailAddress" msgid "primary_email_object" msgstr "primary email of" -msgctxt "EmailAddress" msgid "primary_email_object" msgstr "primary email of" @@ -3231,11 +3240,11 @@ msgid "read_permission" msgstr "can be read by" -msgctxt "CWAttribute" +msgctxt "CWEType" msgid "read_permission" msgstr "read permission" -msgctxt "CWEType" +msgctxt "CWAttribute" msgid "read_permission" msgstr "read permission" @@ -3243,9 +3252,6 @@ msgid "read_permission" msgstr "read permission" -msgid "read_permission_object" -msgstr "has permission to read" - msgctxt "CWGroup" msgid "read_permission_object" msgstr "can be read by" @@ -3254,6 +3260,9 @@ msgid "read_permission_object" msgstr "can be read by" +msgid "read_permission_object" +msgstr "has permission to read" + msgid "regexp matching host(s) to which this config applies" msgstr "" @@ -3290,10 +3299,10 @@ msgid "relation_type" msgstr "relation type" +msgctxt "CWRType" msgid "relation_type_object" msgstr "relation definitions" -msgctxt "CWRType" msgid "relation_type_object" msgstr "relation definitions" @@ -3307,6 +3316,7 @@ msgid "relations deleted" msgstr "" +msgctxt "CWAttribute" msgid "relations_object" msgstr "constrained by" @@ -3314,6 +3324,9 @@ msgid "relations_object" msgstr "constrained by" +msgid "relations_object" +msgstr "constrained by" + msgid "relative url of the bookmarked page" msgstr "" @@ -3327,11 +3340,11 @@ msgid "require_group" msgstr "require group" -msgctxt "CWPermission" +msgctxt "Transition" msgid "require_group" msgstr "require group" -msgctxt "Transition" +msgctxt "CWPermission" msgid "require_group" msgstr "require group" @@ -3339,10 +3352,10 @@ msgid "require_group" msgstr "require group" +msgctxt "CWGroup" msgid "require_group_object" msgstr "required by" -msgctxt "CWGroup" msgid "require_group_object" msgstr "required by" @@ -3543,10 +3556,10 @@ msgid "specializes" msgstr "specializes" +msgctxt "CWEType" msgid "specializes_object" msgstr "specialized by" -msgctxt "CWEType" msgid "specializes_object" msgstr "specialized by" @@ -3583,10 +3596,10 @@ msgid "state_of" msgstr "state of" +msgctxt "Workflow" msgid "state_of_object" msgstr "use states" -msgctxt "Workflow" msgid "state_of_object" msgstr "use states" @@ -3630,17 +3643,17 @@ msgid "subworkflow_exit" msgstr "subworkflow exit" -msgid "subworkflow_exit_object" -msgstr "subworkflow exit of" - msgctxt "SubWorkflowExitPoint" msgid "subworkflow_exit_object" msgstr "subworkflow exit of" +msgid "subworkflow_exit_object" +msgstr "subworkflow exit of" + +msgctxt "Workflow" msgid "subworkflow_object" msgstr "subworkflow of" -msgctxt "Workflow" msgid "subworkflow_object" msgstr "subworkflow of" @@ -3651,10 +3664,10 @@ msgid "subworkflow_state" msgstr "subworkflow state" +msgctxt "State" msgid "subworkflow_state_object" msgstr "exit point" -msgctxt "State" msgid "subworkflow_state_object" msgstr "exit point" @@ -3768,10 +3781,10 @@ msgid "to_entity" msgstr "to entity" +msgctxt "CWEType" msgid "to_entity_object" msgstr "object relations" -msgctxt "CWEType" msgid "to_entity_object" msgstr "object relations" @@ -3785,13 +3798,16 @@ msgid "to_state" msgstr "to state" -msgid "to_state_object" -msgstr "transitions to this state" - msgctxt "State" msgid "to_state_object" msgstr "transitions to this state" +msgid "to_state_object" +msgstr "transitions to this state" + +msgid "today" +msgstr "" + msgid "todo_by" msgstr "to do by" @@ -3836,10 +3852,10 @@ msgid "transition_of" msgstr "transition of" +msgctxt "Workflow" msgid "transition_of_object" msgstr "use transitions" -msgctxt "Workflow" msgid "transition_of_object" msgstr "use transitions" @@ -3856,10 +3872,6 @@ msgid "type" msgstr "type" -msgctxt "CWSource" -msgid "type" -msgstr "" - msgctxt "Transition" msgid "type" msgstr "type" @@ -3868,6 +3880,10 @@ msgid "type" msgstr "type" +msgctxt "CWSource" +msgid "type" +msgstr "" + msgid "type here a sparql query" msgstr "" @@ -3948,17 +3964,14 @@ msgid "update_permission" msgstr "can be updated by" +msgctxt "CWEType" +msgid "update_permission" +msgstr "can be updated by" + msgctxt "CWAttribute" msgid "update_permission" msgstr "can be updated by" -msgctxt "CWEType" -msgid "update_permission" -msgstr "can be updated by" - -msgid "update_permission_object" -msgstr "has permission to update" - msgctxt "CWGroup" msgid "update_permission_object" msgstr "has permission to update" @@ -3967,6 +3980,9 @@ msgid "update_permission_object" msgstr "has permission to update" +msgid "update_permission_object" +msgstr "has permission to update" + msgid "update_relation" msgstr "update" @@ -4000,10 +4016,10 @@ msgid "use_email" msgstr "use email" +msgctxt "EmailAddress" msgid "use_email_object" msgstr "used by" -msgctxt "EmailAddress" msgid "use_email_object" msgstr "used by" @@ -4186,10 +4202,10 @@ msgid "workflow_of" msgstr "workflow of" +msgctxt "CWEType" msgid "workflow_of_object" msgstr "may use workflow" -msgctxt "CWEType" msgid "workflow_of_object" msgstr "may use workflow" diff -r a8fbcf9b6572 -r 8e59c2cdcc99 i18n/es.po --- a/i18n/es.po Fri Feb 11 11:37:10 2011 +0100 +++ b/i18n/es.po Fri Feb 11 12:17:47 2011 +0100 @@ -8,10 +8,10 @@ "Last-Translator: Celso Flores, Carlos Balderas " "\n" "Language-Team: es \n" -"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: \n" #, python-format msgid "" @@ -25,8 +25,8 @@ "url: %(url)s\n" msgstr "" "\n" -"%(user)s ha cambiado su estado de <%(previous_state)s> hacia <" -"%(current_state)s> por la entidad\n" +"%(user)s ha cambiado su estado de <%(previous_state)s> hacia <%" +"(current_state)s> por la entidad\n" "'%(title)s'\n" "\n" "%(comment)s\n" @@ -436,10 +436,10 @@ msgid "Download schema as OWL" msgstr "Descargar el esquema en formato OWL" +msgctxt "inlined:CWUser.use_email.subject" msgid "EmailAddress" msgstr "Correo Electrónico" -msgctxt "inlined:CWUser.use_email.subject" msgid "EmailAddress" msgstr "Correo Electrónico" @@ -1053,9 +1053,6 @@ msgid "add_permission" msgstr "Permiso de agregar" -msgid "add_permission_object" -msgstr "tiene permiso de agregar" - msgctxt "CWGroup" msgid "add_permission_object" msgstr "tiene permiso de agregar" @@ -1064,6 +1061,9 @@ msgid "add_permission_object" msgstr "tiene permiso de agregar" +msgid "add_permission_object" +msgstr "tiene permiso de agregar" + msgid "add_relation" msgstr "agregar" @@ -1073,8 +1073,8 @@ #, python-format msgid "" -"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #" -"%(eidto)s" +"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%" +"(eidto)s" msgstr "" "la relación %(rtype)s de %(frometype)s #%(eidfrom)s a %(toetype)s #%(eidto)s " "ha sido agregada" @@ -1109,9 +1109,6 @@ msgid "allowed_transition" msgstr "transiciones autorizadas" -msgid "allowed_transition_object" -msgstr "Estados de entrada" - msgctxt "BaseTransition" msgid "allowed_transition_object" msgstr "transición autorizada de" @@ -1124,6 +1121,9 @@ msgid "allowed_transition_object" msgstr "transición autorizada de" +msgid "allowed_transition_object" +msgstr "Estados de entrada" + msgid "am/pm calendar (month)" msgstr "calendario am/pm (mes)" @@ -1217,10 +1217,10 @@ msgid "bookmarked_by" msgstr "está en los Favoritos de" +msgctxt "CWUser" msgid "bookmarked_by_object" msgstr "tiene como Favoritos" -msgctxt "CWUser" msgid "bookmarked_by_object" msgstr "tiene como Favoritos" @@ -1261,9 +1261,6 @@ msgid "by_transition" msgstr "transición" -msgid "by_transition_object" -msgstr "cambio de estados" - msgctxt "BaseTransition" msgid "by_transition_object" msgstr "tiene como información" @@ -1276,6 +1273,9 @@ msgid "by_transition_object" msgstr "tiene como información" +msgid "by_transition_object" +msgstr "cambio de estados" + msgid "calendar" msgstr "mostrar un calendario" @@ -1434,10 +1434,10 @@ msgid "condition" msgstr "condición" +msgctxt "RQLExpression" msgid "condition_object" msgstr "condición de" -msgctxt "RQLExpression" msgid "condition_object" msgstr "condición de" @@ -1447,11 +1447,11 @@ msgid "config" msgstr "" -msgctxt "CWSource" +msgctxt "CWSourceHostConfig" msgid "config" msgstr "" -msgctxt "CWSourceHostConfig" +msgctxt "CWSource" msgid "config" msgstr "" @@ -1475,10 +1475,10 @@ msgid "constrained_by" msgstr "Restricción impuesta por" +msgctxt "CWConstraint" msgid "constrained_by_object" msgstr "Restricción de" -msgctxt "CWConstraint" msgid "constrained_by_object" msgstr "Restricción de" @@ -1492,10 +1492,10 @@ msgid "constraint_of" msgstr "" +msgctxt "CWEType" msgid "constraint_of_object" msgstr "" -msgctxt "CWEType" msgid "constraint_of_object" msgstr "" @@ -1593,8 +1593,8 @@ msgstr "Creación de la relación %(linkto)s" msgid "" -"creating CWSourceHostConfig (CWSourceHostConfig cw_host_config_of CWSource " -"%(linkto)s)" +"creating CWSourceHostConfig (CWSourceHostConfig cw_host_config_of CWSource %" +"(linkto)s)" msgstr "" msgid "" @@ -1687,8 +1687,8 @@ "Creación de una Transición Workflow permitida desde el estado %(linkto)s" msgid "" -"creating WorkflowTransition (WorkflowTransition transition_of Workflow " -"%(linkto)s)" +"creating WorkflowTransition (WorkflowTransition transition_of Workflow %" +"(linkto)s)" msgstr "Creación de una Transición Workflow del Workflow %(linkto)s" msgid "creation" @@ -1710,13 +1710,13 @@ msgid "cstrtype" msgstr "Tipo" -msgid "cstrtype_object" -msgstr "utilizado por" - msgctxt "CWConstraintType" msgid "cstrtype_object" msgstr "Tipo de restricciones" +msgid "cstrtype_object" +msgstr "utilizado por" + msgid "csv entities export" msgstr "Exportar entidades en csv" @@ -1846,10 +1846,10 @@ msgid "cw_dont_cross" msgstr "" +msgctxt "CWRType" msgid "cw_dont_cross_object" msgstr "" -msgctxt "CWRType" msgid "cw_dont_cross_object" msgstr "" @@ -1860,10 +1860,10 @@ msgid "cw_host_config_of" msgstr "" +msgctxt "CWSource" msgid "cw_host_config_of_object" msgstr "" -msgctxt "CWSource" msgid "cw_host_config_of_object" msgstr "" @@ -1874,10 +1874,10 @@ msgid "cw_may_cross" msgstr "" +msgctxt "CWRType" msgid "cw_may_cross_object" msgstr "" -msgctxt "CWRType" msgid "cw_may_cross_object" msgstr "" @@ -1894,9 +1894,6 @@ msgid "cw_support" msgstr "" -msgid "cw_support_object" -msgstr "" - msgctxt "CWEType" msgid "cw_support_object" msgstr "" @@ -1905,6 +1902,9 @@ msgid "cw_support_object" msgstr "" +msgid "cw_support_object" +msgstr "" + msgid "cwetype-box" msgstr "Vista \"caja\"" @@ -1941,6 +1941,9 @@ msgid "date" msgstr "Fecha" +msgid "day" +msgstr "" + msgid "deactivate" msgstr "Desactivar" @@ -1973,10 +1976,10 @@ msgid "default_workflow" msgstr "Workflow por defecto" +msgctxt "Workflow" msgid "default_workflow_object" msgstr "Workflow por defecto de" -msgctxt "Workflow" msgid "default_workflow_object" msgstr "Workflow por defecto de" @@ -2062,9 +2065,6 @@ msgid "delete_permission" msgstr "Permiso de eliminar" -msgid "delete_permission_object" -msgstr "posee permiso para eliminar" - msgctxt "CWGroup" msgid "delete_permission_object" msgstr "puede eliminar" @@ -2073,14 +2073,17 @@ msgid "delete_permission_object" msgstr "puede eliminar" +msgid "delete_permission_object" +msgstr "posee permiso para eliminar" + #, python-format msgid "deleted %(etype)s #%(eid)s (%(title)s)" msgstr "Eliminación de la entidad %(etype)s #%(eid)s (%(title)s)" #, python-format msgid "" -"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #" -"%(eidto)s" +"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%" +"(eidto)s" msgstr "" "La relación %(rtype)s de %(frometype)s #%(eidfrom)s a %(toetype)s #%(eidto)s " "ha sido suprimida." @@ -2091,7 +2094,15 @@ msgid "description" msgstr "Descripción" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "description" +msgstr "Descripción" + +msgctxt "CWRelation" +msgid "description" +msgstr "Descripción" + +msgctxt "Workflow" msgid "description" msgstr "Descripción" @@ -2099,38 +2110,38 @@ msgid "description" msgstr "Descripción" -msgctxt "CWEType" -msgid "description" -msgstr "Descripción" - -msgctxt "CWRType" -msgid "description" -msgstr "Descripción" - -msgctxt "CWRelation" -msgid "description" -msgstr "Descripción" - -msgctxt "State" -msgid "description" -msgstr "Descripción" - msgctxt "Transition" msgid "description" msgstr "Descripción" -msgctxt "Workflow" -msgid "description" -msgstr "Descripción" - msgctxt "WorkflowTransition" msgid "description" msgstr "Descripción" +msgctxt "State" +msgid "description" +msgstr "Descripción" + +msgctxt "CWRType" +msgid "description" +msgstr "Descripción" + +msgctxt "BaseTransition" +msgid "description" +msgstr "Descripción" + msgid "description_format" msgstr "Formato" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "description_format" +msgstr "Formato" + +msgctxt "CWRelation" +msgid "description_format" +msgstr "Formato" + +msgctxt "Workflow" msgid "description_format" msgstr "Formato" @@ -2138,7 +2149,15 @@ msgid "description_format" msgstr "Formato" -msgctxt "CWEType" +msgctxt "Transition" +msgid "description_format" +msgstr "Formato" + +msgctxt "WorkflowTransition" +msgid "description_format" +msgstr "Formato" + +msgctxt "State" msgid "description_format" msgstr "Formato" @@ -2146,23 +2165,7 @@ msgid "description_format" msgstr "Formato" -msgctxt "CWRelation" -msgid "description_format" -msgstr "Formato" - -msgctxt "State" -msgid "description_format" -msgstr "Formato" - -msgctxt "Transition" -msgid "description_format" -msgstr "Formato" - -msgctxt "Workflow" -msgid "description_format" -msgstr "Formato" - -msgctxt "WorkflowTransition" +msgctxt "BaseTransition" msgid "description_format" msgstr "Formato" @@ -2188,21 +2191,21 @@ msgid "destination_state" msgstr "Estado destino" +msgctxt "Transition" +msgid "destination_state" +msgstr "Estado destino" + msgctxt "SubWorkflowExitPoint" msgid "destination_state" msgstr "Estado destino" -msgctxt "Transition" -msgid "destination_state" -msgstr "Estado destino" - -msgid "destination_state_object" -msgstr "Destino de" - msgctxt "State" msgid "destination_state_object" msgstr "Estado final de" +msgid "destination_state_object" +msgstr "Destino de" + msgid "detach attached file" msgstr "soltar el archivo existente" @@ -2465,13 +2468,13 @@ msgid "for_user" msgstr "Propiedad del Usuario" -msgid "for_user_object" -msgstr "Utiliza las propiedades" - msgctxt "CWUser" msgid "for_user_object" msgstr "Tiene como preferencia" +msgid "for_user_object" +msgstr "Utiliza las propiedades" + msgid "friday" msgstr "Viernes" @@ -2493,13 +2496,13 @@ msgid "from_entity" msgstr "Relación de la entidad" -msgid "from_entity_object" -msgstr "Relación sujeto" - msgctxt "CWEType" msgid "from_entity_object" msgstr "Entidad de" +msgid "from_entity_object" +msgstr "Relación sujeto" + msgid "from_interval_start" msgstr "De" @@ -2510,13 +2513,13 @@ msgid "from_state" msgstr "Estado de Inicio" -msgid "from_state_object" -msgstr "Transiciones desde este estado" - msgctxt "State" msgid "from_state_object" msgstr "Estado de Inicio de" +msgid "from_state_object" +msgstr "Transiciones desde este estado" + msgid "full text or RQL query" msgstr "Texto de búsqueda o demanda RQL" @@ -2570,8 +2573,8 @@ "graphical representation of the %(rtype)s relation type from %(appid)s data " "model" msgstr "" -"Representación gráfica del modelo de datos para el tipo de relación " -"%(rtype)s de %(appid)s" +"Representación gráfica del modelo de datos para el tipo de relación %(rtype)" +"s de %(appid)s" msgid "group in which a user should be to be allowed to pass this transition" msgstr "Grupo en el cual el usuario debe estar lograr la transición" @@ -2600,6 +2603,9 @@ msgid "header-right" msgstr "" +msgid "help" +msgstr "" + msgid "hide filter form" msgstr "Esconder el filtro" @@ -2689,13 +2695,13 @@ msgid "in_group" msgstr "Forma parte del grupo" -msgid "in_group_object" -msgstr "Miembros" - msgctxt "CWGroup" msgid "in_group_object" msgstr "Contiene los usuarios" +msgid "in_group_object" +msgstr "Miembros" + msgid "in_state" msgstr "Estado" @@ -2748,10 +2754,10 @@ msgid "initial_state" msgstr "Estado inicial" +msgctxt "State" msgid "initial_state_object" msgstr "Estado inicial de" -msgctxt "State" msgid "initial_state_object" msgstr "Estado inicial de" @@ -3017,6 +3023,9 @@ msgid "monday" msgstr "Lunes" +msgid "month" +msgstr "" + msgid "more actions" msgstr "Más acciones" @@ -3032,11 +3041,23 @@ msgid "name" msgstr "Nombre" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "name" +msgstr "Nombre" + +msgctxt "Transition" msgid "name" msgstr "Nombre" -msgctxt "CWCache" +msgctxt "CWSource" +msgid "name" +msgstr "" + +msgctxt "Workflow" +msgid "name" +msgstr "Nombre" + +msgctxt "CWGroup" msgid "name" msgstr "Nombre" @@ -3044,11 +3065,11 @@ msgid "name" msgstr "Nombre" -msgctxt "CWEType" +msgctxt "WorkflowTransition" msgid "name" msgstr "Nombre" -msgctxt "CWGroup" +msgctxt "State" msgid "name" msgstr "Nombre" @@ -3060,23 +3081,11 @@ msgid "name" msgstr "Nombre" -msgctxt "CWSource" -msgid "name" -msgstr "" - -msgctxt "State" +msgctxt "BaseTransition" msgid "name" msgstr "Nombre" -msgctxt "Transition" -msgid "name" -msgstr "Nombre" - -msgctxt "Workflow" -msgid "name" -msgstr "Nombre" - -msgctxt "WorkflowTransition" +msgctxt "CWCache" msgid "name" msgstr "Nombre" @@ -3292,13 +3301,13 @@ msgid "prefered_form" msgstr "Email principal" -msgid "prefered_form_object" -msgstr "Formato preferido sobre" - msgctxt "EmailAddress" msgid "prefered_form_object" msgstr "Email principal de" +msgid "prefered_form_object" +msgstr "Formato preferido sobre" + msgid "preferences" msgstr "Preferencias" @@ -3315,13 +3324,13 @@ msgid "primary_email" msgstr "Dirección principal de correo electrónico" -msgid "primary_email_object" -msgstr "Dirección de email principal (objeto)" - msgctxt "EmailAddress" msgid "primary_email_object" msgstr "Dirección principal de correo electrónico de" +msgid "primary_email_object" +msgstr "Dirección de email principal (objeto)" + msgid "profile" msgstr "" @@ -3346,11 +3355,11 @@ msgid "read_permission" msgstr "Permiso de lectura" -msgctxt "CWAttribute" +msgctxt "CWEType" msgid "read_permission" msgstr "Permiso de Lectura" -msgctxt "CWEType" +msgctxt "CWAttribute" msgid "read_permission" msgstr "Permiso de Lectura" @@ -3358,9 +3367,6 @@ msgid "read_permission" msgstr "Permiso de Lectura" -msgid "read_permission_object" -msgstr "Tiene acceso de lectura a" - msgctxt "CWGroup" msgid "read_permission_object" msgstr "Puede leer" @@ -3369,6 +3375,9 @@ msgid "read_permission_object" msgstr "Puede leer" +msgid "read_permission_object" +msgstr "Tiene acceso de lectura a" + msgid "regexp matching host(s) to which this config applies" msgstr "" @@ -3405,10 +3414,10 @@ msgid "relation_type" msgstr "Tipo de Relación" +msgctxt "CWRType" msgid "relation_type_object" msgstr "Definición de Relaciones" -msgctxt "CWRType" msgid "relation_type_object" msgstr "Definición de Relaciones" @@ -3422,6 +3431,7 @@ msgid "relations deleted" msgstr "Relaciones Eliminadas" +msgctxt "CWAttribute" msgid "relations_object" msgstr "" @@ -3442,11 +3452,11 @@ msgid "require_group" msgstr "Restringida al Grupo" -msgctxt "CWPermission" +msgctxt "Transition" msgid "require_group" msgstr "Restringida al Grupo" -msgctxt "Transition" +msgctxt "CWPermission" msgid "require_group" msgstr "Restringida al Grupo" @@ -3454,10 +3464,10 @@ msgid "require_group" msgstr "Restringida al Grupo" +msgctxt "CWGroup" msgid "require_group_object" msgstr "Posee derechos sobre" -msgctxt "CWGroup" msgid "require_group_object" msgstr "Posee derechos sobre" @@ -3667,10 +3677,10 @@ msgid "specializes" msgstr "Especializa" +msgctxt "CWEType" msgid "specializes_object" msgstr "Especializado por" -msgctxt "CWEType" msgid "specializes_object" msgstr "Especializado por" @@ -3709,10 +3719,10 @@ msgid "state_of" msgstr "Estado de" +msgctxt "Workflow" msgid "state_of_object" msgstr "Tiene por Estado" -msgctxt "Workflow" msgid "state_of_object" msgstr "Tiene por Estado" @@ -3758,17 +3768,17 @@ msgid "subworkflow_exit" msgstr "Salida del Sub-Workflow" -msgid "subworkflow_exit_object" -msgstr "Salida Sub-Workflow de" - msgctxt "SubWorkflowExitPoint" msgid "subworkflow_exit_object" msgstr "Salida Sub-Workflow de" +msgid "subworkflow_exit_object" +msgstr "Salida Sub-Workflow de" + +msgctxt "Workflow" msgid "subworkflow_object" msgstr "Sub-Workflow de" -msgctxt "Workflow" msgid "subworkflow_object" msgstr "Sub-Workflow de" @@ -3779,10 +3789,10 @@ msgid "subworkflow_state" msgstr "Estado de Sub-Workflow" +msgctxt "State" msgid "subworkflow_state_object" msgstr "Estado de Salida de" -msgctxt "State" msgid "subworkflow_state_object" msgstr "Estado de Salida de" @@ -3896,10 +3906,10 @@ msgid "to_entity" msgstr "Por la entidad" +msgctxt "CWEType" msgid "to_entity_object" msgstr "Objeto de la Relación" -msgctxt "CWEType" msgid "to_entity_object" msgstr "Objeto de la Relación" @@ -3913,13 +3923,16 @@ msgid "to_state" msgstr "Hacia el Estado" -msgid "to_state_object" -msgstr "Transición hacia este Estado" - msgctxt "State" msgid "to_state_object" msgstr "Transición hacia este Estado" +msgid "to_state_object" +msgstr "Transición hacia este Estado" + +msgid "today" +msgstr "" + msgid "todo_by" msgstr "Asignada a" @@ -3964,10 +3977,10 @@ msgid "transition_of" msgstr "Transición de" +msgctxt "Workflow" msgid "transition_of_object" msgstr "Utiliza las transiciones" -msgctxt "Workflow" msgid "transition_of_object" msgstr "Utiliza las transiciones" @@ -3984,10 +3997,6 @@ msgid "type" msgstr "Tipo" -msgctxt "CWSource" -msgid "type" -msgstr "" - msgctxt "Transition" msgid "type" msgstr "Tipo" @@ -3996,6 +4005,10 @@ msgid "type" msgstr "Tipo" +msgctxt "CWSource" +msgid "type" +msgstr "" + msgid "type here a sparql query" msgstr "Escriba aquí su consulta en Sparql" @@ -4076,17 +4089,14 @@ msgid "update_permission" msgstr "Puede ser modificado por" +msgctxt "CWEType" +msgid "update_permission" +msgstr "Puede ser modificado por" + msgctxt "CWAttribute" msgid "update_permission" msgstr "Puede ser modificado por" -msgctxt "CWEType" -msgid "update_permission" -msgstr "Puede ser modificado por" - -msgid "update_permission_object" -msgstr "Tiene permiso de modificar" - msgctxt "CWGroup" msgid "update_permission_object" msgstr "Puede modificar" @@ -4095,6 +4105,9 @@ msgid "update_permission_object" msgstr "Puede modificar" +msgid "update_permission_object" +msgstr "Tiene permiso de modificar" + msgid "update_relation" msgstr "Modificar" @@ -4131,13 +4144,13 @@ msgid "use_email" msgstr "Usa el Correo Electrónico" -msgid "use_email_object" -msgstr "Email utilizado por" - msgctxt "EmailAddress" msgid "use_email_object" msgstr "Utilizado por" +msgid "use_email_object" +msgstr "Email utilizado por" + msgid "use_template_format" msgstr "Utilización del formato 'cubicweb template'" @@ -4326,10 +4339,10 @@ msgid "workflow_of" msgstr "Workflow de" +msgctxt "CWEType" msgid "workflow_of_object" msgstr "Utiliza el Workflow" -msgctxt "CWEType" msgid "workflow_of_object" msgstr "Utiliza el Workflow" diff -r a8fbcf9b6572 -r 8e59c2cdcc99 i18n/fr.po --- a/i18n/fr.po Fri Feb 11 11:37:10 2011 +0100 +++ b/i18n/fr.po Fri Feb 11 12:17:47 2011 +0100 @@ -7,10 +7,10 @@ "PO-Revision-Date: 2011-01-03 14:35+0100\n" "Last-Translator: Logilab Team \n" "Language-Team: fr \n" -"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: \n" #, python-format msgid "" @@ -434,10 +434,10 @@ msgid "Download schema as OWL" msgstr "Télécharger le schéma au format OWL" +msgctxt "inlined:CWUser.use_email.subject" msgid "EmailAddress" msgstr "Adresse électronique" -msgctxt "inlined:CWUser.use_email.subject" msgid "EmailAddress" msgstr "Adresse électronique" @@ -1054,9 +1054,6 @@ msgid "add_permission" msgstr "permission d'ajout" -msgid "add_permission_object" -msgstr "a la permission d'ajouter" - msgctxt "CWGroup" msgid "add_permission_object" msgstr "a la permission d'ajouter" @@ -1065,6 +1062,9 @@ msgid "add_permission_object" msgstr "a la permission d'ajouter" +msgid "add_permission_object" +msgstr "a la permission d'ajouter" + msgid "add_relation" msgstr "ajouter" @@ -1074,11 +1074,11 @@ #, python-format msgid "" -"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #" -"%(eidto)s" +"added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%" +"(eidto)s" msgstr "" -"la relation %(rtype)s de %(frometype)s #%(eidfrom)s vers %(toetype)s #" -"%(eidto)s a été ajoutée" +"la relation %(rtype)s de %(frometype)s #%(eidfrom)s vers %(toetype)s #%" +"(eidto)s a été ajoutée" msgid "addrelated" msgstr "ajouter" @@ -1110,9 +1110,6 @@ msgid "allowed_transition" msgstr "transitions autorisées" -msgid "allowed_transition_object" -msgstr "états en entrée" - msgctxt "BaseTransition" msgid "allowed_transition_object" msgstr "transition autorisée de" @@ -1125,6 +1122,9 @@ msgid "allowed_transition_object" msgstr "transition autorisée de" +msgid "allowed_transition_object" +msgstr "états en entrée" + msgid "am/pm calendar (month)" msgstr "calendrier am/pm (mois)" @@ -1218,10 +1218,10 @@ msgid "bookmarked_by" msgstr "utilisé par" +msgctxt "CWUser" msgid "bookmarked_by_object" msgstr "utilise le(s) signet(s)" -msgctxt "CWUser" msgid "bookmarked_by_object" msgstr "utilise le(s) signet(s)" @@ -1263,9 +1263,6 @@ msgid "by_transition" msgstr "transition" -msgid "by_transition_object" -msgstr "changement d'états" - msgctxt "BaseTransition" msgid "by_transition_object" msgstr "a pour information" @@ -1278,6 +1275,9 @@ msgid "by_transition_object" msgstr "a pour information" +msgid "by_transition_object" +msgstr "changement d'états" + msgid "calendar" msgstr "afficher un calendrier" @@ -1436,10 +1436,10 @@ msgid "condition" msgstr "condition" +msgctxt "RQLExpression" msgid "condition_object" msgstr "condition de" -msgctxt "RQLExpression" msgid "condition_object" msgstr "condition de" @@ -1449,11 +1449,11 @@ msgid "config" msgstr "configuration" -msgctxt "CWSource" +msgctxt "CWSourceHostConfig" msgid "config" msgstr "configuration" -msgctxt "CWSourceHostConfig" +msgctxt "CWSource" msgid "config" msgstr "configuration" @@ -1477,10 +1477,10 @@ msgid "constrained_by" msgstr "contraint par" +msgctxt "CWConstraint" msgid "constrained_by_object" msgstr "contrainte de" -msgctxt "CWConstraint" msgid "constrained_by_object" msgstr "contrainte de" @@ -1494,10 +1494,10 @@ msgid "constraint_of" msgstr "contrainte de" +msgctxt "CWEType" msgid "constraint_of_object" msgstr "contraint par" -msgctxt "CWEType" msgid "constraint_of_object" msgstr "contraint par" @@ -1596,8 +1596,8 @@ msgstr "création relation %(linkto)s" msgid "" -"creating CWSourceHostConfig (CWSourceHostConfig cw_host_config_of CWSource " -"%(linkto)s)" +"creating CWSourceHostConfig (CWSourceHostConfig cw_host_config_of CWSource %" +"(linkto)s)" msgstr "création d'une configuration d'hôte pour la source %(linkto)s" msgid "" @@ -1690,8 +1690,8 @@ msgstr "création d'une transition workflow autorisée depuis l'état %(linkto)s" msgid "" -"creating WorkflowTransition (WorkflowTransition transition_of Workflow " -"%(linkto)s)" +"creating WorkflowTransition (WorkflowTransition transition_of Workflow %" +"(linkto)s)" msgstr "création d'une transition workflow du workflow %(linkto)s" msgid "creation" @@ -1713,13 +1713,13 @@ msgid "cstrtype" msgstr "type" -msgid "cstrtype_object" -msgstr "utilisé par" - msgctxt "CWConstraintType" msgid "cstrtype_object" msgstr "type des contraintes" +msgid "cstrtype_object" +msgstr "utilisé par" + msgid "csv entities export" msgstr "export d'entités en CSV" @@ -1852,10 +1852,10 @@ msgid "cw_dont_cross" msgstr "don't cross" +msgctxt "CWRType" msgid "cw_dont_cross_object" msgstr "can't be crossed with" -msgctxt "CWRType" msgid "cw_dont_cross_object" msgstr "can't be crossed with" @@ -1866,10 +1866,10 @@ msgid "cw_host_config_of" msgstr "host configuration of" +msgctxt "CWSource" msgid "cw_host_config_of_object" msgstr "has host configuration" -msgctxt "CWSource" msgid "cw_host_config_of_object" msgstr "has host configuration" @@ -1880,10 +1880,10 @@ msgid "cw_may_cross" msgstr "may cross" +msgctxt "CWRType" msgid "cw_may_cross_object" msgstr "may be crossed with" -msgctxt "CWRType" msgid "cw_may_cross_object" msgstr "may be crossed with" @@ -1900,9 +1900,6 @@ msgid "cw_support" msgstr "support" -msgid "cw_support_object" -msgstr "supported by" - msgctxt "CWEType" msgid "cw_support_object" msgstr "supported by" @@ -1911,6 +1908,9 @@ msgid "cw_support_object" msgstr "supported by" +msgid "cw_support_object" +msgstr "supported by" + msgid "cwetype-box" msgstr "vue \"boîte\"" @@ -1947,6 +1947,9 @@ msgid "date" msgstr "date" +msgid "day" +msgstr "jour" + msgid "deactivate" msgstr "désactiver" @@ -1978,10 +1981,10 @@ msgid "default_workflow" msgstr "workflow par défaut" +msgctxt "Workflow" msgid "default_workflow_object" msgstr "workflow par défaut de" -msgctxt "Workflow" msgid "default_workflow_object" msgstr "workflow par défaut de" @@ -2064,9 +2067,6 @@ msgid "delete_permission" msgstr "permission de supprimer" -msgid "delete_permission_object" -msgstr "a la permission de supprimer" - msgctxt "CWGroup" msgid "delete_permission_object" msgstr "peut supprimer" @@ -2075,14 +2075,17 @@ msgid "delete_permission_object" msgstr "peut supprimer" +msgid "delete_permission_object" +msgstr "a la permission de supprimer" + #, python-format msgid "deleted %(etype)s #%(eid)s (%(title)s)" msgstr "suppression de l'entité %(etype)s #%(eid)s (%(title)s)" #, python-format msgid "" -"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #" -"%(eidto)s" +"deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%" +"(eidto)s" msgstr "" "relation %(rtype)s de %(frometype)s #%(eidfrom)s vers %(toetype)s #%(eidto)s " "supprimée" @@ -2093,7 +2096,15 @@ msgid "description" msgstr "description" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "description" +msgstr "description" + +msgctxt "CWRelation" +msgid "description" +msgstr "description" + +msgctxt "Workflow" msgid "description" msgstr "description" @@ -2101,38 +2112,38 @@ msgid "description" msgstr "description" -msgctxt "CWEType" -msgid "description" -msgstr "description" - -msgctxt "CWRType" -msgid "description" -msgstr "description" - -msgctxt "CWRelation" -msgid "description" -msgstr "description" - -msgctxt "State" -msgid "description" -msgstr "description" - msgctxt "Transition" msgid "description" msgstr "description" -msgctxt "Workflow" -msgid "description" -msgstr "description" - msgctxt "WorkflowTransition" msgid "description" msgstr "description" +msgctxt "State" +msgid "description" +msgstr "description" + +msgctxt "CWRType" +msgid "description" +msgstr "description" + +msgctxt "BaseTransition" +msgid "description" +msgstr "description" + msgid "description_format" msgstr "format" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "description_format" +msgstr "format" + +msgctxt "CWRelation" +msgid "description_format" +msgstr "format" + +msgctxt "Workflow" msgid "description_format" msgstr "format" @@ -2140,7 +2151,15 @@ msgid "description_format" msgstr "format" -msgctxt "CWEType" +msgctxt "Transition" +msgid "description_format" +msgstr "format" + +msgctxt "WorkflowTransition" +msgid "description_format" +msgstr "format" + +msgctxt "State" msgid "description_format" msgstr "format" @@ -2148,23 +2167,7 @@ msgid "description_format" msgstr "format" -msgctxt "CWRelation" -msgid "description_format" -msgstr "format" - -msgctxt "State" -msgid "description_format" -msgstr "format" - -msgctxt "Transition" -msgid "description_format" -msgstr "format" - -msgctxt "Workflow" -msgid "description_format" -msgstr "format" - -msgctxt "WorkflowTransition" +msgctxt "BaseTransition" msgid "description_format" msgstr "format" @@ -2190,21 +2193,21 @@ msgid "destination_state" msgstr "état de destination" +msgctxt "Transition" +msgid "destination_state" +msgstr "état de destination" + msgctxt "SubWorkflowExitPoint" msgid "destination_state" msgstr "état de destination" -msgctxt "Transition" -msgid "destination_state" -msgstr "état de destination" - -msgid "destination_state_object" -msgstr "destination de" - msgctxt "State" msgid "destination_state_object" msgstr "état final de" +msgid "destination_state_object" +msgstr "destination de" + msgid "detach attached file" msgstr "détacher le fichier existant" @@ -2466,13 +2469,13 @@ msgid "for_user" msgstr "propriété de l'utilisateur" -msgid "for_user_object" -msgstr "utilise les propriétés" - msgctxt "CWUser" msgid "for_user_object" msgstr "a pour préférence" +msgid "for_user_object" +msgstr "utilise les propriétés" + msgid "friday" msgstr "vendredi" @@ -2494,13 +2497,13 @@ msgid "from_entity" msgstr "relation de l'entité" -msgid "from_entity_object" -msgstr "relation sujet" - msgctxt "CWEType" msgid "from_entity_object" msgstr "entité de" +msgid "from_entity_object" +msgstr "relation sujet" + msgid "from_interval_start" msgstr "De" @@ -2511,13 +2514,13 @@ msgid "from_state" msgstr "état de départ" -msgid "from_state_object" -msgstr "transitions depuis cet état" - msgctxt "State" msgid "from_state_object" msgstr "état de départ de" +msgid "from_state_object" +msgstr "transitions depuis cet état" + msgid "full text or RQL query" msgstr "texte à rechercher ou requête RQL" @@ -2563,16 +2566,16 @@ "graphical representation of the %(etype)s entity type from %(appid)s data " "model" msgstr "" -"réprésentation graphique du modèle de données pour le type d'entité " -"%(etype)s de %(appid)s" +"réprésentation graphique du modèle de données pour le type d'entité %(etype)" +"s de %(appid)s" #, python-format msgid "" "graphical representation of the %(rtype)s relation type from %(appid)s data " "model" msgstr "" -"réprésentation graphique du modèle de données pour le type de relation " -"%(rtype)s de %(appid)s" +"réprésentation graphique du modèle de données pour le type de relation %" +"(rtype)s de %(appid)s" msgid "group in which a user should be to be allowed to pass this transition" msgstr "" @@ -2602,6 +2605,9 @@ msgid "header-right" msgstr "en-tête (droite)" +msgid "help" +msgstr "" + msgid "hide filter form" msgstr "cacher le filtre" @@ -2690,13 +2696,13 @@ msgid "in_group" msgstr "fait partie du groupe" -msgid "in_group_object" -msgstr "membres" - msgctxt "CWGroup" msgid "in_group_object" msgstr "contient les utilisateurs" +msgid "in_group_object" +msgstr "membres" + msgid "in_state" msgstr "état" @@ -2749,10 +2755,10 @@ msgid "initial_state" msgstr "état initial" +msgctxt "State" msgid "initial_state_object" msgstr "état initial de" -msgctxt "State" msgid "initial_state_object" msgstr "état initial de" @@ -3019,6 +3025,9 @@ msgid "monday" msgstr "lundi" +msgid "month" +msgstr "mois" + msgid "more actions" msgstr "plus d'actions" @@ -3034,11 +3043,23 @@ msgid "name" msgstr "nom" -msgctxt "BaseTransition" +msgctxt "CWEType" +msgid "name" +msgstr "nom" + +msgctxt "Transition" msgid "name" msgstr "nom" -msgctxt "CWCache" +msgctxt "CWSource" +msgid "name" +msgstr "nom" + +msgctxt "Workflow" +msgid "name" +msgstr "nom" + +msgctxt "CWGroup" msgid "name" msgstr "nom" @@ -3046,11 +3067,11 @@ msgid "name" msgstr "nom" -msgctxt "CWEType" +msgctxt "WorkflowTransition" msgid "name" msgstr "nom" -msgctxt "CWGroup" +msgctxt "State" msgid "name" msgstr "nom" @@ -3062,23 +3083,11 @@ msgid "name" msgstr "nom" -msgctxt "CWSource" -msgid "name" -msgstr "nom" - -msgctxt "State" +msgctxt "BaseTransition" msgid "name" msgstr "nom" -msgctxt "Transition" -msgid "name" -msgstr "nom" - -msgctxt "Workflow" -msgid "name" -msgstr "nom" - -msgctxt "WorkflowTransition" +msgctxt "CWCache" msgid "name" msgstr "nom" @@ -3296,13 +3305,13 @@ msgid "prefered_form" msgstr "forme préférée" -msgid "prefered_form_object" -msgstr "forme préférée à" - msgctxt "EmailAddress" msgid "prefered_form_object" msgstr "forme préférée de" +msgid "prefered_form_object" +msgstr "forme préférée à" + msgid "preferences" msgstr "préférences" @@ -3319,13 +3328,13 @@ msgid "primary_email" msgstr "email principal" -msgid "primary_email_object" -msgstr "adresse email principale (object)" - msgctxt "EmailAddress" msgid "primary_email_object" msgstr "adresse principale de" +msgid "primary_email_object" +msgstr "adresse email principale (object)" + msgid "profile" msgstr "profil" @@ -3350,11 +3359,11 @@ msgid "read_permission" msgstr "permission de lire" -msgctxt "CWAttribute" +msgctxt "CWEType" msgid "read_permission" msgstr "permission de lire" -msgctxt "CWEType" +msgctxt "CWAttribute" msgid "read_permission" msgstr "permission de lire" @@ -3362,9 +3371,6 @@ msgid "read_permission" msgstr "permission de lire" -msgid "read_permission_object" -msgstr "a la permission de lire" - msgctxt "CWGroup" msgid "read_permission_object" msgstr "peut lire" @@ -3373,6 +3379,9 @@ msgid "read_permission_object" msgstr "peut lire" +msgid "read_permission_object" +msgstr "a la permission de lire" + msgid "regexp matching host(s) to which this config applies" msgstr "" "expression régulière des noms d'hôtes auxquels cette configuration s'applique" @@ -3410,10 +3419,10 @@ msgid "relation_type" msgstr "type de relation" +msgctxt "CWRType" msgid "relation_type_object" msgstr "définition" -msgctxt "CWRType" msgid "relation_type_object" msgstr "définition" @@ -3427,10 +3436,10 @@ msgid "relations deleted" msgstr "relations supprimées" +msgctxt "CWRType" msgid "relations_object" msgstr "relations de" -msgctxt "CWRType" msgid "relations_object" msgstr "relations de" @@ -3447,11 +3456,11 @@ msgid "require_group" msgstr "restreinte au groupe" -msgctxt "CWPermission" +msgctxt "Transition" msgid "require_group" msgstr "restreinte au groupe" -msgctxt "Transition" +msgctxt "CWPermission" msgid "require_group" msgstr "restreinte au groupe" @@ -3459,10 +3468,10 @@ msgid "require_group" msgstr "restreinte au groupe" +msgctxt "CWGroup" msgid "require_group_object" msgstr "a les droits" -msgctxt "CWGroup" msgid "require_group_object" msgstr "a les droits" @@ -3675,10 +3684,10 @@ msgid "specializes" msgstr "spécialise" +msgctxt "CWEType" msgid "specializes_object" msgstr "parent de" -msgctxt "CWEType" msgid "specializes_object" msgstr "parent de" @@ -3717,13 +3726,13 @@ msgid "state_of" msgstr "état de" -msgid "state_of_object" -msgstr "a pour état" - msgctxt "Workflow" msgid "state_of_object" msgstr "contient les états" +msgid "state_of_object" +msgstr "a pour état" + msgid "status change" msgstr "changer l'état" @@ -3766,20 +3775,20 @@ msgid "subworkflow_exit" msgstr "sortie du sous-workflow" -msgid "subworkflow_exit_object" -msgstr "états de sortie" - msgctxt "SubWorkflowExitPoint" msgid "subworkflow_exit_object" msgstr "états de sortie" -msgid "subworkflow_object" -msgstr "utilisé par la transition" +msgid "subworkflow_exit_object" +msgstr "états de sortie" msgctxt "Workflow" msgid "subworkflow_object" msgstr "sous workflow de" +msgid "subworkflow_object" +msgstr "utilisé par la transition" + msgid "subworkflow_state" msgstr "état du sous-workflow" @@ -3787,10 +3796,10 @@ msgid "subworkflow_state" msgstr "état" +msgctxt "State" msgid "subworkflow_state_object" msgstr "état de sortie de" -msgctxt "State" msgid "subworkflow_state_object" msgstr "état de sortie de" @@ -3905,10 +3914,10 @@ msgid "to_entity" msgstr "pour l'entité" +msgctxt "CWEType" msgid "to_entity_object" msgstr "objet de la relation" -msgctxt "CWEType" msgid "to_entity_object" msgstr "objet de la relation" @@ -3922,13 +3931,16 @@ msgid "to_state" msgstr "état de destination" -msgid "to_state_object" -msgstr "transitions vers cet état" - msgctxt "State" msgid "to_state_object" msgstr "transition vers cet état" +msgid "to_state_object" +msgstr "transitions vers cet état" + +msgid "today" +msgstr "aujourd'hui" + msgid "todo_by" msgstr "à faire par" @@ -3973,10 +3985,10 @@ msgid "transition_of" msgstr "transition de" +msgctxt "Workflow" msgid "transition_of_object" msgstr "a pour transition" -msgctxt "Workflow" msgid "transition_of_object" msgstr "a pour transition" @@ -3993,10 +4005,6 @@ msgid "type" msgstr "type" -msgctxt "CWSource" -msgid "type" -msgstr "type" - msgctxt "Transition" msgid "type" msgstr "type" @@ -4005,6 +4013,10 @@ msgid "type" msgstr "type" +msgctxt "CWSource" +msgid "type" +msgstr "type" + msgid "type here a sparql query" msgstr "Tapez une requête sparql" @@ -4085,17 +4097,14 @@ msgid "update_permission" msgstr "permission de modification" +msgctxt "CWEType" +msgid "update_permission" +msgstr "permission de modifier" + msgctxt "CWAttribute" msgid "update_permission" msgstr "permission de modifier" -msgctxt "CWEType" -msgid "update_permission" -msgstr "permission de modifier" - -msgid "update_permission_object" -msgstr "a la permission de modifier" - msgctxt "CWGroup" msgid "update_permission_object" msgstr "peut modifier" @@ -4104,6 +4113,9 @@ msgid "update_permission_object" msgstr "peut modifier" +msgid "update_permission_object" +msgstr "a la permission de modifier" + msgid "update_relation" msgstr "modifier" @@ -4140,13 +4152,13 @@ msgid "use_email" msgstr "utilise l'adresse électronique" -msgid "use_email_object" -msgstr "adresse utilisée par" - msgctxt "EmailAddress" msgid "use_email_object" msgstr "utilisée par" +msgid "use_email_object" +msgstr "adresse utilisée par" + msgid "use_template_format" msgstr "utilisation du format 'cubicweb template'" @@ -4272,7 +4284,7 @@ msgstr "mercredi" msgid "week" -msgstr "sem." +msgstr "semaine" #, python-format msgid "welcome %s !" @@ -4334,10 +4346,10 @@ msgid "workflow_of" msgstr "workflow de" +msgctxt "CWEType" msgid "workflow_of_object" msgstr "a pour workflow" -msgctxt "CWEType" msgid "workflow_of_object" msgstr "a pour workflow" diff -r a8fbcf9b6572 -r 8e59c2cdcc99 misc/migration/3.10.0_Any.py --- a/misc/migration/3.10.0_Any.py Fri Feb 11 11:37:10 2011 +0100 +++ b/misc/migration/3.10.0_Any.py Fri Feb 11 12:17:47 2011 +0100 @@ -5,7 +5,7 @@ for uri, cfg in config.sources().items(): if uri in ('system', 'admin'): continue - repo.sources_by_uri[uri] = repo.get_source(cfg['adapter'], uri, cfg) + repo.sources_by_uri[uri] = repo.get_source(cfg['adapter'], uri, cfg.copy()) add_entity_type('CWSource') add_relation_definition('CWSource', 'cw_source', 'CWSource') diff -r a8fbcf9b6572 -r 8e59c2cdcc99 misc/migration/3.11.0_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.11.0_Any.py Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,84 @@ +from datetime import datetime + +for rtype in ('cw_support', 'cw_dont_cross', 'cw_may_cross'): + drop_relation_type(rtype) + +add_entity_type('CWSourceSchemaConfig') + +if not 'url' in schema['CWSource'].subjrels: + add_attribute('CWSource', 'url') + add_attribute('CWSource', 'parser') + add_attribute('CWSource', 'latest_retrieval') + +try: + from cubicweb.server.sources.pyrorql import PyroRQLSource +except ImportError: + pass +else: + + from os.path import join + # function to read old python mapping file + def load_mapping_file(source): + mappingfile = source.config['mapping-file'] + mappingfile = join(source.repo.config.apphome, mappingfile) + mapping = {} + execfile(mappingfile, mapping) + for junk in ('__builtins__', '__doc__'): + mapping.pop(junk, None) + mapping.setdefault('support_relations', {}) + mapping.setdefault('dont_cross_relations', set()) + mapping.setdefault('cross_relations', set()) + # do some basic checks of the mapping content + assert 'support_entities' in mapping, \ + 'mapping file should at least define support_entities' + assert isinstance(mapping['support_entities'], dict) + assert isinstance(mapping['support_relations'], dict) + assert isinstance(mapping['dont_cross_relations'], set) + assert isinstance(mapping['cross_relations'], set) + unknown = set(mapping) - set( ('support_entities', 'support_relations', + 'dont_cross_relations', 'cross_relations') ) + assert not unknown, 'unknown mapping attribute(s): %s' % unknown + # relations that are necessarily not crossed + for rtype in ('is', 'is_instance_of', 'cw_source'): + assert rtype not in mapping['dont_cross_relations'], \ + '%s relation should not be in dont_cross_relations' % rtype + assert rtype not in mapping['support_relations'], \ + '%s relation should not be in support_relations' % rtype + return mapping + # for now, only pyrorql sources have a mapping + for source in repo.sources_by_uri.values(): + if not isinstance(source, PyroRQLSource): + continue + sourceentity = session.entity_from_eid(source.eid) + mapping = load_mapping_file(source) + # write mapping as entities + print 'migrating map for', source + for etype, write in mapping['support_entities'].items(): + create_entity('CWSourceSchemaConfig', + cw_for_source=sourceentity, + cw_schema=session.entity_from_eid(schema[etype].eid), + options=write and u'write' or None, + ask_confirm=False) + for rtype, write in mapping['support_relations'].items(): + options = [] + if write: + options.append(u'write') + if rtype in mapping['cross_relations']: + options.append(u'maycross') + create_entity('CWSourceSchemaConfig', + cw_for_source=sourceentity, + cw_schema=session.entity_from_eid(schema[rtype].eid), + options=u':'.join(options) or None, + ask_confirm=False) + for rtype in mapping['dont_cross_relations']: + create_entity('CWSourceSchemaConfig', + cw_for_source=source, + cw_schema=session.entity_from_eid(schema[etype].eid), + options=u'dontcross') + # latest update time cwproperty is now a source attribute (latest_retrieval) + pkey = u'sources.%s.latest-update-time' % source.uri + rset = session.execute('Any V WHERE X is CWProperty, X value V, X pkey %(k)s', + {'k': pkey}) + timestamp = int(rset[0][0]) + sourceentity.set_attributes(latest_retrieval=datetime.fromtimestamp(timestamp)) + session.execute('DELETE CWProperty X WHERE X pkey %(k)s', {'k': pkey}) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 schema.py --- a/schema.py Fri Feb 11 11:37:10 2011 +0100 +++ b/schema.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -65,6 +65,8 @@ NO_I18NCONTEXT = META_RTYPES | WORKFLOW_RTYPES NO_I18NCONTEXT.add('require_permission') +SKIP_COMPOSITE_RELS = [('cw_source', 'subject')] + # set of entity and relation types used to build the schema SCHEMA_TYPES = set(( 'CWEType', 'CWRType', 'CWAttribute', 'CWRelation', @@ -83,8 +85,7 @@ 'SubWorkflowExitPoint')) INTERNAL_TYPES = set(('CWProperty', 'CWPermission', 'CWCache', 'ExternalUri', - 'CWSource', 'CWSourceHostConfig', -)) + 'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig')) _LOGGER = getLogger('cubicweb.schemaloader') @@ -108,7 +109,7 @@ } PUB_SYSTEM_ATTR_PERMS = { 'read': ('managers', 'users', 'guests',), - 'update': ('managers',), + 'update': ('managers',), } RO_REL_PERMS = { 'read': ('managers', 'users', 'guests',), @@ -369,6 +370,14 @@ msg = "can't use RRQLExpression on %s, use an ERQLExpression" raise BadSchemaDefinition(msg % self.type) + def is_subobject(self, strict=False, skiprels=None): + if skiprels is None: + skiprels = SKIP_COMPOSITE_RELS + else: + skiprels += SKIP_COMPOSITE_RELS + return super(CubicWebEntitySchema, self).is_subobject(strict, + skiprels=skiprels) + def attribute_definitions(self): """return an iterator on attribute definitions diff -r a8fbcf9b6572 -r 8e59c2cdcc99 schemas/base.py --- a/schemas/base.py Fri Feb 11 11:37:10 2011 +0100 +++ b/schemas/base.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -21,7 +21,7 @@ _ = unicode from yams.buildobjs import (EntityType, RelationType, RelationDefinition, - SubjectRelation, String, Datetime, Password) + SubjectRelation, String, Datetime, Password, Interval) from cubicweb.schema import ( RQLConstraint, WorkflowableEntityType, ERQLExpression, RRQLExpression, PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS, PUB_SYSTEM_ATTR_PERMS) @@ -258,15 +258,30 @@ 'read': ('managers',), 'update': ('managers',), }) + # put this here and not in a subclass even if it's only for some sources + # since having subclasses on generic relation (cw_source) double the number + # of rdef in the schema, and make ms planning harder since queries solutions + # may changes when sources are specified + url = String(description=_('URLs from which content will be imported. You can put one url per line')) + parser = String(description=_('parser to use to extract entities from content retrieved at given URLs.')) + latest_retrieval = Datetime(description=_('latest synchronization time')) + + +ENTITY_MANAGERS_PERMISSIONS = { + 'read': ('managers',), + 'add': ('managers',), + 'update': ('managers',), + 'delete': ('managers',), + } +RELATION_MANAGERS_PERMISSIONS = { + 'read': ('managers',), + 'add': ('managers',), + 'delete': ('managers',), + } class CWSourceHostConfig(EntityType): - __permissions__ = { - 'read': ('managers',), - 'add': ('managers',), - 'update': ('managers',), - 'delete': ('managers',), - } + __permissions__ = ENTITY_MANAGERS_PERMISSIONS __unique_together__ = [('match_host', 'cw_host_config_of')] match_host = String(required=True, maxsize=128, description=_('regexp matching host(s) to which this config applies')) @@ -282,6 +297,7 @@ class cw_host_config_of(RelationDefinition): + __permissions__ = RELATION_MANAGERS_PERMISSIONS subject = 'CWSourceHostConfig' object = 'CWSource' cardinality = '1*' @@ -297,18 +313,20 @@ subject = '*' object = 'CWSource' cardinality = '1*' - -class cw_support(RelationDefinition): - subject = 'CWSource' - object = ('CWEType', 'CWRType') + composite = 'object' -class cw_dont_cross(RelationDefinition): - subject = 'CWSource' - object = 'CWRType' +class CWSourceSchemaConfig(EntityType): + __permissions__ = ENTITY_MANAGERS_PERMISSIONS + __unique_together__ = [('cw_for_source', 'cw_schema')] + cw_for_source = SubjectRelation( + 'CWSource', inlined=True, cardinality='1*', composite='object', + __permissions__=RELATION_MANAGERS_PERMISSIONS) + cw_schema = SubjectRelation( + ('CWEType', 'CWRType', 'CWAttribute', 'CWRelation'), + inlined=True, cardinality='1*', composite='object', + __permissions__=RELATION_MANAGERS_PERMISSIONS) + options = String(description=_('allowed options depends on the source type')) -class cw_may_cross(RelationDefinition): - subject = 'CWSource' - object = 'CWRType' # "abtract" relation types, no definition in cubicweb itself ################### diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/__init__.py --- a/server/__init__.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/__init__.py Fri Feb 11 12:17:47 2011 +0100 @@ -254,7 +254,7 @@ # available sources registry SOURCE_TYPES = {'native': LazyObject('cubicweb.server.sources.native', 'NativeSQLSource'), - # XXX private sources installed by an external cube 'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'), 'ldapuser': LazyObject('cubicweb.server.sources.ldapuser', 'LDAPUserSource'), + 'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'), } diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/checkintegrity.py --- a/server/checkintegrity.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/checkintegrity.py Fri Feb 11 12:17:47 2011 +0100 @@ -19,8 +19,6 @@ * integrity of a CubicWeb repository. Hum actually only the system database is checked. - -* consistency of multi-sources instance mapping file """ from __future__ import with_statement @@ -32,7 +30,7 @@ from logilab.common.shellutils import ProgressBar -from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES +from cubicweb.schema import PURE_VIRTUAL_RTYPES from cubicweb.server.sqlutils import SQL_PREFIX from cubicweb.server.session import security_enabled @@ -334,103 +332,3 @@ session.set_pool() reindex_entities(repo.schema, session, withpb=withpb) cnx.commit() - - -def info(msg, *args): - if args: - msg = msg % args - print 'INFO: %s' % msg - -def warning(msg, *args): - if args: - msg = msg % args - print 'WARNING: %s' % msg - -def error(msg, *args): - if args: - msg = msg % args - print 'ERROR: %s' % msg - -def check_mapping(schema, mapping, warning=warning, error=error): - # first check stuff found in mapping file exists in the schema - for attr in ('support_entities', 'support_relations'): - for ertype in mapping[attr].keys(): - try: - mapping[attr][ertype] = erschema = schema[ertype] - except KeyError: - error('reference to unknown type %s in %s', ertype, attr) - del mapping[attr][ertype] - else: - if erschema.final or erschema in META_RTYPES: - error('type %s should not be mapped in %s', ertype, attr) - del mapping[attr][ertype] - for attr in ('dont_cross_relations', 'cross_relations'): - for rtype in list(mapping[attr]): - try: - rschema = schema.rschema(rtype) - except KeyError: - error('reference to unknown relation type %s in %s', rtype, attr) - mapping[attr].remove(rtype) - else: - if rschema.final or rschema in VIRTUAL_RTYPES: - error('relation type %s should not be mapped in %s', - rtype, attr) - mapping[attr].remove(rtype) - # check relation in dont_cross_relations aren't in support_relations - for rschema in mapping['dont_cross_relations']: - if rschema in mapping['support_relations']: - info('relation %s is in dont_cross_relations and in support_relations', - rschema) - # check relation in cross_relations are in support_relations - for rschema in mapping['cross_relations']: - if rschema not in mapping['support_relations']: - info('relation %s is in cross_relations but not in support_relations', - rschema) - # check for relation in both cross_relations and dont_cross_relations - for rschema in mapping['cross_relations'] & mapping['dont_cross_relations']: - error('relation %s is in both cross_relations and dont_cross_relations', - rschema) - # now check for more handy things - seen = set() - for eschema in mapping['support_entities'].values(): - for rschema, ttypes, role in eschema.relation_definitions(): - if rschema in META_RTYPES: - continue - ttypes = [ttype for ttype in ttypes if ttype in mapping['support_entities']] - if not rschema in mapping['support_relations']: - somethingprinted = False - for ttype in ttypes: - rdef = rschema.role_rdef(eschema, ttype, role) - seen.add(rdef) - if rdef.role_cardinality(role) in '1+': - error('relation %s with %s as %s and target type %s is ' - 'mandatory but not supported', - rschema, eschema, role, ttype) - somethingprinted = True - elif ttype in mapping['support_entities']: - if rdef not in seen: - warning('%s could be supported', rdef) - somethingprinted = True - if rschema not in mapping['dont_cross_relations']: - if role == 'subject' and rschema.inlined: - error('inlined relation %s of %s should be supported', - rschema, eschema) - elif not somethingprinted and rschema not in seen and rschema not in mapping['cross_relations']: - print 'you may want to specify something for %s' % rschema - seen.add(rschema) - else: - if not ttypes: - warning('relation %s with %s as %s is supported but no target ' - 'type supported', rschema, role, eschema) - if rschema in mapping['cross_relations'] and rschema.inlined: - error('you should unline relation %s which is supported and ' - 'may be crossed ', rschema) - for rschema in mapping['support_relations'].values(): - if rschema in META_RTYPES: - continue - for subj, obj in rschema.rdefs: - if subj in mapping['support_entities'] and obj in mapping['support_entities']: - break - else: - error('relation %s is supported but none if its definitions ' - 'matches supported entities', rschema) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/msplanner.py --- a/server/msplanner.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/msplanner.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -519,6 +519,16 @@ invariant = getattr(lhs, '_q_invariant', False) # XXX NOT NOT neged = srel.neged(traverse_scope=True) or (rel and rel.neged(strict=True)) + has_copy_based_source = False + sources_ = [] + for source in sources: + if source.copy_based_source: + has_copy_based_source = True + if not self.system_source in sources_: + sources_.append(self.system_source) + else: + sources_.append(source) + sources = sources_ if neged: for source in sources: if invariant and source is self.system_source: @@ -535,7 +545,8 @@ if rel is None or (len(var.stinfo['relations']) == 2 and not var.stinfo['selected']): self._remove_source_term(self.system_source, var) - if not (len(sources) > 1 or usesys or invariant): + if not (has_copy_based_source or len(sources) > 1 + or usesys or invariant): if rel is None: srel.parent.remove(srel) else: diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/repository.py --- a/server/repository.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/repository.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -207,12 +207,6 @@ self.init_sources_from_database() if 'CWProperty' in self.schema: self.vreg.init_properties(self.properties()) - # call source's init method to complete their initialisation if - # needed (for instance looking for persistent configuration using an - # internal session, which is not possible until pools have been - # initialized) - for source in self.sources: - source.init() else: # call init_creating so that for instance native source can # configurate tsearch according to postgres version @@ -241,11 +235,12 @@ try: # FIXME: sources should be ordered (add_entity priority) for sourceent in session.execute( - 'Any S, SN, SA, SC WHERE S is CWSource, ' + 'Any S, SN, SA, SC WHERE S is_instance_of CWSource, ' 'S name SN, S type SA, S config SC').entities(): if sourceent.name == 'system': self.system_source.eid = sourceent.eid self.sources_by_eid[sourceent.eid] = self.system_source + self.system_source.init(True, sourceent) continue self.add_source(sourceent, add_to_pools=False) finally: @@ -258,34 +253,41 @@ def add_source(self, sourceent, add_to_pools=True): source = self.get_source(sourceent.type, sourceent.name, - sourceent.host_config) - source.eid = sourceent.eid + sourceent.host_config, sourceent.eid) self.sources_by_eid[sourceent.eid] = source self.sources_by_uri[sourceent.name] = source if self.config.source_enabled(source): - self.sources.append(source) - self.querier.set_planner() - if add_to_pools: - for pool in self.pools: - pool.add_source(source) + # call source's init method to complete their initialisation if + # needed (for instance looking for persistent configuration using an + # internal session, which is not possible until pools have been + # initialized) + source.init(True, sourceent) + if not source.copy_based_source: + self.sources.append(source) + self.querier.set_planner() + if add_to_pools: + for pool in self.pools: + pool.add_source(source) + else: + source.init(False, sourceent) self._clear_planning_caches() def remove_source(self, uri): source = self.sources_by_uri.pop(uri) del self.sources_by_eid[source.eid] - if self.config.source_enabled(source): + if self.config.source_enabled(source) and not source.copy_based_source: self.sources.remove(source) self.querier.set_planner() for pool in self.pools: pool.remove_source(source) self._clear_planning_caches() - def get_source(self, type, uri, source_config): + def get_source(self, type, uri, source_config, eid=None): # set uri and type in source config so it's available through # source_defs() source_config['uri'] = uri source_config['type'] = type - return sources.get_source(type, source_config, self) + return sources.get_source(type, source_config, self, eid) def set_schema(self, schema, resetvreg=True, rebuildinfered=True): if rebuildinfered: @@ -427,33 +429,24 @@ except ZeroDivisionError: pass - def _login_from_email(self, login): - session = self.internal_session() - try: - rset = session.execute('Any L WHERE U login L, U primary_email M, ' - 'M address %(login)s', {'login': login}, - build_descr=False) - if rset.rowcount == 1: - login = rset[0][0] - finally: - session.close() - return login - - def authenticate_user(self, session, login, **kwargs): - """validate login / password, raise AuthenticationError on failure - return associated CWUser instance on success + def check_auth_info(self, session, login, authinfo): + """validate authentication, raise AuthenticationError on failure, return + associated CWUser's eid on success. """ - if self.vreg.config['allow-email-login'] and '@' in login: - login = self._login_from_email(login) for source in self.sources: if source.support_entity('CWUser'): try: - eid = source.authenticate(session, login, **kwargs) - break + return source.authenticate(session, login, **authinfo) except AuthenticationError: continue else: raise AuthenticationError('authentication failed with all sources') + + def authenticate_user(self, session, login, **authinfo): + """validate login / password, raise AuthenticationError on failure + return associated CWUser instance on success + """ + eid = self.check_auth_info(session, login, authinfo) cwuser = self._build_user(session, eid) if self.config.consider_user_state and \ not cwuser.cw_adapt_to('IWorkflowable').state in cwuser.AUTHENTICABLE_STATES: @@ -1023,9 +1016,10 @@ return extid def extid2eid(self, source, extid, etype, session=None, insert=True, - recreate=False): + sourceparams=None): """get eid from a local id. An eid is attributed if no record is found""" - cachekey = (extid, source.uri) + uri = 'system' if source.copy_based_source else source.uri + cachekey = (extid, uri) try: return self._extid_cache[cachekey] except KeyError: @@ -1034,20 +1028,10 @@ if session is None: session = self.internal_session() reset_pool = True - eid = self.system_source.extid2eid(session, source, extid) + eid = self.system_source.extid2eid(session, uri, extid) if eid is not None: self._extid_cache[cachekey] = eid - self._type_source_cache[eid] = (etype, source.uri, extid) - # XXX used with extlite (eg vcsfile), probably not needed anymore - if recreate: - entity = source.before_entity_insertion(session, extid, etype, eid) - entity._cw_recreating = True - if source.should_call_hooks: - self.hm.call_hooks('before_add_entity', session, entity=entity) - # XXX add fti op ? - source.after_entity_insertion(session, extid, entity) - if source.should_call_hooks: - self.hm.call_hooks('after_add_entity', session, entity=entity) + self._type_source_cache[eid] = (etype, uri, extid) if reset_pool: session.reset_pool() return eid @@ -1065,13 +1049,14 @@ try: eid = self.system_source.create_eid(session) self._extid_cache[cachekey] = eid - self._type_source_cache[eid] = (etype, source.uri, extid) - entity = source.before_entity_insertion(session, extid, etype, eid) + self._type_source_cache[eid] = (etype, uri, extid) + entity = source.before_entity_insertion( + session, extid, etype, eid, sourceparams) if source.should_call_hooks: self.hm.call_hooks('before_add_entity', session, entity=entity) # XXX call add_info with complete=False ? self.add_info(session, entity, source, extid) - source.after_entity_insertion(session, extid, entity) + source.after_entity_insertion(session, extid, entity, sourceparams) if source.should_call_hooks: self.hm.call_hooks('after_add_entity', session, entity=entity) session.commit(reset_pool) @@ -1088,7 +1073,7 @@ hook.CleanupNewEidsCacheOp.get_instance(session).add_data(entity.eid) self.system_source.add_info(session, entity, source, extid, complete) - def delete_info(self, session, entity, sourceuri, extid, scleanup=False): + def delete_info(self, session, entity, sourceuri, extid, scleanup=None): """called by external source when some entity known by the system source has been deleted in the external source """ @@ -1097,7 +1082,7 @@ hook.CleanupDeletedEidsCacheOp.get_instance(session).add_data(entity.eid) self._delete_info(session, entity, sourceuri, extid, scleanup) - def delete_info_multi(self, session, entities, sourceuri, extids, scleanup=False): + def delete_info_multi(self, session, entities, sourceuri, extids, scleanup=None): """same as delete_info but accepts a list of entities and extids with the same etype and belonging to the same source """ @@ -1108,7 +1093,7 @@ op.add_data(entity.eid) self._delete_info_multi(session, entities, sourceuri, extids, scleanup) - def _delete_info(self, session, entity, sourceuri, extid, scleanup=False): + def _delete_info(self, session, entity, sourceuri, extid, scleanup=None): """delete system information on deletion of an entity: * delete all remaining relations from/to this entity * call delete info on the system source which will transfer record from @@ -1129,18 +1114,19 @@ rql = 'DELETE X %s Y WHERE X eid %%(x)s' % rtype else: rql = 'DELETE Y %s X WHERE X eid %%(x)s' % rtype - if scleanup: + if scleanup is not None: # source cleaning: only delete relations stored locally - rql += ', NOT (Y cw_source S, S name %(source)s)' + # (here, scleanup + rql += ', NOT (Y cw_source S, S eid %(seid)s)' try: - session.execute(rql, {'x': eid, 'source': sourceuri}, + session.execute(rql, {'x': eid, 'seid': scleanup}, build_descr=False) except: self.exception('error while cascading delete for entity %s ' 'from %s. RQL: %s', entity, sourceuri, rql) self.system_source.delete_info(session, entity, sourceuri, extid) - def _delete_info_multi(self, session, entities, sourceuri, extids, scleanup=False): + def _delete_info_multi(self, session, entities, sourceuri, extids, scleanup=None): """same as _delete_info but accepts a list of entities with the same etype and belinging to the same source. """ @@ -1161,12 +1147,11 @@ rql = 'DELETE X %s Y WHERE X eid IN (%s)' % (rtype, in_eids) else: rql = 'DELETE Y %s X WHERE X eid IN (%s)' % (rtype, in_eids) - if scleanup: + if scleanup is not None: # source cleaning: only delete relations stored locally - rql += ', NOT (Y cw_source S, S name %(source)s)' + rql += ', NOT (Y cw_source S, S eid %(seid)s)' try: - session.execute(rql, {'source': sourceuri}, - build_descr=False) + session.execute(rql, {'seid': scleanup}, build_descr=False) except: self.exception('error while cascading delete for entity %s ' 'from %s. RQL: %s', entities, sourceuri, rql) @@ -1208,6 +1193,8 @@ if suri == 'system': extid = None else: + if source.copy_based_source: + suri = 'system' extid = source.get_extid(entity) self._extid_cache[(str(extid), suri)] = entity.eid self._type_source_cache[entity.eid] = (entity.__regid__, suri, extid) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/serverctl.py --- a/server/serverctl.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/serverctl.py Fri Feb 11 12:17:47 2011 +0100 @@ -915,39 +915,11 @@ mih.cmd_synchronize_schema() -class CheckMappingCommand(Command): - """Check content of the mapping file of an external source. - - The mapping is checked against the instance's schema, searching for - inconsistencies or stuff you may have forgotten. It's higly recommanded to - run it when you setup a multi-sources instance. - - - the identifier of the instance. - - - the mapping file to check. - """ - name = 'check-mapping' - arguments = ' ' - min_args = max_args = 2 - - def run(self, args): - from cubicweb.server.checkintegrity import check_mapping - from cubicweb.server.sources.pyrorql import load_mapping_file - appid, mappingfile = args - config = ServerConfiguration.config_for(appid) - config.quick_start = True - mih = config.migration_handler(connect=False, verbosity=1) - repo = mih.repo_connect() # necessary to get cubes - check_mapping(config.load_schema(), load_mapping_file(mappingfile)) - for cmdclass in (CreateInstanceDBCommand, InitInstanceCommand, GrantUserOnInstanceCommand, ResetAdminPasswordCommand, StartRepositoryCommand, DBDumpCommand, DBRestoreCommand, DBCopyCommand, AddSourceCommand, CheckRepositoryCommand, RebuildFTICommand, SynchronizeInstanceSchemaCommand, - CheckMappingCommand, ): CWCTL.register(cmdclass) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/sources/__init__.py --- a/server/sources/__init__.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/sources/__init__.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -19,12 +19,16 @@ __docformat__ = "restructuredtext en" +import itertools from os.path import join, splitext from datetime import datetime, timedelta from logging import getLogger -import itertools + +from logilab.common import configuration -from cubicweb import set_log_methods, server +from yams.schema import role_name + +from cubicweb import ValidationError, set_log_methods, server from cubicweb.schema import VIRTUAL_RTYPES from cubicweb.server.sqlutils import SQL_PREFIX from cubicweb.server.ssplanner import EditedEntity @@ -75,6 +79,9 @@ class AbstractSource(object): """an abstract class for sources""" + # does the source copy data into the system source, or is it a *true* source + # (i.e. entities are not stored physically here) + copy_based_source = False # boolean telling if modification hooks should be called when something is # modified in this source @@ -103,51 +110,22 @@ # force deactivation (configuration error for instance) disabled = False - def __init__(self, repo, source_config, *args, **kwargs): + # source configuration options + options = () + + def __init__(self, repo, source_config, eid=None): self.repo = repo - self.uri = source_config['uri'] - set_log_methods(self, getLogger('cubicweb.sources.'+self.uri)) self.set_schema(repo.schema) self.support_relations['identity'] = False - self.eid = None + self.eid = eid self.public_config = source_config.copy() self.remove_sensitive_information(self.public_config) - - def init_creating(self): - """method called by the repository once ready to create a new instance""" - pass - - def init(self): - """method called by the repository once ready to handle request""" - pass - - def backup(self, backupfile, confirm): - """method called to create a backup of source's data""" - pass - - def restore(self, backupfile, confirm, drop): - """method called to restore a backup of source's data""" - pass - - def close_pool_connections(self): - for pool in self.repo.pools: - pool._cursors.pop(self.uri, None) - pool.source_cnxs[self.uri][1].close() - - def open_pool_connections(self): - for pool in self.repo.pools: - pool.source_cnxs[self.uri] = (self, self.get_connection()) - - def reset_caches(self): - """method called during test to reset potential source caches""" - pass - - def clear_eid_cache(self, eid, etype): - """clear potential caches for the given eid""" - pass + self.uri = source_config.pop('uri') + set_log_methods(self, getLogger('cubicweb.sources.'+self.uri)) + source_config.pop('type') def __repr__(self): - return '<%s source @%#x>' % (self.uri, id(self)) + return '<%s source %s @%#x>' % (self.uri, self.eid, id(self)) def __cmp__(self, other): """simple comparison function to get predictable source order, with the @@ -161,10 +139,137 @@ return -1 return cmp(self.uri, other.uri) + def backup(self, backupfile, confirm): + """method called to create a backup of source's data""" + pass + + def restore(self, backupfile, confirm, drop): + """method called to restore a backup of source's data""" + pass + + @classmethod + def check_conf_dict(cls, eid, confdict, _=unicode, fail_if_unknown=True): + """check configuration of source entity. Return config dict properly + typed with defaults set. + """ + processed = {} + for optname, optdict in cls.options: + value = confdict.pop(optname, optdict.get('default')) + if value is configuration.REQUIRED: + if not fail_if_unknown: + continue + msg = _('specifying %s is mandatory' % optname) + raise ValidationError(eid, {role_name('config', 'subject'): msg}) + elif value is not None: + # type check + try: + value = configuration.convert(value, optdict, optname) + except Exception, ex: + msg = unicode(ex) # XXX internationalization + raise ValidationError(eid, {role_name('config', 'subject'): msg}) + processed[optname] = value + # cw < 3.10 bw compat + try: + processed['adapter'] = confdict['adapter'] + except: + pass + # check for unknown options + if confdict and not confdict.keys() == ['adapter']: + if fail_if_unknown: + msg = _('unknown options %s') % ', '.join(confdict) + raise ValidationError(eid, {role_name('config', 'subject'): msg}) + else: + logger = getLogger('cubicweb.sources') + logger.warning('unknown options %s', ', '.join(confdict)) + # add options to processed, they may be necessary during migration + processed.update(confdict) + return processed + + @classmethod + def check_config(cls, source_entity): + """check configuration of source entity""" + return cls.check_conf_dict(source_entity.eid, source_entity.host_config, + _=source_entity._cw._) + + def update_config(self, source_entity, typedconfig): + """update configuration from source entity. `typedconfig` is config + properly typed with defaults set + """ + pass + + # source initialization / finalization ##################################### + def set_schema(self, schema): """set the instance'schema""" self.schema = schema + def init_creating(self): + """method called by the repository once ready to create a new instance""" + pass + + def init(self, activated, source_entity): + """method called by the repository once ready to handle request. + `activated` is a boolean flag telling if the source is activated or not. + """ + pass + + PUBLIC_KEYS = ('type', 'uri') + def remove_sensitive_information(self, sourcedef): + """remove sensitive information such as login / password from source + definition + """ + for key in sourcedef.keys(): + if not key in self.PUBLIC_KEYS: + sourcedef.pop(key) + + # connections handling ##################################################### + + def get_connection(self): + """open and return a connection to the source""" + raise NotImplementedError() + + def check_connection(self, cnx): + """Check connection validity, return None if the connection is still + valid else a new connection (called when the pool using the given + connection is being attached to a session). Do nothing by default. + """ + pass + + def close_pool_connections(self): + for pool in self.repo.pools: + pool._cursors.pop(self.uri, None) + pool.source_cnxs[self.uri][1].close() + + def open_pool_connections(self): + for pool in self.repo.pools: + pool.source_cnxs[self.uri] = (self, self.get_connection()) + + def pool_reset(self, cnx): + """the pool using the given connection is being reseted from its current + attached session + + do nothing by default + """ + pass + + # cache handling ########################################################### + + def reset_caches(self): + """method called during test to reset potential source caches""" + pass + + def clear_eid_cache(self, eid, etype): + """clear potential caches for the given eid""" + pass + + # external source api ###################################################### + + def eid2extid(self, eid, session=None): + return self.repo.eid2extid(self, eid, session) + + def extid2eid(self, value, etype, session=None, **kwargs): + return self.repo.extid2eid(self, value, etype, session, **kwargs) + def support_entity(self, etype, write=False): """return true if the given entity's type is handled by this adapter if write is true, return true only if it's a RW support @@ -219,98 +324,59 @@ return rtype in self.cross_relations return rtype not in self.dont_cross_relations - def eid2extid(self, eid, session=None): - return self.repo.eid2extid(self, eid, session) - - def extid2eid(self, value, etype, session=None, **kwargs): - return self.repo.extid2eid(self, value, etype, session, **kwargs) + def before_entity_insertion(self, session, lid, etype, eid, sourceparams): + """called by the repository when an eid has been attributed for an + entity stored here but the entity has not been inserted in the system + table yet. - PUBLIC_KEYS = ('type', 'uri') - def remove_sensitive_information(self, sourcedef): - """remove sensitive information such as login / password from source - definition - """ - for key in sourcedef.keys(): - if not key in self.PUBLIC_KEYS: - sourcedef.pop(key) - - def _cleanup_system_relations(self, session): - """remove relation in the system source referencing entities coming from - this source + This method must return the an Entity instance representation of this + entity. """ - cu = session.system_sql('SELECT eid FROM entities WHERE source=%(uri)s', - {'uri': self.uri}) - myeids = ','.join(str(r[0]) for r in cu.fetchall()) - if not myeids: - return - # delete relations referencing one of those eids - eidcolum = SQL_PREFIX + 'eid' - for rschema in self.schema.relations(): - if rschema.final or rschema.type in VIRTUAL_RTYPES: - continue - if rschema.inlined: - column = SQL_PREFIX + rschema.type - for subjtype in rschema.subjects(): - table = SQL_PREFIX + str(subjtype) - for objtype in rschema.objects(subjtype): - if self.support_entity(objtype): - sql = 'UPDATE %s SET %s=NULL WHERE %s IN (%s);' % ( - table, column, eidcolum, myeids) - session.system_sql(sql) - break - continue - for etype in rschema.subjects(): - if self.support_entity(etype): - sql = 'DELETE FROM %s_relation WHERE eid_from IN (%s);' % ( - rschema.type, myeids) - session.system_sql(sql) - break - for etype in rschema.objects(): - if self.support_entity(etype): - sql = 'DELETE FROM %s_relation WHERE eid_to IN (%s);' % ( - rschema.type, myeids) - session.system_sql(sql) - break + entity = self.repo.vreg['etypes'].etype_class(etype)(session) + entity.eid = eid + entity.cw_edited = EditedEntity(entity) + return entity - def cleanup_entities_info(self, session): - """cleanup system tables from information for entities coming from - this source. This should be called when a source is removed to - properly cleanup the database - """ - self._cleanup_system_relations(session) - # fti / entities tables cleanup - # sqlite doesn't support DELETE FROM xxx USING yyy - dbhelper = session.pool.source('system').dbhelper - session.system_sql('DELETE FROM %s WHERE %s.%s IN (SELECT eid FROM ' - 'entities WHERE entities.source=%%(uri)s)' - % (dbhelper.fti_table, dbhelper.fti_table, - dbhelper.fti_uid_attr), - {'uri': self.uri}) - session.system_sql('DELETE FROM entities WHERE source=%(uri)s', - {'uri': self.uri}) - - # abstract methods to override (at least) in concrete source classes ####### - - def get_connection(self): - """open and return a connection to the source""" - raise NotImplementedError() - - def check_connection(self, cnx): - """check connection validity, return None if the connection is still valid - else a new connection (called when the pool using the given connection is - being attached to a session) - - do nothing by default + def after_entity_insertion(self, session, lid, entity, sourceparams): + """called by the repository after an entity stored here has been + inserted in the system table. """ pass - def pool_reset(self, cnx): - """the pool using the given connection is being reseted from its current - attached session + def _load_mapping(self, session=None, **kwargs): + if not 'CWSourceSchemaConfig' in self.schema: + self.warning('instance is not mapping ready') + return + if session is None: + _session = self.repo.internal_session() + else: + _session = session + try: + for schemacfg in _session.execute( + 'Any CFG,CFGO,S WHERE ' + 'CFG options CFGO, CFG cw_schema S, ' + 'CFG cw_for_source X, X eid %(x)s', {'x': self.eid}).entities(): + self.add_schema_config(schemacfg, **kwargs) + finally: + if session is None: + _session.close() - do nothing by default - """ - pass + def add_schema_config(self, schemacfg, checkonly=False): + """added CWSourceSchemaConfig, modify mapping accordingly""" + msg = schemacfg._cw._("this source doesn't use a mapping") + raise ValidationError(schemacfg.eid, {None: msg}) + + def del_schema_config(self, schemacfg, checkonly=False): + """deleted CWSourceSchemaConfig, modify mapping accordingly""" + msg = schemacfg._cw._("this source doesn't use a mapping") + raise ValidationError(schemacfg.eid, {None: msg}) + + def update_schema_config(self, schemacfg, checkonly=False): + """updated CWSourceSchemaConfig, modify mapping accordingly""" + self.del_schema_config(schemacfg, checkonly) + self.add_schema_config(schemacfg, checkonly) + + # user authentication api ################################################## def authenticate(self, session, login, **kwargs): """if the source support CWUser entity type, it should implement @@ -320,6 +386,8 @@ """ raise NotImplementedError() + # RQL query api ############################################################ + def syntax_tree_search(self, session, union, args=None, cachekey=None, varmap=None, debug=0): """return result from this source for a rql query (actually from a rql @@ -338,27 +406,7 @@ res = self.syntax_tree_search(session, union, args, varmap=varmap) session.pool.source('system').manual_insert(res, table, session) - # system source don't have to implement the two methods below - - def before_entity_insertion(self, session, lid, etype, eid): - """called by the repository when an eid has been attributed for an - entity stored here but the entity has not been inserted in the system - table yet. - - This method must return the an Entity instance representation of this - entity. - """ - entity = self.repo.vreg['etypes'].etype_class(etype)(session) - entity.eid = eid - entity.cw_edited = EditedEntity(entity) - return entity - - def after_entity_insertion(self, session, lid, entity): - """called by the repository after an entity stored here has been - inserted in the system table. - """ - pass - + # write modification api ################################################### # read-only sources don't have to implement methods below def get_extid(self, entity): @@ -534,8 +582,8 @@ except KeyError: raise RuntimeError('Unknown source type %r' % source_type) -def get_source(type, source_config, repo): - """return a source adapter according to the adapter field in the - source's configuration +def get_source(type, source_config, repo, eid): + """return a source adapter according to the adapter field in the source's + configuration """ - return source_adapter(type)(repo, source_config) + return source_adapter(type)(repo, source_config, eid) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/sources/datafeed.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/sources/datafeed.py Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,236 @@ +# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""datafeed sources: copy data from an external data stream into the system +database +""" +from datetime import datetime, timedelta +from base64 import b64decode + +from cubicweb import RegistryNotFound, ObjectNotFound, ValidationError +from cubicweb.server.sources import AbstractSource +from cubicweb.appobject import AppObject + +class DataFeedSource(AbstractSource): + copy_based_source = True + + options = ( + ('synchronize', + {'type' : 'yn', + 'default': True, + 'help': ('Is the repository responsible to automatically import ' + 'content from this source? ' + 'You should say yes unless you don\'t want this behaviour ' + 'or if you use a multiple repositories setup, in which ' + 'case you should say yes on one repository, no on others.'), + 'group': 'datafeed-source', 'level': 2, + }), + ('synchronization-interval', + {'type' : 'time', + 'default': '5min', + 'help': ('Interval in seconds between synchronization with the ' + 'external source (default to 5 minutes, must be >= 1 min).'), + 'group': 'datafeed-source', 'level': 2, + }), + ('delete-entities', + {'type' : 'yn', + 'default': True, + 'help': ('Should already imported entities not found anymore on the ' + 'external source be deleted?'), + 'group': 'datafeed-source', 'level': 2, + }), + + ) + def __init__(self, repo, source_config, eid=None): + AbstractSource.__init__(self, repo, source_config, eid) + self.update_config(None, self.check_conf_dict(eid, source_config)) + + def check_config(self, source_entity): + """check configuration of source entity""" + typedconfig = super(DataFeedSource, self).check_config(source_entity) + if typedconfig['synchronization-interval'] < 60: + _ = source_entity._cw._ + msg = _('synchronization-interval must be greater than 1 minute') + raise ValidationError(source_entity.eid, {'config': msg}) + return typedconfig + + def _entity_update(self, source_entity): + source_entity.complete() + self.parser = source_entity.parser + self.latest_retrieval = source_entity.latest_retrieval + self.urls = [url.strip() for url in source_entity.url.splitlines() + if url.strip()] + + def update_config(self, source_entity, typedconfig): + """update configuration from source entity. `typedconfig` is config + properly typed with defaults set + """ + self.synchro_interval = timedelta(seconds=typedconfig['synchronization-interval']) + if source_entity is not None: + self._entity_update(source_entity) + self.config = typedconfig + + def init(self, activated, source_entity): + if activated: + self._entity_update(source_entity) + self.parser = source_entity.parser + self.load_mapping(source_entity._cw) + + def _get_parser(self, session, **kwargs): + return self.repo.vreg['parsers'].select( + self.parser, session, source=self, **kwargs) + + def load_mapping(self, session): + self.mapping = {} + self.mapping_idx = {} + try: + parser = self._get_parser(session) + except (RegistryNotFound, ObjectNotFound): + return # no parser yet, don't go further + self._load_mapping(session, parser=parser) + + def add_schema_config(self, schemacfg, checkonly=False, parser=None): + """added CWSourceSchemaConfig, modify mapping accordingly""" + if parser is None: + parser = self._get_parser(schemacfg._cw) + parser.add_schema_config(schemacfg, checkonly) + + def del_schema_config(self, schemacfg, checkonly=False, parser=None): + """deleted CWSourceSchemaConfig, modify mapping accordingly""" + if parser is None: + parser = self._get_parser(schemacfg._cw) + parser.del_schema_config(schemacfg, checkonly) + + def fresh(self): + if self.latest_retrieval is None: + return False + return datetime.now() < (self.latest_retrieval + self.synchro_interval) + + def pull_data(self, session, force=False): + if not force and self.fresh(): + return {} + if self.config['delete-entities']: + myuris = self.source_cwuris(session) + else: + myuris = None + parser = self._get_parser(session, sourceuris=myuris) + error = False + self.info('pulling data for source %s', self.uri) + for url in self.urls: + try: + parser.process(url) + except IOError, exc: + self.error('could not pull data while processing %s: %s', + url, exc) + error = True + if error: + self.warning("some error occured, don't attempt to delete entities") + elif self.config['delete-entities'] and myuris: + byetype = {} + for eid, etype in myuris.values(): + byetype.setdefault(etype, []).append(str(eid)) + self.error('delete %s entities %s', self.uri, byetype) + for etype, eids in byetype.iteritems(): + session.execute('DELETE %s X WHERE X eid IN (%s)' + % (etype, ','.join(eids))) + self.latest_retrieval = datetime.now() + session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s', + {'x': self.eid, 'date': self.latest_retrieval}) + return parser.stats + + def before_entity_insertion(self, session, lid, etype, eid, sourceparams): + """called by the repository when an eid has been attributed for an + entity stored here but the entity has not been inserted in the system + table yet. + + This method must return the an Entity instance representation of this + entity. + """ + entity = super(DataFeedSource, self).before_entity_insertion( + session, lid, etype, eid, sourceparams) + entity.cw_edited['cwuri'] = unicode(lid) + entity.cw_edited.set_defaults() + sourceparams['parser'].before_entity_copy(entity, sourceparams) + # avoid query to search full-text indexed attributes + for attr in entity.e_schema.indexable_attributes(): + entity.cw_edited.setdefault(attr, u'') + return entity + + def after_entity_insertion(self, session, lid, entity, sourceparams): + """called by the repository after an entity stored here has been + inserted in the system table. + """ + if session.is_hook_category_activated('integrity'): + entity.cw_edited.check(creation=True) + self.repo.system_source.add_entity(session, entity) + entity.cw_edited.saved = entity._cw_is_saved = True + sourceparams['parser'].after_entity_copy(entity, sourceparams) + + def source_cwuris(self, session): + sql = ('SELECT extid, eid, type FROM entities, cw_source_relation ' + 'WHERE entities.eid=cw_source_relation.eid_from ' + 'AND cw_source_relation.eid_to=%s' % self.eid) + return dict((b64decode(uri), (eid, type)) + for uri, eid, type in session.system_sql(sql)) + + +class DataFeedParser(AppObject): + __registry__ = 'parsers' + + def __init__(self, session, source, sourceuris=None): + self._cw = session + self.source = source + self.sourceuris = sourceuris + self.stats = {'created': set(), + 'updated': set()} + + def add_schema_config(self, schemacfg, checkonly=False): + """added CWSourceSchemaConfig, modify mapping accordingly""" + msg = schemacfg._cw._("this parser doesn't use a mapping") + raise ValidationError(schemacfg.eid, {None: msg}) + + def del_schema_config(self, schemacfg, checkonly=False): + """deleted CWSourceSchemaConfig, modify mapping accordingly""" + msg = schemacfg._cw._("this parser doesn't use a mapping") + raise ValidationError(schemacfg.eid, {None: msg}) + + def extid2entity(self, uri, etype, **sourceparams): + sourceparams['parser'] = self + eid = self.source.extid2eid(str(uri), etype, self._cw, + sourceparams=sourceparams) + if self.sourceuris is not None: + self.sourceuris.pop(str(uri), None) + return self._cw.entity_from_eid(eid, etype) + + def process(self, url): + """main callback: process the url""" + raise NotImplementedError + + def before_entity_copy(self, entity, sourceparams): + raise NotImplementedError + + def after_entity_copy(self, entity, sourceparams): + self.stats['created'].add(entity.eid) + + def created_during_pull(self, entity): + return entity.eid in self.stats['created'] + + def updated_during_pull(self, entity): + return entity.eid in self.stats['updated'] + + def notify_updated(self, entity): + return self.stats['updated'].add(entity.eid) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/sources/ldapuser.py --- a/server/sources/ldapuser.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/sources/ldapuser.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -34,15 +34,13 @@ from __future__ import division from base64 import b64decode -from logilab.common.textutils import splitstrip -from rql.nodes import Relation, VariableRef, Constant, Function - import ldap from ldap.ldapobject import ReconnectLDAPObject from ldap.filter import filter_format, escape_filter_chars from ldapurl import LDAPUrl -from logilab.common.configuration import time_validator +from rql.nodes import Relation, VariableRef, Constant, Function + from cubicweb import AuthenticationError, UnknownEid, RepositoryError from cubicweb.server.utils import cartesian_product from cubicweb.server.sources import (AbstractSource, TrFunc, GlobTrFunc, @@ -168,58 +166,51 @@ ) - def __init__(self, repo, source_config, *args, **kwargs): - AbstractSource.__init__(self, repo, source_config, *args, **kwargs) - self.host = source_config['host'] - self.protocol = source_config.get('protocol', 'ldap') - self.authmode = source_config.get('auth-mode', 'simple') + def __init__(self, repo, source_config, eid=None): + AbstractSource.__init__(self, repo, source_config, eid) + self.update_config(None, self.check_conf_dict(eid, source_config)) + self._conn = None + + def update_config(self, source_entity, typedconfig): + """update configuration from source entity. `typedconfig` is config + properly typed with defaults set + """ + self.host = typedconfig['host'] + self.protocol = typedconfig['protocol'] + self.authmode = typedconfig['auth-mode'] self._authenticate = getattr(self, '_auth_%s' % self.authmode) - self.cnx_dn = source_config.get('data-cnx-dn') or '' - self.cnx_pwd = source_config.get('data-cnx-password') or '' - self.user_base_scope = globals()[source_config['user-scope']] - self.user_base_dn = str(source_config['user-base-dn']) - self.user_base_scope = globals()[source_config['user-scope']] - self.user_classes = splitstrip(source_config['user-classes']) - self.user_login_attr = source_config['user-login-attr'] - self.user_default_groups = splitstrip(source_config['user-default-group']) - self.user_attrs = dict(v.split(':', 1) for v in splitstrip(source_config['user-attrs-map'])) - self.user_filter = source_config.get('user-filter') + self.cnx_dn = typedconfig['data-cnx-dn'] + self.cnx_pwd = typedconfig['data-cnx-password'] + self.user_base_dn = str(typedconfig['user-base-dn']) + self.user_base_scope = globals()[typedconfig['user-scope']] + self.user_login_attr = typedconfig['user-login-attr'] + self.user_default_groups = typedconfig['user-default-group'] + self.user_attrs = typedconfig['user-attrs-map'] self.user_rev_attrs = {'eid': 'dn'} for ldapattr, cwattr in self.user_attrs.items(): self.user_rev_attrs[cwattr] = ldapattr - self.base_filters = self._make_base_filters() - self._conn = None - self._cache = {} - # ttlm is in minutes! - self._cache_ttl = time_validator(None, None, - source_config.get('cache-life-time', 2*60*60)) - self._cache_ttl = max(71, self._cache_ttl) - self._query_cache = TimedCache(self._cache_ttl) - # interval is in seconds ! - self._interval = time_validator(None, None, - source_config.get('synchronization-interval', - 24*60*60)) - - def _make_base_filters(self): - filters = [filter_format('(%s=%s)', ('objectClass', o)) - for o in self.user_classes] - if self.user_filter: - filters += [self.user_filter] - return filters + self.base_filters = [filter_format('(%s=%s)', ('objectClass', o)) + for o in typedconfig['user-classes']] + if typedconfig['user-filter']: + self.base_filters.append(typedconfig['user-filter']) + self._interval = typedconfig['synchronization-interval'] + self._cache_ttl = max(71, typedconfig['cache-life-time']) + self.reset_caches() def reset_caches(self): """method called during test to reset potential source caches""" self._cache = {} self._query_cache = TimedCache(self._cache_ttl) - def init(self): + def init(self, activated, source_entity): """method called by the repository once ready to handle request""" - self.info('ldap init') - # set minimum period of 5min 1s (the additional second is to minimize - # resonnance effet) - self.repo.looping_task(max(301, self._interval), self.synchronize) - self.repo.looping_task(self._cache_ttl // 10, - self._query_cache.clear_expired) + if activated: + self.info('ldap init') + # set minimum period of 5min 1s (the additional second is to + # minimize resonnance effet) + self.repo.looping_task(max(301, self._interval), self.synchronize) + self.repo.looping_task(self._cache_ttl // 10, + self._query_cache.clear_expired) def synchronize(self): """synchronize content known by this repository with content in the @@ -299,7 +290,7 @@ # we really really don't want that raise AuthenticationError() searchfilter = [filter_format('(%s=%s)', (self.user_login_attr, login))] - searchfilter.extend(self._make_base_filters()) + searchfilter.extend(self.base_filters) searchstr = '(&%s)' % ''.join(searchfilter) # first search the user try: @@ -584,7 +575,7 @@ self.debug('ldap built results %s', len(result)) return result - def before_entity_insertion(self, session, lid, etype, eid): + def before_entity_insertion(self, session, lid, etype, eid, sourceparams): """called by the repository when an eid has been attributed for an entity stored here but the entity has not been inserted in the system table yet. @@ -593,18 +584,20 @@ entity. """ self.debug('ldap before entity insertion') - entity = super(LDAPUserSource, self).before_entity_insertion(session, lid, etype, eid) + entity = super(LDAPUserSource, self).before_entity_insertion( + session, lid, etype, eid, sourceparams) res = self._search(session, lid, BASE)[0] for attr in entity.e_schema.indexable_attributes(): entity.cw_edited[attr] = res[self.user_rev_attrs[attr]] return entity - def after_entity_insertion(self, session, lid, entity): + def after_entity_insertion(self, session, lid, entity, sourceparams): """called by the repository after an entity stored here has been inserted in the system table. """ self.debug('ldap after entity insertion') - super(LDAPUserSource, self).after_entity_insertion(session, lid, entity) + super(LDAPUserSource, self).after_entity_insertion( + session, lid, entity, sourceparams) dn = lid for group in self.user_default_groups: session.execute('SET X in_group G WHERE X eid %(x)s, G name %(group)s', diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/sources/native.py --- a/server/sources/native.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/sources/native.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -45,8 +45,10 @@ from logilab.database import get_db_helper from yams import schema2sql as y2sql +from yams.schema import role_name -from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary, UniqueTogetherError +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 @@ -267,6 +269,8 @@ def __init__(self, repo, source_config, *args, **kwargs): SQLAdapterMixIn.__init__(self, source_config) self.authentifiers = [LoginPasswordAuthentifier(self)] + if repo.config['allow-email-login']: + self.authentifiers.insert(0, EmailPasswordAuthentifier(self)) AbstractSource.__init__(self, repo, source_config, *args, **kwargs) # sql generator self._rql_sqlgen = self.sqlgen_class(self.schema, self.dbhelper, @@ -308,6 +312,13 @@ # consuming, find another way return SQLAdapterMixIn.get_connection(self) + def check_config(self, source_entity): + """check configuration of source entity""" + if source_entity.host_config: + msg = source_entity._cw._('the system source has its configuration ' + 'stored on the file-system') + raise ValidationError(source_entity.eid, {role_name('config', 'subject'): msg}) + def add_authentifier(self, authentifier): self.authentifiers.append(authentifier) authentifier.source = self @@ -327,17 +338,21 @@ """execute the query and return its result""" return self.process_result(self.doexec(session, sql, args)) - def init_creating(self): - pool = self.repo._get_pool() - pool.pool_set() + def init_creating(self, pool=None): # check full text index availibility if self.do_fti: - if not self.dbhelper.has_fti_table(pool['system']): + if pool is None: + _pool = self.repo._get_pool() + _pool.pool_set() + else: + _pool = pool + if not self.dbhelper.has_fti_table(_pool['system']): if not self.repo.config.creating: self.critical('no text index table') self.do_fti = False - pool.pool_reset() - self.repo._free_pool(pool) + if pool is None: + _pool.pool_reset() + self.repo._free_pool(_pool) def backup(self, backupfile, confirm): """method called to create a backup of the source's data""" @@ -357,8 +372,8 @@ if self.repo.config.open_connections_pools: self.open_pool_connections() - def init(self): - self.init_creating() + def init(self, activated, source_entity): + self.init_creating(source_entity._cw.pool) def shutdown(self): if self._eid_creation_cnx: @@ -788,13 +803,13 @@ res[-1] = b64decode(res[-1]) return res - def extid2eid(self, session, source, extid): + def extid2eid(self, session, source_uri, extid): """get eid from an external id. Return None if no record found.""" assert isinstance(extid, str) cursor = self.doexec(session, 'SELECT eid FROM entities ' 'WHERE extid=%(x)s AND source=%(s)s', - {'x': b64encode(extid), 's': source.uri}) + {'x': b64encode(extid), 's': source_uri}) # XXX testing rowcount cause strange bug with sqlite, results are there # but rowcount is 0 #if cursor.rowcount > 0: @@ -883,24 +898,24 @@ if extid is not None: assert isinstance(extid, str) extid = b64encode(extid) + uri = 'system' if source.copy_based_source else source.uri attrs = {'type': entity.__regid__, 'eid': entity.eid, 'extid': extid, - 'source': source.uri, 'mtime': datetime.now()} + 'source': uri, 'mtime': datetime.now()} self.doexec(session, self.sqlgen.insert('entities', attrs), attrs) # insert core relations: is, is_instance_of and cw_source - if not hasattr(entity, '_cw_recreating'): - try: - self.doexec(session, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)' - % (entity.eid, eschema_eid(session, entity.e_schema))) - except IndexError: - # during schema serialization, skip - pass - else: - for eschema in entity.e_schema.ancestors() + [entity.e_schema]: - self.doexec(session, 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)' - % (entity.eid, eschema_eid(session, eschema))) - if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10 - self.doexec(session, 'INSERT INTO cw_source_relation(eid_from,eid_to) ' - 'VALUES (%s,%s)' % (entity.eid, source.eid)) + try: + self.doexec(session, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)' + % (entity.eid, eschema_eid(session, entity.e_schema))) + except IndexError: + # during schema serialization, skip + pass + else: + for eschema in entity.e_schema.ancestors() + [entity.e_schema]: + self.doexec(session, 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)' + % (entity.eid, eschema_eid(session, eschema))) + if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10 + self.doexec(session, 'INSERT INTO cw_source_relation(eid_from,eid_to) ' + 'VALUES (%s,%s)' % (entity.eid, source.eid)) # now we can update the full text index if self.do_fti and self.need_fti_indexation(entity.__regid__): if complete: @@ -1524,3 +1539,19 @@ return rset[0][0] except IndexError: raise AuthenticationError('bad password') + + +class EmailPasswordAuthentifier(BaseAuthentifier): + def authenticate(self, session, login, **authinfo): + # email_auth flag prevent from infinite recursion (call to + # repo.check_auth_info at the end of this method may lead us here again) + if not '@' in login or authinfo.pop('email_auth', None): + raise AuthenticationError('not an email') + rset = session.execute('Any L WHERE U login L, U primary_email M, ' + 'M address %(login)s', {'login': login}, + build_descr=False) + if rset.rowcount != 1: + raise AuthenticationError('unexisting email') + login = rset.rows[0][0] + authinfo['email_auth'] = True + return self.source.repo.check_auth_info(session, login, authinfo) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/sources/pyrorql.py --- a/server/sources/pyrorql.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/sources/pyrorql.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -31,11 +31,14 @@ from logilab.common.configuration import REQUIRED from logilab.common.optik_ext import check_yn +from yams.schema import role_name + from rql.nodes import Constant from rql.utils import rqlvar_maker from cubicweb import dbapi, server -from cubicweb import BadConnectionId, UnknownEid, ConnectionError +from cubicweb import ValidationError, BadConnectionId, UnknownEid, ConnectionError +from cubicweb.schema import VIRTUAL_RTYPES from cubicweb.cwconfig import register_persistent_options from cubicweb.server.sources import (AbstractSource, ConnectionWrapper, TimedCache, dbg_st_search, dbg_results) @@ -45,34 +48,6 @@ select, col = union.locate_subquery(col, etype, args) return getattr(select.selection[col], 'uidtype', None) -def load_mapping_file(mappingfile): - mapping = {} - execfile(mappingfile, mapping) - for junk in ('__builtins__', '__doc__'): - mapping.pop(junk, None) - mapping.setdefault('support_relations', {}) - mapping.setdefault('dont_cross_relations', set()) - mapping.setdefault('cross_relations', set()) - - # do some basic checks of the mapping content - assert 'support_entities' in mapping, \ - 'mapping file should at least define support_entities' - assert isinstance(mapping['support_entities'], dict) - assert isinstance(mapping['support_relations'], dict) - assert isinstance(mapping['dont_cross_relations'], set) - assert isinstance(mapping['cross_relations'], set) - unknown = set(mapping) - set( ('support_entities', 'support_relations', - 'dont_cross_relations', 'cross_relations') ) - assert not unknown, 'unknown mapping attribute(s): %s' % unknown - # relations that are necessarily not crossed - mapping['dont_cross_relations'] |= set(('owned_by', 'created_by')) - for rtype in ('is', 'is_instance_of', 'cw_source'): - assert rtype not in mapping['dont_cross_relations'], \ - '%s relation should not be in dont_cross_relations' % rtype - assert rtype not in mapping['support_relations'], \ - '%s relation should not be in support_relations' % rtype - return mapping - class ReplaceByInOperator(Exception): def __init__(self, eids): @@ -96,12 +71,6 @@ 'help': 'identifier of the repository in the pyro name server', 'group': 'pyro-source', 'level': 0, }), - ('mapping-file', - {'type' : 'string', - 'default': REQUIRED, - 'help': 'path to a python file with the schema mapping definition', - 'group': 'pyro-source', 'level': 1, - }), ('cubicweb-user', {'type' : 'string', 'default': REQUIRED, @@ -142,8 +111,8 @@ 'group': 'pyro-source', 'level': 2, }), ('synchronization-interval', - {'type' : 'int', - 'default': 5*60, + {'type' : 'time', + 'default': '5min', 'help': 'interval between synchronization with the external \ repository (default to 5 minutes).', 'group': 'pyro-source', 'level': 2, @@ -154,70 +123,112 @@ PUBLIC_KEYS = AbstractSource.PUBLIC_KEYS + ('base-url',) _conn = None - def __init__(self, repo, source_config, *args, **kwargs): - AbstractSource.__init__(self, repo, source_config, *args, **kwargs) - mappingfile = source_config['mapping-file'] - if not mappingfile[0] == '/': - mappingfile = join(repo.config.apphome, mappingfile) - try: - mapping = load_mapping_file(mappingfile) - except IOError: - self.disabled = True - self.error('cant read mapping file %s, source disabled', - mappingfile) - self.support_entities = {} - self.support_relations = {} - self.dont_cross_relations = set() - self.cross_relations = set() - else: - self.support_entities = mapping['support_entities'] - self.support_relations = mapping['support_relations'] - self.dont_cross_relations = mapping['dont_cross_relations'] - self.cross_relations = mapping['cross_relations'] - baseurl = source_config.get('base-url') + def __init__(self, repo, source_config, eid=None): + AbstractSource.__init__(self, repo, source_config, eid) + self.update_config(None, self.check_conf_dict(eid, source_config, + fail_if_unknown=False)) + self._query_cache = TimedCache(1800) + + def update_config(self, source_entity, processed_config): + """update configuration from source entity""" + # XXX get it through pyro if unset + baseurl = processed_config.get('base-url') if baseurl and not baseurl.endswith('/'): - source_config['base-url'] += '/' - self.config = source_config - myoptions = (('%s.latest-update-time' % self.uri, - {'type' : 'int', 'sitewide': True, - 'default': 0, - 'help': _('timestamp of the latest source synchronization.'), - 'group': 'sources', - }),) - register_persistent_options(myoptions) - self._query_cache = TimedCache(1800) - self._skip_externals = check_yn(None, 'skip-external-entities', - source_config.get('skip-external-entities', False)) + processed_config['base-url'] += '/' + self.config = processed_config + self._skip_externals = processed_config['skip-external-entities'] + if source_entity is not None: + self.latest_retrieval = source_entity.latest_retrieval def reset_caches(self): """method called during test to reset potential source caches""" self._query_cache = TimedCache(1800) - def last_update_time(self): - pkey = u'sources.%s.latest-update-time' % self.uri - session = self.repo.internal_session() + def init(self, activated, source_entity): + """method called by the repository once ready to handle request""" + self.load_mapping(source_entity._cw) + if activated: + interval = self.config['synchronization-interval'] + self.repo.looping_task(interval, self.synchronize) + self.repo.looping_task(self._query_cache.ttl.seconds/10, + self._query_cache.clear_expired) + self.latest_retrieval = source_entity.latest_retrieval + + def load_mapping(self, session=None): + self.support_entities = {} + self.support_relations = {} + self.dont_cross_relations = set(('owned_by', 'created_by')) + self.cross_relations = set() + assert self.eid is not None + self._schemacfg_idx = {} + self._load_mapping(session) + + etype_options = set(('write',)) + rtype_options = set(('maycross', 'dontcross', 'write',)) + + def _check_options(self, schemacfg, allowedoptions): + if schemacfg.options: + options = set(w.strip() for w in schemacfg.options.split(':')) + else: + options = set() + if options - allowedoptions: + options = ', '.join(sorted(options - allowedoptions)) + msg = _('unknown option(s): %s' % options) + raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg}) + return options + + def add_schema_config(self, schemacfg, checkonly=False): + """added CWSourceSchemaConfig, modify mapping accordingly""" try: - rset = session.execute('Any V WHERE X is CWProperty, X value V, X pkey %(k)s', - {'k': pkey}) - if not rset: - # insert it - session.execute('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', - {'k': pkey, 'v': u'0'}) - session.commit() - timestamp = 0 + ertype = schemacfg.schema.name + except AttributeError: + msg = schemacfg._cw._("attribute/relation can't be mapped, only " + "entity and relation types") + raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg}) + if schemacfg.schema.__regid__ == 'CWEType': + options = self._check_options(schemacfg, self.etype_options) + if not checkonly: + self.support_entities[ertype] = 'write' in options + else: # CWRType + if ertype in ('is', 'is_instance_of', 'cw_source') or ertype in VIRTUAL_RTYPES: + msg = schemacfg._cw._('%s relation should not be in mapped') % rtype + raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg}) + options = self._check_options(schemacfg, self.rtype_options) + if 'dontcross' in options: + if 'maycross' in options: + msg = schemacfg._("can't mix dontcross and maycross options") + raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg}) + if 'write' in options: + msg = schemacfg._("can't mix dontcross and write options") + raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg}) + if not checkonly: + self.dont_cross_relations.add(ertype) + elif not checkonly: + self.support_relations[ertype] = 'write' in options + if 'maycross' in options: + self.cross_relations.add(ertype) + if not checkonly: + # add to an index to ease deletion handling + self._schemacfg_idx[schemacfg.eid] = ertype + + def del_schema_config(self, schemacfg, checkonly=False): + """deleted CWSourceSchemaConfig, modify mapping accordingly""" + if checkonly: + return + try: + ertype = self._schemacfg_idx[schemacfg.eid] + if ertype[0].isupper(): + del self.support_entities[ertype] else: - assert len(rset) == 1 - timestamp = int(rset[0][0]) - return datetime.fromtimestamp(timestamp) - finally: - session.close() - - def init(self): - """method called by the repository once ready to handle request""" - interval = int(self.config.get('synchronization-interval', 5*60)) - self.repo.looping_task(interval, self.synchronize) - self.repo.looping_task(self._query_cache.ttl.seconds/10, - self._query_cache.clear_expired) + if ertype in self.support_relations: + del self.support_relations[ertype] + if ertype in self.cross_relations: + self.cross_relations.remove(ertype) + else: + self.dont_cross_relations.remove(ertype) + except: + self.error('while updating mapping consequently to removal of %s', + schemacfg) def local_eid(self, cnx, extid, session): etype, dexturi, dextid = cnx.describe(extid) @@ -245,9 +256,9 @@ return etypes = self.support_entities.keys() if mtime is None: - mtime = self.last_update_time() - updatetime, modified, deleted = extrepo.entities_modified_since(etypes, - mtime) + mtime = self.latest_retrieval + updatetime, modified, deleted = extrepo.entities_modified_since( + etypes, mtime) self._query_cache.clear() repo = self.repo session = repo.internal_session() @@ -273,14 +284,14 @@ if eid is not None: entity = session.entity_from_eid(eid, etype) repo.delete_info(session, entity, self.uri, extid, - scleanup=True) + scleanup=self.eid) except: self.exception('while updating %s with external id %s of source %s', etype, extid, self.uri) continue - session.execute('SET X value %(v)s WHERE X pkey %(k)s', - {'k': u'sources.%s.latest-update-time' % self.uri, - 'v': unicode(int(mktime(updatetime.timetuple())))}) + self.latest_retrieval = updatetime + session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s', + {'x': self.eid, 'date': self.latest_retrieval}) session.commit() finally: session.close() diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/test/unittest_datafeed.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/test/unittest_datafeed.py Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,98 @@ +# copyright 2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +from __future__ import with_statement + +from datetime import timedelta + +from cubicweb.devtools.testlib import CubicWebTC +from cubicweb.server.sources import datafeed + + +class DataFeedTC(CubicWebTC): + def setup_database(self): + self.request().create_entity('CWSource', name=u'myfeed', type=u'datafeed', + parser=u'testparser', url=u'ignored', + config=u'synchronization-interval=1min') + + def test(self): + self.assertIn('myfeed', self.repo.sources_by_uri) + dfsource = self.repo.sources_by_uri['myfeed'] + self.assertNotIn(dfsource, self.repo.sources) + self.assertEqual(dfsource.latest_retrieval, None) + self.assertEqual(dfsource.synchro_interval, timedelta(seconds=60)) + self.assertFalse(dfsource.fresh()) + + class AParser(datafeed.DataFeedParser): + __regid__ = 'testparser' + def process(self, url): + entity = self.extid2entity('http://www.cubicweb.org/', 'Card', + item={'title': u'cubicweb.org', + 'content': u'the cw web site'}) + if not self.created_during_pull(entity): + self.notify_updated(entity) + def before_entity_copy(self, entity, sourceparams): + entity.cw_edited.update(sourceparams['item']) + + with self.temporary_appobjects(AParser): + stats = dfsource.pull_data(self.session, force=True) + self.commit() + # test import stats + self.assertEqual(sorted(stats.keys()), ['created', 'updated']) + self.assertEqual(len(stats['created']), 1) + entity = self.execute('Card X').get_entity(0, 0) + self.assertIn(entity.eid, stats['created']) + self.assertEqual(stats['updated'], set()) + # test imported entities + self.assertEqual(entity.title, 'cubicweb.org') + self.assertEqual(entity.content, 'the cw web site') + self.assertEqual(entity.cwuri, 'http://www.cubicweb.org/') + self.assertEqual(entity.cw_source[0].name, 'myfeed') + self.assertEqual(entity.cw_metainformation(), + {'type': 'Card', + 'source': {'uri': 'system', 'type': 'native'}, + 'extid': 'http://www.cubicweb.org/'} + ) + # test repo cache keys + self.assertEqual(self.repo._type_source_cache[entity.eid], + ('Card', 'system', 'http://www.cubicweb.org/')) + self.assertEqual(self.repo._extid_cache[('http://www.cubicweb.org/', 'system')], + entity.eid) + # test repull + stats = dfsource.pull_data(self.session, force=True) + self.assertEqual(stats['created'], set()) + self.assertEqual(stats['updated'], set((entity.eid,))) + # test repull with caches reseted + self.repo._type_source_cache.clear() + self.repo._extid_cache.clear() + stats = dfsource.pull_data(self.session, force=True) + self.assertEqual(stats['created'], set()) + self.assertEqual(stats['updated'], set((entity.eid,))) + self.assertEqual(self.repo._type_source_cache[entity.eid], + ('Card', 'system', 'http://www.cubicweb.org/')) + self.assertEqual(self.repo._extid_cache[('http://www.cubicweb.org/', 'system')], + entity.eid) + + self.assertEqual(dfsource.source_cwuris(self.session), + {'http://www.cubicweb.org/': (entity.eid, 'Card')} + ) + self.assertTrue(dfsource.latest_retrieval) + self.assertTrue(dfsource.fresh()) + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/test/unittest_ldapuser.py Fri Feb 11 12:17:47 2011 +0100 @@ -49,8 +49,7 @@ """ assert login, 'no login!' searchfilter = [filter_format('(%s=%s)', (self.user_login_attr, login))] - searchfilter.extend([filter_format('(%s=%s)', ('objectClass', o)) - for o in self.user_classes]) + searchfilter.extend(self.base_filters) searchstr = '(&%s)' % ''.join(searchfilter) # first search the user try: @@ -456,8 +455,7 @@ self.pool = repo._get_pool() session = mock_object(pool=self.pool) self.o = RQL2LDAPFilter(ldapsource, session) - self.ldapclasses = ''.join('(objectClass=%s)' % ldapcls - for ldapcls in ldapsource.user_classes) + self.ldapclasses = ''.join(ldapsource.base_filters) def tearDown(self): repo._free_pool(self.pool) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/test/unittest_msplanner.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,7 +20,6 @@ from __future__ import with_statement from logilab.common.decorators import clear_cache - from yams.buildobjs import RelationDefinition from rql import BadRQLQuery @@ -43,7 +42,6 @@ from cubicweb.server.msplanner import MSPlanner, PartPlanInformation class FakeUserROSource(AbstractSource): - uri = 'zzz' support_entities = {'CWUser': False} support_relations = {} def syntax_tree_search(self, *args, **kwargs): @@ -51,7 +49,6 @@ class FakeCardSource(AbstractSource): - uri = 'ccc' support_entities = {'Card': True, 'Note': True, 'State': True} support_relations = {'in_state': True, 'multisource_rel': True, 'multisource_inlined_rel': True, 'multisource_crossed_rel': True,} @@ -61,12 +58,16 @@ def syntax_tree_search(self, *args, **kwargs): return [] + +class FakeDataFeedSource(FakeCardSource): + copy_based_source = True + X_ALL_SOLS = sorted([{'X': 'Affaire'}, {'X': 'BaseTransition'}, {'X': 'Basket'}, {'X': 'Bookmark'}, {'X': 'CWAttribute'}, {'X': 'CWCache'}, {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, {'X': 'CWEType'}, {'X': 'CWGroup'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, {'X': 'CWRType'}, {'X': 'CWRelation'}, - {'X': 'CWSource'}, {'X': 'CWSourceHostConfig'}, + {'X': 'CWSource'}, {'X': 'CWSourceHostConfig'}, {'X': 'CWSourceSchemaConfig'}, {'X': 'CWUser'}, {'X': 'CWUniqueTogetherConstraint'}, {'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailAddress'}, {'X': 'EmailPart'}, @@ -113,6 +114,7 @@ self.schema['CWUser'].set_action_permissions('read', userreadperms) self.add_source(FakeUserROSource, 'ldap') self.add_source(FakeCardSource, 'cards') + self.add_source(FakeDataFeedSource, 'datafeed') def tearDown(self): # restore hijacked security @@ -900,6 +902,7 @@ ueid = self.session.user.eid ALL_SOLS = X_ALL_SOLS[:] ALL_SOLS.remove({'X': 'CWSourceHostConfig'}) # not authorized + ALL_SOLS.remove({'X': 'CWSourceSchemaConfig'}) # not authorized self._test('Any MAX(X)', [('FetchStep', [('Any E WHERE E type "X", E is Note', [{'E': 'Note'}])], [self.cards, self.system], None, {'E': 'table1.C0'}, []), @@ -950,7 +953,7 @@ ueid = self.session.user.eid X_ET_ALL_SOLS = [] for s in X_ALL_SOLS: - if s == {'X': 'CWSourceHostConfig'}: + if s in ({'X': 'CWSourceHostConfig'}, {'X': 'CWSourceSchemaConfig'}): continue # not authorized ets = {'ET': 'CWEType'} ets.update(s) @@ -1957,6 +1960,22 @@ ]) def test_source_specified_1_2(self): + self._test('Card X WHERE X cw_source S, S name "datafeed"', + [('OneFetchStep', [('Any X WHERE X cw_source S, S name "datafeed", X is Card', + [{'X': 'Card', 'S': 'CWSource'}])], + None, None, + [self.system],{}, []) + ]) + + def test_source_specified_1_3(self): + self._test('Any X, SN WHERE X is Card, X cw_source S, S name "datafeed", S name SN', + [('OneFetchStep', [('Any X,SN WHERE X is Card, X cw_source S, S name "datafeed", ' + 'S name SN', + [{'S': 'CWSource', 'SN': 'String', 'X': 'Card'}])], + None, None, [self.system], {}, []) + ]) + + def test_source_specified_1_4(self): sols = [] for sol in X_ALL_SOLS: sol = sol.copy() @@ -2006,6 +2025,14 @@ ]) def test_source_specified_3_2(self): + self._test('Any X,XT WHERE X is Card, X title XT, X cw_source S, S name "datafeed"', + [('OneFetchStep', + [('Any X,XT WHERE X is Card, X title XT, X cw_source S, S name "datafeed"', + [{'X': 'Card', 'XT': 'String', 'S': 'CWSource'}])], + None, None, [self.system], {}, []) + ]) + + def test_source_specified_3_3(self): self.skipTest('oops') self._test('Any STN WHERE X is Note, X type XT, X in_state ST, ST name STN, X cw_source S, S name "cards"', [('OneFetchStep', @@ -2542,7 +2569,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, CWSource, CWSourceHostConfig, CWUniqueTogetherConstraint, 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, CWSource, CWSourceHostConfig, CWSourceSchemaConfig, 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'}, @@ -2559,6 +2586,7 @@ {'U': 'CWUser', 'X': 'CWRelation'}, {'U': 'CWUser', 'X': 'CWSource'}, {'U': 'CWUser', 'X': 'CWSourceHostConfig'}, + {'U': 'CWUser', 'X': 'CWSourceSchemaConfig'}, {'U': 'CWUser', 'X': 'CWUniqueTogetherConstraint'}, {'U': 'CWUser', 'X': 'CWUser'}, {'U': 'CWUser', 'X': 'Division'}, diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/test/unittest_multisources.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ - # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -17,6 +17,7 @@ # with CubicWeb. If not, see . from datetime import datetime, timedelta +from itertools import repeat from cubicweb.devtools import TestServerConfiguration, init_test_database from cubicweb.devtools.testlib import CubicWebTC, refresh_repo @@ -35,7 +36,6 @@ pyro-ns-id = extern cubicweb-user = admin cubicweb-password = gingkow -mapping-file = extern_mapping.py base-url=http://extern.org/ ''' @@ -46,14 +46,22 @@ PyroRQLSource_get_connection = PyroRQLSource.get_connection Connection_close = Connection.close +def add_extern_mapping(source): + source.init_mapping(zip(('Card', 'Affaire', 'State', + 'in_state', 'documented_by', 'multisource_inlined_rel'), + repeat(u'write'))) + + def setUpModule(*args): global repo2, cnx2, repo3, cnx3 cfg1 = ExternalSource1Configuration('data', apphome=TwoSourcesTC.datadir) repo2, cnx2 = init_test_database(config=cfg1) cfg2 = ExternalSource2Configuration('data', apphome=TwoSourcesTC.datadir) repo3, cnx3 = init_test_database(config=cfg2) - cnx3.request().create_entity('CWSource', name=u'extern', type=u'pyrorql', - config=EXTERN_SOURCE_CFG) + src = cnx3.request().create_entity('CWSource', name=u'extern', + type=u'pyrorql', config=EXTERN_SOURCE_CFG) + cnx3.commit() # must commit before adding the mapping + add_extern_mapping(src) cnx3.commit() TestServerConfiguration.no_sqlite_wrap = True @@ -108,11 +116,12 @@ pyro-ns-id = extern-multi cubicweb-user = admin cubicweb-password = gingkow -mapping-file = extern_mapping.py ''')]: - self.request().create_entity('CWSource', name=unicode(uri), - type=u'pyrorql', - config=unicode(config)) + source = self.request().create_entity( + 'CWSource', name=unicode(uri), type=u'pyrorql', + config=unicode(config)) + self.commit() # must commit before adding the mapping + add_extern_mapping(source) self.commit() # trigger discovery self.sexecute('Card X') diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/test/unittest_querier.py --- a/server/test/unittest_querier.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/test/unittest_querier.py Fri Feb 11 12:17:47 2011 +0100 @@ -501,15 +501,15 @@ [[u'description_format', 12], [u'description', 13], [u'name', 15], - [u'created_by', 40], - [u'creation_date', 40], - [u'cw_source', 40], - [u'cwuri', 40], - [u'in_basket', 40], - [u'is', 40], - [u'is_instance_of', 40], - [u'modification_date', 40], - [u'owned_by', 40]]) + [u'created_by', 41], + [u'creation_date', 41], + [u'cw_source', 41], + [u'cwuri', 41], + [u'in_basket', 41], + [u'is', 41], + [u'is_instance_of', 41], + [u'modification_date', 41], + [u'owned_by', 41]]) def test_select_aggregat_having_dumb(self): # dumb but should not raise an error diff -r a8fbcf9b6572 -r 8e59c2cdcc99 server/test/unittest_ssplanner.py --- a/server/test/unittest_ssplanner.py Fri Feb 11 11:37:10 2011 +0100 +++ b/server/test/unittest_ssplanner.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r a8fbcf9b6572 -r 8e59c2cdcc99 sobjects/parsers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sobjects/parsers.py Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,355 @@ +# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""datafeed parser for xml generated by cubicweb""" + +import urllib2 +import StringIO +import os.path as osp +from cookielib import CookieJar +from datetime import datetime, timedelta + +from lxml import etree + +from logilab.common.date import todate, totime +from logilab.common.textutils import splitstrip, text_to_dict + +from yams.constraints import BASE_CONVERTERS +from yams.schema import role_name as rn + +from cubicweb import ValidationError, typed_eid +from cubicweb.server.sources import datafeed + +def ensure_str_keys(dict): + for key in dict: + dict[str(key)] = dict.pop(key) + +# see cubicweb.web.views.xmlrss.SERIALIZERS +DEFAULT_CONVERTERS = BASE_CONVERTERS.copy() +DEFAULT_CONVERTERS['String'] = unicode +DEFAULT_CONVERTERS['Password'] = lambda x: x.encode('utf8') +def convert_date(ustr): + return todate(datetime.strptime(ustr, '%Y-%m-%d')) +DEFAULT_CONVERTERS['Date'] = convert_date +def convert_datetime(ustr): + return datetime.strptime(ustr, '%Y-%m-%d %H:%M:%S') +DEFAULT_CONVERTERS['Datetime'] = convert_datetime +def convert_time(ustr): + return totime(datetime.strptime(ustr, '%H:%M:%S')) +DEFAULT_CONVERTERS['Time'] = convert_time +def convert_interval(ustr): + return time(seconds=int(ustr)) +DEFAULT_CONVERTERS['Interval'] = convert_interval + +# use a cookie enabled opener to use session cookie if any +_OPENER = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) + +def extract_typed_attrs(eschema, stringdict, converters=DEFAULT_CONVERTERS): + typeddict = {} + for rschema in eschema.subject_relations(): + if rschema.final and rschema in stringdict: + if rschema == 'eid': + continue + attrtype = eschema.destination(rschema) + typeddict[rschema.type] = converters[attrtype](stringdict[rschema]) + return typeddict + +def _entity_etree(parent): + for node in list(parent): + item = {'cwtype': unicode(node.tag), + 'cwuri': node.attrib['cwuri'], + 'eid': typed_eid(node.attrib['eid']), + } + rels = {} + for child in node: + role = child.get('role') + if child.get('role'): + # relation + related = rels.setdefault(role, {}).setdefault(child.tag, []) + related += [ritem for ritem, _ in _entity_etree(child)] + else: + # attribute + item[child.tag] = unicode(child.text) + yield item, rels + +def build_search_rql(etype, attrs): + restrictions = [] + for attr in attrs: + restrictions.append('X %(attr)s %%(%(attr)s)s' % {'attr': attr}) + return 'Any X WHERE X is %s, %s' % (etype, ','.join(restrictions)) + +def rtype_role_rql(rtype, role): + if role == 'object': + return 'Y %s X WHERE X eid %%(x)s' % rtype + else: + return 'X %s Y WHERE X eid %%(x)s' % rtype + + +def _check_no_option(action, options, eid, _): + if options: + msg = _("'%s' action doesn't take any options") % action + raise ValidationError(eid, {rn('options', 'subject'): msg}) + +def _check_linkattr_option(action, options, eid, _): + if not 'linkattr' in options: + msg = _("'%s' action require 'linkattr' option") % action + raise ValidationError(eid, {rn('options', 'subject'): msg}) + + +class CWEntityXMLParser(datafeed.DataFeedParser): + """datafeed parser for the 'xml' entity view""" + __regid__ = 'cw.entityxml' + + action_options = { + 'copy': _check_no_option, + 'link-or-create': _check_linkattr_option, + 'link': _check_linkattr_option, + } + + def __init__(self, *args, **kwargs): + super(CWEntityXMLParser, self).__init__(*args, **kwargs) + self.action_methods = { + 'copy': self.related_copy, + 'link-or-create': self.related_link_or_create, + 'link': self.related_link, + } + + # mapping handling ######################################################### + + def add_schema_config(self, schemacfg, checkonly=False): + """added CWSourceSchemaConfig, modify mapping accordingly""" + _ = self._cw._ + try: + rtype = schemacfg.schema.rtype.name + except AttributeError: + msg = _("entity and relation types can't be mapped, only attributes " + "or relations") + raise ValidationError(schemacfg.eid, {rn('cw_for_schema', 'subject'): msg}) + if schemacfg.options: + options = text_to_dict(schemacfg.options) + else: + options = {} + try: + role = options.pop('role') + if role not in ('subject', 'object'): + raise KeyError + except KeyError: + msg = _('"role=subject" or "role=object" must be specified in options') + raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg}) + try: + action = options.pop('action') + self.action_options[action](action, options, schemacfg.eid, _) + except KeyError: + msg = _('"action" must be specified in options; allowed values are ' + '%s') % ', '.join(self.action_methods) + raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg}) + if not checkonly: + if role == 'subject': + etype = schemacfg.schema.stype.name + ttype = schemacfg.schema.otype.name + else: + etype = schemacfg.schema.otype.name + ttype = schemacfg.schema.stype.name + etyperules = self.source.mapping.setdefault(etype, {}) + etyperules.setdefault((rtype, role, action), []).append( + (ttype, options) ) + self.source.mapping_idx[schemacfg.eid] = ( + etype, rtype, role, action, ttype) + + def del_schema_config(self, schemacfg, checkonly=False): + """deleted CWSourceSchemaConfig, modify mapping accordingly""" + etype, rtype, role, action, ttype = self.source.mapping_idx[schemacfg.eid] + rules = self.source.mapping[etype][(rtype, role, action)] + rules = [x for x in rules if not x[0] == ttype] + if not rules: + del self.source.mapping[etype][(rtype, role, action)] + + # import handling ########################################################## + + def process(self, url, partialcommit=True): + """IDataFeedParser main entry point""" + # XXX suppression support according to source configuration. If set, get + # all cwuri of entities from this source, and compare with newly + # imported ones + for item, rels in self.parse(url): + self.process_item(item, rels) + if partialcommit: + # commit+set_pool instead of commit(reset_pool=False) to let + # other a chance to get our pool + self._cw.commit() + self._cw.set_pool() + + def parse(self, url): + if not url.startswith('http'): + stream = StringIO.StringIO(url) + else: + for mappedurl in HOST_MAPPING: + if url.startswith(mappedurl): + url = url.replace(mappedurl, HOST_MAPPING[mappedurl], 1) + break + self.source.info('GET %s', url) + stream = _OPENER.open(url) + return _entity_etree(etree.parse(stream).getroot()) + + def process_one(self, url): + # XXX assert len(root.children) == 1 + for item, rels in self.parse(url): + return self.process_item(item, rels) + + def process_item(self, item, rels): + entity = self.extid2entity(str(item.pop('cwuri')), + item.pop('cwtype'), + item=item) + if not (self.created_during_pull(entity) + or self.updated_during_pull(entity)): + self.notify_updated(entity) + item.pop('eid') + # XXX check modification date + attrs = extract_typed_attrs(entity.e_schema, item) + entity.set_attributes(**attrs) + for (rtype, role, action), rules in self.source.mapping.get(entity.__regid__, {}).iteritems(): + try: + rel = rels[role][rtype] + except KeyError: + self.source.error('relation %s-%s doesn\'t seem exported in %s xml', + rtype, role, entity.__regid__) + continue + try: + actionmethod = self.action_methods[action] + except KeyError: + raise Exception('Unknown action %s' % action) + actionmethod(entity, rtype, role, rel, rules) + return entity + + def before_entity_copy(self, entity, sourceparams): + """IDataFeedParser callback""" + attrs = extract_typed_attrs(entity.e_schema, sourceparams['item']) + entity.cw_edited.update(attrs) + + def related_copy(self, entity, rtype, role, value, rules): + """implementation of 'copy' action + + Takes no option. + """ + assert not any(x[1] for x in rules), "'copy' action takes no option" + ttypes = set([x[0] for x in rules]) + value = [item for item in value if item['cwtype'] in ttypes] + eids = [] # local eids + if not value: + self._clear_relation(entity, rtype, role, ttypes) + return + for item in value: + eids.append(self.process_one(self._complete_url(item)).eid) + self._set_relation(entity, rtype, role, eids) + + def related_link(self, entity, rtype, role, value, rules): + """implementation of 'link' action + + requires an options to control search of the linked entity. + """ + for ttype, options in rules: + assert 'linkattr' in options, ( + "'link-or-create' action require a list of attributes used to " + "search if the entity already exists") + self._related_link(entity, rtype, role, ttype, value, [options['linkattr']], + self._log_not_found) + + def related_link_or_create(self, entity, rtype, role, value, rules): + """implementation of 'link-or-create' action + + requires an options to control search of the linked entity. + """ + for ttype, options in rules: + assert 'linkattr' in options, ( + "'link-or-create' action require a list of attributes used to " + "search if the entity already exists") + self._related_link(entity, rtype, role, ttype, value, [options['linkattr']], + self._create_not_found) + + def _log_not_found(self, entity, rtype, role, ritem, searchvalues): + self.source.error('can find %s entity with attributes %s', + ritem['cwtype'], searchvalues) + + def _create_not_found(self, entity, rtype, role, ritem, searchvalues): + ensure_str_keys(searchvalues) # XXX necessary with python < 2.6 + return self._cw.create_entity(ritem['cwtype'], **searchvalues).eid + + def _related_link(self, entity, rtype, role, ttype, value, searchattrs, + notfound_callback): + eids = [] # local eids + for item in value: + if item['cwtype'] != ttype: + continue + if not all(attr in item for attr in searchattrs): + # need to fetch related entity's xml + ritems = list(self.parse(self._complete_url(item, False))) + assert len(ritems) == 1, 'unexpected xml' + ritem = ritems[0][0] # list of 2-uples + assert all(attr in ritem for attr in searchattrs), \ + 'missing attribute, got %s expected keys %s' % (item, searchattrs) + else: + ritem = item + kwargs = dict((attr, ritem[attr]) for attr in searchattrs) + rql = build_search_rql(item['cwtype'], kwargs) + rset = self._cw.execute(rql, kwargs) + if rset: + assert len(rset) == 1 + eids.append(rset[0][0]) + else: + eid = notfound_callback(entity, rtype, role, ritem, kwargs) + if eid is not None: + eids.append(eid) + if not eids: + self._clear_relation(entity, rtype, role, (ttype,)) + else: + self._set_relation(entity, rtype, role, eids) + + def _complete_url(self, item, add_relations=True): + itemurl = item['cwuri'] + '?vid=xml' + for rtype, role, _ in self.source.mapping.get(item['cwtype'], ()): + itemurl += '&relation=%s_%s' % (rtype, role) + return itemurl + + def _clear_relation(self, entity, rtype, role, ttypes): + if entity.eid not in self.stats['created']: + if len(ttypes) > 1: + typerestr = ', Y is IN(%s)' % ','.join(ttypes) + else: + typerestr = ', Y is %s' % ','.join(ttypes) + self._cw.execute('DELETE ' + rtype_role_rql(rtype, role) + typerestr, + {'x': entity.eid}) + + def _set_relation(self, entity, rtype, role, eids): + eidstr = ','.join(str(eid) for eid in eids) + rql = rtype_role_rql(rtype, role) + self._cw.execute('DELETE %s, NOT Y eid IN (%s)' % (rql, eidstr), + {'x': entity.eid}) + if role == 'object': + rql = 'SET %s, Y eid IN (%s), NOT Y %s X' % (rql, eidstr, rtype) + else: + rql = 'SET %s, Y eid IN (%s), NOT X %s Y' % (rql, eidstr, rtype) + self._cw.execute(rql, {'x': entity.eid}) + +def registration_callback(vreg): + vreg.register_all(globals().values(), __name__) + global HOST_MAPPING + HOST_MAPPING = {} + if vreg.config.apphome: + host_mapping_file = osp.join(vreg.config.apphome, 'hostmapping.py') + if osp.exists(host_mapping_file): + HOST_MAPPING = eval(file(host_mapping_file).read()) + vreg.info('using host mapping %s from %s', HOST_MAPPING, host_mapping_file) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 sobjects/test/data/schema.py --- a/sobjects/test/data/schema.py Fri Feb 11 11:37:10 2011 +0100 +++ b/sobjects/test/data/schema.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,10 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -""" - -""" -from yams.buildobjs import RelationDefinition +from yams.buildobjs import EntityType, RelationDefinition, String, SubjectRelation class comments(RelationDefinition): subject = 'Comment' @@ -26,3 +23,6 @@ cardinality='1*' composite='object' +class Tag(EntityType): + name = String(unique=True) + tags = SubjectRelation('CWUser') diff -r a8fbcf9b6572 -r 8e59c2cdcc99 sobjects/test/unittest_parsers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sobjects/test/unittest_parsers.py Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,170 @@ +# copyright 2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . + +from datetime import datetime + +from cubicweb.devtools.testlib import CubicWebTC + +from cubicweb.sobjects.parsers import CWEntityXMLParser + +orig_parse = CWEntityXMLParser.parse + +def parse(self, url): + try: + url = RELATEDXML[url.split('?')[0]] + except KeyError: + pass + return orig_parse(self, url) + +def setUpModule(): + CWEntityXMLParser.parse = parse + +def tearDownModule(): + CWEntityXMLParser.parse = orig_parse + + +BASEXML = ''.join(u''' + + + sthenault + toto + 2011-01-25 14:14:06 + 2010-01-22 10:27:59 + 2011-01-25 14:14:06 + + + + + + + + + + + + + +'''.splitlines()) + +RELATEDXML ={ + 'http://pouet.org/6': u''' + + +
syt@logilab.fr
+ 2010-04-13 14:35:56 + 2010-04-13 14:35:56 +
+
+''', + 'http://pouet.org/7': u''' + + + users + + +''', + 'http://pouet.org/8': u''' + + + unknown + + +''', + 'http://pouet.org/9': u''' + + + hop + + +''', + 'http://pouet.org/10': u''' + + + unknown + + +''', + } + +class CWEntityXMLParserTC(CubicWebTC): + def setup_database(self): + req = self.request() + source = req.create_entity('CWSource', name=u'myfeed', type=u'datafeed', + parser=u'cw.entityxml', url=BASEXML) + self.commit() + source.init_mapping([(('CWUser', 'use_email', '*'), + u'role=subject\naction=copy'), + (('CWUser', 'in_group', '*'), + u'role=subject\naction=link\nlinkattr=name'), + (('*', 'tags', 'CWUser'), + u'role=object\naction=link-or-create\nlinkattr=name'), + ]) + req.create_entity('Tag', name=u'hop') + + def test_actions(self): + dfsource = self.repo.sources_by_uri['myfeed'] + self.assertEqual(dfsource.mapping, + {u'CWUser': { + (u'in_group', u'subject', u'link'): [ + (u'CWGroup', {u'linkattr': u'name'})], + (u'tags', u'object', u'link-or-create'): [ + (u'Tag', {u'linkattr': u'name'})], + (u'use_email', u'subject', u'copy'): [ + (u'EmailAddress', {})] + } + }) + session = self.repo.internal_session() + stats = dfsource.pull_data(session, force=True) + self.assertEqual(sorted(stats.keys()), ['created', 'updated']) + self.assertEqual(len(stats['created']), 2) + self.assertEqual(stats['updated'], set()) + + user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0) + self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59)) + self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06)) + self.assertEqual(user.cwuri, 'http://pouet.org/5') + self.assertEqual(user.cw_source[0].name, 'myfeed') + self.assertEqual(len(user.use_email), 1) + # copy action + email = user.use_email[0] + self.assertEqual(email.address, 'syt@logilab.fr') + self.assertEqual(email.cwuri, 'http://pouet.org/6') + self.assertEqual(email.cw_source[0].name, 'myfeed') + # link action + self.assertFalse(self.execute('CWGroup X WHERE X name "unknown"')) + groups = sorted([g.name for g in user.in_group]) + self.assertEqual(groups, ['users']) + # link or create action + tags = sorted([t.name for t in user.reverse_tags]) + self.assertEqual(tags, ['hop', 'unknown']) + tag = self.execute('Tag X WHERE X name "unknown"').get_entity(0, 0) + self.assertEqual(tag.cwuri, 'http://testing.fr/cubicweb/%s' % tag.eid) + self.assertEqual(tag.cw_source[0].name, 'system') + + stats = dfsource.pull_data(session, force=True) + self.assertEqual(stats['created'], set()) + self.assertEqual(len(stats['updated']), 2) + self.repo._type_source_cache.clear() + self.repo._extid_cache.clear() + stats = dfsource.pull_data(session, force=True) + self.assertEqual(stats['created'], set()) + self.assertEqual(len(stats['updated']), 2) + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r a8fbcf9b6572 -r 8e59c2cdcc99 test/unittest_schema.py --- a/test/unittest_schema.py Fri Feb 11 11:37:10 2011 +0100 +++ b/test/unittest_schema.py Fri Feb 11 12:17:47 2011 +0100 @@ -163,7 +163,7 @@ 'CWCache', 'CWConstraint', 'CWConstraintType', 'CWEType', 'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation', 'CWPermission', 'CWProperty', 'CWRType', - 'CWSource', 'CWSourceHostConfig', + 'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig', 'CWUniqueTogetherConstraint', 'CWUser', 'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note', 'Password', 'Personne', @@ -171,7 +171,7 @@ 'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint', 'Tag', 'Time', 'Transition', 'TrInfo', 'Workflow', 'WorkflowTransition'] - self.assertListEqual(entities, sorted(expected_entities)) + self.assertListEqual(sorted(expected_entities), entities) relations = sorted([str(r) for r in schema.relations()]) expected_relations = ['add_permission', 'address', 'alias', 'allowed_transition', 'bookmarked_by', 'by_transition', @@ -181,8 +181,7 @@ 'constrained_by', 'constraint_of', 'content', 'content_format', 'created_by', 'creation_date', 'cstrtype', 'custom_workflow', - 'cwuri', 'cw_source', 'cw_host_config_of', - 'cw_support', 'cw_dont_cross', 'cw_may_cross', + 'cwuri', 'cw_for_source', 'cw_host_config_of', 'cw_schema', 'cw_source', 'data', 'data_encoding', 'data_format', 'data_name', 'default_workflow', 'defaultval', 'delete_permission', 'description', 'description_format', 'destination_state', @@ -196,15 +195,15 @@ 'identity', 'in_group', 'in_state', 'indexed', 'initial_state', 'inlined', 'internationalizable', 'is', 'is_instance_of', - 'label', 'last_login_time', 'login', + 'label', 'last_login_time', 'latest_retrieval', 'login', 'mainvars', 'match_host', 'modification_date', 'name', 'nom', - 'ordernum', 'owned_by', + 'options', 'ordernum', 'owned_by', - 'path', 'pkey', 'prefered_form', 'prenom', 'primary_email', + 'parser', 'path', 'pkey', 'prefered_form', 'prenom', 'primary_email', 'read_permission', 'relation_type', 'relations', 'require_group', @@ -212,13 +211,13 @@ 'tags', 'timestamp', 'title', 'to_entity', 'to_state', 'transition_of', 'travaille', 'type', - 'upassword', 'update_permission', 'uri', 'use_email', + 'upassword', 'update_permission', 'url', 'uri', 'use_email', 'value', 'wf_info_for', 'wikiid', 'workflow_of', 'tr_count'] - self.assertListEqual(relations, sorted(expected_relations)) + self.assertListEqual(sorted(expected_relations), relations) eschema = schema.eschema('CWUser') rels = sorted(str(r) for r in eschema.subject_relations()) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 utils.py --- a/utils.py Fri Feb 11 11:37:10 2011 +0100 +++ b/utils.py Fri Feb 11 12:17:47 2011 +0100 @@ -114,7 +114,7 @@ # use networkX instead ? # http://networkx.lanl.gov/reference/algorithms.traversal.html#module-networkx.algorithms.traversal.astar -def transitive_closure_of(entity, relname, _seen=None): +def transitive_closure_of(entity, rtype, _seen=None): """return transitive closure *for the subgraph starting from the given entity* (eg 'parent' entities are not included in the results) """ @@ -122,10 +122,10 @@ _seen = set() _seen.add(entity.eid) yield entity - for child in getattr(entity, relname): + for child in getattr(entity, rtype): if child.eid in _seen: continue - for subchild in transitive_closure_of(child, relname, _seen): + for subchild in transitive_closure_of(child, rtype, _seen): yield subchild diff -r a8fbcf9b6572 -r 8e59c2cdcc99 view.py diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/add_button.png Binary file web/data/add_button.png has changed diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Fri Feb 11 11:37:10 2011 +0100 +++ b/web/data/cubicweb.ajax.js Fri Feb 11 12:17:47 2011 +0100 @@ -608,6 +608,29 @@ $('#'+domid).loadxhtml('json', params, null, 'swap'); } +/* ajax tabs ******************************************************************/ + +function setTab(tabname, cookiename) { + // set appropriate cookie + loadRemote('json', ajaxFuncArgs('set_cookie', null, cookiename, tabname)); + // trigger show + tabname event + triggerLoad(tabname); +} + +function loadNow(eltsel, holesel, reloadable) { + var lazydiv = jQuery(eltsel); + var hole = lazydiv.children(holesel); + if ((hole.length == 0) && ! reloadable) { + /* the hole is already filed */ + return; + } + lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'), {'pageid': pageid}); +} + +function triggerLoad(divid) { + jQuery('#lazy-' + divid).trigger('load_' + divid); +} + /* DEPRECATED *****************************************************************/ preprocessAjaxLoad = cw.utils.deprecatedFunction( diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/cubicweb.calendar.css --- a/web/data/cubicweb.calendar.css Fri Feb 11 11:37:10 2011 +0100 +++ b/web/data/cubicweb.calendar.css Fri Feb 11 12:17:47 2011 +0100 @@ -336,3 +336,48 @@ td.next { text-align: right; } + +/* ------------------------- */ +/* tooltips for fullcalendar */ + +div.calevent div.tooltip { + display: none; /* tooltip hidden */ +} + +div.calevent:hover { + z-index: auto !important; /* in order that the tooltip from the above .calevent div can be put over this div*/ +} + +div.calevent a{ + display: inline; + font-size: none; + font-weight: bold; +} + +div.calevent:hover div.tooltip{ + display: block; + position: absolute; + z-index: 10; + color: black; + border:1px solid black; + background: white; + padding: 5px; + overflow: visible; + width: 200px; +} + +div.tooltip a{ + border: none; + background: none; + color: #2952A3; + text-decoration: none; + } + +div.tooltip a:hover{ + text-decoration: underline; + } + + +div.fc-view{ + overflow: visible; +} \ No newline at end of file diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/cubicweb.css --- a/web/data/cubicweb.css Fri Feb 11 11:37:10 2011 +0100 +++ b/web/data/cubicweb.css Fri Feb 11 12:17:47 2011 +0100 @@ -598,6 +598,10 @@ } /* pagination */ + +div.pagination{ + margin: 0.5em 0; +} span.slice a:visited, span.slice a:hover{ color: %(helperColor)s; @@ -752,7 +756,7 @@ table.listing th { font-weight: bold; font-size: 8pt; - background: %(listingHeaderBgColor)s; + background: %(listingHeaderBgColor)s; padding: 2px 4px; border: 1px solid %(listingBorderColor)s; border-right:none; @@ -881,6 +885,12 @@ background-color: transparent; } +a.addButton { + margin-left: 0.5em; + padding-left: 16px; + background: transparent url("add_button.png") 0% 50% no-repeat; +} + /***************************************/ /* lists */ /***************************************/ diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/cubicweb.lazy.js --- a/web/data/cubicweb.lazy.js Fri Feb 11 11:37:10 2011 +0100 +++ b/web/data/cubicweb.lazy.js Fri Feb 11 12:17:47 2011 +0100 @@ -1,14 +1,1 @@ -function load_now(eltsel, holesel, reloadable) { - var lazydiv = jQuery(eltsel); - var hole = lazydiv.children(holesel); - if ((hole.length == 0) && ! reloadable) { - /* the hole is already filled */ - return; - } - lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl')); -} -function trigger_load(divid) { - jQuery('#lazy-' + divid).trigger('load_' + divid); -} - diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Fri Feb 11 11:37:10 2011 +0100 +++ b/web/data/cubicweb.old.css Fri Feb 11 12:17:47 2011 +0100 @@ -602,6 +602,10 @@ border-bottom: 1px solid #ccc; } +div.pagination{ + margin: 0.5em 0; +} + span.slice a:visited, span.slice a:hover{ color: #555544; @@ -899,6 +903,12 @@ background-color: transparent; } +a.addButton { + margin-left: 0.5em; + padding-left: 16px; + background: transparent url("add_button.png") 0% 50% no-repeat; +} + /***************************************/ /* footer */ /***************************************/ diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/cubicweb.tabs.js --- a/web/data/cubicweb.tabs.js Fri Feb 11 11:37:10 2011 +0100 +++ b/web/data/cubicweb.tabs.js Fri Feb 11 12:17:47 2011 +0100 @@ -1,7 +1,1 @@ -function set_tab(tabname, cookiename) { - // set appropriate cookie - loadRemote('json', ajaxFuncArgs('set_cookie', null, cookiename, tabname)); - // trigger show + tabname event - trigger_load(tabname); -} diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/fullcalendar.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/fullcalendar.css Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,586 @@ +/* + * FullCalendar v1.4.8 Stylesheet + * + * Feel free to edit this file to customize the look of FullCalendar. + * When upgrading to newer versions, please upgrade this file as well, + * porting over any customizations afterwards. + * + * Date: Sat Oct 16 17:10:03 2010 -0700 + * + */ + + +/* TODO: make font sizes look the same in all doctypes */ + + +.fc, +.fc .fc-header, +.fc .fc-content { + font-size: 1em; + } + +.fc { + direction: ltr; + text-align: left; + } + +.fc table { + border-collapse: collapse; + border-spacing: 0; + } + +.fc td, .fc th { + padding: 0; + vertical-align: top; + } + + + +/* Header +------------------------------------------------------------------------*/ + +table.fc-header { + width: 100%; + } + +.fc-header-left { + width: 25%; + } + +.fc-header-left table { + float: left; + } + +.fc-header-center { + width: 50%; + text-align: center; + } + +.fc-header-center table { + margin: 0 auto; + } + +.fc-header-right { + width: 25%; + } + +.fc-header-right table { + float: right; + } + +.fc-header-title { + margin-top: 0; + white-space: nowrap; + } + +.fc-header-space { + padding-left: 10px; + } + +/* right-to-left */ + +.fc-rtl .fc-header-title { + direction: rtl; + } + + + +/* Buttons +------------------------------------------------------------------------*/ + +.fc-header .fc-state-default, +.fc-header .ui-state-default { + margin-bottom: 1em; + cursor: pointer; + } + +.fc-header .fc-state-default { + border-width: 1px 0; + padding: 0 1px; + } + +.fc-header .fc-state-default, +.fc-header .fc-state-default a { + border-style: solid; + } + +.fc-header .fc-state-default a { + display: block; + border-width: 0 1px; + margin: 0 -1px; + width: 100%; + text-decoration: none; + } + +.fc-header .fc-state-default span { + display: block; + border-style: solid; + border-width: 1px 0 1px 1px; + padding: 3px 5px; + } + +.fc-header .ui-state-default { + padding: 4px 6px; + } + +.fc-header .fc-state-default span, +.fc-header .ui-state-default span { + white-space: nowrap; + } + +/* for adjacent buttons */ + +.fc-header .fc-no-right { + padding-right: 0; + } + +.fc-header .fc-no-right a { + margin-right: 0; + border-right: 0; + } + +.fc-header .ui-no-right { + border-right: 0; + } + +/* for fake rounded corners */ + +.fc-header .fc-corner-left { + margin-left: 1px; + padding-left: 0; + } + +.fc-header .fc-corner-right { + margin-right: 1px; + padding-right: 0; + } + +/* DEFAULT button COLORS */ + +.fc-header .fc-state-default, +.fc-header .fc-state-default a { + border-color: #777; /* outer border */ + color: #333; + } + +.fc-header .fc-state-default span { + border-color: #fff #fff #d1d1d1; /* inner border */ + background: #e8e8e8; + } + +/* PRESSED button COLORS (down and active) */ + +.fc-header .fc-state-active a { + color: #fff; + } + +.fc-header .fc-state-down span, +.fc-header .fc-state-active span { + background: #888; + border-color: #808080 #808080 #909090; /* inner border */ + } + +/* DISABLED button COLORS */ + +.fc-header .fc-state-disabled a { + color: #999; + } + +.fc-header .fc-state-disabled, +.fc-header .fc-state-disabled a { + border-color: #ccc; /* outer border */ + } + +.fc-header .fc-state-disabled span { + border-color: #fff #fff #f0f0f0; /* inner border */ + background: #f0f0f0; + } + + + +/* Content Area & Global Cell Styles +------------------------------------------------------------------------*/ + +.fc-widget-content { + border: 1px solid #ccc; /* outer border color */ + } + +.fc-content { + clear: both; + } + +.fc-content .fc-state-default { + border-style: solid; + border-color: #ccc; /* inner border color */ + } + +.fc-content .fc-state-highlight { /* today */ + background: #ffc; + } + +.fc-content .fc-not-today { /* override jq-ui highlight (TODO: ui-widget-content) */ + background: none; + } + +.fc-cell-overlay { /* semi-transparent rectangle while dragging */ + background: #9cf; + opacity: .2; + filter: alpha(opacity=20); /* for IE */ + } + +.fc-view { /* prevents dragging outside of widget */ + width: 100%; + overflow: hidden; + } + + + + + +/* Global Event Styles +------------------------------------------------------------------------*/ + +.fc-event, +.fc-agenda .fc-event-time, +.fc-event a { + border-style: solid; + border-color: #36c; /* default BORDER color (probably the same as background-color) */ + background-color: #36c; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ + } + + /* Use the 'className' CalEvent property and the following + * example CSS to change event color on a per-event basis: + * + * .myclass, + * .fc-agenda .myclass .fc-event-time, + * .myclass a { + * background-color: black; + * border-color: black; + * color: red; + * } + */ + +.fc-event { + text-align: left; + } + +.fc-event a { + overflow: hidden; + font-size: .85em; + text-decoration: none; + cursor: pointer; + } + +.fc-event-editable { + cursor: pointer; + } + +.fc-event-time, +.fc-event-title { + padding: 0 1px; + } + +/* for fake rounded corners */ + +.fc-event a { + display: block; + position: relative; + width: 100%; + height: 100%; + } + +/* right-to-left */ + +.fc-rtl .fc-event a { + text-align: right; + } + +/* resizable */ + +.fc .ui-resizable-handle { + display: block; + position: absolute; + z-index: 99999; + border: 0 !important; /* important overrides pre jquery ui 1.7 styles */ + background: url(data:image/gif;base64,AAAA) !important; /* hover fix for IE */ + } + + + +/* Horizontal Events +------------------------------------------------------------------------*/ + +.fc-event-hori { + border-width: 1px 0; + margin-bottom: 1px; + } + +.fc-event-hori a { + border-width: 0; + } + +/* for fake rounded corners */ + +.fc-content .fc-corner-left { + margin-left: 1px; + } + +.fc-content .fc-corner-left a { + margin-left: -1px; + border-left-width: 1px; + } + +.fc-content .fc-corner-right { + margin-right: 1px; + } + +.fc-content .fc-corner-right a { + margin-right: -1px; + border-right-width: 1px; + } + +/* resizable */ + +.fc-event-hori .ui-resizable-e { + top: 0 !important; /* importants override pre jquery ui 1.7 styles */ + right: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: e-resize; + } + +.fc-event-hori .ui-resizable-w { + top: 0 !important; + left: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: w-resize; + } + +.fc-event-hori .ui-resizable-handle { + _padding-bottom: 14px; /* IE6 had 0 height */ + } + + + + +/* Month View, Basic Week View, Basic Day View +------------------------------------------------------------------------*/ + +.fc-grid table { + width: 100%; + } + +.fc .fc-grid th { + border-width: 0 0 0 1px; + text-align: center; + } + +.fc .fc-grid td { + border-width: 1px 0 0 1px; + } + +.fc-grid th.fc-leftmost, +.fc-grid td.fc-leftmost { + border-left: 0; + } + +.fc-grid .fc-day-number { + float: right; + padding: 0 2px; + } + +.fc-grid .fc-other-month .fc-day-number { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ + } + +.fc-grid .fc-day-content { + clear: both; + padding: 2px 2px 0; /* distance between events and day edges */ + } + +/* event styles */ + +.fc-grid .fc-event-time { + font-weight: bold; + } + +/* right-to-left */ + +.fc-rtl .fc-grid { + direction: rtl; + } + +.fc-rtl .fc-grid .fc-day-number { + float: left; + } + +.fc-rtl .fc-grid .fc-event-time { + float: right; + } + +/* Agenda Week View, Agenda Day View +------------------------------------------------------------------------*/ + +.fc .fc-agenda th, +.fc .fc-agenda td { + border-width: 1px 0 0 1px; + } + +.fc .fc-agenda .fc-leftmost { + border-left: 0; + } + +.fc-agenda tr.fc-first th, +.fc-agenda tr.fc-first td { + border-top: 0; + } + +.fc-agenda-head tr.fc-last th { + border-bottom-width: 1px; + } + +.fc .fc-agenda-head td, +.fc .fc-agenda-body td { + background: none; + } + +.fc-agenda-head th { + text-align: center; + } + +/* the time axis running down the left side */ + +.fc-agenda .fc-axis { + width: 50px; + padding: 0 4px; + vertical-align: middle; + white-space: nowrap; + text-align: right; + font-weight: normal; + } + +/* all-day event cells at top */ + +.fc-agenda-head tr.fc-all-day th { + height: 35px; + } + +.fc-agenda-head td { + padding-bottom: 10px; + } + +.fc .fc-divider div { + font-size: 1px; /* for IE6/7 */ + height: 2px; + } + +.fc .fc-divider .fc-state-default { + background: #eee; /* color for divider between all-day and time-slot events */ + } + +/* body styles */ + +.fc .fc-agenda-body td div { + height: 20px; /* slot height */ + } + +.fc .fc-agenda-body tr.fc-minor th, +.fc .fc-agenda-body tr.fc-minor td { + border-top-style: dotted; + } + +.fc-agenda .fc-day-content { + padding: 2px 2px 0; /* distance between events and day edges */ + } + +/* vertical background columns */ + +.fc .fc-agenda-bg .ui-state-highlight { + background-image: none; /* tall column, don't want repeating background image */ + } + + + +/* Vertical Events +------------------------------------------------------------------------*/ + +.fc-event-vert { + border-width: 0 1px; + } + +.fc-event-vert a { + border-width: 0; + } + +/* for fake rounded corners */ + +.fc-content .fc-corner-top { + margin-top: 1px; + } + +.fc-content .fc-corner-top a { + margin-top: -1px; + border-top-width: 1px; + } + +.fc-content .fc-corner-bottom { + margin-bottom: 1px; + } + +.fc-content .fc-corner-bottom a { + margin-bottom: -1px; + border-bottom-width: 1px; + } + +/* event content */ + +.fc-event-vert span { + display: block; + position: relative; + z-index: 2; + } + +.fc-event-vert span.fc-event-time { + white-space: nowrap; + _white-space: normal; + overflow: hidden; + border: 0; + font-size: 10px; + } + +.fc-event-vert span.fc-event-title { + line-height: 13px; + } + +.fc-event-vert span.fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + opacity: .3; + filter: alpha(opacity=30); /* for IE */ + } + +/* resizable */ + +.fc-event-vert .ui-resizable-s { + bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */ + width: 100% !important; + height: 8px !important; + line-height: 8px !important; + font-size: 11px !important; + font-family: monospace; + text-align: center; + cursor: s-resize; + } + + diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/fullcalendar.min.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/fullcalendar.min.js Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,108 @@ +/* + + FullCalendar v1.4.8 + http://arshaw.com/fullcalendar/ + + Use fullcalendar.css for basic styling. + For event drag & drop, requires jQuery UI draggable. + For event resizing, requires jQuery UI resizable. + + Copyright (c) 2010 Adam Shaw + Dual licensed under the MIT and GPL licenses, located in + MIT-LICENSE.txt and GPL-LICENSE.txt respectively. + + Date: Sat Oct 16 17:10:03 2010 -0700 + +*/ +(function(m,Z){function fb(a){m.extend(true,Pa,a)}function Bb(a,b,f){function h(e){if(qa){E();ea();M();H(e)}else i()}function i(){ya=b.theme?"ui":"fc";a.addClass("fc");b.isRTL&&a.addClass("fc-rtl");b.theme&&a.addClass("ui-widget");qa=m("
").prependTo(a);pa=new Cb(p,b);(ta=pa.render())&&a.prepend(ta);w(b.defaultView);m(window).resize(y);t()||s()}function s(){setTimeout(function(){!F.start&&t()&&H()},0)}function j(){m(window).unbind("resize", +y);pa.destroy();qa.remove();a.removeClass("fc fc-rtl fc-ui-widget")}function l(){return ca.offsetWidth!==0}function t(){return m("body")[0].offsetWidth!==0}function w(e){if(!F||e!=F.name){g++;N();var q=F,J;if(q){if(q.eventsChanged){M();q.eventDirty=q.eventsChanged=false}(q.beforeHide||gb)();Ra(qa,qa.height());q.element.hide()}else Ra(qa,1);qa.css("overflow","hidden");if(F=ka[e])F.element.show();else F=ka[e]=new Ha[e](J=ua=m("
").appendTo(qa), +p);q&&pa.deactivateButton(q.name);pa.activateButton(e);H();qa.css("overflow","");q&&Ra(qa,1);J||(F.afterShow||gb)();g--}}function H(e){if(l()){g++;N();ma===Z&&E();if(!F.start||e||n=F.end){F.render(n,e||0);K(true);!b.lazyFetching||fa()?X():F.renderEvents(Y())}else if(F.sizeDirty||F.eventsDirty||!b.lazyFetching){F.clearEvents();F.sizeDirty&&K();!b.lazyFetching||fa()?X():F.renderEvents(Y())}ia=a.outerWidth();F.sizeDirty=false;F.eventsDirty=false;pa.updateTitle(F.title);e=new Date;e>=F.start&& +e").append(m("").append(m("").append(i(y.left))).append(m("").append(i(y.center))).append(m("").append(i(y.right))))}function h(){E.remove()}function i(y){if(y){var P=m("");m.each(y.split(" "), +function(ea){ea>0&&P.append("");var S;m.each(this.split(","),function(M,X){if(X=="title"){P.append("

 

");S&&S.addClass(K+"-corner-right");S=null}else{var ga;if(a[X])ga=a[X];else if(Ha[X])ga=function(){N.removeClass(K+"-state-hover");a.changeView(X)};if(ga){S&&S.addClass(K+"-no-right");var N;M=b.theme?Wa(b.buttonIcons,X):null;var U=Wa(b.buttonText,X);if(M)N=m("
");else if(U)N=m("");if(N){N.click(function(){N.hasClass(K+"-state-disabled")||ga()}).mousedown(function(){N.not("."+K+"-state-active").not("."+K+"-state-disabled").addClass(K+"-state-down")}).mouseup(function(){N.removeClass(K+"-state-down")}).hover(function(){N.not("."+K+"-state-active").not("."+K+"-state-disabled").addClass(K+"-state-hover")},function(){N.removeClass(K+"-state-hover").removeClass(K+ +"-state-down")}).appendTo(m("").appendTo(P));S?S.addClass(K+"-no-right"):N.addClass(K+"-corner-left");S=N}}}});S&&S.addClass(K+"-corner-right")});return m("").append(P)}}function s(y){E.find("h2.fc-header-title").html(y)}function j(y){E.find("div.fc-button-"+y).addClass(K+"-state-active")}function l(y){E.find("div.fc-button-"+y).removeClass(K+"-state-active")}function t(y){E.find("div.fc-button-"+y).addClass(K+"-state-disabled")}function w(y){E.find("div.fc-button-"+y).removeClass(K+ +"-state-disabled")}var H=this;H.render=f;H.destroy=h;H.updateTitle=s;H.activateButton=j;H.deactivateButton=l;H.disableButton=t;H.enableButton=w;var E=m([]),K}function Db(a,b){function f(c){b.push(c);l(c,N)}function h(c){b=m.grep(b,function(A){return A!=c});G=m.grep(G,function(A){return A.source!=c});N()}function i(c){G=[];s(b,c)}function s(c,A){function C($,fa){var Y=X();if(r!=Y)r.eventsDirty=true;if(I==U&&u<=Y.visStart&&B>=Y.visEnd){if(m.inArray($,b)!=-1){for(Y=0;YB}function H(c){var A,C=G.length,I,V=X().defaultEventEnd,r=c.start-c._start,p=c.end?c.end-(c._end||V(c)):0;for(A=0;Ag){d="";for(g=g;g";for(R=0;R"+(n?"
":"")+"
 
";O(Q,1);fa&&va(Q)}d+=""}F.append(d)}j(F.find("td.fc-new").removeClass("fc-new"));Q=x(u.visStart);F.find("td").each(function(){var ha=m(this);if(Y>1)Q.getMonth()== +q?ha.removeClass("fc-other-month"):ha.addClass("fc-other-month");+Q==+J?ha.removeClass("fc-not-today").addClass("fc-today").addClass(z+"-state-highlight"):ha.addClass("fc-not-today").removeClass("fc-today").removeClass(z+"-state-highlight");ha.find("div.fc-day-number").text(Q.getDate());O(Q,1);fa&&va(Q)});if(Y==1){Q=x(u.visStart);ya.find("th").each(function(ha,da){m(da).text(I(Q,e));da.className=da.className.replace(/^fc-\w+(?= )/,"fc-"+Da[Q.getDay()]);O(Q,1);fa&&va(Q)});Q=x(u.visStart);F.find("td").each(function(ha, +da){da.className=da.className.replace(/^fc-\w+(?= )/,"fc-"+Da[Q.getDay()]);O(Q,1);fa&&va(Q)})}}else{var ba=m("
").appendTo(a);d="";for(g=0;g"+I(Q,e)+"";O(Q,1);fa&&va(Q)}ya=m(d+"").appendTo(ba);d="";Q=x(u.visStart);for(g=0;g";for(R=0;R"+(n?"
"+Q.getDate()+"
":"")+"
 
";O(Q,1);fa&&va(Q)}d+=""}F=m(d+"
").appendTo(ba);j(F.find("td"));ka=m("
").appendTo(a)}}function i(d){qa=d;d=F.find("tr td:first-child");var g=qa-ya.height(),n;if(B("weekMode")=="variable")n= +g=Math.floor(g/(Y==1?2:6));else{n=Math.floor(g/Y);g=g-n*(Y-1)}if(Za===Z){var z=F.find("tr:first").find("td:first");z.height(n);Za=n!=z.height()}if(Za){d.slice(0,-1).height(n);d.slice(-1).height(g)}else{Qa(d.slice(0,-1),n);Qa(d.slice(-1),g)}}function s(d){ta=d;ua.clear();pa=Math.floor(ta/ca);Ka(ya.find("th").slice(0,-1),pa)}function j(d){d.click(l).mousedown(C)}function l(d){if(!B("selectable")){var g=parseInt(this.className.match(/fc\-day(\d+)/)[1]);g=O(x(u.visStart),Math.floor(g/ca)*7+g%ca);G("dayClick", +this,g,true,d)}}function t(d,g,n){n&&ia.build();n=x(u.visStart);for(var z=O(x(n),ca),e=0;e
";for(W=0;W"+F(v,L)+"";O(v,aa);da&&va(v,aa)}oa+=""; +if(p("allDaySlot"))oa+="";oa+="
  
"+p("allDayText")+"
 
 
";ka=m(oa).appendTo(a);w(ka.find("td"));sb=m("
").appendTo(ka); +v=tb();var cb=sa(x(v),ja);sa(v,na);oa="";for(W=0;v";sa(v,p("slotMinutes"));n++}oa+="
"+(!Fa||!wa?F(v,p("axisFormat")):" ")+"
 
";ia=m("
").append(ma=m("
").append(ua= +m(oa))).appendTo(a);H(ia.find("td"));ub=m("
").appendTo(ma);v=x(o);oa="
";for(W=0;W
 
";O(v,aa);da&&va(v,aa)}oa+="
"; +d=m(oa).appendTo(a)}}function i(o,v){if(o===Z)o=R;R=o;Aa={};o=o-ka.height();o=Math.min(o,ua.height());ia.height(o);q=ia.find("tr:first div").height()+1;v&&j()}function s(o){J=o;Ba.clear();ia.width(o).css("overflow","auto");ua.width("");var v=ka.find("tr:first th"),D=ka.find("tr.fc-all-day th:last"),L=d.find("td"),W=ia[0].clientWidth;ua.width(W);W=ia[0].clientWidth;ua.width(W);z=0;Ka(ka.find("tr:lt(2) th:first").add(ia.find("tr:first th")).width(1).each(function(){z=Math.max(z,m(this).outerWidth())}), +z);e=Math.floor((W-z)/g);Ka(L.slice(0,-1),e);Ka(v.slice(1,-2),e);if(o!=W){Ka(v.slice(-2,-1),W-z-e*(g-1));v.slice(-1).show();D.show()}else{ia.css("overflow","hidden");v.slice(-2,-1).width("");v.slice(-1).hide();D.hide()}d.css({top:ka.find("tr").height(),left:z,width:W-z,height:R})}function j(){var o=tb(),v=x(o);v.setHours(p("firstHour"));var D=X(o,v)+1;o=function(){ia.scrollTop(D)};o();setTimeout(o,0)}function l(){Q=ia.scrollTop()}function t(){ia.scrollTop(Q)}function w(o){o.click(E).mousedown(qa)} +function H(o){o.click(E).mousedown(C)}function E(o){if(!p("selectable")){var v=Math.min(g-1,Math.floor((o.pageX-d.offset().left)/e));v=O(x(r.visStart),v*aa+T);var D=this.className.match(/fc-slot(\d+)/);if(D){D=parseInt(D[1])*p("slotMinutes");var L=Math.floor(D/60);v.setHours(L);v.setMinutes(D%60+na);$("dayClick",this,v,false,o)}else $("dayClick",this,v,true,o)}}function K(o,v,D){D&&la.build();var L=x(r.visStart);if(ra){D=Ea(v,L)*aa+T+1;o=Ea(o,L)*aa+T+1}else{D=Ea(o,L);o=Ea(v,L)}D=Math.max(0,D);o=Math.min(g, +o);D=sa(x(o),ja))return ma.height();o=p("slotMinutes");v=v.getHours()*60+v.getMinutes()-na;var D=Math.floor(v/o),L=Aa[D];if(L===Z)L=Aa[D]=ia.find("tr:eq("+D+") td div")[0].offsetTop;return Math.max(0,Math.round(L-1+q*(v%o/o)))}function ga(o){var v=O(x(r.visStart),o.col*aa+T);o=o.row;p("allDaySlot")&&o--;o>=0&&sa(v,na+o*p("slotMinutes"));return v}function N(o){return p("allDaySlot")&&!o.row}function U(){return{left:z,right:J}}function u(){return ka.find("tr.fc-all-day")}function B(o){var v=x(o.start); +if(o.allDay)return v;return sa(v,p("defaultEventMinutes"))}function G(o,v){if(v)return x(o);return sa(x(o),p("slotMinutes"))}function k(o,v,D){if(D)p("allDaySlot")&&K(o,O(x(v),1),true);else c(o,v)}function c(o,v){var D=p("selectHelper");la.build();if(D){var L=Ea(o,r.visStart)*aa+T;if(L>=0&&LW){L.top=W;L.height=wa-W;L.left+=2;L.width-=5;if(m.isFunction(D)){if(o=D(o,v)){L.position="absolute";L.zIndex=8;za=m(o).css(L).appendTo(ma)}}else{za=m(ya({title:"", +start:o,end:v,className:[],editable:false},L,"fc-event fc-event-vert fc-corner-top fc-corner-bottom "));m.browser.msie&&za.find("span.fc-event-bg").hide();za.css("opacity",p("dragOpacity"))}if(za){H(za);ma.append(za);Ka(za,L.width,true);Qa(za,L.height,true)}}}}else P(o,v)}function A(){ca();if(za){za.remove();za=null}}function C(o){if(o.which==1&&p("selectable")){ta(o);var v=this,D;Ca.start(function(L,W){A();if(L&&L.col==W.col&&!N(L)){W=ga(W);L=ga(L);D=[W,sa(x(W),p("slotMinutes")),L,sa(x(L),p("slotMinutes"))].sort(vb); +c(D[0],D[3])}else D=null},o);m(document).one("mouseup",function(L){Ca.stop();if(D){+D[0]==+D[1]&&$("dayClick",v,D[0],false,L);pa(D[0],D[3],false,L)}})}}function I(o,v){Ca.start(function(D){ca();if(D)if(N(D))y(D.row,D.col,D.row,D.col);else{D=ga(D);var L=sa(x(D),p("defaultEventMinutes"));P(D,L)}},v)}function V(o,v,D){var L=Ca.stop();ca();L&&$("drop",o,ga(L),N(L),v,D)}var r=this;r.renderAgenda=h;r.setWidth=s;r.setHeight=i;r.beforeHide=l;r.afterShow=t;r.defaultEventEnd=B;r.timePosition=X;r.dayOfWeekCol= +M;r.cellDate=ga;r.cellIsAllDay=N;r.allDayTR=u;r.allDayBounds=U;r.getHoverListener=function(){return Ca};r.colContentLeft=ea;r.colContentRight=S;r.getDaySegmentContainer=function(){return sb};r.getSlotSegmentContainer=function(){return ub};r.getMinMinute=function(){return na};r.getMaxMinute=function(){return ja};r.getBodyContent=function(){return ma};r.getRowCnt=function(){return 1};r.getColCnt=function(){return g};r.getColWidth=function(){return e};r.getSlotHeight=function(){return q};r.defaultSelectionEnd= +G;r.renderDayOverlay=K;r.renderSelection=k;r.clearSelection=A;r.dragStart=I;r.dragStop=V;jb.call(r,a,b,f);kb.call(r);lb.call(r);Lb.call(r);var p=r.opt,$=r.trigger,fa=r.clearEvents,Y=r.renderOverlay,ca=r.clearOverlays,pa=r.reportSelection,ta=r.unselect,qa=r.daySelectionMousedown,ya=r.slotSegHtml,F=b.formatDate,ka,ia,ma,ua,d,g,n=0,z,e,q,J,R,Q,ba,ha,da,ra,aa,T,na,ja,la,Ca,Ba,Aa={},za,sb,ub;mb(a.addClass("fc-agenda"));la=new nb(function(o,v){function D(xa){return Math.max(oa,Math.min(cb,xa))}var L,W, +wa;d.find("td").each(function(xa,Mb){L=m(Mb);W=L.offset().left;if(xa)wa[1]=W;wa=[W];v[xa]=wa});wa[1]=W+L.outerWidth();if(p("allDaySlot")){L=ka.find("td");W=L.offset().top;o[0]=[W,W+L.outerHeight()]}for(var Fa=ma.offset().top,oa=ia.offset().top,cb=oa+ia.outerHeight(),La=0;La"+Ma(ma(d.start,d.end,P("timeFormat")))+""+Ma(d.title)+""+((d.editable||d.editable===Z&&P("editable"))&&!P("disableResizing")&&m.fn.resizable?"
=
":"")+"
"} +function t(d,g,n){ga(d,g);if(d.editable||d.editable===Z&&P("editable")){H(d,g,n.isStart);n.isEnd&&V(d,g,p())}}function w(d,g,n){ga(d,g);if(d.editable||d.editable===Z&&P("editable")){var z=g.find("span.fc-event-time");E(d,g,z);n.isEnd&&K(d,g,z)}}function H(d,g,n){if(!P("disableDragging")&&g.draggable){var z,e=true,q,J=P("isRTL")?-1:1,R=B(),Q=p(),ba=$(),ha=k();g.draggable({zIndex:9,opacity:P("dragOpacity","month"),revertDuration:P("dragRevertDuration"),start:function(ra,aa){ea("eventDragStart",g,d, +ra,aa);pa(d,g);z=g.width();R.start(function(T,na,ja,la){g.draggable("option","revert",!T||!ja&&!la);F();if(T){q=la*J;if(T.row){if(n&&e){Qa(g.width(Q-10),ba*Math.round((d.end?(d.end-d.start)/Ob:P("defaultEventMinutes"))/P("slotMinutes")));g.draggable("option","grid",[Q,1]);e=false}}else{ya(O(x(d.start),q),O(Oa(d),q));da()}}},ra,"drag")},stop:function(ra,aa){var T=R.stop();F();ea("eventDragStop",g,d,ra,aa);if(T&&(!e||q)){g.find("a").removeAttr("href");T=0;e||(T=Math.round((g.offset().top-fa().offset().top)/ +ba)*P("slotMinutes")+ha-(d.start.getHours()*60+d.start.getMinutes()));ta(this,d,q,T,e,ra,aa)}else{da();m.browser.msie&&g.css("filter","");ca(d,g)}}});function da(){if(!e){g.width(z).height("").draggable("option","grid",null);e=true}}}}function E(d,g,n){if(!P("disableDragging")&&g.draggable){var z,e=false,q,J,R,Q=P("isRTL")?-1:1,ba=B(),ha=r(),da=p(),ra=$();g.draggable({zIndex:9,scroll:false,grid:[da,ra],axis:ha==1?"y":false,opacity:P("dragOpacity"),revertDuration:P("dragRevertDuration"),start:function(na, +ja){ea("eventDragStart",g,d,na,ja);pa(d,g);m.browser.msie&&g.find("span.fc-event-bg").hide();z=g.position();J=R=0;ba.start(function(la,Ca,Ba,Aa){g.draggable("option","revert",!la);F();if(la){q=Aa*Q;if(P("allDaySlot")&&!la.row){if(!e){e=true;n.hide();g.draggable("option","grid",null)}ya(O(x(d.start),q),O(Oa(d),q))}else T()}},na,"drag")},drag:function(na,ja){J=Math.round((ja.position.top-z.top)/ra)*P("slotMinutes");if(J!=R){e||aa(J);R=J}},stop:function(na,ja){var la=ba.stop();F();ea("eventDragStop", +g,d,na,ja);if(la&&(q||J||e))ta(this,d,q,e?0:J,e,na,ja);else{T();g.css(z);aa(0);m.browser.msie&&g.css("filter","").find("span.fc-event-bg").css("display","");ca(d,g)}}});function aa(na){var ja=sa(x(d.start),na),la;if(d.end)la=sa(x(d.end),na);n.text(ma(ja,la,P("timeFormat")))}function T(){if(e){n.css("display","");g.draggable("option","grid",[da,ra]);e=false}}}}function K(d,g,n){if(!P("disableResizing")&&g.resizable){var z,e,q=$();g.resizable({handles:{s:"div.ui-resizable-s"},grid:q,start:function(J, +R){z=e=0;pa(d,g);m.browser.msie&&m.browser.version=="6.0"&&g.css("overflow","hidden");g.css("z-index",9);ea("eventResizeStart",this,d,J,R)},resize:function(J,R){z=Math.round((Math.max(q,g.height())-R.originalSize.height)/q);if(z!=e){n.text(ma(d.start,!z&&!d.end?null:sa(S(d),P("slotMinutes")*z),P("timeFormat")));e=z}},stop:function(J,R){ea("eventResizeStop",this,d,J,R);if(z)qa(this,d,0,P("slotMinutes")*z,J,R);else{g.css("z-index",8);ca(d,g)}}})}}var y=this;y.renderEvents=a;y.rerenderEvents=b;y.clearEvents= +f;y.slotSegHtml=l;y.bindDaySeg=t;qb.call(y);var P=y.opt,ea=y.trigger,S=y.eventEnd,M=y.reportEvents,X=y.clearEventData,ga=y.eventElementHandlers,N=y.setHeight,U=y.getDaySegmentContainer,u=y.getSlotSegmentContainer,B=y.getHoverListener,G=y.getMaxMinute,k=y.getMinMinute,c=y.timePosition,A=y.colContentLeft,C=y.colContentRight,I=y.renderDaySegs,V=y.resizableDayEvent,r=y.getColCnt,p=y.getColWidth,$=y.getSlotHeight,fa=y.getBodyContent,Y=y.reportEventElement,ca=y.showEvents,pa=y.hideEvents,ta=y.eventDrop, +qa=y.eventResize,ya=y.renderDayOverlay,F=y.clearOverlays,ka=y.calendar,ia=ka.formatDate,ma=ka.formatDates,ua=[]}function Nb(a){var b,f,h,i,s,j;for(b=a.length-1;b>0;b--){i=a[b];for(f=0;f"+(!c.allDay&&k.isStart?""+Ma(ga(c.start,c.end,h("timeFormat")))+"":"")+""+Ma(c.title)+""+((c.editable||c.editable===Z&&h("editable"))&&!h("disableResizing")&&m.fn.resizable?"
":"")+"
";k.left=C;k.outerWidth=I-C}ta[0].innerHTML=V;V=ta.children();for(B=0;B div").height($+c)}for(p=0;p"));l[0].parentNode!=j[0]&&l.appendTo(j);h.push(l.css(s).show());return l}function b(){for(var s;s=h.shift();)i.push(s.hide().unbind())}var f=this;f.renderOverlay=a;f.clearOverlays=b;var h=[],i=[]}function nb(a){var b=this,f,h;b.build=function(){f=[];h=[];a(f,h)};b.cell=function(i,s){var j=f.length,l=h.length,t,w=-1,H=-1;for(t=0;t=f[t][0]&&s=h[t][0]&&i=0&&H>=0?{row:w,col:H}:null};b.rect=function(i,s,j,l,t){t=t.offset();return{top:f[i][0]-t.top,left:h[s][0]-t.left,width:h[l][1]-h[s][0],height:f[j][1]-f[i][0]}}}function ob(a){function b(l){l=a.cell(l.pageX,l.pageY);if(!l!=!j||l&&(l.row!=j.row||l.col!=j.col)){if(l){s||(s=l);i(l,s,l.row-s.row,l.col-s.col)}else i(l,s);j=l}}var f=this,h,i,s,j;f.start=function(l,t,w){i=l;s=j=null;a.build();b(t);h=w||"mousemove";m(document).bind(h,b)};f.stop=function(){m(document).unbind(h,b);return j}} +function pb(a){function b(j){return h[j]=h[j]||a(j)}var f=this,h={},i={},s={};f.left=function(j){return i[j]=i[j]===Z?b(j).position().left:i[j]};f.right=function(j){return s[j]=s[j]===Z?f.left(j)+b(j).width():s[j]};f.clear=function(){h={};i={};s={}}}function Ta(a,b,f){a.setFullYear(a.getFullYear()+b);f||Ia(a);return a}function Ua(a,b,f){if(+a){b=a.getMonth()+b;var h=x(a);h.setDate(1);h.setMonth(b);a.setMonth(b);for(f||Ia(a);a.getMonth()!=h.getMonth();)a.setDate(a.getDate()+(aj;w--)if(H= +Qb[f.substring(j,w)]){if(i)E+=H(i,h);j=w-1;break}if(w==j)if(i)E+=t}}return E}function Oa(a){return a.end?Rb(a.end,a.allDay):O(x(a.start),1)}function Rb(a,b){a=x(a);return b||a.getHours()||a.getMinutes()?O(a,1):Ia(a)}function Sb(a,b){return(b.msLength-a.msLength)*100+(a.event.start-b.event.start)}function yb(a,b){return a.end>b.start&&a.startf&&t +h){w=x(h);E=false}else{w=w;E=true}i.push({event:l,start:t,end:w,isStart:H,isEnd:E,msLength:w-t})}}return i.sort(Sb)}function $a(a){var b=[],f,h=a.length,i,s,j,l;for(f=0;f=0;f--){h=a[b[f].toLowerCase()];if(h!==Z)return h}return a[""]}function Ma(a){return a.replace(/&/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
")}function xb(a){return a.id+"/"+a.className+"/"+a.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, +"")}function mb(a){a.attr("unselectable","on").css("MozUserSelect","none").bind("selectstart.ui",function(){return false})}var Pa={defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:true,allDayDefault:true,ignoreTimezone:true,lazyFetching:true,startParam:"start",endParam:"end",titleFormat:{month:"MMMM yyyy",week:"MMM d[ yyyy]{ '—'[ MMM] d yyyy}",day:"dddd, MMM d, yyyy"},columnFormat:{month:"ddd",week:"ddd M/d",day:"dddd M/d"},timeFormat:{"":"h(:mm)t"}, +isRTL:false,firstDay:0,monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],buttonText:{prev:" ◄ ",next:" ► ",prevYear:" << ",nextYear:" >> ", +today:"today",month:"month",week:"week",day:"day"},theme:false,buttonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e"},unselectAuto:true,dropAccept:"*"},Ub={header:{left:"next,prev today",center:"",right:"title"},buttonText:{prev:" ► ",next:" ◄ ",prevYear:" >> ",nextYear:" << "},buttonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w"}},Ga=m.fullCalendar={version:"1.4.8"},Ha=Ga.views={};m.fn.fullCalendar=function(a){if(typeof a== +"string"){var b=Array.prototype.slice.call(arguments,1),f;this.each(function(){var i=m.data(this,"fullCalendar");if(i&&m.isFunction(i[a])){i=i[a].apply(i,b);if(f===Z)f=i;a=="destroy"&&m.removeData(this,"fullCalendar")}});if(f!==Z)return f;return this}var h=a.eventSources||[];delete a.eventSources;if(a.events){h.push(a.events);delete a.events}a=m.extend(true,{},Pa,a.isRTL||a.isRTL===Z&&Pa.isRTL?Ub:{},a);this.each(function(i,s){i=m(s);s=new Bb(i,a,h);i.data("fullCalendar",s);s.render()});return this}; +var Eb=1;Ha.month=Fb;Ha.basicWeek=Gb;Ha.basicDay=Hb;var Za;fb({weekMode:"fixed"});Ha.agendaWeek=Jb;Ha.agendaDay=Kb;fb({allDaySlot:true,allDayText:"all-day",firstHour:6,slotMinutes:30,defaultEventMinutes:120,axisFormat:"h(:mm)tt",timeFormat:{agenda:"h:mm{ - h:mm}"},dragOpacity:{agenda:0.5},minTime:0,maxTime:24});Ga.addDays=O;Ga.cloneDate=x;Ga.parseDate=Xa;Ga.parseISO8601=Ab;Ga.parseTime=bb;Ga.formatDate=Ja;Ga.formatDates=Va;var Da=["sun","mon","tue","wed","thu","fri","sat"],ib=864E5,Pb=36E5,Ob=6E4, +Qb={s:function(a){return a.getSeconds()},ss:function(a){return Na(a.getSeconds())},m:function(a){return a.getMinutes()},mm:function(a){return Na(a.getMinutes())},h:function(a){return a.getHours()%12||12},hh:function(a){return Na(a.getHours()%12||12)},H:function(a){return a.getHours()},HH:function(a){return Na(a.getHours())},d:function(a){return a.getDate()},dd:function(a){return Na(a.getDate())},ddd:function(a,b){return b.dayNamesShort[a.getDay()]},dddd:function(a,b){return b.dayNames[a.getDay()]}, +M:function(a){return a.getMonth()+1},MM:function(a){return Na(a.getMonth()+1)},MMM:function(a,b){return b.monthNamesShort[a.getMonth()]},MMMM:function(a,b){return b.monthNames[a.getMonth()]},yy:function(a){return(a.getFullYear()+"").substring(2)},yyyy:function(a){return a.getFullYear()},t:function(a){return a.getHours()<12?"a":"p"},tt:function(a){return a.getHours()<12?"am":"pm"},T:function(a){return a.getHours()<12?"A":"P"},TT:function(a){return a.getHours()<12?"AM":"PM"},u:function(a){return Ja(a, +"yyyy-MM-dd'T'HH:mm:ss'Z'")},S:function(a){a=a.getDate();if(a>10&&a<20)return"th";return["st","nd","rd"][a%10-1]||"th"}}})(jQuery); diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/jquery.ui.datepicker-de.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/jquery.ui.datepicker-de.js Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,23 @@ +/* German initialisation for the jQuery UI date picker plugin. */ +/* Written by Milian Wolff (mail@milianw.de). */ +jQuery(function($){ + $.datepicker.regional['de'] = { + closeText: 'schließen', + prevText: '<zurück', + nextText: 'Vor>', + currentText: 'heute', + monthNames: ['Januar','Februar','März','April','Mai','Juni', + 'Juli','August','September','Oktober','November','Dezember'], + monthNamesShort: ['Jan','Feb','Mär','Apr','Mai','Jun', + 'Jul','Aug','Sep','Okt','Nov','Dez'], + dayNames: ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'], + dayNamesShort: ['So','Mo','Di','Mi','Do','Fr','Sa'], + dayNamesMin: ['So','Mo','Di','Mi','Do','Fr','Sa'], + weekHeader: 'Wo', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['de']); +}); diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/jquery.ui.datepicker-es.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/jquery.ui.datepicker-es.js Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,23 @@ +/* Inicialización en español para la extensión 'UI date picker' para jQuery. */ +/* Traducido por Vester (xvester@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['es'] = { + closeText: 'Cerrar', + prevText: '<Ant', + nextText: 'Sig>', + currentText: 'Hoy', + monthNames: ['Enero','Febrero','Marzo','Abril','Mayo','Junio', + 'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'], + monthNamesShort: ['Ene','Feb','Mar','Abr','May','Jun', + 'Jul','Ago','Sep','Oct','Nov','Dic'], + dayNames: ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado'], + dayNamesShort: ['Dom','Lun','Mar','Mié','Juv','Vie','Sáb'], + dayNamesMin: ['Do','Lu','Ma','Mi','Ju','Vi','Sá'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['es']); +}); \ No newline at end of file diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/data/jquery.ui.datepicker-fr.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/jquery.ui.datepicker-fr.js Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,23 @@ +/* French initialisation for the jQuery UI date picker plugin. */ +/* Written by Keith Wood (kbwood{at}iinet.com.au) and Stéphane Nahmani (sholby@sholby.net). */ +jQuery(function($){ + $.datepicker.regional['fr'] = { + closeText: 'Fermer', + prevText: '<Préc', + nextText: 'Suiv>', + currentText: 'Courant', + monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin', + 'Juillet','Août','Septembre','Octobre','Novembre','Décembre'], + monthNamesShort: ['Jan','Fév','Mar','Avr','Mai','Jun', + 'Jul','Aoû','Sep','Oct','Nov','Déc'], + dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'], + dayNamesShort: ['Dim','Lun','Mar','Mer','Jeu','Ven','Sam'], + dayNamesMin: ['Di','Lu','Ma','Me','Je','Ve','Sa'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['fr']); +}); \ No newline at end of file diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/formwidgets.py --- a/web/formwidgets.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/formwidgets.py Fri Feb 11 12:17:47 2011 +0100 @@ -580,6 +580,8 @@ def _render(self, form, field, renderer): req = form._cw + if req.lang != 'en': + req.add_js('jquery.ui.datepicker-%s.js' % req.lang) domid = field.dom_id(form, self.suffix) # XXX find a way to understand every format fmt = req.property_value('ui.date-format') diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/test/unittest_application.py --- a/web/test/unittest_application.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/test/unittest_application.py Fri Feb 11 12:17:47 2011 +0100 @@ -24,7 +24,7 @@ from urllib import unquote from logilab.common.testlib import TestCase, unittest_main -from logilab.common.decorators import clear_cache +from logilab.common.decorators import clear_cache, classproperty from cubicweb import AuthenticationError, Unauthorized from cubicweb.devtools.testlib import CubicWebTC @@ -159,6 +159,15 @@ raise self.app.error_handler = raise_hdlr + @classproperty + def config(cls): + try: + return cls.__dict__['_config'] + except KeyError: + config = super(ApplicationTC, cls).config + config.global_set_option('allow-email-login', True) + return config + def test_cnx_user_groups_sync(self): user = self.user() self.assertEqual(user.groups, set(('managers',))) @@ -324,10 +333,9 @@ self.assertAuthFailure(req) self.assertRaises(AuthenticationError, self.app_publish, req, 'login') self.assertEqual(req.cnx, None) - authstr = base64.encodestring('%s:%s' % (origsession.login, origsession.authinfo['password'])) + authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword)) req._headers['Authorization'] = 'basic %s' % authstr self.assertAuthSuccess(req, origsession) - self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']}) self.assertRaises(LogOut, self.app_publish, req, 'logout') self.assertEqual(len(self.open_sessions), 0) @@ -338,10 +346,9 @@ self.failUnless('__login' in form) self.failUnless('__password' in form) self.assertEqual(req.cnx, None) - req.form['__login'] = origsession.login - req.form['__password'] = origsession.authinfo['password'] + req.form['__login'] = self.admlogin + req.form['__password'] = self.admpassword self.assertAuthSuccess(req, origsession) - self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']}) self.assertRaises(LogOut, self.app_publish, req, 'logout') self.assertEqual(len(self.open_sessions), 0) @@ -351,18 +358,17 @@ self.execute('INSERT EmailAddress X: X address %(address)s, U primary_email X ' 'WHERE U login %(login)s', {'address': address, 'login': login}) self.commit() - # option allow-email-login not set + # # option allow-email-login not set req, origsession = self.init_authentication('cookie') - req.form['__login'] = address - req.form['__password'] = origsession.authinfo['password'] - self.assertAuthFailure(req) + # req.form['__login'] = address + # req.form['__password'] = self.admpassword + # self.assertAuthFailure(req) # option allow-email-login set origsession.login = address self.set_option('allow-email-login', True) req.form['__login'] = address - req.form['__password'] = origsession.authinfo['password'] + req.form['__password'] = self.admpassword self.assertAuthSuccess(req, origsession) - self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']}) self.assertRaises(LogOut, self.app_publish, req, 'logout') self.assertEqual(len(self.open_sessions), 0) @@ -382,7 +388,6 @@ asession = req.session self.assertEqual(len(self.open_sessions), 1) self.assertEqual(asession.login, 'anon') - self.assertEqual(asession.authinfo['password'], 'anon') self.failUnless(asession.anonymous_session) self._reset_cookie(req) @@ -400,10 +405,9 @@ authstr = base64.encodestring('toto:pouet') req._headers['Authorization'] = 'basic %s' % authstr self._test_anon_auth_fail(req) - authstr = base64.encodestring('%s:%s' % (origsession.login, origsession.authinfo['password'])) + authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword)) req._headers['Authorization'] = 'basic %s' % authstr self.assertAuthSuccess(req, origsession) - self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']}) self.assertRaises(LogOut, self.app_publish, req, 'logout') self.assertEqual(len(self.open_sessions), 0) @@ -413,11 +417,9 @@ req.form['__login'] = 'toto' req.form['__password'] = 'pouet' self._test_anon_auth_fail(req) - req.form['__login'] = origsession.login - req.form['__password'] = origsession.authinfo['password'] + req.form['__login'] = self.admlogin + req.form['__password'] = self.admpassword self.assertAuthSuccess(req, origsession) - self.assertEqual(req.session.authinfo, - {'password': origsession.authinfo['password']}) self.assertRaises(LogOut, self.app_publish, req, 'logout') self.assertEqual(len(self.open_sessions), 0) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/actions.py --- a/web/views/actions.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/actions.py Fri Feb 11 12:17:47 2011 +0100 @@ -391,6 +391,7 @@ __regid__ = 'siteconfig' title = _('site configuration') order = 10 + category = 'manage' class ManageAction(ManagersAction): diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/authentication.py --- a/web/views/authentication.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/authentication.py Fri Feb 11 12:17:47 2011 +0100 @@ -100,17 +100,13 @@ self.anoninfo = (self.anoninfo[0], {'password': self.anoninfo[1]}) def validate_session(self, req, session): - """check session validity, reconnecting it to the repository if the - associated connection expired in the repository side (hence the - necessity for this method). Return the connected user on success. + """check session validity and return the connected user on success. raise :exc:`InvalidSession` if session is corrupted for a reason or another and should be closed also invoked while going from anonymous to logged in """ - # with this authentication manager, session is actually a dbapi - # connection for retriever in self.authinforetrievers: if retriever.request_has_auth_info(req): login = retriever.revalidate_login(req) @@ -135,8 +131,7 @@ def authenticate(self, req): """authenticate user using connection information found in the request, and return corresponding a :class:`~cubicweb.dbapi.Connection` instance, - as well as login and authentication information dictionary used to open - the connection. + as well as login used to open the connection. raise :exc:`cubicweb.AuthenticationError` if authentication failed (no authentication info found or wrong user/password) @@ -152,8 +147,7 @@ continue # the next one may succeed for retriever_ in self.authinforetrievers: retriever_.authenticated(retriever, req, cnx, login, authinfo) - return cnx, login, authinfo - + return cnx, login # false if no authentication info found, eg this is not an # authentication failure if 'login' in locals(): @@ -162,7 +156,7 @@ if login: cnx = self._authenticate(login, authinfo) cnx.anonymous_connection = True - return cnx, login, authinfo + return cnx, login raise AuthenticationError() def _authenticate(self, login, authinfo): diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/baseviews.py --- a/web/views/baseviews.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/baseviews.py Fri Feb 11 12:17:47 2011 +0100 @@ -31,7 +31,7 @@ from rql import nodes -from logilab.mtconverter import TransformError, xml_escape, xml_escape +from logilab.mtconverter import TransformError, xml_escape from cubicweb import NoSelectableObject, tags from cubicweb.selectors import yes, empty_rset, one_etype_rset, match_kwargs diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/boxes.py --- a/web/views/boxes.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/boxes.py Fri Feb 11 12:17:47 2011 +0100 @@ -208,7 +208,6 @@ raise component.EmptyComponent() self.items = [] - class RsetBox(component.CtxComponent): """helper view class to display an rset in a sidebox""" __select__ = nonempty_rset() & match_kwargs('title', 'vid') diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/calendar.py --- a/web/views/calendar.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/calendar.py Fri Feb 11 12:17:47 2011 +0100 @@ -20,15 +20,28 @@ __docformat__ = "restructuredtext en" _ = unicode -from datetime import datetime, date, timedelta +import copy +from datetime import timedelta from logilab.mtconverter import xml_escape -from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime +from logilab.common.date import todatetime +from cubicweb.utils import json_dumps from cubicweb.interfaces import ICalendarable from cubicweb.selectors import implements, adaptable from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat +# useful constants & functions ################################################ + +ONEDAY = timedelta(1) + +WEEKDAYS = (_("monday"), _("tuesday"), _("wednesday"), _("thursday"), + _("friday"), _("saturday"), _("sunday")) +MONTHNAMES = ( _('january'), _('february'), _('march'), _('april'), _('may'), + _('june'), _('july'), _('august'), _('september'), _('october'), + _('november'), _('december') + ) + class ICalendarableAdapter(EntityAdapter): __needs_bw_compat__ = True @@ -44,21 +57,10 @@ @property @implements_adapter_compat('ICalendarable') def stop(self): - """return stop state""" + """return stop date""" raise NotImplementedError -# useful constants & functions ################################################ - -ONEDAY = timedelta(1) - -WEEKDAYS = (_("monday"), _("tuesday"), _("wednesday"), _("thursday"), - _("friday"), _("saturday"), _("sunday")) -MONTHNAMES = ( _('january'), _('february'), _('march'), _('april'), _('may'), - _('june'), _('july'), _('august'), _('september'), _('october'), - _('november'), _('december') - ) - # Calendar views ############################################################## try: @@ -146,9 +148,6 @@ self.w('
%s'%self._cw.format_date(icalendarable.start or icalendarable.stop)) -class CalendarLargeItemView(CalendarItemView): - __regid__ = 'calendarlargeitem' - class _TaskEntry(object): def __init__(self, task, color, index=0): @@ -170,413 +169,91 @@ return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar() -class OneMonthCal(EntityView): - """At some point, this view will probably replace ampm calendars""" - __regid__ = 'onemonthcal' - __select__ = adaptable('ICalendarable') - - paginable = False - title = _('one month') - - def call(self): - self._cw.add_js('cubicweb.ajax.js') - self._cw.add_css('cubicweb.calendar.css') - # XXX: restrict courses directy with RQL - _today = datetime.today() - - if 'year' in self._cw.form: - year = int(self._cw.form['year']) - else: - year = _today.year - if 'month' in self._cw.form: - month = int(self._cw.form['month']) - else: - month = _today.month - - first_day_of_month = date(year, month, 1) - firstday = first_day_of_month - timedelta(first_day_of_month.weekday()) - if month >= 12: - last_day_of_month = date(year + 1, 1, 1) - timedelta(1) - else: - last_day_of_month = date(year, month + 1, 1) - timedelta(1) - # date range exclude last day so we should at least add one day, hence - # the 7 - lastday = last_day_of_month + timedelta(7 - last_day_of_month.weekday()) - month_dates = list(date_range(firstday, lastday)) - dates = {} - task_max = 0 - for row in xrange(self.cw_rset.rowcount): - task = self.cw_rset.get_entity(row, 0) - if len(self.cw_rset[row]) > 1 and self.cw_rset.description[row][1] == 'CWUser': - user = self.cw_rset.get_entity(row, 1) - else: - user = None - the_dates = [] - icalendarable = task.cw_adapt_to('ICalendarable') - tstart = icalendarable.start - if tstart: - tstart = todate(icalendarable.start) - if tstart > lastday: - continue - the_dates = [tstart] - tstop = icalendarable.stop - if tstop: - tstop = todate(tstop) - if tstop < firstday: - continue - the_dates = [tstop] - if tstart and tstop: - if tstart.isocalendar() == tstop.isocalendar(): - if firstday <= tstart <= lastday: - the_dates = [tstart] - else: - the_dates = date_range(max(tstart, firstday), - min(tstop + ONEDAY, lastday)) - if not the_dates: - continue - - for d in the_dates: - d_tasks = dates.setdefault((d.year, d.month, d.day), {}) - t_users = d_tasks.setdefault(task, set()) - t_users.add( user ) - if len(d_tasks) > task_max: - task_max = len(d_tasks) - - days = [] - nrows = max(3, task_max) - # colors here are class names defined in cubicweb.css - colors = [ "col%x" % i for i in range(12) ] - next_color_index = 0 - - visited_tasks = {} # holds a description of a task - task_colors = {} # remember a color assigned to a task - for mdate in month_dates: - d_tasks = dates.get((mdate.year, mdate.month, mdate.day), {}) - rows = [None] * nrows - # every task that is "visited" for the first time - # require a special treatment, so we put them in - # 'postpone' - postpone = [] - for task in d_tasks: - if task in visited_tasks: - task_descr = visited_tasks[ task ] - rows[task_descr.index] = task_descr - else: - postpone.append(task) - for task in postpone: - # to every 'new' task we must affect a color - # (which must be the same for every user concerned - # by the task) - for i, t in enumerate(rows): - if t is None: - if task in task_colors: - color = task_colors[task] - else: - color = colors[next_color_index] - next_color_index = (next_color_index+1)%len(colors) - task_colors[task] = color - task_descr = _TaskEntry(task, color, i) - rows[i] = task_descr - visited_tasks[task] = task_descr - break - else: - raise RuntimeError("is it possible we got it wrong?") - - days.append( rows ) - - curdate = first_day_of_month - self.w(u'
') - # build schedule - self.w(u'') - prevlink, nextlink = self._prevnext_links(curdate) # XXX - self.w(u'' - u'' % - (xml_escape(prevlink), self._cw._(curdate.strftime('%B').lower()), - curdate.year, xml_escape(nextlink))) - - # output header - self.w(u'' % - tuple(self._cw._(day) for day in WEEKDAYS)) - # build calendar - for mdate, task_rows in zip(month_dates, days): - if mdate.weekday() == 0: - self.w(u'') - self._build_calendar_cell(mdate, task_rows, curdate) - if mdate.weekday() == 6: - self.w(u'') - self.w(u'
<<%s %s>>
%s%s%s%s%s%s%s
') - - def _prevnext_links(self, curdate): - prevdate = curdate - timedelta(31) - nextdate = curdate + timedelta(31) - rql = self.cw_rset.printable_rql() - prevlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql, - vid='onemonthcal', - year=prevdate.year, - month=prevdate.month) - nextlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql, - vid='onemonthcal', - year=nextdate.year, - month=nextdate.month) - return prevlink, nextlink - - def _build_calendar_cell(self, celldate, rows, curdate): - curmonth = curdate.month - classes = "" - if celldate.month != curmonth: - classes += " outOfRange" - if celldate == date.today(): - classes += " today" - self.w(u'' % classes) - self.w(u'
' % classes) - self.w(u'
%s
' % celldate.day) - - if len(self.cw_rset.column_types(0)) == 1: - etype = list(self.cw_rset.column_types(0))[0] - url = self._cw.build_url(vid='creation', etype=etype, - schedule=True, - start=self._cw.format_date(celldate), stop=self._cw.format_date(celldate), - __redirectrql=self.cw_rset.printable_rql(), - __redirectparams=self._cw.build_url_params(year=curdate.year, month=curmonth), - __redirectvid=self.__regid__ - ) - self.w(u'' % (xml_escape(url), self._cw._(u'add'))) - self.w(u' ') - self.w(u'
') - self.w(u'
') - for task_descr in rows: - if task_descr: - task = task_descr.task - self.w(u'
' % task_descr.color) - task.view('calendaritem', w=self.w ) - url = task.absolute_url(vid='edition', - __redirectrql=self.cw_rset.printable_rql(), - __redirectparams=self._cw.build_url_params(year=curdate.year, month=curmonth), - __redirectvid=self.__regid__ - ) - - self.w(u'
' % xml_escape(url)) - task.view('tooltip', w=self.w ) - self.w(u'
') - else: - self.w(u'
') - self.w(u" ") - self.w(u'
') - self.w(u'
') - self.w(u'') - - -class OneWeekCal(EntityView): - """At some point, this view will probably replace ampm calendars""" - __regid__ = 'oneweekcal' +class CalendarView(EntityView): + __regid__ = 'calendar' __select__ = adaptable('ICalendarable') paginable = False - title = _('one week') + title = _('calendar') + + fullcalendar_options = { + 'firstDay': 1, + 'header': {'left': 'prev,next today', + 'center': 'title', + 'right': 'month,agendaWeek,agendaDay', + }, + 'editable': True, + 'defaultView': 'month', + 'timeFormat': {'month': '', + '': 'H:mm'}, + 'firstHour': 8, + 'axisFormat': 'H:mm', + 'columnFormat': {'month': 'dddd', + 'agendaWeek': 'dddd yyyy/M/dd', + 'agendaDay': 'dddd yyyy/M/dd'} + } + def call(self): - self._cw.add_js( ('cubicweb.ajax.js', 'cubicweb.calendar.js') ) - self._cw.add_css('cubicweb.calendar.css') - # XXX: restrict directly with RQL - _today = datetime.today() - if 'year' in self._cw.form: - year = int(self._cw.form['year']) - else: - year = _today.year - if 'week' in self._cw.form: - week = int(self._cw.form['week']) - else: - week = _today.isocalendar()[1] - # week - 1 since we get week number > 0 while we want it to start from 0 - first_day_of_week = todate(strptime('%s-%s-1' % (year, week - 1), '%Y-%U-%w')) - lastday = first_day_of_week + timedelta(6) - firstday = first_day_of_week - dates = [[] for i in range(7)] - task_colors = {} # remember a color assigned to a task - # colors here are class names defined in cubicweb.css - colors = [ "col%x" % i for i in range(12) ] - next_color_index = 0 - done_tasks = set() - for row in xrange(self.cw_rset.rowcount): - task = self.cw_rset.get_entity(row, 0) - if task.eid in done_tasks: - continue - done_tasks.add(task.eid) - the_dates = [] - icalendarable = task.cw_adapt_to('ICalendarable') - tstart = icalendarable.start - tstop = icalendarable.stop - if tstart: - tstart = todate(tstart) - if tstart > lastday: - continue - the_dates = [tstart] - if tstop: - tstop = todate(tstop) - if tstop < firstday: - continue - the_dates = [tstop] - if tstart and tstop: - the_dates = date_range(max(tstart, firstday), - min(tstop + ONEDAY, lastday)) - if not the_dates: - continue + self._cw.demote_to_html() + self._cw.add_css(('fullcalendar.css', 'cubicweb.calendar.css')) + self._cw.add_js(('jquery.ui.js', 'fullcalendar.min.js', 'jquery.qtip.min.js')) + self.add_onload() + # write calendar div to load jquery fullcalendar object + self.w(u'
') - if task not in task_colors: - task_colors[task] = colors[next_color_index] - next_color_index = (next_color_index+1) % len(colors) - - for d in the_dates: - day = d.weekday() - task_descr = _TaskEntry(task, task_colors[task]) - dates[day].append(task_descr) - - self.w(u'
') - # build schedule - self.w(u'') - prevlink, nextlink = self._prevnext_links(first_day_of_week) # XXX - self.w(u'') - self.w(u'' - u'' % - (xml_escape(prevlink), first_day_of_week.year, - self._cw._(u'week'), first_day_of_week.isocalendar()[1], - xml_escape(nextlink))) - - # output header - self.w(u'') - self.w(u'') # column for hours - _today = date.today() - for i, day in enumerate(WEEKDAYS): - wdate = first_day_of_week + timedelta(i) - if wdate.isocalendar() == _today.isocalendar(): - self.w(u'' % (self._cw._(day), self._cw.format_date(wdate))) - else: - self.w(u'' % (self._cw._(day), self._cw.format_date(wdate))) - self.w(u'') - - # build week calendar - self.w(u'') - self.w(u'') - for i, day in enumerate(WEEKDAYS): - wdate = first_day_of_week + timedelta(i) - classes = "" - if wdate.isocalendar() == _today.isocalendar(): - classes = " today" - self.w(u'') - self.w(u'') - self.w(u'
<<%s %s %s>>
%s
%s
%s
%s
') # column for hours - extra = "" - for h in range(8, 20): - self.w(u'
'%extra) - self.w(u'%02d:00'%h) - self.w(u'
') - self.w(u'
' % (classes, day)) - if len(self.cw_rset.column_types(0)) == 1: - etype = list(self.cw_rset.column_types(0))[0] - url = self._cw.build_url(vid='creation', etype=etype, - schedule=True, - __redirectrql=self.cw_rset.printable_rql(), - __redirectparams=self._cw.build_url_params(year=year, week=week), - __redirectvid=self.__regid__ - ) - extra = ' ondblclick="addCalendarItem(event, hmin=8, hmax=20, year=%s, month=%s, day=%s, duration=2, baseurl=\'%s\')"' % ( - wdate.year, wdate.month, wdate.day, xml_escape(url)) - else: - extra = "" - self.w(u'
'% extra) - for h in range(8, 20): - self.w(u'
'%((h-7)*8)) - self.w(u'
') - if dates[i]: - self._build_calendar_cell(wdate, dates[i]) - self.w(u'
') - self.w(u'
') - self.w(u'
') - self.w(u'
 
') - - def _build_calendar_cell(self, date, task_descrs): - inday_tasks = [t for t in task_descrs if t.is_one_day_task() and t.in_working_hours()] - wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()] - inday_tasks.sort(key=lambda t:t.start) - sorted_tasks = [] - for i, t in enumerate(wholeday_tasks): - t.index = i - ncols = len(wholeday_tasks) - while inday_tasks: - t = inday_tasks.pop(0) - for i, c in enumerate(sorted_tasks): - if not c or c[-1].stop <= t.start: - c.append(t) - t.index = i+ncols - break - else: - t.index = len(sorted_tasks) + ncols - sorted_tasks.append([t]) - ncols += len(sorted_tasks) - if ncols == 0: - return - - inday_tasks = [] - for tasklist in sorted_tasks: - inday_tasks += tasklist - width = 100.0/ncols - for task_desc in wholeday_tasks + inday_tasks: - task = task_desc.task - start_hour = 8 - start_min = 0 - stop_hour = 20 - stop_min = 0 - if task_desc.start: - if date < todate(task_desc.start) < date + ONEDAY: - start_hour = max(8, task_desc.start.hour) - start_min = task_desc.start.minute - if task_desc.stop: - if date < todate(task_desc.stop) < date + ONEDAY: - stop_hour = min(20, task_desc.stop.hour) - if stop_hour < 20: - stop_min = task_desc.stop.minute - - height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8) - top = 100.0*(start_hour+start_min/60.0-8)/(20-8) - left = width*task_desc.index - style = "height: %s%%; width: %s%%; top: %s%%; left: %s%%; " % \ - (height, width, top, left) - self.w(u'
' % \ - (task_desc.color, style)) - task.view('calendaritem', dates=False, w=self.w) - url = task.absolute_url(vid='edition', - __redirectrql=self.cw_rset.printable_rql(), - __redirectparams=self._cw.build_url_params(year=date.year, week=date.isocalendar()[1]), - __redirectvid=self.__regid__ - ) - - self.w(u'
' % xml_escape(url)) - task.view('tooltip', w=self.w) - self.w(u'
') - if task_desc.start is None: - self.w(u'
') - self.w(u'
') - self.w(u'
') - self.w(u'
') - self.w(u'
') - self.w(u'
') - self.w(u'end') - self.w(u'
') - self.w(u'
') - self.w(u'
') + def add_onload(self): + fullcalendar_options = self.fullcalendar_options.copy() + fullcalendar_options['events'] = self.get_events() + fullcalendar_options['buttonText'] = {'today': self._cw._('today'), + 'month': self._cw._('month'), + 'week': self._cw._('week'), + 'day': self._cw._('day')} + # js callback to add a tooltip and to put html in event's title + js = """ + var options = %s; + options.eventRender = function(event, $element) { + // add a tooltip for each event + var div = '
'+ event.description+ '
'; + $element.append(div); + // allow to have html tags in event's title + $element.find('span.fc-event-title').html($element.find('span.fc-event-title').text()); + }; + $("#calendar").fullCalendar(options); + """ #" + self._cw.add_onload(js % json_dumps(fullcalendar_options)) - def _prevnext_links(self, curdate): - prevdate = curdate - timedelta(7) - nextdate = curdate + timedelta(7) - rql = self.cw_rset.printable_rql() - prevlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql, - vid='oneweekcal', - year=prevdate.year, - week=prevdate.isocalendar()[1]) - nextlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql, - vid='oneweekcal', - year=nextdate.year, - week=nextdate.isocalendar()[1]) - return prevlink, nextlink + def get_events(self): + events = [] + for entity in self.cw_rset.entities(): + icalendarable = entity.cw_adapt_to('ICalendarable') + event = {'eid': entity.eid, + 'title': entity.view('calendaritem'), + 'url': xml_escape(entity.absolute_url()), + 'className': 'calevent', + 'description': entity.view('tooltip'), + } + start_date = icalendarable.start + if not start_date: + start_date = icalendarable.stop + event['start'] = start_date.strftime('%Y-%m-%dT%H:%M') + event['allDay'] = True + if icalendarable.stop: + event['end'] = icalendarable.stop.strftime('%Y-%m-%dT%H:%M') + event['allDay'] = False + events.append(event) + return events + +class OneMonthCal(CalendarView): + __regid__ = 'onemonthcal' + + title = _('one month') + +class OneWeekCal(CalendarView): + __regid__ = 'oneweekcal' + + title = _('one week') + fullcalendar_options = CalendarView.fullcalendar_options.copy() + fullcalendar_options['defaultView'] = 'agendaWeek' diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/cwsources.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/views/cwsources.py Fri Feb 11 12:17:47 2011 +0100 @@ -0,0 +1,229 @@ +# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""Specific views for data sources and related entities (eg CWSource, +CWSourceHostConfig, CWSourceSchemaConfig). +""" + +__docformat__ = "restructuredtext en" +_ = unicode + +from itertools import repeat, chain + +from cubicweb.selectors import is_instance, score_entity, match_user_groups +from cubicweb.view import EntityView, StartupView +from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name +from cubicweb.web import uicfg +from cubicweb.web.views import tabs, actions + +# source primary views ######################################################### + +_pvs = uicfg.primaryview_section +_pvs.tag_object_of(('*', 'cw_for_source', 'CWSource'), 'hidden') + + +class CWSourcePrimaryView(tabs.TabbedPrimaryView): + __select__ = is_instance('CWSource') + tabs = [_('cwsource-main'), _('cwsource-mapping')] + default_tab = 'cwsource-main' + + +class CWSourceMainTab(tabs.PrimaryTab): + __regid__ = 'cwsource-main' + __select__ = tabs.PrimaryTab.__select__ & is_instance('CWSource') + + +MAPPED_SOURCE_TYPES = set( ('pyrorql', 'datafeed') ) + +class CWSourceMappingTab(EntityView): + __regid__ = 'cwsource-mapping' + __select__ = (tabs.PrimaryTab.__select__ & is_instance('CWSource') + & match_user_groups('managers') + & score_entity(lambda x:x.type in MAPPED_SOURCE_TYPES)) + + def entity_call(self, entity): + _ = self._cw._ + self.w('

%s

' % _('Entity and relation supported by this source')) + eschema = self._cw.vreg.schema.eschema('CWSourceSchemaConfig') + if eschema.has_perm(self._cw, 'add'): + self.w(u'%s' % ( + self._cw.build_url('add/%s' % eschema), + self._cw._('add a CWSourceSchemaConfig'))) + self.w(u'
') + rset = self._cw.execute( + 'Any X, SCH, XO ORDERBY ET WHERE X options XO, X cw_for_source S, S eid %(s)s, ' + 'X cw_schema SCH, SCH is ET', {'s': entity.eid}) + self.wview('table', rset, 'noresult') + # self.w('

%s

' % _('Relations that should not be crossed')) + # self.w('

%s

' % _( + # 'By default, when a relation is not supported by a source, it is ' + # 'supposed that a local relation may point to an entity from the ' + # 'external source. Relations listed here won\'t have this ' + # '"crossing" behaviour.')) + # self.wview('list', entity.related('cw_dont_cross'), 'noresult') + # self.w('

%s

' % _('Relations that can be crossed')) + # self.w('

%s

' % _( + # 'By default, when a relation is supported by a source, it is ' + # 'supposed that a local relation can\'t point to an entity from the ' + # 'external source. Relations listed here may have this ' + # '"crossing" behaviour anyway.')) + # self.wview('list', entity.related('cw_may_cross'), 'noresult') + checker = MAPPING_CHECKERS.get(entity.type, MappingChecker)(entity) + checker.check() + if (checker.errors or checker.warnings or checker.infos): + self.w('

%s

' % _('Detected problems')) + errors = zip(repeat(_('error')), checker.errors) + warnings = zip(repeat(_('warning')), checker.warnings) + infos = zip(repeat(_('warning')), checker.infos) + self.wview('pyvaltable', pyvalue=chain(errors, warnings, infos)) + + +class MappingChecker(object): + def __init__(self, cwsource): + self.cwsource = cwsource + self.errors = [] + self.warnings = [] + self.infos = [] + self.schema = cwsource._cw.vreg.schema + + def init(self): + # supported entity types + self.sentities = set() + # supported relations + self.srelations = {} + # avoid duplicated messages + self.seen = set() + # first get mapping as dict/sets + for schemacfg in self.cwsource.reverse_cw_for_source: + self.init_schemacfg(schemacfg) + + def init_schemacfg(self, schemacfg): + cwerschema = schemacfg.schema + if cwerschema.__regid__ == 'CWEType': + self.sentities.add(cwerschema.name) + elif cwerschema.__regid__ == 'CWRType': + assert not cwerschema.name in self.srelations + self.srelations[cwerschema.name] = None + else: # CWAttribute/CWRelation + self.srelations.setdefault(cwerschema.rtype.name, []).append( + (cwerschema.stype.name, cwerschema.otype.name) ) + + def check(self): + self.init() + error = self.errors.append + warning = self.warnings.append + info = self.infos.append + for etype in self.sentities: + eschema = self.schema[etype] + for rschema, ttypes, role in eschema.relation_definitions(): + if rschema in META_RTYPES: + continue + ttypes = [ttype for ttype in ttypes if ttype in self.sentities] + if not rschema in self.srelations: + for ttype in ttypes: + rdef = rschema.role_rdef(etype, ttype, role) + self.seen.add(rdef) + if rdef.role_cardinality(role) in '1+': + error(_('relation %(type)s with %(etype)s as %(role)s ' + 'and target type %(target)s is mandatory but ' + 'not supported') % + {'rtype': rschema, 'etype': etype, 'role': role, + 'target': ttype}) + elif ttype in self.sentities: + warning(_('%s could be supported') % rdef) + elif not ttypes: + warning(_('relation %(rtype)s with %(etype)s as %(role)s is ' + 'supported but no target type supported') % + {'rtype': rschema, 'role': role, 'etype': etype}) + for rtype in self.srelations: + rschema = self.schema[rtype] + for subj, obj in rschema.rdefs: + if subj in self.sentities and obj in self.sentities: + break + else: + error(_('relation %s is supported but none if its definitions ' + 'matches supported entities') % rtype) + self.custom_check() + + def custom_check(self): + pass + + +class PyroRQLMappingChecker(MappingChecker): + """pyrorql source mapping checker""" + + def init(self): + self.dontcross = set() + self.maycross = set() + super(PyroRQLMappingChecker, self).init() + + def init_schemacfg(self, schemacfg): + options = schemacfg.options or () + if 'dontcross' in options: + self.dontcross.add(schemacfg.schema.name) + else: + super(PyroRQLMappingChecker, self).init_schemacfg(schemacfg) + if 'maycross' in options: + self.maycross.add(schemacfg.schema.name) + + def custom_check(self): + error = self.errors.append + info = self.infos.append + for etype in self.sentities: + eschema = self.schema[etype] + for rschema, ttypes, role in eschema.relation_definitions(): + if rschema in META_RTYPES: + continue + if not rschema in self.srelations: + if rschema not in self.dontcross: + if role == 'subject' and rschema.inlined: + error(_('inlined relation %(rtype)s of %(etype)s ' + 'should be supported') % + {'rtype': rschema, 'etype': etype}) + elif (rschema not in self.seen and rschema not in self.maycross): + info(_('you may want to specify something for %s') % + rschema) + self.seen.add(rschema) + elif rschema in self.maycross and rschema.inlined: + error(_('you should un-inline relation %s which is ' + 'supported and may be crossed ') % rschema) + +MAPPING_CHECKERS = { + 'pyrorql': PyroRQLMappingChecker, + } + +# sources management view ###################################################### + +class ManageSourcesAction(actions.ManagersAction): + __regid__ = 'cwsource' + title = _('data sources') + category = 'manage' + +class CWSourceManagementView(StartupView): + __regid__ = 'cw.source-management' + rql = ('Any S, ST, SN ORDERBY SN WHERE S is CWSource, S name SN, S type ST') + title = _('data sources management') + + def call(self, **kwargs): + self.w('

%s

' % self._cw._(self.title)) + eschema = self._cw.vreg.schema.eschema('CWSource') + if eschema.has_perm(self._cw, 'add'): + self.w(u'%s' % ( + self._cw.build_url('add/%s' % eschema), + self._cw._('add a CWSource'))) + self.w(u'
') + self.wview('table', self._cw.execute(self.rql), displaycols=range(2)) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/cwuser.py --- a/web/views/cwuser.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/cwuser.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -24,10 +24,11 @@ from logilab.mtconverter import xml_escape +from cubicweb.schema import display_name from cubicweb.selectors import one_line_rset, is_instance, match_user_groups -from cubicweb.view import EntityView +from cubicweb.view import EntityView, StartupView from cubicweb.web import action, uicfg, formwidgets -from cubicweb.web.views import tabs +from cubicweb.web.views import tabs, tableview, actions _pvs = uicfg.primaryview_section _pvs.tag_attribute(('CWUser', 'login'), 'hidden') @@ -157,3 +158,50 @@ entity = self.cw_rset.complete_entity(row, col) self.w(u'%s' % ( entity.absolute_url(), entity.name, entity.printable_value('name'))) + + +# user / groups management views ############################################### + +class ManageUsersAction(actions.ManagersAction): + __regid__ = 'cwuser' # see rewrite rule /cwuser + title = _('users and groups') + category = 'manage' + + +class CWUserManagementView(StartupView): + __regid__ = 'cw.user-management' + rql = ('Any U, F, S, U, L ORDERBY L WHERE U is CWUser, U login L, U firstname F, U surname S') + title = _('users and groups management') + + def call(self, **kwargs): + self.w('

%s

' % self._cw._(self.title)) + for etype in ('CWUser', 'CWGroup'): + eschema = self._cw.vreg.schema.eschema(etype) + if eschema.has_perm(self._cw, 'add'): + self.w(u'%s' % ( + self._cw.build_url('add/%s' % eschema), + self._cw._('add a %s' % etype).capitalize())) + self.w(u'
') + self.wview('cw.user-table', self._cw.execute(self.rql)) + + +class CWUserTable(tableview.EditableTableView): + __regid__ = 'cw.user-table' + __select__ = is_instance('CWUser') + + def call(self, **kwargs): + headers = (display_name(self._cw, 'CWUser', 'plural'), + self._cw._('firstname'), self._cw._('surname'), + display_name(self._cw, 'CWGroup', 'plural')) + super(CWUserTable, self).call( + paginate=True, cellvids={3: 'cw.user-table.group-cell'}, + headers=headers, **kwargs) + + +class CWUserGroupCell(EntityView): + __regid__ = 'cw.user-table.group-cell' + __select__ = is_instance('CWUser') + + def cell_call(self, row, col, **kwargs): + entity = self.cw_rset.get_entity(row, col) + self.w(entity.view('reledit', rtype='in_group', role='subject')) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/debug.py --- a/web/views/debug.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/debug.py Fri Feb 11 12:17:47 2011 +0100 @@ -27,7 +27,7 @@ from cubicweb import BadConnectionId from cubicweb.selectors import none_rset, match_user_groups from cubicweb.view import StartupView -from cubicweb.web.views import actions +from cubicweb.web.views import actions, tabs def dict_to_html(w, dict): # XHTML doesn't allow emtpy
    nodes @@ -39,12 +39,24 @@ w(u'
') - class SiteInfoAction(actions.ManagersAction): __regid__ = 'siteinfo' __select__ = match_user_groups('users','managers') - title = _('info') - order = 30 + title = _('siteinfo') + category = 'manage' + order = 1000 + + +class SiteInfoView(tabs.TabsMixin, StartupView): + __regid__ = 'siteinfo' + title = _('Site information') + tabs = [_('info'), _('registry'), _('gc')] + default_tab = 'info' + + def call(self, **kwargs): + """The default view representing the instance's management""" + self.w(u'

%s

' % self._cw._(self.title)) + self.render_tabs(self.tabs, self.default_tab) class ProcessInformationView(StartupView): @@ -61,7 +73,7 @@ _ = req._ w = self.w # generic instance information - w(u'

%s

' % _('Instance')) + w(u'

%s

' % _('Instance')) w(u'') w(u'' % ( _('config type'), self._cw.vreg.config.name)) @@ -82,7 +94,7 @@ w(u'
%s%s
') # repository information repo = req.vreg.config.repository(None) - w(u'

%s

' % _('Repository')) + w(u'

%s

' % _('Repository')) w(u'

%s

' % _('resources usage')) w(u'') stats = repo.stats() @@ -107,7 +119,7 @@ else: w(u'

%s

' % _('no repository sessions found')) # web server information - w(u'

%s

' % _('Web server')) + w(u'

%s

' % _('Web server')) w(u'
') w(u'' % ( _('base url'), req.base_url())) @@ -146,7 +158,7 @@ cache_max_age = 0 def call(self, **kwargs): - self.w(u'

%s

' % self._cw._("Registry's content")) + self.w(u'

%s

' % self._cw._("Registry's content")) keys = sorted(self._cw.vreg) url = xml_escape(self._cw.url()) self.w(u'

%s

\n' % ' - '.join('%s' @@ -154,7 +166,7 @@ for key in keys: if key in ('boxes', 'contentnavigation'): # those are bw compat registries continue - self.w(u'

%s

' % (key, key)) + self.w(u'

%s

' % (key, key)) if self._cw.vreg[key]: values = sorted(self._cw.vreg[key].iteritems()) self.wview('pyvaltable', pyvalue=[(key, xml_escape(repr(val))) @@ -186,7 +198,7 @@ lookupclasses += (InternalSession, Session) except ImportError: pass # no server part installed - self.w(u'

%s

' % _('Garbage collection information')) + self.w(u'

%s

' % _('Garbage collection information')) counters, ocounters, garbage = gc_info(lookupclasses, viewreferrersclasses=()) self.w(u'

%s

' % self._cw._('Looked up classes')) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/old_calendar.py --- a/web/views/old_calendar.py Fri Feb 11 11:37:10 2011 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,575 +0,0 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of CubicWeb. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""html calendar views""" - -__docformat__ = "restructuredtext en" -_ = unicode - -from datetime import date, time, timedelta - -from logilab.mtconverter import xml_escape -from logilab.common.date import (ONEDAY, ONEWEEK, days_in_month, previous_month, - next_month, first_day, last_day, date_range) - -from cubicweb.interfaces import ICalendarViews -from cubicweb.selectors import implements, adaptable -from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat - -class ICalendarViewsAdapter(EntityAdapter): - """calendar views interface""" - __needs_bw_compat__ = True - __regid__ = 'ICalendarViews' - __select__ = implements(ICalendarViews, warn=False) # XXX for bw compat, should be abstract - - @implements_adapter_compat('ICalendarViews') - def matching_dates(self, begin, end): - """ - :param begin: day considered as begin of the range (`DateTime`) - :param end: day considered as end of the range (`DateTime`) - - :return: - a list of dates (`DateTime`) in the range [`begin`, `end`] on which - this entity apply - """ - raise NotImplementedError - - -# used by i18n tools -WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"), - _("friday"), _("saturday"), _("sunday")] -MONTHNAMES = [ _('january'), _('february'), _('march'), _('april'), _('may'), - _('june'), _('july'), _('august'), _('september'), _('october'), - _('november'), _('december') - ] - -class _CalendarView(EntityView): - """base calendar view containing helpful methods to build calendar views""" - __select__ = adaptable('ICalendarViews') - paginable = False - - # Navigation building methods / views #################################### - - PREV = u'<<  <' - NEXT = u'>  >>' - NAV_HEADER = u"""
%s%s
- -
%s%s
-""" % (PREV, NEXT) - - def nav_header(self, date, smallshift=3, bigshift=9): - """prints shortcut links to go to previous/next steps (month|week)""" - prev1 = previous_month(date, smallshift) - next1 = next_month(date, smallshift) - prev2 = previous_month(date, bigshift) - next2 = next_month(date, bigshift) - rql = self.cw_rset.printable_rql() - return self.NAV_HEADER % ( - xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev2.year, - month=prev2.month)), - xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev1.year, - month=prev1.month)), - xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next1.year, - month=next1.month)), - xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next2.year, - month=next2.month))) - - - # Calendar building methods ############################################## - - def build_calendars(self, schedule, begin, end): - """build several HTML calendars at once, one for each month - between begin and end - """ - return [self.build_calendar(schedule, date) - for date in date_range(begin, end, incmonth=1)] - - def build_calendar(self, schedule, first_day): - """method responsible for building *one* HTML calendar""" - # FIXME iterates between [first_day-first_day.day_of_week ; - # last_day+6-last_day.day_of_week] - umonth = self._cw.format_date(first_day, '%B %Y') # localized month name - rows = [] - current_row = [NO_CELL] * first_day.weekday() - for daynum in xrange(0, days_in_month(first_day)): - # build cell day - day = first_day + timedelta(daynum) - events = schedule.get(day) - if events: - events = [u'\n'.join(event) for event in events.values()] - current_row.append(CELL % (daynum+1, '\n'.join(events))) - else: - current_row.append(EMPTY_CELL % (daynum+1)) - # store & reset current row on Sundays - if day.weekday() == 6: - rows.append(u'%s%s' % (WEEKNUM_CELL % day.isocalendar()[1], ''.join(current_row))) - current_row = [] - current_row.extend([NO_CELL] * (6-day.weekday())) - rql = self.cw_rset.printable_rql() - if day.weekday() != 6: - rows.append(u'%s%s' % (WEEKNUM_CELL % day.isocalendar()[1], ''.join(current_row))) - url = self._cw.build_url(rql=rql, vid='calendarmonth', - year=first_day.year, month=first_day.month) - monthlink = u'%s' % (xml_escape(url), umonth) - return CALENDAR(self._cw) % (monthlink, '\n'.join(rows)) - - def _mk_schedule(self, begin, end, itemvid='calendaritem'): - """private method that gathers information from resultset - and builds calendars according to it - - :param begin: begin of date range - :param end: end of date rangs - :param itemvid: which view to call to render elements in cells - - returns { day1 : { hour : [views] }, - day2 : { hour : [views] } ... } - """ - # put this here since all sub views are calling this method - self._cw.add_css('cubicweb.calendar.css') - schedule = {} - for row in xrange(len(self.cw_rset.rows)): - entity = self.cw_rset.get_entity(row, 0) - infos = u'
' - infos += self._cw.view(itemvid, self.cw_rset, row=row) - infos += u'
' - for date_ in entity.cw_adapt_to('ICalendarViews').matching_dates(begin, end): - day = date(date_.year, date_.month, date_.day) - try: - dt = time(date_.hour, date_.minute, date_.second) - except AttributeError: - # date instance - dt = time(0, 0, 0) - schedule.setdefault(day, {}) - schedule[day].setdefault(dt, []).append(infos) - return schedule - - - @staticmethod - def get_date_range(day, shift=4): - """returns a couple (begin, end) - - is the first day of current_month - shift - is the last day of current_month + (shift+1) - """ - begin = first_day(previous_month(day, shift)) - end = last_day(next_month(day, shift)) - return begin, end - - def _build_ampm_cells(self, events): - """create a view without any hourly details. - - :param events: dictionnary with all events classified by hours - """ - # split events according am/pm - am_events = [event for e_time, e_list in events.iteritems() - if 0 <= e_time.hour < 12 - for event in e_list] - pm_events = [event for e_time, e_list in events.iteritems() - if 12 <= e_time.hour < 24 - for event in e_list] - # format each am/pm cell - if am_events: - am_content = AMPM_CONTENT % ("amCell", "am", '\n'.join(am_events)) - else: - am_content = AMPM_EMPTY % ("amCell", "am") - if pm_events: - pm_content = AMPM_CONTENT % ("pmCell", "pm", '\n'.join(pm_events)) - else: - pm_content = AMPM_EMPTY % ("pmCell", "pm") - return am_content, pm_content - - - -class YearCalendarView(_CalendarView): - __regid__ = 'calendaryear' - title = _('calendar (year)') - - def call(self, year=None, month=None): - """this view renders a 3x3 calendars' table""" - year = year or int(self._cw.form.get('year', date.today().year)) - month = month or int(self._cw.form.get('month', date.today().month)) - center_date = date(year, month, 1) - begin, end = self.get_date_range(day=center_date) - schedule = self._mk_schedule(begin, end) - self.w(self.nav_header(center_date)) - calendars = tuple(self.build_calendars(schedule, begin, end)) - self.w(SMALL_CALENDARS_PAGE % calendars) - - -class SemesterCalendarView(_CalendarView): - """this view renders three semesters as three rows of six columns, - one column per month - """ - __regid__ = 'calendarsemester' - title = _('calendar (semester)') - - def call(self, year=None, month=None): - year = year or int(self._cw.form.get('year', date.today().year)) - month = month or int(self._cw.form.get('month', date.today().month)) - begin = previous_month(date(year, month, 1), 2) - end = next_month(date(year, month, 1), 3) - schedule = self._mk_schedule(begin, end) - self.w(self.nav_header(date(year, month, 1), 1, 6)) - self.w(u'') - self.build_calendars(schedule, begin, end) - self.w(u'
') - self.w(self.nav_header(date(year, month, 1), 1, 6)) - - def build_calendars(self, schedule, begin, end): - self.w(u'') - rql = self.cw_rset.printable_rql() - for cur_month in date_range(begin, end, incmonth=1): - umonth = u'%s %s' % (self._cw.format_date(cur_month, '%B'), cur_month.year) - url = self._cw.build_url(rql=rql, vid=self.__regid__, - year=cur_month.year, month=cur_month.month) - self.w(u'%s' % (xml_escape(url), - umonth)) - self.w(u'') - _ = self._cw._ - for day_num in xrange(31): - self.w(u'') - for cur_month in date_range(begin, end, incmonth=1): - if day_num >= days_in_month(cur_month): - self.w(u'%s%s' % (NO_CELL, NO_CELL)) - else: - day = date(cur_month.year, cur_month.month, day_num+1) - events = schedule.get(day) - self.w(u'%s %s\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1)) - self.format_day_events(day, events) - self.w(u'') - - def format_day_events(self, day, events): - if events: - events = ['\n'.join(event) for event in events.values()] - self.w(WEEK_CELL % '\n'.join(events)) - else: - self.w(WEEK_EMPTY_CELL) - - -class MonthCalendarView(_CalendarView): - """this view renders a 3x1 calendars' table""" - __regid__ = 'calendarmonth' - title = _('calendar (month)') - - def call(self, year=None, month=None): - year = year or int(self._cw.form.get('year', date.today().year)) - month = month or int(self._cw.form.get('month', date.today().month)) - center_date = date(year, month, 1) - begin, end = self.get_date_range(day=center_date, shift=1) - schedule = self._mk_schedule(begin, end) - calendars = self.build_calendars(schedule, begin, end) - self.w(self.nav_header(center_date, 1, 3)) - self.w(BIG_CALENDARS_PAGE % tuple(calendars)) - self.w(self.nav_header(center_date, 1, 3)) - - -class WeekCalendarView(_CalendarView): - """this view renders a calendar for week events""" - __regid__ = 'calendarweek' - title = _('calendar (week)') - - def call(self, year=None, week=None): - year = year or int(self._cw.form.get('year', date.today().year)) - week = week or int(self._cw.form.get('week', date.today().isocalendar()[1])) - day0 = date(year, 1, 1) - first_day_of_week = day0 - day0.weekday()*ONEDAY + ONEWEEK - begin, end = first_day_of_week- ONEWEEK, first_day_of_week + 2*ONEWEEK - schedule = self._mk_schedule(begin, end, itemvid='calendarlargeitem') - self.w(self.nav_header(first_day_of_week)) - self.w(u'') - _weeks = [(first_day_of_week-ONEWEEK, first_day_of_week-ONEDAY), - (first_day_of_week, first_day_of_week+6*ONEDAY), - (first_day_of_week+ONEWEEK, first_day_of_week+13*ONEDAY)] - self.build_calendar(schedule, _weeks) - self.w(u'
') - self.w(self.nav_header(first_day_of_week)) - - def build_calendar(self, schedule, weeks): - rql = self.cw_rset.printable_rql() - _ = self._cw._ - for monday, sunday in weeks: - umonth = self._cw.format_date(monday, '%B %Y') - url = self._cw.build_url(rql=rql, vid='calendarmonth', - year=monday.year, month=monday.month) - monthlink = '%s' % (xml_escape(url), umonth) - self.w(u'%s %s (%s)' \ - % (_('week'), monday.isocalendar()[1], monthlink)) - for day in date_range(monday, sunday+ONEDAY): - self.w(u'') - self.w(u'%s' % _(WEEKDAYS[day.weekday()])) - self.w(u'%s' % (day.strftime('%Y-%m-%d'))) - events = schedule.get(day) - if events: - events = ['\n'.join(event) for event in events.values()] - self.w(WEEK_CELL % '\n'.join(events)) - else: - self.w(WEEK_EMPTY_CELL) - self.w(u'') - - def nav_header(self, date, smallshift=1, bigshift=3): - """prints shortcut links to go to previous/next steps (month|week)""" - prev1 = date - ONEWEEK * smallshift - prev2 = date - ONEWEEK * bigshift - next1 = date + ONEWEEK * smallshift - next2 = date + ONEWEEK * bigshift - rql = self.cw_rset.printable_rql() - return self.NAV_HEADER % ( - xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev2.year, week=prev2.isocalendar()[1])), - xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev1.year, week=prev1.isocalendar()[1])), - xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next1.year, week=next1.isocalendar()[1])), - xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next2.year, week=next2.isocalendar()[1]))) - - - -class AMPMYearCalendarView(YearCalendarView): - __regid__ = 'ampmcalendaryear' - title = _('am/pm calendar (year)') - - def build_calendar(self, schedule, first_day): - """method responsible for building *one* HTML calendar""" - umonth = self._cw.format_date(first_day, '%B %Y') # localized month name - rows = [] # each row is: (am,pm), (am,pm) ... week_title - current_row = [(NO_CELL, NO_CELL, NO_CELL)] * first_day.weekday() - rql = self.cw_rset.printable_rql() - for daynum in xrange(0, days_in_month(first_day)): - # build cells day - day = first_day + timedelta(daynum) - events = schedule.get(day) - if events: - current_row.append((AMPM_DAY % (daynum+1),) + self._build_ampm_cells(events)) - else: - current_row.append((AMPM_DAY % (daynum+1), - AMPM_EMPTY % ("amCell", "am"), - AMPM_EMPTY % ("pmCell", "pm"))) - # store & reset current row on Sundays - if day.weekday() == 6: - url = self._cw.build_url(rql=rql, vid='ampmcalendarweek', - year=day.year, week=day.isocalendar()[1]) - weeklink = '%s' % (xml_escape(url), - day.isocalendar()[1]) - current_row.append(WEEKNUM_CELL % weeklink) - rows.append(current_row) - current_row = [] - current_row.extend([(NO_CELL, NO_CELL, NO_CELL)] * (6-day.weekday())) - url = self._cw.build_url(rql=rql, vid='ampmcalendarweek', - year=day.year, week=day.isocalendar()[1]) - weeklink = '%s' % (xml_escape(url), day.isocalendar()[1]) - current_row.append(WEEKNUM_CELL % weeklink) - rows.append(current_row) - # build two rows for each week: am & pm - formatted_rows = [] - for row in rows: - week_title = row.pop() - day_row = [day for day, am, pm in row] - am_row = [am for day, am, pm in row] - pm_row = [pm for day, am, pm in row] - formatted_rows.append('%s%s'% (week_title, '\n'.join(day_row))) - formatted_rows.append(' %s'% '\n'.join(am_row)) - formatted_rows.append(' %s'% '\n'.join(pm_row)) - # tigh everything together - url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth', - year=first_day.year, month=first_day.month) - monthlink = '%s' % (xml_escape(url), umonth) - return CALENDAR(self._cw) % (monthlink, '\n'.join(formatted_rows)) - - - -class AMPMSemesterCalendarView(SemesterCalendarView): - """this view renders a 3x1 calendars' table""" - __regid__ = 'ampmcalendarsemester' - title = _('am/pm calendar (semester)') - - def build_calendars(self, schedule, begin, end): - self.w(u'') - rql = self.cw_rset.printable_rql() - for cur_month in date_range(begin, end, incmonth=1): - umonth = u'%s %s' % (self._cw.format_date(cur_month, '%B'), cur_month.year) - url = self._cw.build_url(rql=rql, vid=self.__regid__, - year=cur_month.year, month=cur_month.month) - self.w(u'%s' % (xml_escape(url), - umonth)) - self.w(u'') - _ = self._cw._ - for day_num in xrange(31): - self.w(u'') - for cur_month in date_range(begin, end, incmonth=1): - if day_num >= days_in_month(cur_month): - self.w(u'%s%s%s' % (NO_CELL, NO_CELL, NO_CELL)) - else: - day = date(cur_month.year, cur_month.month, day_num+1) - events = schedule.get(day) - self.w(u'%s %s\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), - day_num+1)) - self.format_day_events(day, events) - self.w(u'') - - def format_day_events(self, day, events): - if events: - self.w(u'\n'.join(self._build_ampm_cells(events))) - else: - self.w(u'%s %s'% (AMPM_EMPTY % ("amCell", "am"), - AMPM_EMPTY % ("pmCell", "pm"))) - - -class AMPMMonthCalendarView(MonthCalendarView): - """this view renders a 3x1 calendars' table""" - __regid__ = 'ampmcalendarmonth' - title = _('am/pm calendar (month)') - - def build_calendar(self, schedule, first_day): - """method responsible for building *one* HTML calendar""" - umonth = self._cw.format_date(first_day, '%B %Y') # localized month name - rows = [] # each row is: (am,pm), (am,pm) ... week_title - current_row = [(NO_CELL, NO_CELL, NO_CELL)] * first_day.weekday() - rql = self.cw_rset.printable_rql() - for daynum in xrange(0, days_in_month(first_day)): - # build cells day - day = first_day + timedelta(daynum) - events = schedule.get(day) - if events: - current_row.append((AMPM_DAY % (daynum+1),) + self._build_ampm_cells(events)) - else: - current_row.append((AMPM_DAY % (daynum+1), - AMPM_EMPTY % ("amCell", "am"), - AMPM_EMPTY % ("pmCell", "pm"))) - # store & reset current row on Sundays - if day.weekday() == 6: - url = self._cw.build_url(rql=rql, vid='ampmcalendarweek', - year=day.year, week=day.isocalendar()[1]) - weeklink = '%s' % (xml_escape(url), - day.isocalendar()[1]) - current_row.append(WEEKNUM_CELL % weeklink) - rows.append(current_row) - current_row = [] - current_row.extend([(NO_CELL, NO_CELL, NO_CELL)] * (6-day.weekday())) - url = self._cw.build_url(rql=rql, vid='ampmcalendarweek', - year=day.year, week=day.isocalendar()[1]) - weeklink = '%s' % (xml_escape(url), - day.isocalendar()[1]) - current_row.append(WEEKNUM_CELL % weeklink) - rows.append(current_row) - # build two rows for each week: am & pm - formatted_rows = [] - for row in rows: - week_title = row.pop() - day_row = [day for day, am, pm in row] - am_row = [am for day, am, pm in row] - pm_row = [pm for day, am, pm in row] - formatted_rows.append('%s%s'% (week_title, '\n'.join(day_row))) - formatted_rows.append(' %s'% '\n'.join(am_row)) - formatted_rows.append(' %s'% '\n'.join(pm_row)) - # tigh everything together - url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth', - year=first_day.year, month=first_day.month) - monthlink = '%s' % (xml_escape(url), - umonth) - return CALENDAR(self._cw) % (monthlink, '\n'.join(formatted_rows)) - - - -class AMPMWeekCalendarView(WeekCalendarView): - """this view renders a 3x1 calendars' table""" - __regid__ = 'ampmcalendarweek' - title = _('am/pm calendar (week)') - - def build_calendar(self, schedule, weeks): - rql = self.cw_rset.printable_rql() - w = self.w - _ = self._cw._ - for monday, sunday in weeks: - umonth = self._cw.format_date(monday, '%B %Y') - url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth', - year=monday.year, month=monday.month) - monthlink = '%s' % (xml_escape(url), umonth) - w(u'%s' % ( - WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink))) - w(u'%s '% _(u'Date')) - for day in date_range(monday, sunday+ONEDAY): - events = schedule.get(day) - style = day.weekday() % 2 and "even" or "odd" - w(u'' % style) - if events: - hours = events.keys() - hours.sort() - w(AMPM_DAYWEEK % ( - len(hours), _(WEEKDAYS[day.weekday()]), - self._cw.format_date(day))) - w(AMPM_WEEK_CELL % ( - hours[0].hour, hours[0].minute, - '\n'.join(events[hours[0]]))) - w(u'') - for hour in hours[1:]: - w(u'%s'% ( - style, AMPM_WEEK_CELL % (hour.hour, hour.minute, - '\n'.join(events[hour])))) - else: - w(AMPM_DAYWEEK_EMPTY % ( - _(WEEKDAYS[day.weekday()]), - self._cw.format_date(day))) - w(WEEK_EMPTY_CELL) - w(u'') - - -SMALL_CALENDARS_PAGE = u""" - - - -
%s%s%s
%s%s%s
%s%s%s
-""" - -BIG_CALENDARS_PAGE = u""" - - - -
%s
%s
%s
-""" - -WEEKNUM_CELL = u'%s' - -def CALENDAR(req): - _ = req._ - WEEKNUM_HEADER = u'%s' % _('week') - CAL_HEADER = WEEKNUM_HEADER + u' \n'.join([u'%s' % _(day)[0].upper() - for day in WEEKDAYS]) - return u""" - - - %s - -%%s -
%%s
-""" % (CAL_HEADER,) - - -DAY_TEMPLATE = """%(daylabel)s%(dmydate)s%(dayschedule)s -""" - -NO_CELL = u'' -EMPTY_CELL = u'%s' -CELL = u'%s
%s
' - -AMPM_DAY = u'%d' -AMPM_EMPTY = u'%s' -AMPM_CONTENT = u'%s
%s
' - -WEEK_TITLE = u'%s %s (%s)' -WEEK_EMPTY_CELL = u' ' -WEEK_CELL = u'
%s
' - -AMPM_DAYWEEK_EMPTY = u'%s %s' -AMPM_DAYWEEK = u'%s %s' -AMPM_WEEK_CELL = u'
%02d:%02d - %s
' diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/primary.py --- a/web/views/primary.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/primary.py Fri Feb 11 12:17:47 2011 +0100 @@ -52,10 +52,8 @@ """ return [] - def cell_call(self, row, col): - self.cw_row = row - self.cw_col = col - entity = self.cw_rset.complete_entity(row, col) + def entity_call(self, entity): + entity.complete() self.render_entity(entity) def render_entity(self, entity): diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/reledit.py --- a/web/views/reledit.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/reledit.py Fri Feb 11 12:17:47 2011 +0100 @@ -81,7 +81,7 @@ self._cw.add_js(('cubicweb.reledit.js', 'cubicweb.edition.js', 'cubicweb.ajax.js')) entity = self.cw_rset.get_entity(row, col) rschema = self._cw.vreg.schema[rtype] - self._rules = rctrl.etype_get(entity.e_schema, rschema, role, '*') + self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*') if rvid is not None or default_value is not None: warn('[3.9] specifying rvid/default_value on select is deprecated, ' 'reledit_ctrl rtag to control this' % self, DeprecationWarning) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/schema.py --- a/web/views/schema.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/schema.py Fri Feb 11 12:17:47 2011 +0100 @@ -145,7 +145,7 @@ __regid__ = 'schema' title = _('instance schema') tabs = [_('schema-diagram'), _('schema-entity-types'), - _('schema-relation-types'), _('schema-security')] + _('schema-relation-types')] default_tab = 'schema-diagram' def call(self): @@ -183,110 +183,6 @@ self.wview('table', self._cw.execute( 'Any X ORDERBY N WHERE X is CWRType, X name N, X final FALSE')) - -class SchemaPermissionsTab(SecurityViewMixIn, StartupView): - __regid__ = 'schema-security' - __select__ = StartupView.__select__ & match_user_groups('managers') - - def call(self, display_relations=True): - skiptypes = skip_types(self._cw) - schema = self._cw.vreg.schema - # compute entities - entities = sorted(eschema for eschema in schema.entities() - if not (eschema.final or eschema in skiptypes)) - # compute relations - if display_relations: - relations = sorted(rschema for rschema in schema.relations() - if not (rschema.final - or rschema in skiptypes - or rschema in META_RTYPES)) - else: - relations = [] - # index - _ = self._cw._ - url = xml_escape(self._cw.build_url('schema')) - self.w(u'
') - self.w(u'

%s

' % _('Index')) - self.w(u'

%s

' % _('Entity types')) - ents = [] - for eschema in sorted(entities): - ents.append(u'%s' % ( - url, eschema.type, eschema.type)) - self.w(u', '.join(ents)) - self.w(u'

%s

' % _('Relation types')) - rels = [] - for rschema in sorted(relations): - rels.append(u'%s' % ( - url , rschema.type, rschema.type)) - self.w(u', '.join(rels)) - # permissions tables - self.display_entities(entities) - if relations: - self.display_relations(relations) - self.w(u'
') - - def has_non_default_perms(self, rdef): - """return true if the given *attribute* relation definition has custom - permission - """ - for action in rdef.ACTIONS: - def_rqlexprs = [] - def_groups = [] - for perm in DEFAULT_ATTRPERMS[action]: - if not isinstance(perm, basestring): - def_rqlexprs.append(perm.expression) - else: - def_groups.append(perm) - rqlexprs = [rql.expression for rql in rdef.get_rqlexprs(action)] - groups = rdef.get_groups(action) - if groups != frozenset(def_groups) or \ - frozenset(rqlexprs) != frozenset(def_rqlexprs): - return True - return False - - def display_entities(self, entities): - _ = self._cw._ - url = xml_escape(self._cw.build_url('schema')) - self.w(u'

%s

' % _('Permissions for entity types')) - for eschema in entities: - self.w(u'

%s (%s) ' % ( - eschema.type, self._cw.build_url('cwetype/%s' % eschema.type), - eschema.type, _(eschema.type))) - self.w(u'%s' % ( - url, self._cw.uiprops['UP_ICON'], _('up'))) - self.w(u'

') - self.w(u'
') - self.permissions_table(eschema) - # display entity attributes only if they have some permissions modified - modified_attrs = [] - for attr, etype in eschema.attribute_definitions(): - rdef = eschema.rdef(attr) - if attr not in META_RTYPES and self.has_non_default_perms(rdef): - modified_attrs.append(rdef) - if modified_attrs: - self.w(u'

%s

' % _('Attributes with non default permissions:')) - self.w(u'
') - self.w(u'
') - for rdef in modified_attrs: - attrtype = str(rdef.rtype) - self.w(u'

%s (%s)

' % (attrtype, _(attrtype))) - self.permissions_table(rdef) - self.w(u'
') - - def display_relations(self, relations): - _ = self._cw._ - url = xml_escape(self._cw.build_url('schema')) - self.w(u'

%s

' % _('Permissions for relations')) - for rschema in relations: - self.w(u'

%s (%s) ' % ( - rschema.type, self._cw.build_url('cwrtype/%s' % rschema.type), - rschema.type, _(rschema.type))) - self.w(u'%s' % ( - url, self._cw.uiprops['UP_ICON'], _('up'))) - self.w(u'

') - self.grouped_permissions_table(rschema) - - # CWEType ###################################################################### # register msgid generated in entity relations tables @@ -794,13 +690,14 @@ __select__ = facet.AttributeFacet.__select__ & is_instance('CWEType', 'CWRType') rtype = 'final' + class ViewSchemaAction(action.Action): __regid__ = 'schema' __select__ = yes() title = _("site schema") - category = 'siteactions' order = 30 + category = 'manage' def url(self): return self._cw.build_url(self.__regid__) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/sessions.py --- a/web/views/sessions.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/sessions.py Fri Feb 11 12:17:47 2011 +0100 @@ -69,8 +69,8 @@ raise :exc:`cubicweb.AuthenticationError` if authentication failed (no authentication info found or wrong user/password) """ - cnx, login, authinfo = self.authmanager.authenticate(req) - session = DBAPISession(cnx, login, authinfo) + cnx, login = self.authmanager.authenticate(req) + session = DBAPISession(cnx, login) self._sessions[session.sessionid] = session # associate the connection to the current request req.set_session(session) diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/startup.py --- a/web/views/startup.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/startup.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,14 +15,15 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""Set of HTML startup views. A startup view is global, e.g. doesn't -apply to a result set. +"""Set of HTML startup views. A startup view is global, e.g. doesn't apply to a +result set. """ __docformat__ = "restructuredtext en" _ = unicode from logilab.common.textutils import unormalize +from logilab.common.deprecation import deprecated from logilab.mtconverter import xml_escape from cubicweb.view import StartupView @@ -35,101 +36,59 @@ title = _('manage') http_cache_manager = httpcache.EtagHTTPCacheManager add_etype_links = () - - def display_folders(self): - return False + skip_startup_views = set( ('index', 'manage', 'schema', 'owl', 'changelog', + 'systempropertiesform', 'propertiesform', + 'cw.user-management', 'cw.source-management', + 'siteinfo', 'info', 'registry', 'gc', + 'tree') ) def call(self, **kwargs): """The default view representing the instance's management""" self._cw.add_css('cubicweb.manageview.css') self.w(u'

%s

' % self._cw.property_value('ui.site-title')) - if not self.display_folders(): - self._main_index() - else: - self.w(u'\n') - self.w(u'') - self.w(u'
') - self._main_index() - self.w(u'') - self.folders() - self.w(u'
\n') + self.entities() + self.manage_actions() + self.startup_views() - def _main_index(self): - req = self._cw - manager = req.user.matching_groups('managers') - if not manager and 'Card' in self._cw.vreg.schema: - rset = self._cw.execute('Card X WHERE X wikiid "index"') - else: - rset = None - if rset: - self.wview('inlined', rset, row=0) - else: - self.entities() + def manage_actions(self): + allactions = self._cw.vreg['actions'].possible_actions(self._cw) + if allactions.get('manage'): self.w(u'
 
') - self.startup_views() - if manager and 'Card' in self._cw.vreg.schema: - self.w(u'
 
') - if rset: - href = rset.get_entity(0, 0).absolute_url(vid='edition') - label = self._cw._('edit the index page') - else: - href = req.build_url('view', vid='creation', etype='Card', wikiid='index') - label = self._cw._('create an index page') - self.w(u'
%s\n' % (xml_escape(href), label)) - - def folders(self): - self.w(u'

%s

\n' % self._cw._('Browse by category')) - self._cw.vreg['views'].select('tree', self._cw).render(w=self.w, maxlevel=1) - - def create_links(self): - self.w(u'') def startup_views(self): - self.w(u'

%s

\n' % self._cw._('Startup views')) - self.startupviews_table() - - def startupviews_table(self): - views = self._cw.vreg['views'].possible_views(self._cw, None) + views = [v for v in self._cw.vreg['views'].possible_views(self._cw, None) + if v.category == 'startupview' + and v.__regid__ not in self.skip_startup_views] if not views: return + self.w(u'
 
') + self.w(u'

%s

\n' % self._cw._('Startup views')) self.w(u'
    ') for v in sorted(views, key=lambda x: self._cw._(x.title)): - if v.category != 'startupview' or v.__regid__ in ('index', 'tree', 'manage'): - continue self.w('
  • %s
  • ' % ( xml_escape(v.url()), xml_escape(self._cw._(v.title).capitalize()))) self.w(u'
') def entities(self): schema = self._cw.vreg.schema - self.w(u'

%s

\n' % self._cw._('Browse by entity type')) - manager = self._cw.user.matching_groups('managers') - self.w(u'') - if manager: - self.w(u'\n' % self._cw._('application entities')) - self.entity_types_table(eschema for eschema in schema.entities() - if uicfg.indexview_etype_section.get(eschema) == 'application') - if manager: - self.w(u'\n' % self._cw._('system entities')) - self.entity_types_table(eschema for eschema in schema.entities() - if uicfg.indexview_etype_section.get(eschema) == 'system') - if 'CWAttribute' in schema: # check schema support - self.w(u'\n' % self._cw._('schema entities')) - self.entity_types_table(eschema for eschema in schema.entities() - if uicfg.indexview_etype_section.get(eschema) == 'schema') - self.w(u'
%s
%s
%s
') + eschemas = [eschema for eschema in schema.entities() + if uicfg.indexview_etype_section.get(eschema) == 'application'] + if eschemas: + self.w(u'
 
') + self.w(u'

%s

\n' % self._cw._('Browse by entity type')) + self.w(u'') + self.entity_types_table(eschemas) + self.w(u'
') def entity_types_table(self, eschemas): - newline = 0 infos = sorted(self.entity_types(eschemas), - key=lambda (l,a,e):unormalize(l)) + key=lambda (l,a,e): unormalize(l)) q, r = divmod(len(infos), 2) if r: infos.append( (None, ' ', ' ') ) @@ -140,10 +99,9 @@ self.w(u'%s%s\n' % (addlink2, etypelink2)) self.w(u'\n') - def entity_types(self, eschemas): - """return a list of formatted links to get a list of entities of - a each entity's types + """return an iterator on formatted links to get a list of entities of + each entity types """ req = self._cw for eschema in eschemas: @@ -161,6 +119,18 @@ xml_escape(url), label, nb) if eschema.has_perm(req, 'add'): yield (label, etypelink, self.add_entity_link(etype)) + else: + yield (label, etypelink, u'') + + def create_links(self): + self.w(u'') def add_entity_link(self, etype): """creates a [+] link for adding an entity""" @@ -168,11 +138,21 @@ return u'[+]' % ( xml_escape(url), self._cw.__('add a %s' % etype)) + @deprecated('[3.11] display_folders method is deprecated, backport it if needed') + def display_folders(self): + return False + + @deprecated('[3.11] folders method is deprecated, backport it if needed') + def folders(self): + self.w(u'

%s

\n' % self._cw._('Browse by category')) + self._cw.vreg['views'].select('tree', self._cw).render(w=self.w, maxlevel=1) + class IndexView(ManageView): __regid__ = 'index' title = _('view_index') + @deprecated('[3.11] display_folders method is deprecated, backport it if needed') def display_folders(self): return 'Folder' in self._cw.vreg.schema and self._cw.execute('Any COUNT(X) WHERE X is Folder')[0][0] diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/tabs.py --- a/web/views/tabs.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/tabs.py Fri Feb 11 12:17:47 2011 +0100 @@ -24,29 +24,28 @@ from logilab.mtconverter import xml_escape from cubicweb import NoSelectableObject, role +from cubicweb import tags, uilib, utils from cubicweb.selectors import partial_has_related_entities from cubicweb.view import EntityView -from cubicweb import tags, uilib -from cubicweb.utils import make_uid from cubicweb.web.views import primary class LazyViewMixin(object): - """provides two convenience methods for the tab machinery - can also be used to lazy-load arbitrary views + """provides two convenience methods for the tab machinery. + + Can also be used to lazy-load arbitrary views. """ def _prepare_bindings(self, vid, reloadable): self._cw.add_onload(u""" jQuery('#lazy-%(vid)s').bind('%(event)s', function(event) { - load_now('#lazy-%(vid)s', '#%(vid)s-hole', %(reloadable)s); + loadNow('#lazy-%(vid)s', '#%(vid)s-hole', %(reloadable)s); });""" % {'event': 'load_%s' % vid, 'vid': vid, 'reloadable' : str(reloadable).lower()}) def lazyview(self, vid, rql=None, eid=None, rset=None, tabid=None, reloadable=False, show_spinbox=True, w=None): - """ a lazy version of wview """ + """a lazy version of wview""" w = w or self.w - self._cw.add_js('cubicweb.lazy.js') urlparams = {'vid' : vid, 'fname' : 'view'} if rql: urlparams['rql'] = rql @@ -70,16 +69,14 @@ self._prepare_bindings(tabid, reloadable) def forceview(self, vid): - """trigger an event that will force immediate loading of the view - on dom readyness + """trigger an event that will force immediate loading of the view on dom + readyness """ - self._cw.add_js('cubicweb.lazy.js') - self._cw.add_onload("trigger_load('%s');" % vid) + self._cw.add_onload(uilib.js.triggerLoad(vid)) class TabsMixin(LazyViewMixin): - """a tab mixin - """ + """a tab mixin to easily get jQuery based, lazy, ajax tabs""" @property def cookie_name(self): @@ -104,11 +101,11 @@ active_tab = uilib.domid(default_tab) viewsvreg = self._cw.vreg['views'] for tab in tabs: - try: + if isinstance(tab, basestring): + tabid, tabkwargs = tab, {} + else: tabid, tabkwargs = tab tabkwargs = tabkwargs.copy() - except ValueError: - tabid, tabkwargs = tab, {} tabkwargs.setdefault('rset', self.cw_rset) vid = tabkwargs.get('vid', tabid) domid = uilib.domid(tabid) @@ -128,20 +125,19 @@ entity.view(default, w=self.w) return self._cw.add_css('ui.tabs.css') - self._cw.add_js(('ui.core.js', 'ui.tabs.js', - 'cubicweb.ajax.js', 'cubicweb.tabs.js', 'cubicweb.lazy.js')) + self._cw.add_js(('ui.core.js', 'ui.tabs.js', 'cubicweb.ajax.js')) # prune tabs : not all are to be shown tabs, active_tab = self.prune_tabs(tabs, default) # build the html structure w = self.w - uid = entity and entity.eid or make_uid('tab') + uid = entity and entity.eid or utils.make_uid('tab') w(u'
' % uid) w(u'
') - # call the set_tab() JS function *after* each tab is generated + # 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 > ul').tabs( { selected: %(tabindex)s }); - set_tab('%(domid)s', '%(cookiename)s'); + setTab('%(domid)s', '%(cookiename)s'); """ % {'tabindex' : active_tab_idx, 'domid' : active_tab, 'eeid' : (entity and entity.eid or uid), diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/urlpublishing.py --- a/web/views/urlpublishing.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/urlpublishing.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -156,7 +156,7 @@ [[/]/]* """ - priority = 2 + priority = 3 def evaluate_path(self, req, parts): if not (0 < len(parts) < 4): @@ -214,7 +214,8 @@ URL rewrite rule definitions are stored in URLRewriter objects """ - priority = 3 + priority = 2 + def evaluate_path(self, req, parts): # uri <=> req._twreq.path or req._twreq.uri uri = req.url_unquote('/' + '/'.join(parts)) @@ -236,6 +237,7 @@ / """ priority = 4 + def evaluate_path(self, req, parts): if len(parts) < 2: raise PathDontMatch() diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/urlrewrite.py --- a/web/views/urlrewrite.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/urlrewrite.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -90,12 +90,14 @@ ('/index', dict(vid='index')), ('/myprefs', dict(vid='propertiesform')), ('/siteconfig', dict(vid='systempropertiesform')), - ('/siteinfo', dict(vid='info')), + ('/siteinfo', dict(vid='siteinfo')), ('/manage', dict(vid='manage')), ('/notfound', dict(vid='404')), ('/error', dict(vid='error')), ('/sparql', dict(vid='sparql')), ('/processinfo', dict(vid='processinfo')), + (rgx('/cwuser', re.I), dict(vid='cw.user-management')), + (rgx('/cwsource', re.I), dict(vid='cw.source-management')), # XXX should be case insensitive as 'create', but I would like to find another way than # relying on the etype_selector (rgx('/schema/([^/]+?)/?'), dict(vid='primary', rql=r'Any X WHERE X is CWEType, X name "\1"')), diff -r a8fbcf9b6572 -r 8e59c2cdcc99 web/views/xmlrss.py --- a/web/views/xmlrss.py Fri Feb 11 11:37:10 2011 +0100 +++ b/web/views/xmlrss.py Fri Feb 11 12:17:47 2011 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,6 +20,7 @@ __docformat__ = "restructuredtext en" _ = unicode +from base64 import b64encode from time import timezone from logilab.mtconverter import xml_escape @@ -31,6 +32,18 @@ from cubicweb.uilib import simple_sgml_tag from cubicweb.web import httpcache, component +def encode_bytes(value): + return '' % b64encode(value.getvalue()) + +# see cubicweb.sobjects.parser.DEFAULT_CONVERTERS +SERIALIZERS = { + 'String': xml_escape, + 'Bytes': encode_bytes, + 'Date': lambda x: x.strftime('%Y-%m-%d'), + 'Datetime': lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), + 'Time': lambda x: x.strftime('%H:%M:%S'), + 'Interval': lambda x: x.days * 60*60*24 + x.seconds, + } # base xml views ############################################################## @@ -61,24 +74,52 @@ def cell_call(self, row, col): """ element as an item for an xml feed """ entity = self.cw_rset.complete_entity(row, col) - self.w(u'<%s>\n' % (entity.e_schema)) + self.w(u'<%s eid="%s" cwuri="%s">\n' + % (entity.e_schema, entity.eid, xml_escape(entity.cwuri))) for rschema, attrschema in entity.e_schema.attribute_definitions(): attr = rschema.type - if attr == 'eid': - value = entity.eid + if attr in ('eid', 'cwuri'): + continue else: try: value = entity.cw_attr_cache[attr] except KeyError: # Bytes continue - if value is not None: - if attrschema == 'Bytes': - from base64 import b64encode - value = '' % b64encode(value.getvalue()) - elif isinstance(value, basestring): - value = xml_escape(value) + if value is None: + self.w(u' <%s/>\n' % attr) + else: + try: + value = SERIALIZERS[attrschema](value) + except KeyError: + pass self.w(u' <%s>%s\n' % (attr, value, attr)) + for relstr in self._cw.list_form_param('relation'): + try: + rtype, role = relstr.split('-') + except ValueError: + self.error('badly formated relation name %r', relstr) + continue + if role == 'subject': + getrschema = entity.e_schema.subjrels + elif role == 'object': + getrschema = entity.e_schema.objrels + else: + self.error('badly formated relation name %r', relstr) + continue + if not rtype in getrschema: + self.error('unexisting relation %r', relstr) + continue + self.w(u' <%s role="%s">\n' % (rtype, role)) + for related in entity.related(rtype, role, entities=True): + # XXX put unique attributes as xml attribute, they are much + # probably used to search existing entities in client data feed, + # and putting it here may avoid an extra request to get those + # attributes values + self.w(u' <%s eid="%s" cwuri="%s"/>\n' + % (related.e_schema, related.eid, + xml_escape(related.cwuri))) + self.w(u' \n' % rtype) self.w(u'\n' % (entity.e_schema)) @@ -234,7 +275,6 @@ if entity.creator: self._marker('dc:creator', entity.dc_creator()) - def _marker(self, marker, value): if value: self.w(u' <%s>%s\n' % (marker, xml_escape(value), marker))