# HG changeset patch # User Alexandre Fayolle # Date 1300264856 -3600 # Node ID 36e91d19188b8807ae9db11d32dcb1353cb8475d # Parent 1b07eb7180a256ae31fa35ed3beaf07b6302e711# Parent b8e35cde46e9a6608f2e18eb3acbc45155eae640 merged back pylint help changes to stable diff -r b8e35cde46e9 -r 36e91d19188b .hgtags --- a/.hgtags Wed Mar 16 09:37:46 2011 +0100 +++ b/.hgtags Wed Mar 16 09:40:56 2011 +0100 @@ -181,3 +181,7 @@ bf5d9a1415e3c9abe6b68ba3b24a8ad741f9de3c cubicweb-debian-version-3.10.7-1 e581a86a68f089946a98c966ebca7aee58a5718f cubicweb-version-3.10.8 132b525de25bc75ed6389c45aee77e847cb3a437 cubicweb-debian-version-3.10.8-1 +37432cede4fe55b97fc2e9be0a2dd20e8837a848 cubicweb-version-3.11.0 +8daabda9f571863e8754f8ab722744c417ba3abf cubicweb-debian-version-3.11.0-1 +d0410eb4d8bbf657d7f32b0c681db09b1f8119a0 cubicweb-version-3.11.1 +77318f1ec4aae3523d455e884daf3708c3c79af7 cubicweb-debian-version-3.11.1-1 diff -r b8e35cde46e9 -r 36e91d19188b __pkginfo__.py --- a/__pkginfo__.py Wed Mar 16 09:37:46 2011 +0100 +++ b/__pkginfo__.py Wed Mar 16 09:40:56 2011 +0100 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 10, 9) +numversion = (3, 11, 1) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -40,10 +40,10 @@ ] __depends__ = { - 'logilab-common': '>= 0.54.0', + 'logilab-common': '>= 0.55.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 @@ -62,6 +62,7 @@ 'pycrypto': '', # for crypto extensions 'fyzz': '>= 0.1.0', # for sparql 'vobject': '>= 0.6.0', # for ical view + 'rdflib': None, # #'Products.FCKeditor':'', #'SimpleTAL':'>= 4.1.6', } diff -r b8e35cde46e9 -r 36e91d19188b dbapi.py --- a/dbapi.py Wed Mar 16 09:37:46 2011 +0100 +++ b/dbapi.py Wed Mar 16 09:40:56 2011 +0100 @@ -233,11 +233,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) @@ -602,9 +601,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 b8e35cde46e9 -r 36e91d19188b debian/changelog --- a/debian/changelog Wed Mar 16 09:37:46 2011 +0100 +++ b/debian/changelog Wed Mar 16 09:40:56 2011 +0100 @@ -1,3 +1,15 @@ +cubicweb (3.11.1-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Mon, 07 Mar 2011 17:21:28 +0100 + +cubicweb (3.11.0-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Fri, 18 Feb 2011 10:27:22 +0100 + cubicweb (3.10.8-1) unstable; urgency=low * new upstream release diff -r b8e35cde46e9 -r 36e91d19188b debian/control --- a/debian/control Wed Mar 16 09:37:46 2011 +0100 +++ b/debian/control Wed Mar 16 09:40:56 2011 +0100 @@ -83,7 +83,7 @@ Architecture: all XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3) -Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-imaging +Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-imaging, python-rdflib Description: web interface library for the CubicWeb framework CubicWeb is a semantic web application framework. . @@ -97,7 +97,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${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.55.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 b8e35cde46e9 -r 36e91d19188b debian/copyright --- a/debian/copyright Wed Mar 16 09:37:46 2011 +0100 +++ b/debian/copyright Wed Mar 16 09:40:56 2011 +0100 @@ -8,7 +8,7 @@ Copyright: - Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). + Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE). http://www.logilab.fr/ -- mailto:contact@logilab.fr License: diff -r b8e35cde46e9 -r 36e91d19188b devtools/cwwindmill.py --- a/devtools/cwwindmill.py Wed Mar 16 09:37:46 2011 +0100 +++ b/devtools/cwwindmill.py Wed Mar 16 09:40:56 2011 +0100 @@ -86,9 +86,11 @@ test_dir = __file__ - Instead of toggle `edit_test` value, try `pytest -i` + Instead of toggle `edit_test` value, try `python -f` """ browser = 'firefox' +<<<<<<< /home/syt/src/fcubicweb/cubicweb/devtools/cwwindmill.py + edit_test = "-i" in sys.argv # detection for pytest invocation # Windmill use case are written with no anonymous user anonymous_allowed = False diff -r b8e35cde46e9 -r 36e91d19188b devtools/fake.py --- a/devtools/fake.py Wed Mar 16 09:37:46 2011 +0100 +++ b/devtools/fake.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b devtools/fill.py --- a/devtools/fill.py Wed Mar 16 09:37:46 2011 +0100 +++ b/devtools/fill.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b devtools/httptest.py diff -r b8e35cde46e9 -r 36e91d19188b devtools/repotest.py --- a/devtools/repotest.py Wed Mar 16 09:37:46 2011 +0100 +++ b/devtools/repotest.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b devtools/testlib.py --- a/devtools/testlib.py Wed Mar 16 09:37:46 2011 +0100 +++ b/devtools/testlib.py Wed Mar 16 09:40:56 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 @@ -481,7 +480,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] @@ -492,7 +492,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 b8e35cde46e9 -r 36e91d19188b doc/book/en/annexes/rql/debugging.rst --- a/doc/book/en/annexes/rql/debugging.rst Wed Mar 16 09:37:46 2011 +0100 +++ b/doc/book/en/annexes/rql/debugging.rst Wed Mar 16 09:40:56 2011 +0100 @@ -33,7 +33,7 @@ Enable verbose output ~~~~~~~~~~~~~~~~~~~~~ -It may be interested to enable a verboser output to debug your RQL statements: +To debug your RQL statements, it can be useful to enable a verbose output: .. sourcecode:: python diff -r b8e35cde46e9 -r 36e91d19188b doc/book/en/annexes/rql/language.rst --- a/doc/book/en/annexes/rql/language.rst Wed Mar 16 09:37:46 2011 +0100 +++ b/doc/book/en/annexes/rql/language.rst Wed Mar 16 09:40:56 2011 +0100 @@ -153,7 +153,7 @@ - Aggregate Functions: COUNT, MIN, MAX, AVG, SUM, GROUP_CONCAT Having -`````` +``````` The HAVING clause, as in SQL, has been originally introduced to restrict a query according to value returned by an aggregate function, e.g.:: @@ -214,7 +214,12 @@ Exists -`````` +``````` + +You can use `EXISTS` when you want to know if some expression is true and do not +need the complete set of elements that make it true. Testing for existence is +much faster than fetching the complete set of results. + :: Any X ORDERBY PN,N diff -r b8e35cde46e9 -r 36e91d19188b doc/book/en/tutorials/advanced/part03_bfss.rst --- a/doc/book/en/tutorials/advanced/part03_bfss.rst Wed Mar 16 09:37:46 2011 +0100 +++ b/doc/book/en/tutorials/advanced/part03_bfss.rst Wed Mar 16 09:40:56 2011 +0100 @@ -23,20 +23,20 @@ from cubicweb.server.sources import storage class ServerStartupHook(hook.Hook): - __regid__ = 'sytweb.serverstartup' - events = ('server_startup', 'server_maintenance') + __regid__ = 'sytweb.serverstartup' + events = ('server_startup', 'server_maintenance') - def __call__(self): - bfssdir = join(self.repo.config.appdatahome, 'bfss') - if not exists(bfssdir): - makedirs(bfssdir) - print 'created', bfssdir - storage = storages.BytesFileSystemStorage(bfssdir) - set_attribute_storage(self.repo, 'File', 'data', storage) + def __call__(self): + bfssdir = join(self.repo.config.appdatahome, 'bfss') + if not exists(bfssdir): + makedirs(bfssdir) + print 'created', bfssdir + storage = storages.BytesFileSystemStorage(bfssdir) + set_attribute_storage(self.repo, 'File', 'data', storage) .. Note:: - * how we built the hook's registry identifier (_`_regid__`): you can introduce + * how we built the hook's registry identifier (`__regid__`): you can introduce 'namespaces' by using there python module like naming identifiers. This is especially import for hooks where you usually want a new custom hook, not overriding / specializing an existant one, but the concept may be applied to @@ -50,48 +50,48 @@ * the path given to the storage is the place where file added through the ui (or in the database before migration) will be located - * be ware that by doing this, you can't anymore write queries that will try to + * beware that by doing this, you can't anymore write queries that will try to restrict on File `data` attribute. Hopefuly we don't do that usually on file's content or more generally on attributes for the Bytes type Now, if you've already added some photos through the web ui, you'll have to migrate existing data so file's content will be stored on the file-system instead of the database. There is a migration command to do so, let's run it in the -cubicweb shell (in actual life, you'd have to put it in a migration script as we -seen last time): +cubicweb shell (in real life, you would have to put it in a migration script as we +have seen last time): :: $ cubicweb-ctl shell sytweb - entering the migration python shell - just type migration commands or arbitrary python code and type ENTER to execute it - type "exit" or Ctrl-D to quit the shell and resume operation - >>> storage_changed('File', 'data') - [........................] + entering the migration python shell + just type migration commands or arbitrary python code and type ENTER to execute it + type "exit" or Ctrl-D to quit the shell and resume operation + >>> storage_changed('File', 'data') + [........................] -That's it. Now, file added through the web ui will have their content stored on +That's it. Now, files added through the web ui will have their content stored on the file-system, and you'll also be able to import files from the file-system as explained in the next part. Step 2: importing some data into the instance ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Hey, we start to have some nice features, let give us a try on this new web +Hey, we start to have some nice features, let us give a try to this new web site. For instance if I have a 'photos/201005WePyrenees' containing pictures for a particular event, I can import it to my web site by typing :: $ cubicweb-ctl fsimport -F sytweb photos/201005WePyrenees/ ** importing directory /home/syt/photos/201005WePyrenees - importing IMG_8314.JPG - importing IMG_8274.JPG - importing IMG_8286.JPG - importing IMG_8308.JPG - importing IMG_8304.JPG + importing IMG_8314.JPG + importing IMG_8274.JPG + importing IMG_8286.JPG + importing IMG_8308.JPG + importing IMG_8304.JPG .. Note:: - The -F option tell that folders should be mapped, hence my photos will be - all under a Folder entity corresponding to the file-system folder. + The -F option means that folders should be mapped, hence my photos will be + linked to a Folder entity corresponding to the file-system folder. Let's take a look at the web ui: @@ -103,11 +103,11 @@ .. image:: ../../images/tutos-photowebsite_ui2.png -Yeah, it's there! You can also notice that I can see some entities as well as +Yeah, it's there! You will notice that I can see some entities as well as folders and images the anonymous user can't. It just works **everywhere in the ui** since it's handled at the repository level, thanks to our security model. -Now if I click on the newly inserted folder, I can see +Now if I click on the recently inserted folder, I can see .. image:: ../../images/tutos-photowebsite_ui3.png @@ -124,7 +124,7 @@ We started to see here an advanced feature of our repository: the ability to store some parts of our data-model into a custom storage, outside the database. There is currently only the :class:`BytesFileSystemStorage` available, -but you can expect to see more coming in a near future (our write your own!). +but you can expect to see more coming in a near future (or write your own!). Also, we can know start to feed our web-site with some nice pictures! The site isn't perfect (far from it actually) but it's usable, and we can diff -r b8e35cde46e9 -r 36e91d19188b entities/adapters.py --- a/entities/adapters.py Wed Mar 16 09:37:46 2011 +0100 +++ b/entities/adapters.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b entities/schemaobjs.py --- a/entities/schemaobjs.py Wed Mar 16 09:37:46 2011 +0100 +++ b/entities/schemaobjs.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b entities/sources.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/sources.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b hooks/__init__.py --- a/hooks/__init__.py Wed Mar 16 09:37:46 2011 +0100 +++ b/hooks/__init__.py Wed Mar 16 09:40:56 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.get('created'): + source.info('added %s entities', len(stats['created'])) + if stats.get('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 b8e35cde46e9 -r 36e91d19188b hooks/integrity.py --- a/hooks/integrity.py Wed Mar 16 09:37:46 2011 +0100 +++ b/hooks/integrity.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b hooks/notification.py --- a/hooks/notification.py Wed Mar 16 09:37:46 2011 +0100 +++ b/hooks/notification.py Wed Mar 16 09:40:56 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,9 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""some hooks to handle notification on entity's changes +"""some hooks to handle notification on entity's changes""" -""" __docformat__ = "restructuredtext en" from logilab.common.textutils import normalize_text diff -r b8e35cde46e9 -r 36e91d19188b hooks/syncsources.py --- a/hooks/syncsources.py Wed Mar 16 09:37:46 2011 +0100 +++ b/hooks/syncsources.py Wed Mar 16 09:40:56 2011 +0100 @@ -1,6 +1,29 @@ +# 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 socket import gethostname + +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 +31,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 +39,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 +62,102 @@ 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): + if self.entity.match(gethostname()): + if self.event == 'after_update_entity' and \ + not 'config' in self.entity.cw_edited: + return + 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 b8e35cde46e9 -r 36e91d19188b hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Wed Mar 16 09:37:46 2011 +0100 +++ b/hooks/test/unittest_hooks.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b hooks/test/unittest_integrity.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hooks/test/unittest_integrity.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b i18n/de.po --- a/i18n/de.po Wed Mar 16 09:37:46 2011 +0100 +++ b/i18n/de.po Wed Mar 16 09:40:56 2011 +0100 @@ -43,6 +43,13 @@ msgstr " :" #, python-format +msgid "\"action\" must be specified in options; allowed values are %s" +msgstr "" + +msgid "\"role=subject\" or \"role=object\" must be specified in options" +msgstr "" + +#, python-format msgid "%(attr)s set to %(newvalue)s" msgstr "%(attr)s geändert in %(newvalue)s" @@ -127,6 +134,10 @@ msgstr "%d Jahre" #, python-format +msgid "%s could be supported" +msgstr "" + +#, python-format msgid "%s error report" msgstr "%s Fehlerbericht" @@ -135,6 +146,10 @@ msgstr "%s unbekannt(e)" #, python-format +msgid "%s relation should not be in mapped" +msgstr "" + +#, python-format msgid "%s software version of the database" msgstr "Software-Version der Datenbank %s" @@ -142,6 +157,14 @@ msgid "%s updated" msgstr "%s aktualisiert" +#, python-format +msgid "'%s' action doesn't take any options" +msgstr "" + +#, python-format +msgid "'%s' action require 'linkattr' option" +msgstr "" + msgid "(UNEXISTANT EID)" msgstr "(EID nicht gefunden)" @@ -224,9 +247,6 @@ msgid "Attributes permissions:" msgstr "Rechte der Attribute" -msgid "Attributes with non default permissions:" -msgstr "Attribute mit nicht-standard-Berechtigungen" - # schema pot file, generated on 2009-09-16 16:46:55 # # singular and plural forms for each entity type @@ -347,6 +367,12 @@ msgid "CWSourceHostConfig_plural" msgstr "" +msgid "CWSourceSchemaConfig" +msgstr "" + +msgid "CWSourceSchemaConfig_plural" +msgstr "" + msgid "CWSource_plural" msgstr "" @@ -433,6 +459,9 @@ msgid "Decimal_plural" msgstr "Dezimalzahlen" +msgid "Detected problems" +msgstr "" + msgid "Do you want to delete the following element(s) ?" msgstr "Wollen Sie das/die folgend(n) Element(e) löschen?" @@ -452,8 +481,8 @@ msgid "Entities" msgstr "Entitäten" -msgid "Entity types" -msgstr "Entitätstypen" +msgid "Entity and relation supported by this source" +msgstr "" msgid "ExternalUri" msgstr "Externer Uri" @@ -485,9 +514,6 @@ msgid "Help" msgstr "Hilfe" -msgid "Index" -msgstr "Index" - msgid "Instance" msgstr "Instanz" @@ -509,6 +535,9 @@ msgid "Looked up classes" msgstr "gesuchte Klassen" +msgid "Manage" +msgstr "" + msgid "Most referenced classes" msgstr "meist-referenzierte Klassen" @@ -554,6 +583,9 @@ msgid "New CWSourceHostConfig" msgstr "" +msgid "New CWSourceSchemaConfig" +msgstr "" + msgid "New CWUniqueTogetherConstraint" msgstr "Neue unique-together-Einschränkung" @@ -611,12 +643,6 @@ msgid "Password_plural" msgstr "Passwörter" -msgid "Permissions for entity types" -msgstr "Berechtigungen für Entitätstypen" - -msgid "Permissions for relations" -msgstr "Berechtigungen für Relationen" - msgid "Please note that this is only a shallow copy" msgstr "Achtung: dies ist nur eine flache Kopie!" @@ -647,9 +673,6 @@ msgid "Registry's content" msgstr "Inhalt der Registry" -msgid "Relation types" -msgstr "Relationstypen" - msgid "Relations" msgstr "Relationen" @@ -666,6 +689,9 @@ msgid "Search for" msgstr "Suchen" +msgid "Site information" +msgstr "" + msgid "SizeConstraint" msgstr "Größeneinschränkung" @@ -764,6 +790,9 @@ msgid "This CWSourceHostConfig" msgstr "" +msgid "This CWSourceSchemaConfig" +msgstr "" + msgid "This CWUniqueTogetherConstraint" msgstr "Diese unique-together-Einschränkung" @@ -818,6 +847,9 @@ msgid "Transition_plural" msgstr "Übergänge" +msgid "URLs from which content will be imported. You can put one url per line" +msgstr "" + msgid "UniqueConstraint" msgstr "eindeutige Einschränkung" @@ -1017,6 +1049,10 @@ msgid "add WorkflowTransition transition_of Workflow object" msgstr "Workflow-Übergang" +#, python-format +msgid "add a %s" +msgstr "" + msgctxt "inlined:CWRelation.from_entity.subject" msgid "add a CWEType" msgstr "einen Entitätstyp hinzufügen" @@ -1029,6 +1065,12 @@ msgid "add a CWRType" msgstr "einen Relationstyp hinzufügen" +msgid "add a CWSource" +msgstr "" + +msgid "add a CWSourceSchemaConfig" +msgstr "" + msgctxt "inlined:CWUser.use_email.subject" msgid "add a EmailAddress" msgstr "Email-Adresse hinzufügen" @@ -1097,6 +1139,9 @@ msgid "allow to set a specific workflow for an entity" msgstr "erlaube, einen bestimmten Workflow für eine Entität zu setzen" +msgid "allowed options depends on the source type" +msgstr "" + msgid "allowed transitions from this state" msgstr "erlaubte Übergänge von diesem Zustand" @@ -1122,18 +1167,6 @@ msgid "allowed_transition_object" msgstr "ausstehende Zustände" -msgid "am/pm calendar (month)" -msgstr "am/pm Kalender (Monat)" - -msgid "am/pm calendar (semester)" -msgstr "am/pm Kalender (Halbjahr)" - -msgid "am/pm calendar (week)" -msgstr "am/pm Kalender (Woche)" - -msgid "am/pm calendar (year)" -msgstr "am/pm Kalender (Jahr)" - msgid "an electronic mail address associated to a short alias" msgstr "Eine E-Mail-Adresse wurde mit einem Alias verknüpft." @@ -1159,9 +1192,6 @@ msgid "anonymous" msgstr "anonym" -msgid "application entities" -msgstr "Anwendungs-Entitäten" - msgid "april" msgstr "April" @@ -1182,6 +1212,9 @@ msgid "attribute" msgstr "Attribut" +msgid "attribute/relation can't be mapped, only entity and relation types" +msgstr "" + msgid "august" msgstr "August" @@ -1278,18 +1311,6 @@ msgid "calendar" msgstr "Kalender anzeigen" -msgid "calendar (month)" -msgstr "Kalender (monatlich)" - -msgid "calendar (semester)" -msgstr "Kalender (halbjährlich)" - -msgid "calendar (week)" -msgstr "Kalender (wöchentlich)" - -msgid "calendar (year)" -msgstr "Kalender (jährlich)" - msgid "can not resolve entity types:" msgstr "Die Typen konnten nicht ermittelt werden:" @@ -1303,6 +1324,9 @@ msgid "can't change the %s attribute" msgstr "Kann das Attribut %s nicht ändern." +msgid "can't change this relation" +msgstr "" + #, python-format msgid "can't connect to source %s, some data may be missing" msgstr "Keine Verbindung zu der Quelle %s, einige Daten könnten fehlen" @@ -1314,6 +1338,12 @@ msgid "can't have multiple exits on the same state" msgstr "Mehrere Ausgänge aus demselben Zustand nicht möglich." +msgid "can't mix dontcross and maycross options" +msgstr "" + +msgid "can't mix dontcross and write options" +msgstr "" + #, python-format msgid "can't parse %(value)r (expected %(format)s)" msgstr "" @@ -1550,9 +1580,6 @@ msgid "create an index for quick search on this attribute" msgstr "Erstelle einen Index zur schnellen Suche über dieses Attribut" -msgid "create an index page" -msgstr "Eine Index-Seite anlegen" - msgid "created on" msgstr "angelegt am" @@ -1820,18 +1847,18 @@ msgid "custom_workflow_object" msgstr "angepasster Workflow von" -msgid "cw_dont_cross" +msgid "cw_for_source" +msgstr "" + +msgctxt "CWSourceSchemaConfig" +msgid "cw_for_source" +msgstr "" + +msgid "cw_for_source_object" msgstr "" msgctxt "CWSource" -msgid "cw_dont_cross" -msgstr "" - -msgid "cw_dont_cross_object" -msgstr "" - -msgctxt "CWRType" -msgid "cw_dont_cross_object" +msgid "cw_for_source_object" msgstr "" msgid "cw_host_config_of" @@ -1848,18 +1875,30 @@ msgid "cw_host_config_of_object" msgstr "" -msgid "cw_may_cross" +msgid "cw_schema" +msgstr "" + +msgctxt "CWSourceSchemaConfig" +msgid "cw_schema" msgstr "" -msgctxt "CWSource" -msgid "cw_may_cross" +msgid "cw_schema_object" msgstr "" -msgid "cw_may_cross_object" +msgctxt "CWAttribute" +msgid "cw_schema_object" +msgstr "" + +msgctxt "CWEType" +msgid "cw_schema_object" msgstr "" msgctxt "CWRType" -msgid "cw_may_cross_object" +msgid "cw_schema_object" +msgstr "" + +msgctxt "CWRelation" +msgid "cw_schema_object" msgstr "" msgid "cw_source" @@ -1868,24 +1907,6 @@ msgid "cw_source_object" msgstr "" -msgid "cw_support" -msgstr "" - -msgctxt "CWSource" -msgid "cw_support" -msgstr "" - -msgid "cw_support_object" -msgstr "" - -msgctxt "CWEType" -msgid "cw_support_object" -msgstr "" - -msgctxt "CWRType" -msgid "cw_support_object" -msgstr "" - msgid "cwetype-box" msgstr "Box-Ansicht" @@ -1913,15 +1934,30 @@ msgid "cwrtype-permissions" msgstr "Berechtigungen" +msgid "cwsource-main" +msgstr "" + +msgid "cwsource-mapping" +msgstr "" + msgid "cwuri" msgstr "interner URI" msgid "data directory url" msgstr "URL des Daten-Pools" +msgid "data sources" +msgstr "" + +msgid "data sources management" +msgstr "" + msgid "date" msgstr "Datum" +msgid "day" +msgstr "" + msgid "deactivate" msgstr "deaktivieren" @@ -2221,9 +2257,6 @@ msgid "edit canceled" msgstr "Änderungen verwerfen" -msgid "edit the index page" -msgstr "Index-Seite bearbeiten" - msgid "editable-table" msgstr "bearbeitbare Tabelle" @@ -2248,6 +2281,9 @@ msgid "entities deleted" msgstr "Entitäten gelöscht" +msgid "entity and relation types can't be mapped, only attributes or relations" +msgstr "" + msgid "entity copied" msgstr "Entität kopiert" @@ -2287,6 +2323,9 @@ msgid "entity update" msgstr "Aktualisierung der Entität" +msgid "error" +msgstr "" + msgid "error while embedding page" msgstr "Fehler beim Einbetten der Seite" @@ -2503,6 +2542,9 @@ msgid "fulltextindexed" msgstr "indizierter Text" +msgid "gc" +msgstr "" + msgid "generic plot" msgstr "generischer Plot" @@ -2732,6 +2774,10 @@ msgid "inlined" msgstr "eingereiht" +#, python-format +msgid "inlined relation %(rtype)s of %(etype)s should be supported" +msgstr "" + msgid "instance home" msgstr "Startseite der Instanz" @@ -2836,9 +2882,19 @@ msgid "latest modification time of an entity" msgstr "Datum der letzten Änderung einer Entität" +msgid "latest synchronization time" +msgstr "" + msgid "latest update on" msgstr "letzte Änderung am" +msgid "latest_retrieval" +msgstr "" + +msgctxt "CWSource" +msgid "latest_retrieval" +msgstr "" + msgid "left" msgstr "links" @@ -2990,6 +3046,9 @@ msgid "monday" msgstr "Montag" +msgid "month" +msgstr "" + msgid "more actions" msgstr "weitere Aktionen" @@ -3179,6 +3238,10 @@ msgid "options" msgstr "Optionen" +msgctxt "CWSourceSchemaConfig" +msgid "options" +msgstr "" + msgid "order" msgstr "Reihenfolge" @@ -3219,6 +3282,16 @@ "Notwendige Daten scheinen nicht mehr gültig zu sein. Bitte laden Sie die " "Seite neu und beginnen Sie von vorn." +msgid "parser" +msgstr "" + +msgctxt "CWSource" +msgid "parser" +msgstr "" + +msgid "parser to use to extract entities from content retrieved at given URLs." +msgstr "" + msgid "password" msgstr "Passwort" @@ -3315,6 +3388,9 @@ msgid "rdef-permissions" msgstr "Rechte" +msgid "rdf" +msgstr "" + msgid "read" msgstr "Lesen" @@ -3363,6 +3439,24 @@ msgid "relation %(relname)s of %(ent)s" msgstr "Relation %(relname)s von %(ent)s" +#, python-format +msgid "" +"relation %(rtype)s with %(etype)s as %(role)s is supported but no target " +"type supported" +msgstr "" + +#, python-format +msgid "" +"relation %(type)s with %(etype)s as %(role)s and target type %(target)s is " +"mandatory but not supported" +msgstr "" + +#, python-format +msgid "" +"relation %s is supported but none if its definitions matches supported " +"entities" +msgstr "" + msgid "relation add" msgstr "Relation hinzufügen" @@ -3402,7 +3496,7 @@ msgctxt "CWRType" msgid "relations_object" -msgstr "" +msgstr "Relationen von" msgid "relative url of the bookmarked page" msgstr "URL relativ zu der Seite" @@ -3485,9 +3579,6 @@ msgid "saturday" msgstr "Samstag" -msgid "schema entities" -msgstr "Entitäten, die das Schema definieren" - msgid "schema's permissions definitions" msgstr "Im Schema definierte Rechte" @@ -3500,9 +3591,6 @@ msgid "schema-relation-types" msgstr "Relationstypen" -msgid "schema-security" -msgstr "Rechte" - msgid "search" msgstr "suchen" @@ -3614,6 +3702,9 @@ "Eine Eigenschaft für die gesamte Website kann nicht für einen Nutzer gesetzt " "werden." +msgid "siteinfo" +msgstr "" + msgid "some errors occurred:" msgstr "Einige Fehler sind aufgetreten" @@ -3650,6 +3741,10 @@ msgid "specializes_object" msgstr "Vorgänger von" +#, python-format +msgid "specifying %s is mandatory" +msgstr "" + msgid "startup views" msgstr "Start-Ansichten" @@ -3779,8 +3874,8 @@ msgid "symmetric" msgstr "symmetrisch" -msgid "system entities" -msgstr "System-Entitäten" +msgid "synchronization-interval must be greater than 1 minute" +msgstr "" msgid "table" msgstr "Tabelle" @@ -3812,6 +3907,9 @@ msgid "the prefered email" msgstr "primäre E-Mail-Adresse" +msgid "the system source has its configuration stored on the file-system" +msgstr "" + #, python-format msgid "the value \"%s\" is already used, use another one" msgstr "" @@ -3823,9 +3921,15 @@ msgid "this entity is currently owned by" msgstr "Diese Entität gehört:" +msgid "this parser doesn't use a mapping" +msgstr "" + msgid "this resource does not exist" msgstr "cette ressource est introuvable" +msgid "this source doesn't use a mapping" +msgstr "" + msgid "thursday" msgstr "Donnerstag" @@ -3839,9 +3943,6 @@ msgid "timestamp" msgstr "gültig seit" -msgid "timestamp of the latest source synchronization." -msgstr "Zeitstempel der letzten Synchronisierung mit der Quelle." - msgid "timetable" msgstr "Zeitplan" @@ -3897,6 +3998,9 @@ msgid "to_state_object" msgstr "Übergang zu diesem Zustand" +msgid "today" +msgstr "" + msgid "todo_by" msgstr "zu erledigen bis" @@ -4031,15 +4135,23 @@ msgstr "(Externe) Entität nicht gefunden" #, python-format +msgid "unknown option(s): %s" +msgstr "" + +#, python-format +msgid "unknown options %s" +msgstr "" + +#, python-format msgid "unknown property key %s" msgstr "Unbekannter Eigentumsschlüssel %s" +msgid "unknown source type" +msgstr "" + msgid "unknown vocabulary:" msgstr "Unbekanntes Wörterbuch : " -msgid "up" -msgstr "nach oben" - msgid "upassword" msgstr "Passwort" @@ -4089,6 +4201,13 @@ msgid "uri" msgstr "URI" +msgid "url" +msgstr "" + +msgctxt "CWSource" +msgid "url" +msgstr "" + msgid "use template languages" msgstr "Verwenden Sie Templating-Sprachen" @@ -4154,6 +4273,12 @@ msgid "users" msgstr "Nutzer" +msgid "users and groups" +msgstr "" + +msgid "users and groups management" +msgstr "" + msgid "users using this bookmark" msgstr "Nutzer, die dieses Lesezeichen verwenden" @@ -4236,6 +4361,9 @@ msgid "visible" msgstr "sichtbar" +msgid "warning" +msgstr "" + msgid "we are not yet ready to handle this query" msgstr "Momentan können wir diese sparql-Anfrage noch nicht ausführen." @@ -4330,12 +4458,86 @@ msgid "you have been logged out" msgstr "Sie sind jetzt abgemeldet." +#, python-format +msgid "you may want to specify something for %s" +msgstr "" + msgid "you should probably delete that property" msgstr "Sie sollten diese Eigenschaft wahrscheinlich löschen." +#, python-format +msgid "you should un-inline relation %s which is supported and may be crossed " +msgstr "" + +#~ msgid "Attributes with non default permissions:" +#~ msgstr "Attribute mit nicht-standard-Berechtigungen" + +#~ msgid "Entity types" +#~ msgstr "Entitätstypen" + +#~ msgid "Index" +#~ msgstr "Index" + +#~ msgid "Permissions for entity types" +#~ msgstr "Berechtigungen für Entitätstypen" + +#~ msgid "Permissions for relations" +#~ msgstr "Berechtigungen für Relationen" + +#~ msgid "Relation types" +#~ msgstr "Relationstypen" + +#~ msgid "am/pm calendar (month)" +#~ msgstr "am/pm Kalender (Monat)" + +#~ msgid "am/pm calendar (semester)" +#~ msgstr "am/pm Kalender (Halbjahr)" + +#~ msgid "am/pm calendar (week)" +#~ msgstr "am/pm Kalender (Woche)" + +#~ msgid "am/pm calendar (year)" +#~ msgstr "am/pm Kalender (Jahr)" + +#~ msgid "application entities" +#~ msgstr "Anwendungs-Entitäten" + +#~ msgid "calendar (month)" +#~ msgstr "Kalender (monatlich)" + +#~ msgid "calendar (semester)" +#~ msgstr "Kalender (halbjährlich)" + +#~ msgid "calendar (week)" +#~ msgstr "Kalender (wöchentlich)" + +#~ msgid "calendar (year)" +#~ msgstr "Kalender (jährlich)" + #~ msgid "" #~ "can't set inlined=%(inlined)s, %(stype)s %(rtype)s %(otype)s has " #~ "cardinality=%(card)s" #~ msgstr "" -#~ "Kann 'inlined' = %(inlined)s nicht zuweisen, %(stype)s %(rtype)s " -#~ "%(otype)s hat die Kardinalität %(card)s" +#~ "Kann 'inlined' = %(inlined)s nicht zuweisen, %(stype)s %(rtype)s %(otype)" +#~ "s hat die Kardinalität %(card)s" + +#~ msgid "create an index page" +#~ msgstr "Eine Index-Seite anlegen" + +#~ msgid "edit the index page" +#~ msgstr "Index-Seite bearbeiten" + +#~ msgid "schema entities" +#~ msgstr "Entitäten, die das Schema definieren" + +#~ msgid "schema-security" +#~ msgstr "Rechte" + +#~ msgid "system entities" +#~ msgstr "System-Entitäten" + +#~ msgid "timestamp of the latest source synchronization." +#~ msgstr "Zeitstempel der letzten Synchronisierung mit der Quelle." + +#~ msgid "up" +#~ msgstr "nach oben" diff -r b8e35cde46e9 -r 36e91d19188b i18n/en.po --- a/i18n/en.po Wed Mar 16 09:37:46 2011 +0100 +++ b/i18n/en.po Wed Mar 16 09:40:56 2011 +0100 @@ -35,6 +35,13 @@ msgstr ":" #, python-format +msgid "\"action\" must be specified in options; allowed values are %s" +msgstr "" + +msgid "\"role=subject\" or \"role=object\" must be specified in options" +msgstr "" + +#, python-format msgid "%(attr)s set to %(newvalue)s" msgstr "" @@ -119,6 +126,10 @@ msgstr "" #, python-format +msgid "%s could be supported" +msgstr "" + +#, python-format msgid "%s error report" msgstr "" @@ -127,6 +138,10 @@ msgstr "" #, python-format +msgid "%s relation should not be in mapped" +msgstr "" + +#, python-format msgid "%s software version of the database" msgstr "" @@ -134,6 +149,14 @@ msgid "%s updated" msgstr "" +#, python-format +msgid "'%s' action doesn't take any options" +msgstr "" + +#, python-format +msgid "'%s' action require 'linkattr' option" +msgstr "" + msgid "(UNEXISTANT EID)" msgstr "" @@ -213,9 +236,6 @@ msgid "Attributes permissions:" msgstr "" -msgid "Attributes with non default permissions:" -msgstr "" - # schema pot file, generated on 2009-09-16 16:46:55 # # singular and plural forms for each entity type @@ -331,10 +351,16 @@ msgstr "Data source" msgid "CWSourceHostConfig" -msgstr "Host configuration" +msgstr "Source host configuration" msgid "CWSourceHostConfig_plural" -msgstr "Host configurations" +msgstr "Source host configurations" + +msgid "CWSourceSchemaConfig" +msgstr "Source schema configuration" + +msgid "CWSourceSchemaConfig_plural" +msgstr "Source schema configurations" msgid "CWSource_plural" msgstr "Data sources" @@ -409,6 +435,9 @@ msgid "Decimal_plural" msgstr "Decimal numbers" +msgid "Detected problems" +msgstr "" + msgid "Do you want to delete the following element(s) ?" msgstr "" @@ -428,7 +457,7 @@ msgid "Entities" msgstr "" -msgid "Entity types" +msgid "Entity and relation supported by this source" msgstr "" msgid "ExternalUri" @@ -461,9 +490,6 @@ msgid "Help" msgstr "" -msgid "Index" -msgstr "" - msgid "Instance" msgstr "" @@ -485,6 +511,9 @@ msgid "Looked up classes" msgstr "" +msgid "Manage" +msgstr "" + msgid "Most referenced classes" msgstr "" @@ -528,7 +557,10 @@ msgstr "New source" msgid "New CWSourceHostConfig" -msgstr "New host configuration" +msgstr "New source host configuration" + +msgid "New CWSourceSchemaConfig" +msgstr "New source schema configuration" msgid "New CWUniqueTogetherConstraint" msgstr "New unicity constraint" @@ -585,12 +617,6 @@ msgid "Password_plural" msgstr "Passwords" -msgid "Permissions for entity types" -msgstr "" - -msgid "Permissions for relations" -msgstr "" - msgid "Please note that this is only a shallow copy" msgstr "" @@ -621,9 +647,6 @@ msgid "Registry's content" msgstr "" -msgid "Relation types" -msgstr "" - msgid "Relations" msgstr "" @@ -640,6 +663,9 @@ msgid "Search for" msgstr "" +msgid "Site information" +msgstr "" + msgid "SizeConstraint" msgstr "size constraint" @@ -736,7 +762,10 @@ msgstr "This data source" msgid "This CWSourceHostConfig" -msgstr "This host configuration" +msgstr "This source host configuration" + +msgid "This CWSourceSchemaConfig" +msgstr "This source schema configuration" msgid "This CWUniqueTogetherConstraint" msgstr "This unicity constraint" @@ -792,6 +821,9 @@ msgid "Transition_plural" msgstr "Transitions" +msgid "URLs from which content will be imported. You can put one url per line" +msgstr "" + msgid "UniqueConstraint" msgstr "unique constraint" @@ -977,6 +1009,10 @@ msgid "add WorkflowTransition transition_of Workflow object" msgstr "workflow-transition" +#, python-format +msgid "add a %s" +msgstr "" + msgctxt "inlined:CWRelation.from_entity.subject" msgid "add a CWEType" msgstr "add an entity type" @@ -989,6 +1025,12 @@ msgid "add a CWRType" msgstr "add a relation type" +msgid "add a CWSource" +msgstr "add a source" + +msgid "add a CWSourceSchemaConfig" +msgstr "add an item to mapping " + msgctxt "inlined:CWUser.use_email.subject" msgid "add a EmailAddress" msgstr "add an email address" @@ -1055,6 +1097,9 @@ msgid "allow to set a specific workflow for an entity" msgstr "" +msgid "allowed options depends on the source type" +msgstr "" + msgid "allowed transitions from this state" msgstr "" @@ -1080,18 +1125,6 @@ msgid "allowed_transition_object" msgstr "incoming states" -msgid "am/pm calendar (month)" -msgstr "" - -msgid "am/pm calendar (semester)" -msgstr "" - -msgid "am/pm calendar (week)" -msgstr "" - -msgid "am/pm calendar (year)" -msgstr "" - msgid "an electronic mail address associated to a short alias" msgstr "" @@ -1116,9 +1149,6 @@ msgid "anonymous" msgstr "" -msgid "application entities" -msgstr "" - msgid "april" msgstr "" @@ -1137,6 +1167,9 @@ msgid "attribute" msgstr "" +msgid "attribute/relation can't be mapped, only entity and relation types" +msgstr "" + msgid "august" msgstr "" @@ -1233,18 +1266,6 @@ msgid "calendar" msgstr "" -msgid "calendar (month)" -msgstr "" - -msgid "calendar (semester)" -msgstr "" - -msgid "calendar (week)" -msgstr "" - -msgid "calendar (year)" -msgstr "" - msgid "can not resolve entity types:" msgstr "" @@ -1258,6 +1279,9 @@ msgid "can't change the %s attribute" msgstr "" +msgid "can't change this relation" +msgstr "" + #, python-format msgid "can't connect to source %s, some data may be missing" msgstr "" @@ -1269,6 +1293,12 @@ msgid "can't have multiple exits on the same state" msgstr "" +msgid "can't mix dontcross and maycross options" +msgstr "" + +msgid "can't mix dontcross and write options" +msgstr "" + #, python-format msgid "can't parse %(value)r (expected %(format)s)" msgstr "" @@ -1399,11 +1429,11 @@ msgctxt "CWSource" msgid "config" -msgstr "" +msgstr "configuration" msgctxt "CWSourceHostConfig" msgid "config" -msgstr "" +msgstr "configuration" msgid "config mode" msgstr "" @@ -1503,9 +1533,6 @@ msgid "create an index for quick search on this attribute" msgstr "" -msgid "create an index page" -msgstr "" - msgid "created on" msgstr "" @@ -1775,71 +1802,65 @@ msgid "custom_workflow_object" msgstr "custom workflow of" -msgid "cw_dont_cross" -msgstr "" +msgid "cw_for_source" +msgstr "for source" + +msgctxt "CWSourceSchemaConfig" +msgid "cw_for_source" +msgstr "for source" + +msgid "cw_for_source_object" +msgstr "mapping" msgctxt "CWSource" -msgid "cw_dont_cross" -msgstr "" - -msgid "cw_dont_cross_object" -msgstr "" - -msgctxt "CWRType" -msgid "cw_dont_cross_object" -msgstr "" +msgid "cw_for_source_object" +msgstr "mapping" msgid "cw_host_config_of" -msgstr "" +msgstr "source" msgctxt "CWSourceHostConfig" msgid "cw_host_config_of" -msgstr "" +msgstr "source" msgid "cw_host_config_of_object" -msgstr "" +msgstr "host configuration" msgctxt "CWSource" msgid "cw_host_config_of_object" -msgstr "" - -msgid "cw_may_cross" -msgstr "" - -msgctxt "CWSource" -msgid "cw_may_cross" -msgstr "" - -msgid "cw_may_cross_object" -msgstr "" +msgstr "host configuration" + +msgid "cw_schema" +msgstr "maps" + +msgctxt "CWSourceSchemaConfig" +msgid "cw_schema" +msgstr "maps" + +msgid "cw_schema_object" +msgstr "mapped by" + +msgctxt "CWAttribute" +msgid "cw_schema_object" +msgstr "mapped by" + +msgctxt "CWEType" +msgid "cw_schema_object" +msgstr "mapped by" msgctxt "CWRType" -msgid "cw_may_cross_object" -msgstr "" +msgid "cw_schema_object" +msgstr "mapped by" + +msgctxt "CWRelation" +msgid "cw_schema_object" +msgstr "mapped by" msgid "cw_source" -msgstr "" +msgstr "source" msgid "cw_source_object" -msgstr "" - -msgid "cw_support" -msgstr "" - -msgctxt "CWSource" -msgid "cw_support" -msgstr "" - -msgid "cw_support_object" -msgstr "" - -msgctxt "CWEType" -msgid "cw_support_object" -msgstr "" - -msgctxt "CWRType" -msgid "cw_support_object" -msgstr "" +msgstr "contains entities" msgid "cwetype-box" msgstr "\"box\" view" @@ -1868,15 +1889,30 @@ msgid "cwrtype-permissions" msgstr "permissions" +msgid "cwsource-main" +msgstr "description" + +msgid "cwsource-mapping" +msgstr "mapping" + msgid "cwuri" msgstr "internal uri" msgid "data directory url" msgstr "" +msgid "data sources" +msgstr "" + +msgid "data sources management" +msgstr "" + msgid "date" msgstr "" +msgid "day" +msgstr "" + msgid "deactivate" msgstr "" @@ -2166,9 +2202,6 @@ msgid "edit canceled" msgstr "" -msgid "edit the index page" -msgstr "" - msgid "editable-table" msgstr "" @@ -2193,6 +2226,9 @@ msgid "entities deleted" msgstr "" +msgid "entity and relation types can't be mapped, only attributes or relations" +msgstr "" + msgid "entity copied" msgstr "" @@ -2231,6 +2267,9 @@ msgid "entity update" msgstr "" +msgid "error" +msgstr "" + msgid "error while embedding page" msgstr "" @@ -2340,11 +2379,11 @@ msgctxt "CWEType" msgid "final" -msgstr "" +msgstr "final" msgctxt "CWRType" msgid "final" -msgstr "" +msgstr "final" msgid "first name" msgstr "" @@ -2354,7 +2393,7 @@ msgctxt "CWUser" msgid "firstname" -msgstr "" +msgstr "firstname" msgid "foaf" msgstr "" @@ -2445,6 +2484,9 @@ msgid "fulltextindexed" msgstr "fulltext indexed" +msgid "gc" +msgstr "memory leak" + msgid "generic plot" msgstr "" @@ -2657,6 +2699,10 @@ msgid "inlined" msgstr "inlined" +#, python-format +msgid "inlined relation %(rtype)s of %(etype)s should be supported" +msgstr "" + msgid "instance home" msgstr "" @@ -2671,7 +2717,7 @@ msgctxt "CWAttribute" msgid "internationalizable" -msgstr "" +msgstr "internationalizable" #, python-format msgid "invalid action %r" @@ -2756,9 +2802,19 @@ msgid "latest modification time of an entity" msgstr "" +msgid "latest synchronization time" +msgstr "" + msgid "latest update on" msgstr "" +msgid "latest_retrieval" +msgstr "latest retrieval" + +msgctxt "CWSource" +msgid "latest_retrieval" +msgstr "latest retrieval" + msgid "left" msgstr "" @@ -2836,7 +2892,7 @@ msgctxt "RQLExpression" msgid "mainvars" -msgstr "main vars" +msgstr "main variables" msgid "manage" msgstr "" @@ -2860,11 +2916,11 @@ msgstr "" msgid "match_host" -msgstr "" +msgstr "match host" msgctxt "CWSourceHostConfig" msgid "match_host" -msgstr "" +msgstr "match host" msgid "maximum number of characters in short description" msgstr "" @@ -2903,6 +2959,9 @@ msgid "monday" msgstr "" +msgid "month" +msgstr "" + msgid "more actions" msgstr "" @@ -2928,15 +2987,15 @@ msgctxt "CWConstraintType" msgid "name" -msgstr "" +msgstr "name" msgctxt "CWEType" msgid "name" -msgstr "" +msgstr "name" msgctxt "CWGroup" msgid "name" -msgstr "" +msgstr "name" msgctxt "CWPermission" msgid "name" @@ -2948,7 +3007,7 @@ msgctxt "CWSource" msgid "name" -msgstr "" +msgstr "name" msgctxt "State" msgid "name" @@ -2956,15 +3015,15 @@ msgctxt "Transition" msgid "name" -msgstr "" +msgstr "name" msgctxt "Workflow" msgid "name" -msgstr "" +msgstr "name" msgctxt "WorkflowTransition" msgid "name" -msgstr "" +msgstr "name" msgid "name of the cache" msgstr "" @@ -3090,6 +3149,10 @@ msgid "options" msgstr "" +msgctxt "CWSourceSchemaConfig" +msgid "options" +msgstr "options" + msgid "order" msgstr "" @@ -3129,6 +3192,16 @@ msgstr "" "some necessary data seem expired, please reload the page and try again." +msgid "parser" +msgstr "" + +msgctxt "CWSource" +msgid "parser" +msgstr "parser" + +msgid "parser to use to extract entities from content retrieved at given URLs." +msgstr "" + msgid "password" msgstr "" @@ -3225,6 +3298,9 @@ msgid "rdef-permissions" msgstr "permissions" +msgid "rdf" +msgstr "" + msgid "read" msgstr "" @@ -3273,6 +3349,24 @@ msgid "relation %(relname)s of %(ent)s" msgstr "" +#, python-format +msgid "" +"relation %(rtype)s with %(etype)s as %(role)s is supported but no target " +"type supported" +msgstr "" + +#, python-format +msgid "" +"relation %(type)s with %(etype)s as %(role)s and target type %(target)s is " +"mandatory but not supported" +msgstr "" + +#, python-format +msgid "" +"relation %s is supported but none if its definitions matches supported " +"entities" +msgstr "" + msgid "relation add" msgstr "" @@ -3302,7 +3396,7 @@ msgctxt "CWUniqueTogetherConstraint" msgid "relations" -msgstr "" +msgstr "relations" msgid "relations deleted" msgstr "" @@ -3392,9 +3486,6 @@ msgid "saturday" msgstr "" -msgid "schema entities" -msgstr "" - msgid "schema's permissions definitions" msgstr "" @@ -3407,9 +3498,6 @@ msgid "schema-relation-types" msgstr "relations" -msgid "schema-security" -msgstr "permissions" - msgid "search" msgstr "" @@ -3516,6 +3604,9 @@ msgid "site-wide property can't be set for user" msgstr "" +msgid "siteinfo" +msgstr "site information" + msgid "some errors occurred:" msgstr "" @@ -3550,6 +3641,10 @@ msgid "specializes_object" msgstr "specialized by" +#, python-format +msgid "specifying %s is mandatory" +msgstr "" + msgid "startup views" msgstr "" @@ -3614,7 +3709,7 @@ msgctxt "WorkflowTransition" msgid "subworkflow" -msgstr "" +msgstr "subworkflow" msgid "" "subworkflow isn't a workflow for the same types as the transition's workflow" @@ -3675,7 +3770,7 @@ msgid "symmetric" msgstr "symmetric" -msgid "system entities" +msgid "synchronization-interval must be greater than 1 minute" msgstr "" msgid "table" @@ -3708,6 +3803,9 @@ msgid "the prefered email" msgstr "" +msgid "the system source has its configuration stored on the file-system" +msgstr "" + #, python-format msgid "the value \"%s\" is already used, use another one" msgstr "" @@ -3718,9 +3816,15 @@ msgid "this entity is currently owned by" msgstr "" +msgid "this parser doesn't use a mapping" +msgstr "" + msgid "this resource does not exist" msgstr "" +msgid "this source doesn't use a mapping" +msgstr "" + msgid "thursday" msgstr "" @@ -3734,9 +3838,6 @@ msgid "timestamp" msgstr "timestamp" -msgid "timestamp of the latest source synchronization." -msgstr "" - msgid "timetable" msgstr "" @@ -3745,7 +3846,7 @@ msgctxt "Bookmark" msgid "title" -msgstr "" +msgstr "title" msgid "to" msgstr "" @@ -3792,6 +3893,9 @@ msgid "to_state_object" msgstr "transitions to this state" +msgid "today" +msgstr "" + msgid "todo_by" msgstr "to do by" @@ -3858,7 +3962,7 @@ msgctxt "CWSource" msgid "type" -msgstr "" +msgstr "type" msgctxt "Transition" msgid "type" @@ -3926,15 +4030,23 @@ msgstr "" #, python-format +msgid "unknown option(s): %s" +msgstr "" + +#, python-format +msgid "unknown options %s" +msgstr "" + +#, python-format msgid "unknown property key %s" msgstr "" +msgid "unknown source type" +msgstr "" + msgid "unknown vocabulary:" msgstr "" -msgid "up" -msgstr "" - msgid "upassword" msgstr "password" @@ -3982,7 +4094,14 @@ msgctxt "ExternalUri" msgid "uri" -msgstr "" +msgstr "uri" + +msgid "url" +msgstr "" + +msgctxt "CWSource" +msgid "url" +msgstr "url" msgid "use template languages" msgstr "" @@ -4040,6 +4159,12 @@ msgid "users" msgstr "" +msgid "users and groups" +msgstr "" + +msgid "users and groups management" +msgstr "" + msgid "users using this bookmark" msgstr "" @@ -4120,6 +4245,9 @@ msgid "visible" msgstr "" +msgid "warning" +msgstr "" + msgid "we are not yet ready to handle this query" msgstr "" @@ -4212,5 +4340,20 @@ msgid "you have been logged out" msgstr "" +#, python-format +msgid "you may want to specify something for %s" +msgstr "" + msgid "you should probably delete that property" msgstr "" + +#, python-format +msgid "you should un-inline relation %s which is supported and may be crossed " +msgstr "" + +#~ msgctxt "CWAttribute" +#~ msgid "relations_object" +#~ msgstr "constrained by" + +#~ msgid "schema-security" +#~ msgstr "permissions" diff -r b8e35cde46e9 -r 36e91d19188b i18n/es.po --- a/i18n/es.po Wed Mar 16 09:37:46 2011 +0100 +++ b/i18n/es.po Wed Mar 16 09:40:56 2011 +0100 @@ -41,6 +41,13 @@ msgstr ":" #, python-format +msgid "\"action\" must be specified in options; allowed values are %s" +msgstr "" + +msgid "\"role=subject\" or \"role=object\" must be specified in options" +msgstr "" + +#, python-format msgid "%(attr)s set to %(newvalue)s" msgstr "%(attr)s modificado a %(newvalue)s" @@ -125,6 +132,10 @@ msgstr "%d años" #, python-format +msgid "%s could be supported" +msgstr "" + +#, python-format msgid "%s error report" msgstr "%s reporte de errores" @@ -133,6 +144,10 @@ msgstr "%s no estimado(s)" #, python-format +msgid "%s relation should not be in mapped" +msgstr "" + +#, python-format msgid "%s software version of the database" msgstr "versión sistema de la base para %s" @@ -140,6 +155,14 @@ msgid "%s updated" msgstr "%s actualizado" +#, python-format +msgid "'%s' action doesn't take any options" +msgstr "" + +#, python-format +msgid "'%s' action require 'linkattr' option" +msgstr "" + msgid "(UNEXISTANT EID)" msgstr "(EID INEXISTENTE" @@ -222,9 +245,6 @@ msgid "Attributes permissions:" msgstr "Permisos de atributos:" -msgid "Attributes with non default permissions:" -msgstr "Atributos con permisos no estándares" - # schema pot file, generated on 2009-09-16 16:46:55 # # singular and plural forms for each entity type @@ -345,6 +365,12 @@ msgid "CWSourceHostConfig_plural" msgstr "" +msgid "CWSourceSchemaConfig" +msgstr "" + +msgid "CWSourceSchemaConfig_plural" +msgstr "" + msgid "CWSource_plural" msgstr "" @@ -430,6 +456,9 @@ msgid "Decimal_plural" msgstr "Decimales" +msgid "Detected problems" +msgstr "" + msgid "Do you want to delete the following element(s) ?" msgstr "Desea eliminar el(los) elemento(s) siguiente(s)" @@ -449,8 +478,8 @@ msgid "Entities" msgstr "Entidades" -msgid "Entity types" -msgstr "Tipos de entidades" +msgid "Entity and relation supported by this source" +msgstr "" msgid "ExternalUri" msgstr "Uri externo" @@ -482,9 +511,6 @@ msgid "Help" msgstr "Ayuda" -msgid "Index" -msgstr "Índice" - msgid "Instance" msgstr "Instancia" @@ -506,6 +532,9 @@ msgid "Looked up classes" msgstr "Clases buscadas" +msgid "Manage" +msgstr "" + msgid "Most referenced classes" msgstr "Clases más referenciadas" @@ -551,6 +580,9 @@ msgid "New CWSourceHostConfig" msgstr "" +msgid "New CWSourceSchemaConfig" +msgstr "" + msgid "New CWUniqueTogetherConstraint" msgstr "" @@ -606,12 +638,6 @@ msgid "Password_plural" msgstr "Contraseñas" -msgid "Permissions for entity types" -msgstr "Permisos por tipos de entidad" - -msgid "Permissions for relations" -msgstr "Permisos por las relaciones" - msgid "Please note that this is only a shallow copy" msgstr "Recuerde que sólo es una copia superficial" @@ -642,9 +668,6 @@ msgid "Registry's content" msgstr "Contenido del registro" -msgid "Relation types" -msgstr "Tipos de relación" - msgid "Relations" msgstr "Relaciones" @@ -661,6 +684,9 @@ msgid "Search for" msgstr "Buscar" +msgid "Site information" +msgstr "" + msgid "SizeConstraint" msgstr "Restricción de tamaño" @@ -759,6 +785,9 @@ msgid "This CWSourceHostConfig" msgstr "" +msgid "This CWSourceSchemaConfig" +msgstr "" + msgid "This CWUniqueTogetherConstraint" msgstr "" @@ -813,6 +842,9 @@ msgid "Transition_plural" msgstr "Transiciones" +msgid "URLs from which content will be imported. You can put one url per line" +msgstr "" + msgid "UniqueConstraint" msgstr "Restricción de Unicidad" @@ -1019,6 +1051,10 @@ msgid "add WorkflowTransition transition_of Workflow object" msgstr "Transición Workflow" +#, python-format +msgid "add a %s" +msgstr "" + msgctxt "inlined:CWRelation.from_entity.subject" msgid "add a CWEType" msgstr "Agregar un tipo de entidad" @@ -1031,6 +1067,12 @@ msgid "add a CWRType" msgstr "Agregar un tipo de relación" +msgid "add a CWSource" +msgstr "" + +msgid "add a CWSourceSchemaConfig" +msgstr "" + msgctxt "inlined:CWUser.use_email.subject" msgid "add a EmailAddress" msgstr "Agregar correo electrónico" @@ -1099,6 +1141,9 @@ msgid "allow to set a specific workflow for an entity" msgstr "permite definir un Workflow específico para una entidad" +msgid "allowed options depends on the source type" +msgstr "" + msgid "allowed transitions from this state" msgstr "transiciones autorizadas desde este estado" @@ -1124,18 +1169,6 @@ msgid "allowed_transition_object" msgstr "transición autorizada de" -msgid "am/pm calendar (month)" -msgstr "calendario am/pm (mes)" - -msgid "am/pm calendar (semester)" -msgstr "calendario am/pm (semestre)" - -msgid "am/pm calendar (week)" -msgstr "calendario am/pm (semana)" - -msgid "am/pm calendar (year)" -msgstr "calendario am/pm (año)" - msgid "an electronic mail address associated to a short alias" msgstr "una dirección electrónica asociada a este alias" @@ -1160,9 +1193,6 @@ msgid "anonymous" msgstr "anónimo" -msgid "application entities" -msgstr "Entidades de la aplicación" - msgid "april" msgstr "Abril" @@ -1183,6 +1213,9 @@ msgid "attribute" msgstr "Atributo" +msgid "attribute/relation can't be mapped, only entity and relation types" +msgstr "" + msgid "august" msgstr "Agosto" @@ -1279,18 +1312,6 @@ msgid "calendar" msgstr "mostrar un calendario" -msgid "calendar (month)" -msgstr "calendario (mensual)" - -msgid "calendar (semester)" -msgstr "calendario (semestral)" - -msgid "calendar (week)" -msgstr "calendario (semanal)" - -msgid "calendar (year)" -msgstr "calendario (anual)" - msgid "can not resolve entity types:" msgstr "Imposible de interpretar los tipos de entidades:" @@ -1304,6 +1325,9 @@ msgid "can't change the %s attribute" msgstr "no puede modificar el atributo %s" +msgid "can't change this relation" +msgstr "" + #, python-format msgid "can't connect to source %s, some data may be missing" msgstr "no se puede conectar a la fuente %s, algunos datos pueden faltar" @@ -1315,6 +1339,12 @@ msgid "can't have multiple exits on the same state" msgstr "no puede tener varias salidas en el mismo estado" +msgid "can't mix dontcross and maycross options" +msgstr "" + +msgid "can't mix dontcross and write options" +msgstr "" + #, python-format msgid "can't parse %(value)r (expected %(format)s)" msgstr "no puede analizar %(value)r (formato requerido : %(format)s)" @@ -1324,8 +1354,8 @@ "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality=" "%(card)s" msgstr "" -"no puede poner 'inlined' = True, %(stype)s %(rtype)s %(otype)s " - "tiene cardinalidad %(card)s" +"no puede poner 'inlined' = True, %(stype)s %(rtype)s %(otype)s tiene " +"cardinalidad %(card)s" msgid "cancel" msgstr "" @@ -1560,9 +1590,6 @@ msgid "create an index for quick search on this attribute" msgstr "Crear un índice para accelerar las búsquedas sobre este atributo" -msgid "create an index page" -msgstr "Crear una página de inicio" - msgid "created on" msgstr "creado el" @@ -1839,18 +1866,18 @@ msgid "custom_workflow_object" msgstr "Workflow de" -msgid "cw_dont_cross" +msgid "cw_for_source" +msgstr "" + +msgctxt "CWSourceSchemaConfig" +msgid "cw_for_source" +msgstr "" + +msgid "cw_for_source_object" msgstr "" msgctxt "CWSource" -msgid "cw_dont_cross" -msgstr "" - -msgid "cw_dont_cross_object" -msgstr "" - -msgctxt "CWRType" -msgid "cw_dont_cross_object" +msgid "cw_for_source_object" msgstr "" msgid "cw_host_config_of" @@ -1867,18 +1894,30 @@ msgid "cw_host_config_of_object" msgstr "" -msgid "cw_may_cross" +msgid "cw_schema" +msgstr "" + +msgctxt "CWSourceSchemaConfig" +msgid "cw_schema" msgstr "" -msgctxt "CWSource" -msgid "cw_may_cross" +msgid "cw_schema_object" msgstr "" -msgid "cw_may_cross_object" +msgctxt "CWAttribute" +msgid "cw_schema_object" +msgstr "" + +msgctxt "CWEType" +msgid "cw_schema_object" msgstr "" msgctxt "CWRType" -msgid "cw_may_cross_object" +msgid "cw_schema_object" +msgstr "" + +msgctxt "CWRelation" +msgid "cw_schema_object" msgstr "" msgid "cw_source" @@ -1887,24 +1926,6 @@ msgid "cw_source_object" msgstr "" -msgid "cw_support" -msgstr "" - -msgctxt "CWSource" -msgid "cw_support" -msgstr "" - -msgid "cw_support_object" -msgstr "" - -msgctxt "CWEType" -msgid "cw_support_object" -msgstr "" - -msgctxt "CWRType" -msgid "cw_support_object" -msgstr "" - msgid "cwetype-box" msgstr "Vista \"caja\"" @@ -1932,15 +1953,30 @@ msgid "cwrtype-permissions" msgstr "Permisos" +msgid "cwsource-main" +msgstr "" + +msgid "cwsource-mapping" +msgstr "" + msgid "cwuri" msgstr "Uri Interna" msgid "data directory url" msgstr "Url del repertorio de datos" +msgid "data sources" +msgstr "" + +msgid "data sources management" +msgstr "" + msgid "date" msgstr "Fecha" +msgid "day" +msgstr "" + msgid "deactivate" msgstr "Desactivar" @@ -2250,9 +2286,6 @@ msgid "edit canceled" msgstr "Edición cancelada" -msgid "edit the index page" -msgstr "Modificar la página de inicio" - msgid "editable-table" msgstr "Tabla modificable" @@ -2277,6 +2310,9 @@ msgid "entities deleted" msgstr "Entidades eliminadas" +msgid "entity and relation types can't be mapped, only attributes or relations" +msgstr "" + msgid "entity copied" msgstr "Entidad copiada" @@ -2317,6 +2353,9 @@ msgid "entity update" msgstr "Actualización de la Entidad" +msgid "error" +msgstr "" + msgid "error while embedding page" msgstr "Error durante la inclusión de la página" @@ -2534,6 +2573,9 @@ msgid "fulltextindexed" msgstr "Texto indexado" +msgid "gc" +msgstr "" + msgid "generic plot" msgstr "Gráfica Genérica" @@ -2762,6 +2804,10 @@ msgid "inlined" msgstr "Inlined" +#, python-format +msgid "inlined relation %(rtype)s of %(etype)s should be supported" +msgstr "" + msgid "instance home" msgstr "Repertorio de la Instancia" @@ -2865,9 +2911,19 @@ msgid "latest modification time of an entity" msgstr "Fecha de la última modificación de una entidad " +msgid "latest synchronization time" +msgstr "" + msgid "latest update on" msgstr "Actualizado el" +msgid "latest_retrieval" +msgstr "" + +msgctxt "CWSource" +msgid "latest_retrieval" +msgstr "" + msgid "left" msgstr "izquierda" @@ -3017,6 +3073,9 @@ msgid "monday" msgstr "Lunes" +msgid "month" +msgstr "" + msgid "more actions" msgstr "Más acciones" @@ -3206,6 +3265,10 @@ msgid "options" msgstr "Opciones" +msgctxt "CWSourceSchemaConfig" +msgid "options" +msgstr "" + msgid "order" msgstr "Orden" @@ -3244,6 +3307,16 @@ msgid "pageid-not-found" msgstr "Página no encontrada." +msgid "parser" +msgstr "" + +msgctxt "CWSource" +msgid "parser" +msgstr "" + +msgid "parser to use to extract entities from content retrieved at given URLs." +msgstr "" + msgid "password" msgstr "Contraseña" @@ -3340,6 +3413,9 @@ msgid "rdef-permissions" msgstr "Permisos" +msgid "rdf" +msgstr "" + msgid "read" msgstr "Lectura" @@ -3388,6 +3464,24 @@ msgid "relation %(relname)s of %(ent)s" msgstr "relación %(relname)s de %(ent)s" +#, python-format +msgid "" +"relation %(rtype)s with %(etype)s as %(role)s is supported but no target " +"type supported" +msgstr "" + +#, python-format +msgid "" +"relation %(type)s with %(etype)s as %(role)s and target type %(target)s is " +"mandatory but not supported" +msgstr "" + +#, python-format +msgid "" +"relation %s is supported but none if its definitions matches supported " +"entities" +msgstr "" + msgid "relation add" msgstr "Agregar Relación" @@ -3511,9 +3605,6 @@ msgid "saturday" msgstr "Sábado" -msgid "schema entities" -msgstr "Entidades del esquema" - msgid "schema's permissions definitions" msgstr "Definiciones de permisos del esquema" @@ -3526,9 +3617,6 @@ msgid "schema-relation-types" msgstr "Relaciones" -msgid "schema-security" -msgstr "Seguridad" - msgid "search" msgstr "Buscar" @@ -3639,6 +3727,9 @@ msgid "site-wide property can't be set for user" msgstr "Una propiedad específica al Sistema no puede ser propia al usuario" +msgid "siteinfo" +msgstr "" + msgid "some errors occurred:" msgstr "Algunos errores encontrados :" @@ -3674,6 +3765,10 @@ msgid "specializes_object" msgstr "Especializado por" +#, python-format +msgid "specifying %s is mandatory" +msgstr "" + msgid "startup views" msgstr "Vistas de inicio" @@ -3803,8 +3898,8 @@ msgid "symmetric" msgstr "Simétrico" -msgid "system entities" -msgstr "Entidades del sistema" +msgid "synchronization-interval must be greater than 1 minute" +msgstr "" msgid "table" msgstr "Tabla" @@ -3836,6 +3931,9 @@ msgid "the prefered email" msgstr "Dirección principal de email" +msgid "the system source has its configuration stored on the file-system" +msgstr "" + #, python-format msgid "the value \"%s\" is already used, use another one" msgstr "El valor \"%s\" ya esta en uso, favor de utilizar otro" @@ -3846,9 +3944,15 @@ msgid "this entity is currently owned by" msgstr "Esta Entidad es propiedad de" +msgid "this parser doesn't use a mapping" +msgstr "" + msgid "this resource does not exist" msgstr "Este recurso no existe" +msgid "this source doesn't use a mapping" +msgstr "" + msgid "thursday" msgstr "Jueves" @@ -3862,9 +3966,6 @@ msgid "timestamp" msgstr "Válido desde" -msgid "timestamp of the latest source synchronization." -msgstr "Fecha de la última sincronización de la fuente." - msgid "timetable" msgstr "Tablero de tiempos" @@ -3920,6 +4021,9 @@ msgid "to_state_object" msgstr "Transición hacia este Estado" +msgid "today" +msgstr "" + msgid "todo_by" msgstr "Asignada a" @@ -4054,15 +4158,23 @@ msgstr "Entidad externa desconocida" #, python-format +msgid "unknown option(s): %s" +msgstr "" + +#, python-format +msgid "unknown options %s" +msgstr "" + +#, python-format msgid "unknown property key %s" msgstr "Clave de Propiedad desconocida: %s" +msgid "unknown source type" +msgstr "" + msgid "unknown vocabulary:" msgstr "Vocabulario desconocido: " -msgid "up" -msgstr "Arriba" - msgid "upassword" msgstr "Contraseña" @@ -4112,6 +4224,13 @@ msgid "uri" msgstr "URI" +msgid "url" +msgstr "" + +msgctxt "CWSource" +msgid "url" +msgstr "" + msgid "use template languages" msgstr "Utilizar plantillas de lenguaje" @@ -4177,6 +4296,12 @@ msgid "users" msgstr "Usuarios" +msgid "users and groups" +msgstr "" + +msgid "users and groups management" +msgstr "" + msgid "users using this bookmark" msgstr "Usuarios utilizando este Favorito" @@ -4257,6 +4382,9 @@ msgid "visible" msgstr "Visible" +msgid "warning" +msgstr "" + msgid "we are not yet ready to handle this query" msgstr "Aún no podemos manejar este tipo de consulta Sparql" @@ -4352,5 +4480,79 @@ msgid "you have been logged out" msgstr "Ha terminado la sesión" +#, python-format +msgid "you may want to specify something for %s" +msgstr "" + msgid "you should probably delete that property" msgstr "Debería probablamente suprimir esta propriedad" + +#, python-format +msgid "you should un-inline relation %s which is supported and may be crossed " +msgstr "" + +#~ msgid "Attributes with non default permissions:" +#~ msgstr "Atributos con permisos no estándares" + +#~ msgid "Entity types" +#~ msgstr "Tipos de entidades" + +#~ msgid "Index" +#~ msgstr "Índice" + +#~ msgid "Permissions for entity types" +#~ msgstr "Permisos por tipos de entidad" + +#~ msgid "Permissions for relations" +#~ msgstr "Permisos por las relaciones" + +#~ msgid "Relation types" +#~ msgstr "Tipos de relación" + +#~ msgid "am/pm calendar (month)" +#~ msgstr "calendario am/pm (mes)" + +#~ msgid "am/pm calendar (semester)" +#~ msgstr "calendario am/pm (semestre)" + +#~ msgid "am/pm calendar (week)" +#~ msgstr "calendario am/pm (semana)" + +#~ msgid "am/pm calendar (year)" +#~ msgstr "calendario am/pm (año)" + +#~ msgid "application entities" +#~ msgstr "Entidades de la aplicación" + +#~ msgid "calendar (month)" +#~ msgstr "calendario (mensual)" + +#~ msgid "calendar (semester)" +#~ msgstr "calendario (semestral)" + +#~ msgid "calendar (week)" +#~ msgstr "calendario (semanal)" + +#~ msgid "calendar (year)" +#~ msgstr "calendario (anual)" + +#~ msgid "create an index page" +#~ msgstr "Crear una página de inicio" + +#~ msgid "edit the index page" +#~ msgstr "Modificar la página de inicio" + +#~ msgid "schema entities" +#~ msgstr "Entidades del esquema" + +#~ msgid "schema-security" +#~ msgstr "Seguridad" + +#~ msgid "system entities" +#~ msgstr "Entidades del sistema" + +#~ msgid "timestamp of the latest source synchronization." +#~ msgstr "Fecha de la última sincronización de la fuente." + +#~ msgid "up" +#~ msgstr "Arriba" diff -r b8e35cde46e9 -r 36e91d19188b i18n/fr.po --- a/i18n/fr.po Wed Mar 16 09:37:46 2011 +0100 +++ b/i18n/fr.po Wed Mar 16 09:40:56 2011 +0100 @@ -40,6 +40,16 @@ msgstr " :" #, python-format +msgid "\"action\" must be specified in options; allowed values are %s" +msgstr "" +"\"action\" doit être specifié dans les options; les valeurs autorisées " +"sont : %s" + +msgid "\"role=subject\" or \"role=object\" must be specified in options" +msgstr "" +"\"role=subject\" ou \"role=object\" doit être specifié dans les options" + +#, python-format msgid "%(attr)s set to %(newvalue)s" msgstr "%(attr)s modifié à %(newvalue)s" @@ -124,6 +134,10 @@ msgstr "%d années" #, python-format +msgid "%s could be supported" +msgstr "%s pourrait être supporté" + +#, python-format msgid "%s error report" msgstr "%s rapport d'erreur" @@ -132,6 +146,10 @@ msgstr "%s non estimé(s)" #, python-format +msgid "%s relation should not be in mapped" +msgstr "la relation %s ne devrait pas ếtre mappé" + +#, python-format msgid "%s software version of the database" msgstr "version logicielle de la base pour %s" @@ -139,6 +157,14 @@ msgid "%s updated" msgstr "%s mis à jour" +#, python-format +msgid "'%s' action doesn't take any options" +msgstr "l'action '%s' ne prend pas d'option" + +#, python-format +msgid "'%s' action require 'linkattr' option" +msgstr "l'action '%s' nécessite une option 'linkattr'" + msgid "(UNEXISTANT EID)" msgstr "(EID INTROUVABLE)" @@ -220,9 +246,6 @@ msgid "Attributes permissions:" msgstr "Permissions des attributs" -msgid "Attributes with non default permissions:" -msgstr "Attributs ayant des permissions non-standard" - # schema pot file, generated on 2009-09-16 16:46:55 # # singular and plural forms for each entity type @@ -343,6 +366,12 @@ msgid "CWSourceHostConfig_plural" msgstr "Configurations de source" +msgid "CWSourceSchemaConfig" +msgstr "Configuration de schéma de source" + +msgid "CWSourceSchemaConfig_plural" +msgstr "Configurations de schéma de source" + msgid "CWSource_plural" msgstr "Source de données" @@ -428,6 +457,9 @@ msgid "Decimal_plural" msgstr "Nombres décimaux" +msgid "Detected problems" +msgstr "Problèmes détectés" + msgid "Do you want to delete the following element(s) ?" msgstr "Voulez-vous supprimer le(s) élément(s) suivant(s) ?" @@ -447,8 +479,8 @@ msgid "Entities" msgstr "entités" -msgid "Entity types" -msgstr "Types d'entités" +msgid "Entity and relation supported by this source" +msgstr "Entités et relations supportés par cette source" msgid "ExternalUri" msgstr "Uri externe" @@ -480,9 +512,6 @@ msgid "Help" msgstr "Aide" -msgid "Index" -msgstr "Index" - msgid "Instance" msgstr "Instance" @@ -504,6 +533,9 @@ msgid "Looked up classes" msgstr "Classes recherchées" +msgid "Manage" +msgstr "Administration" + msgid "Most referenced classes" msgstr "Classes les plus référencées" @@ -549,6 +581,9 @@ msgid "New CWSourceHostConfig" msgstr "Nouvelle configuration de source" +msgid "New CWSourceSchemaConfig" +msgstr "Nouvelle partie de mapping de source" + msgid "New CWUniqueTogetherConstraint" msgstr "Nouvelle contrainte unique_together" @@ -604,12 +639,6 @@ msgid "Password_plural" msgstr "Mots de passe" -msgid "Permissions for entity types" -msgstr "Permissions pour les types d'entités" - -msgid "Permissions for relations" -msgstr "Permissions pour les relations" - msgid "Please note that this is only a shallow copy" msgstr "Attention, cela n'effectue qu'une copie de surface" @@ -640,9 +669,6 @@ msgid "Registry's content" msgstr "Contenu du registre" -msgid "Relation types" -msgstr "Types de relation" - msgid "Relations" msgstr "Relations" @@ -659,6 +685,9 @@ msgid "Search for" msgstr "Rechercher" +msgid "Site information" +msgstr "Information du site" + msgid "SizeConstraint" msgstr "contrainte de taille" @@ -760,6 +789,9 @@ msgid "This CWSourceHostConfig" msgstr "Cette configuration de source" +msgid "This CWSourceSchemaConfig" +msgstr "Cette partie de mapping de source" + msgid "This CWUniqueTogetherConstraint" msgstr "Cette contrainte unique_together" @@ -814,6 +846,11 @@ msgid "Transition_plural" msgstr "Transitions" +msgid "URLs from which content will be imported. You can put one url per line" +msgstr "" +"URLs depuis lesquelles le contenu sera importé. Vous pouvez mettre une URL " +"par ligne." + msgid "UniqueConstraint" msgstr "contrainte d'unicité" @@ -1020,6 +1057,10 @@ msgid "add WorkflowTransition transition_of Workflow object" msgstr "transition workflow" +#, python-format +msgid "add a %s" +msgstr "ajouter un %s" + msgctxt "inlined:CWRelation.from_entity.subject" msgid "add a CWEType" msgstr "ajouter un type d'entité sujet" @@ -1032,6 +1073,12 @@ msgid "add a CWRType" msgstr "ajouter un type de relation" +msgid "add a CWSource" +msgstr "ajouter une source" + +msgid "add a CWSourceSchemaConfig" +msgstr "ajouter une partie de mapping" + msgctxt "inlined:CWUser.use_email.subject" msgid "add a EmailAddress" msgstr "ajouter une adresse électronique" @@ -1100,6 +1147,9 @@ msgid "allow to set a specific workflow for an entity" msgstr "permet de spécifier un workflow donné pour une entité" +msgid "allowed options depends on the source type" +msgstr "les options autorisées dépendent du type de la source" + msgid "allowed transitions from this state" msgstr "transitions autorisées depuis cet état" @@ -1125,18 +1175,6 @@ msgid "allowed_transition_object" msgstr "transition autorisée de" -msgid "am/pm calendar (month)" -msgstr "calendrier am/pm (mois)" - -msgid "am/pm calendar (semester)" -msgstr "calendrier am/pm (semestre)" - -msgid "am/pm calendar (week)" -msgstr "calendrier am/pm (semaine)" - -msgid "am/pm calendar (year)" -msgstr "calendrier am/pm (année)" - msgid "an electronic mail address associated to a short alias" msgstr "une adresse électronique associée à un alias" @@ -1161,9 +1199,6 @@ msgid "anonymous" msgstr "anonyme" -msgid "application entities" -msgstr "entités applicatives" - msgid "april" msgstr "avril" @@ -1184,6 +1219,11 @@ msgid "attribute" msgstr "attribut" +msgid "attribute/relation can't be mapped, only entity and relation types" +msgstr "" +"les attributs et relations ne peuvent être mappés, uniquement les types " +"d'entité et de relation" + msgid "august" msgstr "août" @@ -1281,18 +1321,6 @@ msgid "calendar" msgstr "afficher un calendrier" -msgid "calendar (month)" -msgstr "calendrier (mensuel)" - -msgid "calendar (semester)" -msgstr "calendrier (semestriel)" - -msgid "calendar (week)" -msgstr "calendrier (hebdo)" - -msgid "calendar (year)" -msgstr "calendrier (annuel)" - msgid "can not resolve entity types:" msgstr "impossible d'interpréter les types d'entités :" @@ -1306,6 +1334,9 @@ msgid "can't change the %s attribute" msgstr "ne peut changer l'attribut %s" +msgid "can't change this relation" +msgstr "ne peut modifier cette relation" + #, python-format msgid "can't connect to source %s, some data may be missing" msgstr "ne peut se connecter à la source %s, des données peuvent manquer" @@ -1317,6 +1348,12 @@ msgid "can't have multiple exits on the same state" msgstr "ne peut avoir plusieurs sorties sur le même état" +msgid "can't mix dontcross and maycross options" +msgstr "ne peut mélanger dontcross et maycross options" + +msgid "can't mix dontcross and write options" +msgstr "ne peut mélanger dontcross et write options" + #, python-format msgid "can't parse %(value)r (expected %(format)s)" msgstr "ne peut analyser %(value)r (format attendu : %(format)s)" @@ -1326,8 +1363,8 @@ "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality=" "%(card)s" msgstr "" -"ne peut mettre 'inlined'=Vrai, %(stype)s %(rtype)s %(otype)s a " -"pour cardinalité %(card)s" +"ne peut mettre 'inlined'=Vrai, %(stype)s %(rtype)s %(otype)s a pour " +"cardinalité %(card)s" msgid "cancel" msgstr "annuler" @@ -1563,9 +1600,6 @@ msgid "create an index for quick search on this attribute" msgstr "créer un index pour accélérer les recherches sur cet attribut" -msgid "create an index page" -msgstr "créer une page d'accueil" - msgid "created on" msgstr "créé le" @@ -1845,19 +1879,19 @@ msgid "custom_workflow_object" msgstr "workflow de" -msgid "cw_dont_cross" -msgstr "don't cross" +msgid "cw_for_source" +msgstr "source" + +msgctxt "CWSourceSchemaConfig" +msgid "cw_for_source" +msgstr "source" + +msgid "cw_for_source_object" +msgstr "élément de mapping" msgctxt "CWSource" -msgid "cw_dont_cross" -msgstr "don't cross" - -msgid "cw_dont_cross_object" -msgstr "can't be crossed with" - -msgctxt "CWRType" -msgid "cw_dont_cross_object" -msgstr "can't be crossed with" +msgid "cw_for_source_object" +msgstr "élément de mapping" msgid "cw_host_config_of" msgstr "host configuration of" @@ -1873,19 +1907,31 @@ msgid "cw_host_config_of_object" msgstr "has host configuration" -msgid "cw_may_cross" -msgstr "may cross" - -msgctxt "CWSource" -msgid "cw_may_cross" -msgstr "may cross" - -msgid "cw_may_cross_object" -msgstr "may be crossed with" +msgid "cw_schema" +msgstr "schéma" + +msgctxt "CWSourceSchemaConfig" +msgid "cw_schema" +msgstr "schéma" + +msgid "cw_schema_object" +msgstr "mappé par" + +msgctxt "CWAttribute" +msgid "cw_schema_object" +msgstr "mappé par" + +msgctxt "CWEType" +msgid "cw_schema_object" +msgstr "mappé par" msgctxt "CWRType" -msgid "cw_may_cross_object" -msgstr "may be crossed with" +msgid "cw_schema_object" +msgstr "mappé par" + +msgctxt "CWRelation" +msgid "cw_schema_object" +msgstr "mappé par" msgid "cw_source" msgstr "from data source" @@ -1893,24 +1939,6 @@ msgid "cw_source_object" msgstr "entities" -msgid "cw_support" -msgstr "support" - -msgctxt "CWSource" -msgid "cw_support" -msgstr "support" - -msgid "cw_support_object" -msgstr "supported by" - -msgctxt "CWEType" -msgid "cw_support_object" -msgstr "supported by" - -msgctxt "CWRType" -msgid "cw_support_object" -msgstr "supported by" - msgid "cwetype-box" msgstr "vue \"boîte\"" @@ -1938,15 +1966,30 @@ msgid "cwrtype-permissions" msgstr "permissions" +msgid "cwsource-main" +msgstr "description" + +msgid "cwsource-mapping" +msgstr "mapping" + msgid "cwuri" msgstr "uri interne" msgid "data directory url" msgstr "url du répertoire de données" +msgid "data sources" +msgstr "sources de données" + +msgid "data sources management" +msgstr "gestion des sources de données" + msgid "date" msgstr "date" +msgid "day" +msgstr "jour" + msgid "deactivate" msgstr "désactiver" @@ -2252,9 +2295,6 @@ msgid "edit canceled" msgstr "édition annulée" -msgid "edit the index page" -msgstr "éditer la page d'accueil" - msgid "editable-table" msgstr "table éditable" @@ -2279,6 +2319,11 @@ msgid "entities deleted" msgstr "entités supprimées" +msgid "entity and relation types can't be mapped, only attributes or relations" +msgstr "" +"les types d'entités et de relations ne peuvent être mappés, uniquement les " +"relations" + msgid "entity copied" msgstr "entité copiée" @@ -2318,6 +2363,9 @@ msgid "entity update" msgstr "mise à jour d'entité" +msgid "error" +msgstr "erreur" + msgid "error while embedding page" msgstr "erreur pendant l'inclusion de la page" @@ -2535,6 +2583,9 @@ msgid "fulltextindexed" msgstr "texte indexé" +msgid "gc" +msgstr "fuite mémoire" + msgid "generic plot" msgstr "tracé de courbes standard" @@ -2763,6 +2814,12 @@ msgid "inlined" msgstr "mise en ligne" +#, python-format +msgid "inlined relation %(rtype)s of %(etype)s should be supported" +msgstr "" +"la relation %(rtype)s du type d'entité %(etype)s doit être supportée " +"('inlined')" + msgid "instance home" msgstr "répertoire de l'instance" @@ -2867,9 +2924,19 @@ msgid "latest modification time of an entity" msgstr "date de dernière modification d'une entité" +msgid "latest synchronization time" +msgstr "date de la dernière synchronisation" + msgid "latest update on" msgstr "dernière mise à jour" +msgid "latest_retrieval" +msgstr "dernière synchronisation" + +msgctxt "CWSource" +msgid "latest_retrieval" +msgstr "date de la dernière synchronisation de la source." + msgid "left" msgstr "gauche" @@ -3019,6 +3086,9 @@ msgid "monday" msgstr "lundi" +msgid "month" +msgstr "mois" + msgid "more actions" msgstr "plus d'actions" @@ -3208,6 +3278,10 @@ msgid "options" msgstr "options" +msgctxt "CWSourceSchemaConfig" +msgid "options" +msgstr "options" + msgid "order" msgstr "ordre" @@ -3248,6 +3322,18 @@ "des données nécessaires semblent expirées, veuillez recharger la page et " "recommencer." +msgid "parser" +msgstr "parseur" + +msgctxt "CWSource" +msgid "parser" +msgstr "parseur" + +msgid "parser to use to extract entities from content retrieved at given URLs." +msgstr "" +"parseur à utiliser pour extraire entités et relations du contenu récupéré " +"aux URLs données" + msgid "password" msgstr "mot de passe" @@ -3344,6 +3430,9 @@ msgid "rdef-permissions" msgstr "permissions" +msgid "rdf" +msgstr "rdf" + msgid "read" msgstr "lecture" @@ -3393,6 +3482,30 @@ msgid "relation %(relname)s of %(ent)s" msgstr "relation %(relname)s de %(ent)s" +#, python-format +msgid "" +"relation %(rtype)s with %(etype)s as %(role)s is supported but no target " +"type supported" +msgstr "" +"la relation %(rtype)s avec %(etype)s comme %(role)s est supportée mais aucun " +"type cible n'est supporté" + +#, python-format +msgid "" +"relation %(type)s with %(etype)s as %(role)s and target type %(target)s is " +"mandatory but not supported" +msgstr "" +"la relation %(rtype)s avec %(etype)s comme %(role)s est obligatoire mais non " +"supportée" + +#, python-format +msgid "" +"relation %s is supported but none if its definitions matches supported " +"entities" +msgstr "" +"la relation %s est supportée mais aucune de ses définitions ne correspondent " +"aux types d'entités supportés" + msgid "relation add" msgstr "ajout de relation" @@ -3517,9 +3630,6 @@ msgid "saturday" msgstr "samedi" -msgid "schema entities" -msgstr "entités définissant le schéma" - msgid "schema's permissions definitions" msgstr "permissions définies dans le schéma" @@ -3532,9 +3642,6 @@ msgid "schema-relation-types" msgstr "types de relations" -msgid "schema-security" -msgstr "permissions" - msgid "search" msgstr "rechercher" @@ -3644,6 +3751,9 @@ msgid "site-wide property can't be set for user" msgstr "une propriété spécifique au site ne peut être propre à un utilisateur" +msgid "siteinfo" +msgstr "informations" + msgid "some errors occurred:" msgstr "des erreurs sont survenues" @@ -3682,6 +3792,10 @@ msgid "specializes_object" msgstr "parent de" +#, python-format +msgid "specifying %s is mandatory" +msgstr "spécifier %s est obligatoire" + msgid "startup views" msgstr "vues de départ" @@ -3811,8 +3925,8 @@ msgid "symmetric" msgstr "symétrique" -msgid "system entities" -msgstr "entités systèmes" +msgid "synchronization-interval must be greater than 1 minute" +msgstr "synchronization-interval doit être supérieur à 1 minute" msgid "table" msgstr "table" @@ -3844,6 +3958,9 @@ msgid "the prefered email" msgstr "l'adresse électronique principale" +msgid "the system source has its configuration stored on the file-system" +msgstr "la source système a sa configuration stockée sur le système de fichier" + #, python-format msgid "the value \"%s\" is already used, use another one" msgstr "la valeur \"%s\" est déjà utilisée, veuillez utiliser une autre valeur" @@ -3855,9 +3972,15 @@ msgid "this entity is currently owned by" msgstr "cette entité appartient à" +msgid "this parser doesn't use a mapping" +msgstr "ce parseur n'utilise pas de mapping" + msgid "this resource does not exist" msgstr "cette ressource est introuvable" +msgid "this source doesn't use a mapping" +msgstr "cette source n'utilise pas de mapping" + msgid "thursday" msgstr "jeudi" @@ -3871,9 +3994,6 @@ msgid "timestamp" msgstr "valide depuis" -msgid "timestamp of the latest source synchronization." -msgstr "date de la dernière synchronisation avec la source." - msgid "timetable" msgstr "emploi du temps" @@ -3929,6 +4049,9 @@ msgid "to_state_object" msgstr "transition vers cet état" +msgid "today" +msgstr "aujourd'hui" + msgid "todo_by" msgstr "à faire par" @@ -4063,15 +4186,23 @@ msgstr "entité (externe) introuvable" #, python-format +msgid "unknown option(s): %s" +msgstr "option(s) inconnue(s) : %s" + +#, python-format +msgid "unknown options %s" +msgstr "options inconnues : %s" + +#, python-format msgid "unknown property key %s" msgstr "clé de propriété inconnue : %s" +msgid "unknown source type" +msgstr "type de source inconnu" + msgid "unknown vocabulary:" msgstr "vocabulaire inconnu : " -msgid "up" -msgstr "haut" - msgid "upassword" msgstr "mot de passe" @@ -4121,6 +4252,13 @@ msgid "uri" msgstr "uri" +msgid "url" +msgstr "url" + +msgctxt "CWSource" +msgid "url" +msgstr "url" + msgid "use template languages" msgstr "utiliser les langages de template" @@ -4184,6 +4322,12 @@ msgid "users" msgstr "utilisateurs" +msgid "users and groups" +msgstr "utilisateurs et groupes" + +msgid "users and groups management" +msgstr "gestion des utilisateurs et groupes" + msgid "users using this bookmark" msgstr "utilisateurs utilisant ce signet" @@ -4264,6 +4408,9 @@ msgid "visible" msgstr "visible" +msgid "warning" +msgstr "attention" + msgid "we are not yet ready to handle this query" msgstr "" "nous ne sommes pas capable de gérer ce type de requête sparql pour le moment" @@ -4272,7 +4419,7 @@ msgstr "mercredi" msgid "week" -msgstr "sem." +msgstr "semaine" #, python-format msgid "welcome %s !" @@ -4360,5 +4507,75 @@ msgid "you have been logged out" msgstr "vous avez été déconnecté" +#, python-format +msgid "you may want to specify something for %s" +msgstr "vous désirez peut-être spécifié quelque chose pour la relation %s" + msgid "you should probably delete that property" msgstr "vous devriez probablement supprimer cette propriété" + +#, python-format +msgid "you should un-inline relation %s which is supported and may be crossed " +msgstr "" +"vous devriez enlevé la mise en ligne de la relation %s qui est supportée et " +"peut-être croisée" + +#~ msgid "Attributes with non default permissions:" +#~ msgstr "Attributs ayant des permissions non-standard" + +#~ msgid "Entity types" +#~ msgstr "Types d'entités" + +#~ msgid "Permissions for entity types" +#~ msgstr "Permissions pour les types d'entités" + +#~ msgid "Permissions for relations" +#~ msgstr "Permissions pour les relations" + +#~ msgid "Relation types" +#~ msgstr "Types de relation" + +#~ msgid "am/pm calendar (month)" +#~ msgstr "calendrier am/pm (mois)" + +#~ msgid "am/pm calendar (semester)" +#~ msgstr "calendrier am/pm (semestre)" + +#~ msgid "am/pm calendar (week)" +#~ msgstr "calendrier am/pm (semaine)" + +#~ msgid "am/pm calendar (year)" +#~ msgstr "calendrier am/pm (année)" + +#~ msgid "application entities" +#~ msgstr "entités applicatives" + +#~ msgid "calendar (month)" +#~ msgstr "calendrier (mensuel)" + +#~ msgid "calendar (semester)" +#~ msgstr "calendrier (semestriel)" + +#~ msgid "calendar (week)" +#~ msgstr "calendrier (hebdo)" + +#~ msgid "calendar (year)" +#~ msgstr "calendrier (annuel)" + +#~ msgid "create an index page" +#~ msgstr "créer une page d'accueil" + +#~ msgid "edit the index page" +#~ msgstr "éditer la page d'accueil" + +#~ msgid "schema entities" +#~ msgstr "entités définissant le schéma" + +#~ msgid "schema-security" +#~ msgstr "permissions" + +#~ msgid "system entities" +#~ msgstr "entités systèmes" + +#~ msgid "timestamp of the latest source synchronization." +#~ msgstr "date de la dernière synchronisation avec la source." diff -r b8e35cde46e9 -r 36e91d19188b misc/migration/3.10.0_Any.py --- a/misc/migration/3.10.0_Any.py Wed Mar 16 09:37:46 2011 +0100 +++ b/misc/migration/3.10.0_Any.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b 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 Wed Mar 16 09:40:56 2011 +0100 @@ -0,0 +1,85 @@ +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[rtype].eid), + options=u'dontcross', + ask_confirm=False) + # 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 b8e35cde46e9 -r 36e91d19188b schema.py --- a/schema.py Wed Mar 16 09:37:46 2011 +0100 +++ b/schema.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b schemas/base.py --- a/schemas/base.py Wed Mar 16 09:37:46 2011 +0100 +++ b/schemas/base.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b server/__init__.py --- a/server/__init__.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/__init__.py Wed Mar 16 09:40:56 2011 +0100 @@ -62,9 +62,9 @@ DEBUG |= debugmode class debugged(object): - """repository debugging context manager / decorator + """Context manager and decorator to help debug the repository. - Can be used either as a context manager: + It can be used either as a context manager: >>> with debugged(server.DBG_RQL | server.DBG_REPO): ... # some code in which you want to debug repository activity, @@ -77,8 +77,8 @@ ... # some code in which you want to debug repository activity, ... # seing information about RQL being executed an repository events - debug mode will be reseted at its original value when leaving the "with" - block or the decorated function + The debug mode will be reset to its original value when leaving the "with" + block or the decorated function. """ def __init__(self, debugmode): self.debugmode = debugmode @@ -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 b8e35cde46e9 -r 36e91d19188b server/checkintegrity.py --- a/server/checkintegrity.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/checkintegrity.py Wed Mar 16 09:40:56 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 @@ -377,103 +375,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 b8e35cde46e9 -r 36e91d19188b server/msplanner.py --- a/server/msplanner.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/msplanner.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b server/repository.py --- a/server/repository.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/repository.py Wed Mar 16 09:40:56 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: @@ -1029,9 +1022,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: @@ -1040,20 +1034,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 @@ -1071,13 +1055,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) @@ -1094,7 +1079,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 """ @@ -1103,7 +1088,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 """ @@ -1114,7 +1099,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 @@ -1135,18 +1120,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. """ @@ -1167,12 +1153,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) @@ -1214,6 +1199,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 b8e35cde46e9 -r 36e91d19188b server/serverctl.py --- a/server/serverctl.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/serverctl.py Wed Mar 16 09:40:56 2011 +0100 @@ -927,39 +927,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 b8e35cde46e9 -r 36e91d19188b server/sources/__init__.py --- a/server/sources/__init__.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/sources/__init__.py Wed Mar 16 09:40:56 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): @@ -536,8 +584,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 b8e35cde46e9 -r 36e91d19188b server/sources/datafeed.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/sources/datafeed.py Wed Mar 16 09:40:56 2011 +0100 @@ -0,0 +1,237 @@ +# 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: + if parser.process(url): + error = True + 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 b8e35cde46e9 -r 36e91d19188b server/sources/ldapuser.py --- a/server/sources/ldapuser.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/sources/ldapuser.py Wed Mar 16 09:40:56 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,52 @@ ) - 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.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() 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 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 +291,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 +576,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 +585,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 b8e35cde46e9 -r 36e91d19188b server/sources/native.py --- a/server/sources/native.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/sources/native.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b server/sources/pyrorql.py --- a/server/sources/pyrorql.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/sources/pyrorql.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b server/test/unittest_datafeed.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/test/unittest_datafeed.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/test/unittest_ldapuser.py Wed Mar 16 09:40:56 2011 +0100 @@ -50,8 +50,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: @@ -453,8 +452,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 b8e35cde46e9 -r 36e91d19188b server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/test/unittest_msplanner.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/test/unittest_multisources.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b server/test/unittest_querier.py --- a/server/test/unittest_querier.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/test/unittest_querier.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b server/test/unittest_ssplanner.py --- a/server/test/unittest_ssplanner.py Wed Mar 16 09:37:46 2011 +0100 +++ b/server/test/unittest_ssplanner.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b sobjects/parsers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sobjects/parsers.py Wed Mar 16 09:40:56 2011 +0100 @@ -0,0 +1,382 @@ +# 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): + if '.' in ustr: # assume %Y-%m-%d %H:%M:%S.mmmmmm + ustr = ustr.split('.',1)[0] + 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() +try: + from logilab.common import urllib2ext + _OPENER.add_handler(urllib2ext.HTTPGssapiAuthHandler()) +except ImportError: # python-kerberos not available + pass +_OPENER.add_handler(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): + try: + item = {'cwtype': unicode(node.tag), + 'cwuri': node.attrib['cwuri'], + 'eid': typed_eid(node.attrib['eid']), + } + except KeyError: + # cw < 3.11 compat mode XXX + item = {'cwtype': unicode(node.tag), + 'cwuri': node.find('cwuri').text, + 'eid': typed_eid(node.find('eid').text), + } + 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 + error = False + for item, rels in self.parse(url): + cwuri = item['cwuri'] + try: + 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() + except ValidationError, exc: + if partialcommit: + self.source.error('Skipping %s because of validation error %s' % (cwuri, exc)) + self._cw.rollback() + self._cw.set_pool() + error = True + else: + raise + return error + + 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 b8e35cde46e9 -r 36e91d19188b sobjects/test/data/schema.py --- a/sobjects/test/data/schema.py Wed Mar 16 09:37:46 2011 +0100 +++ b/sobjects/test/data/schema.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b sobjects/test/unittest_parsers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sobjects/test/unittest_parsers.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b test/unittest_schema.py --- a/test/unittest_schema.py Wed Mar 16 09:37:46 2011 +0100 +++ b/test/unittest_schema.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b utils.py --- a/utils.py Wed Mar 16 09:37:46 2011 +0100 +++ b/utils.py Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b view.py diff -r b8e35cde46e9 -r 36e91d19188b web/data/add_button.png Binary file web/data/add_button.png has changed diff -r b8e35cde46e9 -r 36e91d19188b web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Wed Mar 16 09:37:46 2011 +0100 +++ b/web/data/cubicweb.ajax.js Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b web/data/cubicweb.calendar.css --- a/web/data/cubicweb.calendar.css Wed Mar 16 09:37:46 2011 +0100 +++ b/web/data/cubicweb.calendar.css Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b web/data/cubicweb.css --- a/web/data/cubicweb.css Wed Mar 16 09:37:46 2011 +0100 +++ b/web/data/cubicweb.css Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b web/data/cubicweb.htmlhelpers.js --- a/web/data/cubicweb.htmlhelpers.js Wed Mar 16 09:37:46 2011 +0100 +++ b/web/data/cubicweb.htmlhelpers.js Wed Mar 16 09:40:56 2011 +0100 @@ -16,11 +16,15 @@ * available and inspects the tag manually otherwise.) */ function baseuri() { - var uri = document.baseURI; - if (uri) { // some browsers don't define baseURI - return uri.toLowerCase(); + if (typeof BASE_URL === 'undefined') { + // backward compatibility, BASE_URL might be undefined + var uri = document.baseURI; + if (uri) { // some browsers don't define baseURI + return uri.toLowerCase(); + } + return jQuery('base').attr('href').toLowerCase(); } - return jQuery('base').attr('href').toLowerCase(); + return BASE_URL; } /** diff -r b8e35cde46e9 -r 36e91d19188b web/data/cubicweb.lazy.js --- a/web/data/cubicweb.lazy.js Wed Mar 16 09:37:46 2011 +0100 +++ b/web/data/cubicweb.lazy.js Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Wed Mar 16 09:37:46 2011 +0100 +++ b/web/data/cubicweb.old.css Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b web/data/cubicweb.tabs.js --- a/web/data/cubicweb.tabs.js Wed Mar 16 09:37:46 2011 +0100 +++ b/web/data/cubicweb.tabs.js Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b web/data/fullcalendar.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/fullcalendar.css Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b web/data/fullcalendar.min.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/fullcalendar.min.js Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b 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 Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b 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 Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b 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 Wed Mar 16 09:40:56 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 b8e35cde46e9 -r 36e91d19188b web/facet.py --- a/web/facet.py Wed Mar 16 09:37:46 2011 +0100 +++ b/web/facet.py Wed Mar 16 09:40:56 2011 +0100 @@ -1169,11 +1169,11 @@ def _render(self): if self.selected: cssclass = ' facetValueSelected' - imgsrc = self._cw.datadir_url + self.selected_img + imgsrc = self._cw.data_url(self.selected_img) imgalt = self._cw._('selected') else: cssclass = '' - imgsrc = self._cw.datadir_url + self.unselected_img + imgsrc = self._cw.data_url(self.unselected_img) imgalt = self._cw._('not selected') self.w(u'
\n' % (cssclass, xml_escape(unicode(self.value)))) @@ -1198,11 +1198,11 @@ self.w(u'
\n' % facetid) if self.selected: cssclass = ' facetValueSelected' - imgsrc = self._cw.datadir_url + self.selected_img + imgsrc = self._cw.data_url(self.selected_img) imgalt = self._cw._('selected') else: cssclass = '' - imgsrc = self._cw.datadir_url + self.unselected_img + imgsrc = self._cw.data_url(self.unselected_img) imgalt = self._cw._('not selected') self.w(u'
\n' % (cssclass, xml_escape(unicode(self.value)))) diff -r b8e35cde46e9 -r 36e91d19188b web/formfields.py --- a/web/formfields.py Wed Mar 16 09:37:46 2011 +0100 +++ b/web/formfields.py Wed Mar 16 09:40:56 2011 +0100 @@ -733,7 +733,7 @@ wdgs.append(u'%s' % (xml_escape(uilib.toggle_action(divid)), form._cw._('show advanced fields'), - xml_escape(form._cw.build_url('data/puce_down.png')), + xml_escape(form._cw.data_url('puce_down.png')), form._cw._('show advanced fields'))) wdgs.append(u'