--- a/.hgtags Thu Mar 10 15:18:22 2011 +0100
+++ b/.hgtags Fri Mar 11 09:47:09 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
--- a/__pkginfo__.py Thu Mar 10 15:18:22 2011 +0100
+++ b/__pkginfo__.py Fri Mar 11 09:47:09 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',
}
--- a/dbapi.py Thu Mar 10 15:18:22 2011 +0100
+++ b/dbapi.py Fri Mar 11 09:47:09 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)
@@ -599,9 +598,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)
--- a/debian/changelog Thu Mar 10 15:18:22 2011 +0100
+++ b/debian/changelog Fri Mar 11 09:47:09 2011 +0100
@@ -1,3 +1,15 @@
+cubicweb (3.11.1-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Mon, 07 Mar 2011 17:21:28 +0100
+
+cubicweb (3.11.0-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Fri, 18 Feb 2011 10:27:22 +0100
+
cubicweb (3.10.8-1) unstable; urgency=low
* new upstream release
--- a/debian/control Thu Mar 10 15:18:22 2011 +0100
+++ b/debian/control Fri Mar 11 09:47:09 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
--- a/debian/copyright Thu Mar 10 15:18:22 2011 +0100
+++ b/debian/copyright Fri Mar 11 09:47:09 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:
--- a/devtools/cwwindmill.py Thu Mar 10 15:18:22 2011 +0100
+++ b/devtools/cwwindmill.py Fri Mar 11 09:47:09 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 <test script> -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
--- a/devtools/fake.py Thu Mar 10 15:18:22 2011 +0100
+++ b/devtools/fake.py Fri Mar 11 09:47:09 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:
--- a/devtools/fill.py Thu Mar 10 15:18:22 2011 +0100
+++ b/devtools/fill.py Fri Mar 11 09:47:09 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)
--- a/devtools/repotest.py Thu Mar 10 15:18:22 2011 +0100
+++ b/devtools/repotest.py Fri Mar 11 09:47:09 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
--- a/devtools/testlib.py Thu Mar 10 15:18:22 2011 +0100
+++ b/devtools/testlib.py Fri Mar 11 09:47:09 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:
--- a/entities/adapters.py Thu Mar 10 15:18:22 2011 +0100
+++ b/entities/adapters.py Fri Mar 11 09:47:09 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
--- a/entities/schemaobjs.py Thu Mar 10 15:18:22 2011 +0100
+++ b/entities/schemaobjs.py Fri Mar 11 09:47:09 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'])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/sources.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
+"""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]
--- a/hooks/__init__.py Thu Mar 10 15:18:22 2011 +0100
+++ b/hooks/__init__.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
-"""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)
--- a/hooks/integrity.py Thu Mar 10 15:18:22 2011 +0100
+++ b/hooks/integrity.py Fri Mar 11 09:47:09 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.
--- a/hooks/notification.py Thu Mar 10 15:18:22 2011 +0100
+++ b/hooks/notification.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
-"""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
--- a/hooks/syncsources.py Thu Mar 10 15:18:22 2011 +0100
+++ b/hooks/syncsources.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
+"""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)) )
--- a/hooks/test/unittest_hooks.py Thu Mar 10 15:18:22 2011 +0100
+++ b/hooks/test/unittest_hooks.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
"""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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/test/unittest_integrity.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
+"""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()
--- a/i18n/de.po Thu Mar 10 15:18:22 2011 +0100
+++ b/i18n/de.po Fri Mar 11 09:47:09 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"
--- a/i18n/en.po Thu Mar 10 15:18:22 2011 +0100
+++ b/i18n/en.po Fri Mar 11 09:47:09 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"
--- a/i18n/es.po Thu Mar 10 15:18:22 2011 +0100
+++ b/i18n/es.po Fri Mar 11 09:47:09 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"
--- a/i18n/fr.po Thu Mar 10 15:18:22 2011 +0100
+++ b/i18n/fr.po Fri Mar 11 09:47:09 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."
--- a/misc/migration/3.10.0_Any.py Thu Mar 10 15:18:22 2011 +0100
+++ b/misc/migration/3.10.0_Any.py Fri Mar 11 09:47:09 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')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.11.0_Any.py Fri Mar 11 09:47:09 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})
--- a/schema.py Thu Mar 10 15:18:22 2011 +0100
+++ b/schema.py Fri Mar 11 09:47:09 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
--- a/schemas/base.py Thu Mar 10 15:18:22 2011 +0100
+++ b/schemas/base.py Fri Mar 11 09:47:09 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 ###################
--- a/server/__init__.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/__init__.py Fri Mar 11 09:47:09 2011 +0100
@@ -254,7 +254,7 @@
# available sources registry
SOURCE_TYPES = {'native': LazyObject('cubicweb.server.sources.native', 'NativeSQLSource'),
- # XXX private sources installed by an external cube
'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'),
'ldapuser': LazyObject('cubicweb.server.sources.ldapuser', 'LDAPUserSource'),
+ 'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'),
}
--- a/server/checkintegrity.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/checkintegrity.py Fri Mar 11 09:47:09 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)
--- a/server/msplanner.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/msplanner.py Fri Mar 11 09:47:09 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:
--- a/server/repository.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/repository.py Fri Mar 11 09:47:09 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)
--- a/server/serverctl.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/serverctl.py Fri Mar 11 09:47:09 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.
-
- <instance>
- the identifier of the instance.
-
- <mapping file>
- the mapping file to check.
- """
- name = 'check-mapping'
- arguments = '<instance> <mapping file>'
- 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)
--- a/server/sources/__init__.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/sources/__init__.py Fri Mar 11 09:47:09 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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/datafeed.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
+"""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)
--- a/server/sources/ldapuser.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/sources/ldapuser.py Fri Mar 11 09:47:09 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',
--- a/server/sources/native.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/sources/native.py Fri Mar 11 09:47:09 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)
--- a/server/sources/pyrorql.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/sources/pyrorql.py Fri Mar 11 09:47:09 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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_datafeed.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
+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()
--- a/server/test/unittest_ldapuser.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/test/unittest_ldapuser.py Fri Mar 11 09:47:09 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)
--- a/server/test/unittest_msplanner.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/test/unittest_msplanner.py Fri Mar 11 09:47:09 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'},
--- a/server/test/unittest_multisources.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/test/unittest_multisources.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
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')
--- a/server/test/unittest_querier.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/test/unittest_querier.py Fri Mar 11 09:47:09 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
--- a/server/test/unittest_ssplanner.py Thu Mar 10 15:18:22 2011 +0100
+++ b/server/test/unittest_ssplanner.py Fri Mar 11 09:47:09 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.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/parsers.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
+"""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)
--- a/sobjects/test/data/schema.py Thu Mar 10 15:18:22 2011 +0100
+++ b/sobjects/test/data/schema.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
-"""
-
-"""
-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')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/test/unittest_parsers.py Fri Mar 11 09:47:09 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 <http://www.gnu.org/licenses/>.
+
+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'''
+<rset size="1">
+ <CWUser eid="5" cwuri="http://pouet.org/5">
+ <login>sthenault</login>
+ <upassword>toto</upassword>
+ <last_login_time>2011-01-25 14:14:06</last_login_time>
+ <creation_date>2010-01-22 10:27:59</creation_date>
+ <modification_date>2011-01-25 14:14:06</modification_date>
+ <use_email role="subject">
+ <EmailAddress cwuri="http://pouet.org/6" eid="6"/>
+ </use_email>
+ <in_group role="subject">
+ <CWGroup cwuri="http://pouet.org/7" eid="7"/>
+ <CWGroup cwuri="http://pouet.org/8" eid="8"/>
+ </in_group>
+ <tags role="object">
+ <Tag cwuri="http://pouet.org/9" eid="9"/>
+ <Tag cwuri="http://pouet.org/10" eid="10"/>
+ </tags>
+ </CWUser>
+</rset>
+'''.splitlines())
+
+RELATEDXML ={
+ 'http://pouet.org/6': u'''
+<rset size="1">
+ <EmailAddress eid="6" cwuri="http://pouet.org/6">
+ <address>syt@logilab.fr</address>
+ <modification_date>2010-04-13 14:35:56</modification_date>
+ <creation_date>2010-04-13 14:35:56</creation_date>
+ </EmailAddress>
+</rset>
+''',
+ 'http://pouet.org/7': u'''
+<rset size="1">
+ <CWGroup eid="7" cwuri="http://pouet.org/7">
+ <name>users</name>
+ </CWGroup>
+</rset>
+''',
+ 'http://pouet.org/8': u'''
+<rset size="1">
+ <CWGroup eid="8" cwuri="http://pouet.org/8">
+ <name>unknown</name>
+ </CWGroup>
+</rset>
+''',
+ 'http://pouet.org/9': u'''
+<rset size="1">
+ <Tag eid="9" cwuri="http://pouet.org/9">
+ <name>hop</name>
+ </Tag>
+</rset>
+''',
+ 'http://pouet.org/10': u'''
+<rset size="1">
+ <Tag eid="10" cwuri="http://pouet.org/10">
+ <name>unknown</name>
+ </Tag>
+</rset>
+''',
+ }
+
+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()
--- a/test/unittest_schema.py Thu Mar 10 15:18:22 2011 +0100
+++ b/test/unittest_schema.py Fri Mar 11 09:47:09 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())
--- a/utils.py Thu Mar 10 15:18:22 2011 +0100
+++ b/utils.py Fri Mar 11 09:47:09 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
Binary file web/data/add_button.png has changed
--- a/web/data/cubicweb.ajax.js Thu Mar 10 15:18:22 2011 +0100
+++ b/web/data/cubicweb.ajax.js Fri Mar 11 09:47:09 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(
--- a/web/data/cubicweb.calendar.css Thu Mar 10 15:18:22 2011 +0100
+++ b/web/data/cubicweb.calendar.css Fri Mar 11 09:47:09 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
--- a/web/data/cubicweb.css Thu Mar 10 15:18:22 2011 +0100
+++ b/web/data/cubicweb.css Fri Mar 11 09:47:09 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 */
/***************************************/
--- a/web/data/cubicweb.htmlhelpers.js Thu Mar 10 15:18:22 2011 +0100
+++ b/web/data/cubicweb.htmlhelpers.js Fri Mar 11 09:47:09 2011 +0100
@@ -16,11 +16,15 @@
* available and inspects the <base> 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;
}
/**
--- a/web/data/cubicweb.lazy.js Thu Mar 10 15:18:22 2011 +0100
+++ b/web/data/cubicweb.lazy.js Fri Mar 11 09:47:09 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);
-}
-
--- a/web/data/cubicweb.old.css Thu Mar 10 15:18:22 2011 +0100
+++ b/web/data/cubicweb.old.css Fri Mar 11 09:47:09 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 */
/***************************************/
--- a/web/data/cubicweb.tabs.js Thu Mar 10 15:18:22 2011 +0100
+++ b/web/data/cubicweb.tabs.js Fri Mar 11 09:47:09 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);
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/fullcalendar.css Fri Mar 11 09:47:09 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;
+ }
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/fullcalendar.min.js Fri Mar 11 09:47:09 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("<div class='fc-content "+ya+"-widget-content' style='position:relative'/>").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("<div class='fc-view fc-view-"+e+"' style='position:absolute'/>").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.start||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<F.end?pa.disableButton("today"):pa.enableButton("today");g--;F.trigger("viewDisplay",ca)}}function E(){ma=b.contentHeight?b.contentHeight:b.height?b.height-(ta?ta.height():0)-Sa(qa[0]):Math.round(qa.width()/Math.max(b.aspectRatio,0.5))}function K(e){g++;F.setHeight(ma,e);if(ua){ua.css("position","relative");ua=null}F.setWidth(qa.width(),e);g--}function y(){if(!g)if(F.start){var e=++d;setTimeout(function(){if(e==d&&!g&&l())if(ia!=(ia=a.outerWidth())){g++;P();F.trigger("windowResize",ca);g--}},200)}else s()}
+function P(){ea();if(l()){E();K();N();F.rerenderEvents();F.sizeDirty=false}}function ea(){m.each(ka,function(e,q){q.sizeDirty=true})}function S(){M();if(l()){F.clearEvents();F.renderEvents(Y());F.eventsDirty=false}}function M(){m.each(ka,function(e,q){q.eventsDirty=true})}function X(){$(function(e){F.renderEvents(e)})}function ga(e,q,J){F.select(e,q,J===Z?true:J)}function N(){F&&F.unselect()}function U(){H(-1)}function u(){H(1)}function B(){Ta(n,-1);H()}function G(){Ta(n,1);H()}function k(){n=new Date;
+H()}function c(e,q,J){if(e instanceof Date)n=x(e);else hb(n,e,q,J);H()}function A(e,q,J){e!==Z&&Ta(n,e);q!==Z&&Ua(n,q);J!==Z&&O(n,J);H()}function C(){return x(n)}function I(){return F}function V(e,q){if(q===Z)return b[e];if(e=="height"||e=="contentHeight"||e=="aspectRatio"){b[e]=q;P()}}function r(e,q){if(b[e])return b[e].apply(q||ca,Array.prototype.slice.call(arguments,2))}var p=this;p.options=b;p.render=h;p.destroy=j;p.changeView=w;p.select=ga;p.unselect=N;p.rerenderEvents=S;p.prev=U;p.next=u;p.prevYear=
+B;p.nextYear=G;p.today=k;p.gotoDate=c;p.incrementDate=A;p.formatDate=function(e,q){return Ja(e,q,b)};p.formatDates=function(e,q,J){return Va(e,q,J,b)};p.getDate=C;p.getView=I;p.option=V;p.trigger=r;Db.call(p,b,f);var $=p.fetchEvents,fa=p.isFetchNeeded,Y=p.clientEvents,ca=a[0],pa,ta,qa,ya,F,ka={},ia,ma,ua,d=0,g=0,n=new Date;hb(n,b.year,b.month,b.date);var z;b.droppable&&m(document).bind("dragstart",function(e,q){var J=e.target,R=m(J);if(!R.parents(".fc").length){var Q=b.dropAccept;if(m.isFunction(Q)?
+Q.call(J,R):R.is(Q)){z=J;F.dragStart(z,e,q)}}}).bind("dragstop",function(e,q){if(z){F.dragStop(z,e,q);z=null}})}function Cb(a,b){function f(){K=b.theme?"ui":"fc";var y=b.header;if(y)return E=m("<table class='fc-header'/>").append(m("<tr/>").append(m("<td class='fc-header-left'/>").append(i(y.left))).append(m("<td class='fc-header-center'/>").append(i(y.center))).append(m("<td class='fc-header-right'/>").append(i(y.right))))}function h(){E.remove()}function i(y){if(y){var P=m("<tr/>");m.each(y.split(" "),
+function(ea){ea>0&&P.append("<td><span class='fc-header-space'/></td>");var S;m.each(this.split(","),function(M,X){if(X=="title"){P.append("<td><h2 class='fc-header-title'> </h2></td>");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("<div class='fc-button-"+X+" ui-state-default'><a><span class='ui-icon ui-icon-"+
+M+"'/></a></div>");else if(U)N=m("<div class='fc-button-"+X+" "+K+"-state-default'><a><span>"+U+"</span></a></div>");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("<td/>").appendTo(P));S?S.addClass(K+"-no-right"):N.addClass(K+"-corner-left");S=N}}}});S&&S.addClass(K+"-corner-right")});return m("<table/>").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;Y<fa.length;Y++){S(fa[Y]);fa[Y].source=
+$}G=G.concat(fa)}--V||A&&A(G)}}var I=++U,V=c.length,r=X();u=x(r.visStart);B=x(r.visEnd);for(var p=0;p<c.length;p++)j(c[p],C)}function j(c,A){function C(r){A(c,r)}function I(r){C(r);ea()}if(typeof c=="string"){var V={};V[a.startParam]=Math.round(u.getTime()/1E3);V[a.endParam]=Math.round(B.getTime()/1E3);if(a.cacheParam)V[a.cacheParam]=(new Date).getTime();P();m.ajax({url:c,dataType:"json",data:V,cache:a.cacheParam||false,success:I})}else if(m.isFunction(c)){P();c(x(u),x(B),I)}else C(c)}function l(c,
+A){s([c],A)}function t(){i(N)}function w(){var c=X();return!u||c.visStart<u||c.visEnd>B}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;A<C;A++){I=G[A];if(I._id==c._id&&I!=c){I.start=new Date(+I.start+r);I.end=c.end?I.end?new Date(+I.end+p):new Date(+V(I)+p):null;I.title=c.title;I.url=c.url;I.allDay=c.allDay;I.className=c.className;I.editable=c.editable;S(I)}}S(c);N()}function E(c,A){S(c);if(!c.source){if(A)(c.source=b[0]).push(c);G.push(c)}N()}
+function K(c){if(c){if(!m.isFunction(c)){var A=c+"";c=function(I){return I._id==A}}G=m.grep(G,c,true);for(C=0;C<b.length;C++)if(typeof b[C]=="object")b[C]=m.grep(b[C],c,true)}else{G=[];for(var C=0;C<b.length;C++)if(typeof b[C]=="object")b[C]=[]}N()}function y(c){if(m.isFunction(c))return m.grep(G,c);else if(c){c+="";return m.grep(G,function(A){return A._id==c})}return G}function P(){k++||ga("loading",null,true)}function ea(){--k||ga("loading",null,false)}function S(c){c._id=c._id||(c.id===Z?"_fc"+
+Eb++:c.id+"");if(c.date){if(!c.start)c.start=c.date;delete c.date}c._start=x(c.start=Xa(c.start,a.ignoreTimezone));c.end=Xa(c.end,a.ignoreTimezone);if(c.end&&c.end<=c.start)c.end=null;c._end=c.end?x(c.end):null;if(c.allDay===Z)c.allDay=a.allDayDefault;if(c.className){if(typeof c.className=="string")c.className=c.className.split(/\s+/)}else c.className=[]}var M=this;M.fetchEvents=i;M.refetchEvents=t;M.isFetchNeeded=w;M.addEventSource=f;M.removeEventSource=h;M.updateEvent=H;M.renderEvent=E;M.removeEvents=
+K;M.clientEvents=y;M.normalizeEvent=S;var X=M.getView,ga=M.trigger,N=M.rerenderEvents,U=0,u,B,G=[],k=0;b.unshift([])}function Fb(a,b){function f(l,t){if(t){Ua(l,t);l.setDate(1)}l=x(l,true);l.setDate(1);t=Ua(x(l),1);var w=x(l),H=x(t),E=i("firstDay"),K=i("weekends")?0:1;if(K){va(w);va(H,-1,true)}O(w,-((w.getDay()-Math.max(E,K)+7)%7));O(H,(7-H.getDay()+Math.max(E,K))%7);E=Math.round((H-w)/(ib*7));if(i("weekMode")=="fixed"){O(H,(6-E)*7);E=6}h.title=j(l,i("titleFormat"));h.start=l;h.end=t;h.visStart=w;
+h.visEnd=H;s(E,K?5:7,true)}var h=this;h.render=f;Ya.call(h,a,b,"month");var i=h.opt,s=h.renderBasic,j=b.formatDate}function Gb(a,b){function f(l,t){t&&O(l,t*7);l=O(x(l),-((l.getDay()-i("firstDay")+7)%7));t=O(x(l),7);var w=x(l),H=x(t),E=i("weekends");if(!E){va(w);va(H,-1,true)}h.title=j(w,O(x(H),-1),i("titleFormat"));h.start=l;h.end=t;h.visStart=w;h.visEnd=H;s(1,E?7:5,false)}var h=this;h.render=f;Ya.call(h,a,b,"basicWeek");var i=h.opt,s=h.renderBasic,j=b.formatDates}function Hb(a,b){function f(l,t){if(t){O(l,
+t);i("weekends")||va(l,t<0?-1:1)}h.title=j(l,i("titleFormat"));h.start=h.visStart=x(l,true);h.end=h.visEnd=O(x(h.start),1);s(1,1,false)}var h=this;h.render=f;Ya.call(h,a,b,"basicDay");var i=h.opt,s=h.renderBasic,j=b.formatDate}function Ya(a,b,f){function h(d,g,n){Y=d;ca=g;if(V=B("isRTL")){r=-1;p=ca-1}else{r=1;p=0}$=B("firstDay");fa=B("weekends")?0:1;var z=B("theme")?"ui":"fc",e=B("columnFormat"),q=u.start.getMonth(),J=Ia(new Date),R,Q=x(u.visStart);if(F){k();g=F.find("tr").length;if(Y<g)F.find("tr:gt("+
+(Y-1)+")").remove();else if(Y>g){d="";for(g=g;g<Y;g++){d+="<tr class='fc-week"+g+"'>";for(R=0;R<ca;R++){d+="<td class='fc-"+Da[Q.getDay()]+" "+z+"-state-default fc-new fc-day"+(g*ca+R)+(R==p?" fc-leftmost":"")+"'>"+(n?"<div class='fc-day-number'></div>":"")+"<div class='fc-day-content'><div style='position:relative'> </div></div></td>";O(Q,1);fa&&va(Q)}d+="</tr>"}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("<table/>").appendTo(a);d="<thead><tr>";for(g=0;g<ca;g++){d+="<th class='fc-"+Da[Q.getDay()]+" "+z+"-state-default"+(g==p?" fc-leftmost":"")+"'>"+I(Q,e)+"</th>";O(Q,1);fa&&va(Q)}ya=m(d+"</tr></thead>").appendTo(ba);d="<tbody>";Q=x(u.visStart);for(g=0;g<Y;g++){d+="<tr class='fc-week"+g+"'>";for(R=0;R<ca;R++){d+="<td class='fc-"+Da[Q.getDay()]+" "+z+"-state-default fc-day"+(g*ca+R)+(R==p?" fc-leftmost":
+"")+(Y>1&&Q.getMonth()!=q?" fc-other-month":"")+(+Q==+J?" fc-today "+z+"-state-highlight":" fc-not-today")+"'>"+(n?"<div class='fc-day-number'>"+Q.getDate()+"</div>":"")+"<div class='fc-day-content'><div style='position:relative'> </div></div></td>";O(Q,1);fa&&va(Q)}d+="</tr>"}F=m(d+"</tbody>").appendTo(ba);j(F.find("td"));ka=m("<div style='position:absolute;z-index:8;top:0;left:0'/>").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<Y;e++){var q=new Date(Math.max(n,d)),J=new Date(Math.min(z,g));if(q<J){var R;if(V){R=Ea(J,n)*r+p+1;q=Ea(q,n)*r+p+1}else{R=Ea(q,n);q=Ea(J,n)}j(w(e,R,e,q-1))}O(n,7);O(z,7)}}function w(d,g,n,z){d=ia.rect(d,g,n,z,a);return c(d,a)}function H(d){return x(d)}function E(d,g){t(d,O(x(g),1),true)}function K(){A()}function y(d,g){ma.start(function(n){A();n&&w(n.row,n.col,n.row,n.col)},g)}function P(d,g,n){var z=ma.stop();
+A();if(z){z=ga(z);G("drop",d,z,true,g,n)}}function ea(d){return x(d.start)}function S(d){return ua.left(d)}function M(d){return ua.right(d)}function X(d){return(d-Math.max($,fa)+ca)%ca}function ga(d){return O(x(u.visStart),d.row*7+d.col*r+p)}function N(d){return F.find("tr:eq("+d+")")}function U(){return{left:0,right:ta}}var u=this;u.renderBasic=h;u.setHeight=i;u.setWidth=s;u.renderDayOverlay=t;u.defaultSelectionEnd=H;u.renderSelection=E;u.clearSelection=K;u.dragStart=y;u.dragStop=P;u.defaultEventEnd=
+ea;u.getHoverListener=function(){return ma};u.colContentLeft=S;u.colContentRight=M;u.dayOfWeekCol=X;u.cellDate=ga;u.cellIsAllDay=function(){return true};u.allDayTR=N;u.allDayBounds=U;u.getRowCnt=function(){return Y};u.getColCnt=function(){return ca};u.getColWidth=function(){return pa};u.getDaySegmentContainer=function(){return ka};jb.call(u,a,b,f);kb.call(u);lb.call(u);Ib.call(u);var B=u.opt,G=u.trigger,k=u.clearEvents,c=u.renderOverlay,A=u.clearOverlays,C=u.daySelectionMousedown,I=b.formatDate,V,
+r,p,$,fa,Y,ca,pa,ta,qa,ya,F,ka,ia,ma,ua;mb(a.addClass("fc-grid"));ia=new nb(function(d,g){var n,z,e,q=F.find("tr:first td");if(V)q=m(q.get().reverse());q.each(function(J,R){n=m(R);z=n.offset().left;if(J)e[1]=z;e=[z];g[J]=e});e[1]=z+n.outerWidth();F.find("tr").each(function(J,R){n=m(R);z=n.offset().top;if(J)e[1]=z;e=[z];d[J]=e});e[1]=z+n.outerHeight()});ma=new ob(ia);ua=new pb(function(d){return F.find("td:eq("+d+") div div")})}function Ib(){function a(G){w(B=G);U(h(G))}function b(G){f();U(h(B),G)}
+function f(){H();ea().empty()}function h(G){var k=ga(),c=N(),A=x(j.visStart);c=O(x(A),c);var C=m.map(G,Oa),I,V,r,p,$,fa,Y=[];for(I=0;I<k;I++){V=$a(ab(G,C,A,c));for(r=0;r<V.length;r++){p=V[r];for($=0;$<p.length;$++){fa=p[$];fa.row=I;fa.level=r;Y.push(fa)}}O(A,7);O(c,7)}return Y}function i(G,k,c){E(G,k);if(G.editable||G.editable===Z&&l("editable")){s(G,k);c.isEnd&&u(G,k)}}function s(G,k){if(!l("disableDragging")&&k.draggable){var c=S(),A;k.draggable({zIndex:9,delay:50,opacity:l("dragOpacity"),revertDuration:l("dragRevertDuration"),
+start:function(C,I){t("eventDragStart",k,G,C,I);y(G,k);c.start(function(V,r,p,$){k.draggable("option","revert",!V||!p&&!$);X();if(V){A=p*7+$*(l("isRTL")?-1:1);M(O(x(G.start),A),O(Oa(G),A))}else A=0},C,"drag")},stop:function(C,I){c.stop();X();t("eventDragStop",k,G,C,I);if(A){k.find("a").removeAttr("href");P(this,G,A,0,G.allDay,C,I)}else{m.browser.msie&&k.css("filter","");K(G,k)}}})}}var j=this;j.renderEvents=a;j.rerenderEvents=b;j.clearEvents=f;j.bindDaySeg=i;qb.call(j);var l=j.opt,t=j.trigger,w=j.reportEvents,
+H=j.clearEventData,E=j.eventElementHandlers,K=j.showEvents,y=j.hideEvents,P=j.eventDrop,ea=j.getDaySegmentContainer,S=j.getHoverListener,M=j.renderDayOverlay,X=j.clearOverlays,ga=j.getRowCnt,N=j.getColCnt,U=j.renderDaySegs,u=j.resizableDayEvent,B=[]}function Jb(a,b){function f(l,t){t&&O(l,t*7);l=O(x(l),-((l.getDay()-i("firstDay")+7)%7));t=O(x(l),7);var w=x(l),H=x(t),E=i("weekends");if(!E){va(w);va(H,-1,true)}h.title=j(w,O(x(H),-1),i("titleFormat"));h.start=l;h.end=t;h.visStart=w;h.visEnd=H;s(E?7:
+5)}var h=this;h.render=f;rb.call(h,a,b,"agendaWeek");var i=h.opt,s=h.renderAgenda,j=b.formatDates}function Kb(a,b){function f(l,t){if(t){O(l,t);i("weekends")||va(l,t<0?-1:1)}t=x(l,true);var w=O(x(t),1);h.title=j(l,i("titleFormat"));h.start=h.visStart=t;h.end=h.visEnd=w;s(1)}var h=this;h.render=f;rb.call(h,a,b,"agendaDay");var i=h.opt,s=h.renderAgenda,j=b.formatDate}function rb(a,b,f){function h(o){g=o;ba=p("theme")?"ui":"fc";da=p("weekends")?0:1;ha=p("firstDay");if(ra=p("isRTL")){aa=-1;T=g-1}else{aa=
+1;T=0}na=bb(p("minTime"));ja=bb(p("maxTime"));o=ra?O(x(r.visEnd),-1):x(r.visStart);var v=x(o),D=Ia(new Date),L=p("columnFormat");if(ka){fa();ka.find("tr:first th").slice(1,-1).each(function(La,xa){m(xa).text(F(v,L));xa.className=xa.className.replace(/^fc-\w+(?= )/,"fc-"+Da[v.getDay()]);O(v,aa);da&&va(v,aa)});v=x(o);d.find("td").each(function(La,xa){xa.className=xa.className.replace(/^fc-\w+(?= )/,"fc-"+Da[v.getDay()]);+v==+D?m(xa).removeClass("fc-not-today").addClass("fc-today").addClass(ba+"-state-highlight"):
+m(xa).addClass("fc-not-today").removeClass("fc-today").removeClass(ba+"-state-highlight");O(v,aa);da&&va(v,aa)})}else{var W,wa,Fa=p("slotMinutes")%15==0,oa="<div class='fc-agenda-head' style='position:relative;z-index:4'><table style='width:100%'><tr class='fc-first"+(p("allDaySlot")?"":" fc-last")+"'><th class='fc-leftmost "+ba+"-state-default'> </th>";for(W=0;W<g;W++){oa+="<th class='fc-"+Da[v.getDay()]+" "+ba+"-state-default'>"+F(v,L)+"</th>";O(v,aa);da&&va(v,aa)}oa+="<th class='"+ba+"-state-default'> </th></tr>";
+if(p("allDaySlot"))oa+="<tr class='fc-all-day'><th class='fc-axis fc-leftmost "+ba+"-state-default'>"+p("allDayText")+"</th><td colspan='"+g+"' class='"+ba+"-state-default'><div class='fc-day-content'><div style='position:relative'> </div></div></td><th class='"+ba+"-state-default'> </th></tr><tr class='fc-divider fc-last'><th colspan='"+(g+2)+"' class='"+ba+"-state-default fc-leftmost'><div/></th></tr>";oa+="</table></div>";ka=m(oa).appendTo(a);w(ka.find("td"));sb=m("<div style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(ka);
+v=tb();var cb=sa(x(v),ja);sa(v,na);oa="<table>";for(W=0;v<cb;W++){wa=v.getMinutes();oa+="<tr class='"+(!W?"fc-first":!wa?"":"fc-minor")+"'><th class='fc-axis fc-leftmost "+ba+"-state-default'>"+(!Fa||!wa?F(v,p("axisFormat")):" ")+"</th><td class='fc-slot"+W+" "+ba+"-state-default'><div style='position:relative'> </div></td></tr>";sa(v,p("slotMinutes"));n++}oa+="</table>";ia=m("<div class='fc-agenda-body' style='position:relative;z-index:2;overflow:auto'/>").append(ma=m("<div style='position:relative;overflow:hidden'>").append(ua=
+m(oa))).appendTo(a);H(ia.find("td"));ub=m("<div style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(ma);v=x(o);oa="<div class='fc-agenda-bg' style='position:absolute;z-index:1'><table style='width:100%;height:100%'><tr class='fc-first'>";for(W=0;W<g;W++){oa+="<td class='fc-"+Da[v.getDay()]+" "+ba+"-state-default "+(!W?"fc-leftmost ":"")+(+v==+D?ba+"-state-highlight fc-today":"fc-not-today")+"'><div class='fc-day-content'><div> </div></div></td>";O(v,aa);da&&va(v,aa)}oa+="</tr></table></div>";
+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<o&&w(y(0,D,0,o-1))}function y(o,v,D,L){o=la.rect(o,v,D,L,ka);return Y(o,ka)}function P(o,v){for(var D=x(r.visStart),L=O(x(D),1),W=0;W<g;W++){var wa=new Date(Math.max(D,o)),Fa=new Date(Math.min(L,v));if(wa<Fa){var oa=W*aa+T;oa=la.rect(0,oa,0,oa,ma);wa=X(D,wa);Fa=X(D,Fa);oa.top=wa;oa.height=Fa-wa;H(Y(oa,ma))}O(D,1);O(L,1)}}function ea(o){return z+Ba.left(o)}function S(o){return z+Ba.right(o)}function M(o){return(o-Math.max(ha,da)+g)%g*aa+T}function X(o,v){o=x(o,true);if(v<sa(x(o),na))return 0;
+if(v>=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&&L<g){L=la.rect(0,L,0,L,ma);var W=X(o,o),wa=X(o,v);if(wa>W){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<n;La++)o.push([D(Fa+q*La),D(Fa+q*(La+1))])});Ca=new ob(la);Ba=new pb(function(o){return d.find("td:eq("+o+") div div")})}function Lb(){function a(d,g){M(ua=d);var n,z=d.length,e=[],q=[];for(n=0;n<z;n++)d[n].allDay?e.push(d[n]):
+q.push(d[n]);if(P("allDaySlot")){I(h(e),g);N()}j(i(q),g)}function b(d){f();a(ua,d)}function f(){X();U().empty();u().empty()}function h(d){d=$a(ab(d,m.map(d,Oa),y.visStart,y.visEnd));var g,n=d.length,z,e,q,J=[];for(g=0;g<n;g++){z=d[g];for(e=0;e<z.length;e++){q=z[e];q.row=0;q.level=g;J.push(q)}}return J}function i(d){var g=r(),n=k(),z=G(),e=sa(x(y.visStart),n),q=m.map(d,s),J,R,Q,ba,ha,da,ra=[];for(J=0;J<g;J++){R=$a(ab(d,q,e,sa(x(e),z-n)));Nb(R);for(Q=0;Q<R.length;Q++){ba=R[Q];for(ha=0;ha<ba.length;ha++){da=
+ba[ha];da.col=J;da.level=Q;ra.push(da)}}O(e,1,true)}return ra}function s(d){return d.end?x(d.end):sa(x(d.start),P("defaultEventMinutes"))}function j(d,g){var n,z=d.length,e,q,J,R,Q,ba,ha,da,ra,aa,T="",na,ja,la={},Ca={},Ba=u(),Aa;n=r();if(na=P("isRTL")){ja=-1;Aa=n-1}else{ja=1;Aa=0}for(n=0;n<z;n++){e=d[n];q=e.event;J="fc-event fc-event-vert ";if(e.isStart)J+="fc-corner-top ";if(e.isEnd)J+="fc-corner-bottom ";R=c(e.start,e.start);Q=c(e.start,e.end);ba=e.col;ha=e.level;da=e.forward||0;ra=A(ba*ja+Aa);
+aa=C(ba*ja+Aa)-ra;aa=Math.min(aa-6,aa*0.95);ba=ha?aa/(ha+da+1):da?(aa/(da+1)-6)*2:aa;ha=ra+aa/(ha+da+1)*ha*ja+(na?aa-ba:0);e.top=R;e.left=ha;e.outerWidth=ba;e.outerHeight=Q-R;T+=l(q,e,J)}Ba[0].innerHTML=T;na=Ba.children();for(n=0;n<z;n++){e=d[n];q=e.event;T=m(na[n]);ja=ea("eventRender",q,q,T);if(ja===false)T.remove();else{if(ja&&ja!==true){T.remove();T=m(ja).css({position:"absolute",top:e.top,left:e.left}).appendTo(Ba)}e.element=T;if(q._id===g)w(q,T,e);else T[0]._fci=n;Y(q,T)}}wb(Ba,d,w);for(n=0;n<
+z;n++){e=d[n];if(T=e.element){q=la[g=e.key=xb(T[0])];e.vsides=q===Z?(la[g]=Sa(T[0],true)):q;q=Ca[g];e.hsides=q===Z?(Ca[g]=db(T[0],true)):q;g=T.find("span.fc-event-title");if(g.length)e.titleTop=g[0].offsetTop}}for(n=0;n<z;n++){e=d[n];if(T=e.element){T[0].style.width=Math.max(0,e.outerWidth-e.hsides)+"px";la=Math.max(0,e.outerHeight-e.vsides);T[0].style.height=la+"px";q=e.event;if(e.titleTop!==Z&&la-e.titleTop<10){T.find("span.fc-event-time").text(ia(q.start,P("timeFormat"))+" - "+q.title);T.find("span.fc-event-title").remove()}ea("eventAfterRender",
+q,q,T)}}}function l(d,g,n){return"<div class='"+n+d.className.join(" ")+"' style='position:absolute;z-index:8;top:"+g.top+"px;left:"+g.left+"px'><a"+(d.url?" href='"+Ma(d.url)+"'":"")+"><span class='fc-event-bg'></span><span class='fc-event-time'>"+Ma(ma(d.start,d.end,P("timeFormat")))+"</span><span class='fc-event-title'>"+Ma(d.title)+"</span></a>"+((d.editable||d.editable===Z&&P("editable"))&&!P("disableResizing")&&m.fn.resizable?"<div class='ui-resizable-handle ui-resizable-s'>=</div>":"")+"</div>"}
+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<i.length;f++){s=i[f];for(h=0;h<a[b-1].length;h++){j=a[b-1][h];if(yb(s,j))j.forward=Math.max(j.forward||0,(s.forward||0)+1)}}}}function jb(a,b,f){function h(k,c){k=G[k];if(typeof k=="object")return Wa(k,c||f);return k}function i(k,c){return b.trigger.apply(b,[k,c||M].concat(Array.prototype.slice.call(arguments,2),[M]))}
+function s(k){U={};var c,A=k.length,C;for(c=0;c<A;c++){C=k[c];if(U[C._id])U[C._id].push(C);else U[C._id]=[C]}}function j(){u=[];B={}}function l(k){return k.end?x(k.end):X(k)}function t(k,c){u.push(c);if(B[k._id])B[k._id].push(c);else B[k._id]=[c]}function w(k,c){c.click(function(A){if(!c.hasClass("ui-draggable-dragging")&&!c.hasClass("ui-resizable-resizing"))return i("eventClick",this,k,A)}).hover(function(A){i("eventMouseover",this,k,A)},function(A){i("eventMouseout",this,k,A)})}function H(k,c){K(k,
+c,"show")}function E(k,c){K(k,c,"hide")}function K(k,c,A){k=B[k._id];var C,I=k.length;for(C=0;C<I;C++)k[C][0]!=c[0]&&k[C][A]()}function y(k,c,A,C,I,V,r){var p=c.allDay,$=c._id;ea(U[$],A,C,I);i("eventDrop",k,c,A,C,I,function(){ea(U[$],-A,-C,p);N()},V,r);M.eventsChanged=true;N($)}function P(k,c,A,C,I,V){var r=c._id;S(U[r],A,C);i("eventResize",k,c,A,C,function(){S(U[r],-A,-C);N()},I,V);M.eventsChanged=true;N(r)}function ea(k,c,A,C){A=A||0;for(var I,V=k.length,r=0;r<V;r++){I=k[r];if(C!==Z)I.allDay=C;
+sa(O(I.start,c,true),A);if(I.end)I.end=sa(O(I.end,c,true),A);ga(I,G)}}function S(k,c,A){A=A||0;for(var C,I=k.length,V=0;V<I;V++){C=k[V];C.end=sa(O(l(C),c,true),A);ga(C,G)}}var M=this;M.element=a;M.calendar=b;M.name=f;M.opt=h;M.trigger=i;M.reportEvents=s;M.clearEventData=j;M.eventEnd=l;M.reportEventElement=t;M.eventElementHandlers=w;M.showEvents=H;M.hideEvents=E;M.eventDrop=y;M.eventResize=P;var X=M.defaultEventEnd,ga=b.normalizeEvent,N=b.rerenderEvents,U={},u=[],B={},G=b.options}function qb(){function a(N,
+U){var u=h("isRTL"),B,G=N.length,k,c,A,C,I,V="",r,p={},$={},fa=[],Y=[];B=y();r=B.left;var ca=B.right,pa=w();H();var ta=M();for(B=0;B<G;B++){k=N[B];c=k.event;A="fc-event fc-event-hori ";if(u){if(k.isStart)A+="fc-corner-right ";if(k.isEnd)A+="fc-corner-left ";C=k.isEnd?P(S(k.end.getDay()-1)):r;I=k.isStart?ea(S(k.start.getDay())):ca}else{if(k.isStart)A+="fc-corner-left ";if(k.isEnd)A+="fc-corner-right ";C=k.isStart?P(S(k.start.getDay())):r;I=k.isEnd?ea(S(k.end.getDay()-1)):ca}V+="<div class='"+A+c.className.join(" ")+
+"' style='position:absolute;z-index:8;left:"+C+"px'><a"+(c.url?" href='"+Ma(c.url)+"'":"")+">"+(!c.allDay&&k.isStart?"<span class='fc-event-time'>"+Ma(ga(c.start,c.end,h("timeFormat")))+"</span>":"")+"<span class='fc-event-title'>"+Ma(c.title)+"</span></a>"+((c.editable||c.editable===Z&&h("editable"))&&!h("disableResizing")&&m.fn.resizable?"<div class='ui-resizable-handle ui-resizable-"+(u?"w":"e")+"'></div>":"")+"</div>";k.left=C;k.outerWidth=I-C}ta[0].innerHTML=V;V=ta.children();for(B=0;B<G;B++){k=
+N[B];u=m(V[B]);c=k.event;r=i("eventRender",c,c,u);if(r===false)u.remove();else{if(r&&r!==true){u.remove();u=m(r).css({position:"absolute",left:k.left}).appendTo(ta)}k.element=u;if(c._id===U)X(c,u,k);else u[0]._fci=B;s(c,u)}}wb(ta,N,X);for(B=0;B<G;B++){k=N[B];if(u=k.element){c=p[U=k.key=xb(u[0])];k.hsides=c===Z?(p[U]=db(u[0],true)):c}}for(B=0;B<G;B++){k=N[B];if(u=k.element)u[0].style.width=Math.max(0,k.outerWidth-k.hsides)+"px"}for(B=0;B<G;B++){k=N[B];if(u=k.element){c=$[U=k.key];k.outerHeight=u[0].offsetHeight+
+(c===Z?($[U]=zb(u[0])):c)}}for(p=B=0;p<pa;p++){for($=U=c=0;B<G&&(k=N[B]).row==p;){if(k.level!=U){$+=c;c=0;U++}c=Math.max(c,k.outerHeight||0);k.top=$;B++}fa[p]=K(p).find("td:first div.fc-day-content > div").height($+c)}for(p=0;p<pa;p++)Y[p]=fa[p][0].offsetTop;for(B=0;B<G;B++){k=N[B];if(u=k.element){u[0].style.top=Y[k.row]+k.top+"px";c=k.event;i("eventAfterRender",c,c,u)}}}function b(N,U){if(!h("disableResizing")&&U.resizable){var u=E();U.resizable({handles:h("isRTL")?{w:"div.ui-resizable-w"}:{e:"div.ui-resizable-e"},
+grid:u,minWidth:u/2,containment:f.element.parent().parent(),start:function(B,G){U.css("z-index",9);l(N,U);i("eventResizeStart",this,N,B,G)},stop:function(B,G){i("eventResizeStop",this,N,B,G);var k=Math.round((U.width()-G.originalSize.width)/u);if(k)t(this,N,k,0,B,G);else{U.css("z-index",8);j(N,U)}}})}}var f=this;f.renderDaySegs=a;f.resizableDayEvent=b;var h=f.opt,i=f.trigger,s=f.reportEventElement,j=f.showEvents,l=f.hideEvents,t=f.eventResize,w=f.getRowCnt,H=f.getColCnt,E=f.getColWidth,K=f.allDayTR,
+y=f.allDayBounds,P=f.colContentLeft,ea=f.colContentRight,S=f.dayOfWeekCol,M=f.getDaySegmentContainer,X=f.bindDaySeg,ga=f.calendar.formatDates}function lb(){function a(E,K,y){b();K||(K=l(E,y));t(E,K,y);f(E,K,y)}function b(E){if(H){H=false;w();j("unselect",null,E)}}function f(E,K,y,P){H=true;j("select",null,E,K,y,P)}function h(E){var K=i.cellDate,y=i.cellIsAllDay,P=i.getHoverListener();if(E.which==1&&s("selectable")){b(E);var ea=this,S;P.start(function(M,X){w();if(M&&y(M)){S=[K(X),K(M)].sort(vb);t(S[0],
+S[1],true)}else S=null},E);m(document).one("mouseup",function(M){P.stop();if(S){+S[0]==+S[1]&&j("dayClick",ea,S[0],true,M);f(S[0],S[1],true,M)}})}}var i=this;i.select=a;i.unselect=b;i.reportSelection=f;i.daySelectionMousedown=h;var s=i.opt,j=i.trigger,l=i.defaultSelectionEnd,t=i.renderSelection,w=i.clearSelection,H=false;s("selectable")&&s("unselectAuto")&&m(document).mousedown(function(E){var K=s("unselectCancel");if(K)if(m(E.target).parents(K).length)return;b(E)})}function kb(){function a(s,j){var l=
+i.shift();l||(l=m("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>"));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<j;t++)if(s>=f[t][0]&&s<f[t][1]){w=t;break}for(t=0;t<l;t++)if(i>=h[t][0]&&i<h[t][1]){H=
+t;break}return w>=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()+(a<h?1:-1))}return a}function O(a,
+b,f){if(+a){b=a.getDate()+b;var h=x(a);h.setHours(9);h.setDate(b);a.setDate(b);f||Ia(a);eb(a,h)}return a}function eb(a,b){if(+a)for(;a.getDate()!=b.getDate();)a.setTime(+a+(a<b?1:-1)*Pb)}function sa(a,b){a.setMinutes(a.getMinutes()+b);return a}function Ia(a){a.setHours(0);a.setMinutes(0);a.setSeconds(0);a.setMilliseconds(0);return a}function x(a,b){if(b)return Ia(new Date(+a));return new Date(+a)}function tb(){var a=0,b;do b=new Date(1970,a++,1);while(b.getHours());return b}function va(a,b,f){for(b=
+b||1;!a.getDay()||f&&a.getDay()==1||!f&&a.getDay()==6;)O(a,b);return a}function Ea(a,b){return Math.round((x(a,true)-x(b,true))/ib)}function hb(a,b,f,h){if(b!==Z&&b!=a.getFullYear()){a.setDate(1);a.setMonth(0);a.setFullYear(b)}if(f!==Z&&f!=a.getMonth()){a.setDate(1);a.setMonth(f)}h!==Z&&a.setDate(h)}function Xa(a,b){if(typeof a=="object")return a;if(typeof a=="number")return new Date(a*1E3);if(typeof a=="string"){if(a.match(/^\d+$/))return new Date(parseInt(a)*1E3);if(b===Z)b=true;return Ab(a,b)||
+(a?new Date(a):null)}return null}function Ab(a,b){a=a.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$/);if(!a)return null;var f=new Date(a[1],0,1);if(b||!a[14]){b=new Date(a[1],0,1,9,0);if(a[3]){f.setMonth(a[3]-1);b.setMonth(a[3]-1)}if(a[5]){f.setDate(a[5]);b.setDate(a[5])}eb(f,b);a[7]&&f.setHours(a[7]);a[8]&&f.setMinutes(a[8]);a[10]&&f.setSeconds(a[10]);a[12]&&f.setMilliseconds(Number("0."+a[12])*1E3);eb(f,b)}else{f.setUTCFullYear(a[1],
+a[3]?a[3]-1:0,a[5]||1);f.setUTCHours(a[7]||0,a[8]||0,a[10]||0,a[12]?Number("0."+a[12])*1E3:0);b=Number(a[16])*60+Number(a[17]);b*=a[15]=="-"?1:-1;f=new Date(+f+b*60*1E3)}return f}function bb(a){if(typeof a=="number")return a*60;if(typeof a=="object")return a.getHours()*60+a.getMinutes();if(a=a.match(/(\d+)(?::(\d+))?\s*(\w+)?/)){var b=parseInt(a[1]);if(a[3]){b%=12;if(a[3].toLowerCase().charAt(0)=="p")b+=12}return b*60+(a[2]?parseInt(a[2]):0)}}function Ja(a,b,f){return Va(a,null,b,f)}function Va(a,
+b,f,h){h=h||Pa;var i=a,s=b,j,l=f.length,t,w,H,E="";for(j=0;j<l;j++){t=f.charAt(j);if(t=="'")for(w=j+1;w<l;w++){if(f.charAt(w)=="'"){if(i){E+=w==j+1?"'":f.substring(j+1,w);j=w}break}}else if(t=="(")for(w=j+1;w<l;w++){if(f.charAt(w)==")"){j=Ja(i,f.substring(j+1,w),h);if(parseInt(j.replace(/\D/,"")))E+=j;j=w;break}}else if(t=="[")for(w=j+1;w<l;w++){if(f.charAt(w)=="]"){t=f.substring(j+1,w);j=Ja(i,t,h);if(j!=Ja(s,t,h))E+=j;j=w;break}}else if(t=="{"){i=b;s=a}else if(t=="}"){i=a;s=b}else{for(w=l;w>j;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.start<b.end}function ab(a,b,f,h){var i=[],s,j=a.length,l,t,w,H,E;for(s=0;s<j;s++){l=a[s];t=l.start;w=b[s];if(w>f&&t<h){if(t<f){t=x(f);H=false}else{t=t;H=true}if(w>
+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<h;f++){i=a[f];for(s=0;;){j=false;if(b[s])for(l=0;l<b[s].length;l++)if(yb(b[s][l],i)){j=true;break}if(j)s++;else break}if(b[s])b[s].push(i);else b[s]=[i]}return b}function wb(a,b,f){a.unbind("mouseover").mouseover(function(h){for(var i=h.target,s;i!=this;){s=i;i=i.parentNode}if((i=s._fci)!==Z){s._fci=Z;s=b[i];f(s.event,s.element,
+s);m(h.target).trigger(h)}h.stopPropagation()})}function Ka(a,b,f){a.each(function(h,i){i.style.width=Math.max(0,b-db(i,f))+"px"})}function Qa(a,b,f){a.each(function(h,i){i.style.height=Math.max(0,b-Sa(i,f))+"px"})}function db(a,b){return(parseFloat(m.curCSS(a,"paddingLeft",true))||0)+(parseFloat(m.curCSS(a,"paddingRight",true))||0)+(parseFloat(m.curCSS(a,"borderLeftWidth",true))||0)+(parseFloat(m.curCSS(a,"borderRightWidth",true))||0)+(b?Tb(a):0)}function Tb(a){return(parseFloat(m.curCSS(a,"marginLeft",
+true))||0)+(parseFloat(m.curCSS(a,"marginRight",true))||0)}function Sa(a,b){return(parseFloat(m.curCSS(a,"paddingTop",true))||0)+(parseFloat(m.curCSS(a,"paddingBottom",true))||0)+(parseFloat(m.curCSS(a,"borderTopWidth",true))||0)+(parseFloat(m.curCSS(a,"borderBottomWidth",true))||0)+(b?zb(a):0)}function zb(a){return(parseFloat(m.curCSS(a,"marginTop",true))||0)+(parseFloat(m.curCSS(a,"marginBottom",true))||0)}function Ra(a,b){b=typeof b=="number"?b+"px":b;a[0].style.cssText+=";min-height:"+b+";_height:"+
+b}function gb(){}function vb(a,b){return a-b}function Na(a){return(a<10?"0":"")+a}function Wa(a,b){if(a[b]!==Z)return a[b];b=b.split(/(?=[A-Z])/);for(var f=b.length-1,h;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(/"/g,""").replace(/\n/g,"<br />")}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);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery.ui.datepicker-de.js Fri Mar 11 09:47:09 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']);
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery.ui.datepicker-es.js Fri Mar 11 09:47:09 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/jquery.ui.datepicker-fr.js Fri Mar 11 09:47:09 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
--- a/web/facet.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/facet.py Fri Mar 11 09:47:09 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'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'
% (cssclass, xml_escape(unicode(self.value))))
@@ -1198,11 +1198,11 @@
self.w(u'<div id="%s" class="facet">\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'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'
% (cssclass, xml_escape(unicode(self.value))))
--- a/web/formfields.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/formfields.py Fri Mar 11 09:47:09 2011 +0100
@@ -733,7 +733,7 @@
wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
(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'<div id="%s" class="hidden">' % divid)
if self.name_field:
--- a/web/formwidgets.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/formwidgets.py Fri Mar 11 09:47:09 2011 +0100
@@ -580,6 +580,8 @@
def _render(self, form, field, renderer):
req = form._cw
+ if req.lang != 'en':
+ req.add_js('jquery.ui.datepicker-%s.js' % req.lang)
domid = field.dom_id(form, self.suffix)
# XXX find a way to understand every format
fmt = req.property_value('ui.date-format')
@@ -768,7 +770,7 @@
fname = entity.autocomplete_initfuncs[field.name]
else:
fname = self.autocomplete_initfunc
- return entity._cw.datadir_url + fname
+ return entity._cw.data_url(fname)
class RestrictedAutoCompletionWidget(AutoCompletionWidget):
--- a/web/request.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/request.py Fri Mar 11 09:47:09 2011 +0100
@@ -420,7 +420,7 @@
self.add_js('fckeditor/fckeditor.js')
self.html_headers.define_var('fcklang', self.lang)
self.html_headers.define_var('fckconfigpath',
- self.build_url('data/cubicweb.fckcwconfig.js'))
+ self.data_url('cubicweb.fckcwconfig.js'))
def use_fckeditor(self):
return self.vreg.config.fckeditor_installed() and self.property_value('ui.fckeditor')
@@ -559,7 +559,7 @@
jsfiles = (jsfiles,)
for jsfile in jsfiles:
if localfile:
- jsfile = self.datadir_url + jsfile
+ jsfile = self.data_url(jsfile)
self.html_headers.add_js(jsfile)
def add_css(self, cssfiles, media=u'all', localfile=True, ieonly=False,
@@ -588,7 +588,7 @@
add_css = self.html_headers.add_css
for cssfile in cssfiles:
if localfile:
- cssfile = self.datadir_url + cssfile
+ cssfile = self.data_url(cssfile)
add_css(cssfile, media, *extraargs)
@deprecated('[3.9] use ajax_replace_url() instead, naming rql and vid arguments')
@@ -645,6 +645,10 @@
"""returns the absolute path of the base url"""
return urlsplit(self.base_url())[2]
+ def data_url(self, relpath):
+ """returns the absolute path for a data resouce"""
+ return self.datadir_url + relpath
+
@cached
def from_controller(self):
"""return the id (string) of the controller issuing the request"""
--- a/web/test/data/schema.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/data/schema.py Fri Mar 11 09:47:09 2011 +0100
@@ -91,3 +91,5 @@
title = String(maxsize=32, required=True, fulltextindexed=True)
concerns = SubjectRelation('Project', composite='object')
+# used by windmill for `test_edit_relation`
+from cubes.folder.schema import Folder
--- a/web/test/jstests/test_ajax.js Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/jstests/test_ajax.js Fri Mar 11 09:47:09 2011 +0100
@@ -44,7 +44,7 @@
callback: function() {
var origLength = scriptsIncluded.length;
scriptsIncluded = jsSources();
- // check that foo.js has been inserted in <head>
+ // check that foo.js has been *appended* to <head>
equals(scriptsIncluded.length, origLength + 1);
equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
// check that <div class="ajaxHtmlHead"> has been removed
@@ -105,7 +105,7 @@
test('test callback after synchronous request with parameters', function() {
var deferred = new Deferred();
var result = jQuery.ajax({
- url: './ajax_url0.html',
+ url: '/../ajax_url0.html',
async: false,
beforeSend: function(xhr) {
deferred._req = xhr;
--- a/web/test/test_windmill.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/test_windmill.py Fri Mar 11 09:47:09 2011 +0100
@@ -1,8 +1,12 @@
# Run all scenarii found in windmill directory
+from os.path import join, dirname
from cubicweb.devtools.cwwindmill import (CubicWebWindmillUseCase,
unittest_main)
-class CubicWebWindmillUseCase(CubicWebWindmillUseCase): pass
+class CubicWebWindmillUseCase(CubicWebWindmillUseCase):
+ #test_dir = join(dirname(__file__), "windmill/test_edit_relation.py")
+ pass
+
if __name__ == '__main__':
unittest_main()
--- a/web/test/unittest_application.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/unittest_application.py Fri Mar 11 09:47:09 2011 +0100
@@ -24,7 +24,7 @@
from urllib import unquote
from logilab.common.testlib import TestCase, unittest_main
-from logilab.common.decorators import clear_cache
+from logilab.common.decorators import clear_cache, classproperty
from cubicweb import AuthenticationError, Unauthorized
from cubicweb.devtools.testlib import CubicWebTC
@@ -159,6 +159,15 @@
raise
self.app.error_handler = raise_hdlr
+ @classproperty
+ def config(cls):
+ try:
+ return cls.__dict__['_config']
+ except KeyError:
+ config = super(ApplicationTC, cls).config
+ config.global_set_option('allow-email-login', True)
+ return config
+
def test_cnx_user_groups_sync(self):
user = self.user()
self.assertEqual(user.groups, set(('managers',)))
@@ -324,10 +333,9 @@
self.assertAuthFailure(req)
self.assertRaises(AuthenticationError, self.app_publish, req, 'login')
self.assertEqual(req.cnx, None)
- authstr = base64.encodestring('%s:%s' % (origsession.login, origsession.authinfo['password']))
+ authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
req._headers['Authorization'] = 'basic %s' % authstr
self.assertAuthSuccess(req, origsession)
- self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']})
self.assertRaises(LogOut, self.app_publish, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
@@ -338,10 +346,9 @@
self.failUnless('__login' in form)
self.failUnless('__password' in form)
self.assertEqual(req.cnx, None)
- req.form['__login'] = origsession.login
- req.form['__password'] = origsession.authinfo['password']
+ req.form['__login'] = self.admlogin
+ req.form['__password'] = self.admpassword
self.assertAuthSuccess(req, origsession)
- self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']})
self.assertRaises(LogOut, self.app_publish, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
@@ -351,18 +358,17 @@
self.execute('INSERT EmailAddress X: X address %(address)s, U primary_email X '
'WHERE U login %(login)s', {'address': address, 'login': login})
self.commit()
- # option allow-email-login not set
+ # # option allow-email-login not set
req, origsession = self.init_authentication('cookie')
- req.form['__login'] = address
- req.form['__password'] = origsession.authinfo['password']
- self.assertAuthFailure(req)
+ # req.form['__login'] = address
+ # req.form['__password'] = self.admpassword
+ # self.assertAuthFailure(req)
# option allow-email-login set
origsession.login = address
self.set_option('allow-email-login', True)
req.form['__login'] = address
- req.form['__password'] = origsession.authinfo['password']
+ req.form['__password'] = self.admpassword
self.assertAuthSuccess(req, origsession)
- self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']})
self.assertRaises(LogOut, self.app_publish, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
@@ -382,7 +388,6 @@
asession = req.session
self.assertEqual(len(self.open_sessions), 1)
self.assertEqual(asession.login, 'anon')
- self.assertEqual(asession.authinfo['password'], 'anon')
self.failUnless(asession.anonymous_session)
self._reset_cookie(req)
@@ -400,10 +405,9 @@
authstr = base64.encodestring('toto:pouet')
req._headers['Authorization'] = 'basic %s' % authstr
self._test_anon_auth_fail(req)
- authstr = base64.encodestring('%s:%s' % (origsession.login, origsession.authinfo['password']))
+ authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
req._headers['Authorization'] = 'basic %s' % authstr
self.assertAuthSuccess(req, origsession)
- self.assertEqual(req.session.authinfo, {'password': origsession.authinfo['password']})
self.assertRaises(LogOut, self.app_publish, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
@@ -413,11 +417,9 @@
req.form['__login'] = 'toto'
req.form['__password'] = 'pouet'
self._test_anon_auth_fail(req)
- req.form['__login'] = origsession.login
- req.form['__password'] = origsession.authinfo['password']
+ req.form['__login'] = self.admlogin
+ req.form['__password'] = self.admpassword
self.assertAuthSuccess(req, origsession)
- self.assertEqual(req.session.authinfo,
- {'password': origsession.authinfo['password']})
self.assertRaises(LogOut, self.app_publish, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
--- a/web/test/unittest_form.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/unittest_form.py Fri Mar 11 09:47:09 2011 +0100
@@ -92,9 +92,15 @@
form.content_type = 'text/html'
pageinfo = self._check_html(form.render(), form, template=None)
inputs = pageinfo.find_tag('select', False)
- self.failUnless(any(attrs for t, attrs in inputs if attrs.get('name') == 'in_group-subject:A'))
+ ok = False
+ for selectnode in pageinfo.matching_nodes('select', name='from_in_group-subject:A'):
+ for optionnode in selectnode:
+ self.assertEqual(optionnode.get('value'), str(geid))
+ self.assertEqual(ok, False)
+ ok = True
+ self.assertEqual(ok, True, 'expected option not found')
inputs = pageinfo.find_tag('input', False)
- self.failIf(any(attrs for t, attrs in inputs if attrs.get('name') == '__linkto'))
+ self.failIf(list(pageinfo.matching_nodes('input', name='__linkto')))
def test_reledit_composite_field(self):
rset = self.execute('INSERT BlogEntry X: X title "cubicweb.org", X content "hop"')
--- a/web/test/unittest_urlpublisher.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/unittest_urlpublisher.py Fri Mar 11 09:47:09 2011 +0100
@@ -56,41 +56,50 @@
self.assertRaises(NotFound, self.process, '123/345')
self.assertRaises(NotFound, self.process, 'not_eid')
- def test_rest_path(self):
+ def test_rest_path_etype(self):
"""tests the rest path resolution"""
- ctrl, rset = self.process('CWUser')
+ ctrl, rset = self.process('CWEType')
self.assertEqual(ctrl, 'view')
- self.assertEqual(rset.description[0][0], 'CWUser')
+ self.assertEqual(rset.description[0][0], 'CWEType')
self.assertEqual(rset.printable_rql(),
- "Any X,AA,AB,AC,AD ORDERBY AA WHERE X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD")
+ "Any X,AA,AB ORDERBY AA WHERE X is CWEType, X name AA, X modification_date AB")
+
+ def test_rest_path_by_attr(self):
ctrl, rset = self.process('CWUser/login/admin')
self.assertEqual(ctrl, 'view')
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'CWUser')
self.assertEqual(rset.printable_rql(), 'Any X,AA,AB,AC,AD WHERE X login "admin", X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD')
+
+ def test_rest_path_unique_attr(self):
ctrl, rset = self.process('cwuser/admin')
self.assertEqual(ctrl, 'view')
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'CWUser')
self.assertEqual(rset.printable_rql(), 'Any X,AA,AB,AC,AD WHERE X login "admin", X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD')
- ctrl, rset = self.process('cwuser/eid/%s'%rset[0][0])
+
+ def test_rest_path_eid(self):
+ ctrl, rset = self.process('cwuser/eid/%s' % self.user().eid)
self.assertEqual(ctrl, 'view')
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'CWUser')
self.assertEqual(rset.printable_rql(), 'Any X,AA,AB,AC,AD WHERE X eid %s, X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD' % rset[0][0])
- # test non-ascii paths
+
+ def test_rest_path_non_ascii_paths(self):
ctrl, rset = self.process('CWUser/login/%C3%BFsa%C3%BFe')
self.assertEqual(ctrl, 'view')
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'CWUser')
self.assertEqual(rset.printable_rql(), u'Any X,AA,AB,AC,AD WHERE X login "\xffsa\xffe", X is CWUser, X login AA, X firstname AB, X surname AC, X modification_date AD')
- # test quoted paths
+
+ def test_rest_path_quoted_paths(self):
ctrl, rset = self.process('BlogEntry/title/hell%27o')
self.assertEqual(ctrl, 'view')
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'BlogEntry')
self.assertEqual(rset.printable_rql(), u'Any X,AA,AB,AC WHERE X title "hell\'o", X is BlogEntry, X creation_date AA, X title AB, X modification_date AC')
- # errors
+
+ def test_rest_path_errors(self):
self.assertRaises(NotFound, self.process, 'CWUser/eid/30000')
self.assertRaises(NotFound, self.process, 'Workcases')
self.assertRaises(NotFound, self.process, 'CWUser/inexistant_attribute/joe')
--- a/web/test/unittest_urlrewrite.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/unittest_urlrewrite.py Fri Mar 11 09:47:09 2011 +0100
@@ -47,12 +47,14 @@
('/schema', dict(vid='schema')),
('/myprefs', dict(vid='propertiesform')),
('/siteconfig', dict(vid='systempropertiesform')),
- ('/siteinfo', dict(vid='info')),
+ ('/siteinfo', dict(vid='siteinfo')),
('/manage', dict(vid='manage')),
('/notfound', dict(vid='404')),
('/error', dict(vid='error')),
('/sparql', dict(vid='sparql')),
('/processinfo', dict(vid='processinfo')),
+ ('/cwuser$', {'vid': 'cw.user-management'}),
+ ('/cwsource$', {'vid': 'cw.source-management'}),
('/schema/([^/]+?)/?$', {'rql': r'Any X WHERE X is CWEType, X name "\1"', 'vid': 'primary'}),
('/add/([^/]+?)/?$' , dict(vid='creation', etype=r'\1')),
('/doc/images/(.+?)/?$', dict(fid='\\1', vid='wdocimages')),
--- a/web/test/unittest_viewselector.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/unittest_viewselector.py Fri Mar 11 09:47:09 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.
@@ -27,25 +27,32 @@
traced_selection)
from cubicweb.web import NoSelectableObject
from cubicweb.web.action import Action
-from cubicweb.web.views import (primary, baseviews, tableview, editforms,
- calendar, management, embedding, actions,
- startup, cwuser, schema, xbel, vcard, owl,
- treeview, idownloadable, wdoc, debug,
- cwproperties, workflow, xmlrss, csvexport)
+from cubicweb.web.views import (
+ primary, baseviews, tableview, editforms, calendar, management, embedding,
+ actions, startup, cwuser, schema, xbel, vcard, owl, treeview, idownloadable,
+ wdoc, debug, cwuser, cwproperties, cwsources, workflow, xmlrss, rdf,
+ csvexport)
from cubes.folder import views as folderviews
USERACTIONS = [actions.UserPreferencesAction,
actions.UserInfoAction,
actions.LogoutAction]
-SITEACTIONS = [actions.SiteConfigurationAction,
- actions.ManageAction,
- schema.ViewSchemaAction,
- debug.SiteInfoAction]
+SITEACTIONS = [actions.ManageAction]
FOOTERACTIONS = [wdoc.HelpAction,
wdoc.ChangeLogAction,
wdoc.AboutAction,
actions.PoweredByAction]
+MANAGEACTIONS = [actions.SiteConfigurationAction,
+ schema.ViewSchemaAction,
+ cwuser.ManageUsersAction,
+ cwsources.ManageSourcesAction,
+ debug.SiteInfoAction]
+
+if hasattr(rdf, 'RDFView') is not None: # not available if rdf lib not installed
+ RDFVIEWS = [('rdf', rdf.RDFView)]
+
+assert RDFVIEWS
class ViewSelectorTC(CubicWebTC):
@@ -83,6 +90,8 @@
req = self.request()
self.assertListEqual(self.pviews(req, None),
[('changelog', wdoc.ChangeLogView),
+ ('cw.source-management', cwsources.CWSourceManagementView),
+ ('cw.user-management', cwuser.CWUserManagementView),
('gc', debug.GCView),
('index', startup.IndexView),
('info', debug.ProcessInformationView),
@@ -91,6 +100,7 @@
('propertiesform', cwproperties.CWPropertiesForm),
('registry', debug.RegistryView),
('schema', schema.SchemaView),
+ ('siteinfo', debug.SiteInfoView),
('systempropertiesform', cwproperties.SystemCWPropertiesForm),
('tree', folderviews.FolderTreeView),
])
@@ -112,7 +122,7 @@
('list', baseviews.ListView),
('oneline', baseviews.OneLineView),
('owlabox', owl.OWLABOXView),
- ('primary', cwuser.CWGroupPrimaryView),
+ ('primary', cwuser.CWGroupPrimaryView)] + RDFVIEWS + [
('rsetxml', xmlrss.XMLRsetView),
('rss', xmlrss.RSSView),
('sameetypelist', baseviews.SameETypeListView),
@@ -136,7 +146,7 @@
('list', baseviews.ListView),
('oneline', baseviews.OneLineView),
('owlabox', owl.OWLABOXView),
- ('primary', cwuser.CWGroupPrimaryView),
+ ('primary', cwuser.CWGroupPrimaryView)] + RDFVIEWS + [
('rsetxml', xmlrss.XMLRsetView),
('rss', xmlrss.RSSView),
('sameetypelist', baseviews.SameETypeListView),
@@ -191,7 +201,7 @@
('list', baseviews.ListView),
('oneline', baseviews.OneLineView),
('owlabox', owl.OWLABOXView),
- ('primary', primary.PrimaryView),
+ ('primary', primary.PrimaryView),] + RDFVIEWS + [
('rsetxml', xmlrss.XMLRsetView),
('rss', xmlrss.RSSView),
('secondary', baseviews.SecondaryView),
@@ -218,6 +228,7 @@
rset = req.execute('CWUser X')
self.assertListEqual(self.pviews(req, rset),
[('csvexport', csvexport.CSVRsetView),
+ ('cw.user-table', cwuser.CWUserTable),
('ecsvexport', csvexport.CSVEntityView),
('editable-table', tableview.EditableTableView),
('filetree', treeview.FileTreeView),
@@ -225,7 +236,7 @@
('list', baseviews.ListView),
('oneline', baseviews.OneLineView),
('owlabox', owl.OWLABOXView),
- ('primary', primary.PrimaryView),
+ ('primary', primary.PrimaryView)] + RDFVIEWS + [
('rsetxml', xmlrss.XMLRsetView),
('rss', xmlrss.RSSView),
('sameetypelist', baseviews.SameETypeListView),
@@ -244,6 +255,7 @@
self.assertDictEqual(self.pactionsdict(req, None, skipcategories=()),
{'useractions': USERACTIONS,
'siteactions': SITEACTIONS,
+ 'manage': MANAGEACTIONS,
'footer': FOOTERACTIONS,
})
@@ -253,6 +265,7 @@
self.assertDictEqual(self.pactionsdict(req, rset, skipcategories=()),
{'useractions': USERACTIONS,
'siteactions': SITEACTIONS,
+ 'manage': MANAGEACTIONS,
'footer': FOOTERACTIONS,
})
@@ -262,6 +275,7 @@
self.assertDictEqual(self.pactionsdict(req, rset, skipcategories=()),
{'useractions': USERACTIONS,
'siteactions': SITEACTIONS,
+ 'manage': MANAGEACTIONS,
'footer': FOOTERACTIONS,
'mainactions': [actions.MultipleEditAction],
'moreactions': [actions.DeleteAction,
@@ -274,6 +288,7 @@
self.assertDictEqual(self.pactionsdict(req, rset, skipcategories=()),
{'useractions': USERACTIONS,
'siteactions': SITEACTIONS,
+ 'manage': MANAGEACTIONS,
'footer': FOOTERACTIONS,
'moreactions': [actions.DeleteAction],
})
@@ -284,6 +299,7 @@
self.assertDictEqual(self.pactionsdict(req, rset, skipcategories=()),
{'useractions': USERACTIONS,
'siteactions': SITEACTIONS,
+ 'manage': MANAGEACTIONS,
'footer': FOOTERACTIONS,
})
@@ -293,6 +309,7 @@
self.assertDictEqual(self.pactionsdict(req, rset, skipcategories=()),
{'useractions': USERACTIONS,
'siteactions': SITEACTIONS,
+ 'manage': MANAGEACTIONS,
'footer': FOOTERACTIONS,
'mainactions': [actions.ModifyAction,
actions.ViewSameCWEType],
@@ -508,6 +525,7 @@
{'useractions': USERACTIONS,
'siteactions': SITEACTIONS,
'footer': FOOTERACTIONS,
+ 'manage': MANAGEACTIONS,
'mainactions': [actions.ModifyAction, actions.ViewSameCWEType],
'moreactions': [actions.ManagePermissionsAction,
actions.AddRelatedActions,
@@ -522,6 +540,7 @@
{'useractions': USERACTIONS,
'siteactions': SITEACTIONS,
'footer': FOOTERACTIONS,
+ 'manage': MANAGEACTIONS,
'mainactions': [actions.ModifyAction, actions.ViewSameCWEType],
'moreactions': [actions.ManagePermissionsAction,
actions.AddRelatedActions,
--- a/web/test/windmill/test_creation.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/windmill/test_creation.py Fri Mar 11 09:47:09 2011 +0100
@@ -26,7 +26,8 @@
client.type(text=u'myuser', id=u'upassword-subject:A')
client.type(text=u'myuser', name=u'upassword-subject-confirm:A')
client.type(text=u'myuser', id=u'firstname-subject:A')
- client.select(option=u'managers', id=u'in_group-subject:A')
+ client.select(option=u'managers', id=u'from_in_group-subject:A')
+ client.click(id=u'cwinoutadd')
client.waits.forPageLoad(timeout=u'20000')
client.click(id=u'adduse_email:Alink')
client.waits.forPageLoad(timeout=u'20000')
--- a/web/test/windmill/test_edit_relation.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/test/windmill/test_edit_relation.py Fri Mar 11 09:47:09 2011 +0100
@@ -22,8 +22,8 @@
client.type(text=u'folder1', id=u'name-subject:A')
client.click(value=u'button_ok')
client.waits.forPageLoad(timeout=u'20000')
- client.waits.forElement(link=u'add Folder filed_under Folder object', timeout=u'8000')
- client.click(link=u'add Folder filed_under Folder object')
+ client.waits.forElement(link=u'add add Folder filed_under Folder object', timeout=u'8000')
+ client.click(link=u'add add Folder filed_under Folder object')
client.waits.forPageLoad(timeout=u'20000')
client.waits.forElement(timeout=u'8000', id=u'name-subject:A')
client.click(id=u'name-subject:A')
@@ -44,8 +44,8 @@
client.click(link=u'x')
client.click(value=u'button_ok')
client.waits.forPageLoad(timeout=u'20000')
- client.waits.forElement(link=u'add Folder filed_under Folder object', timeout=u'8000')
- client.click(link=u'add Folder filed_under Folder object')
+ client.waits.forElement(link=u'add add Folder filed_under Folder object', timeout=u'8000')
+ client.click(link=u'add add Folder filed_under Folder object')
client.waits.forPageLoad(timeout=u'20000')
client.type(text=u'subfolder2', id=u'name-subject:A')
client.click(value=u'button_ok')
--- a/web/views/actions.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/actions.py Fri Mar 11 09:47:09 2011 +0100
@@ -391,6 +391,7 @@
__regid__ = 'siteconfig'
title = _('site configuration')
order = 10
+ category = 'manage'
class ManageAction(ManagersAction):
--- a/web/views/authentication.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/authentication.py Fri Mar 11 09:47:09 2011 +0100
@@ -100,17 +100,13 @@
self.anoninfo = (self.anoninfo[0], {'password': self.anoninfo[1]})
def validate_session(self, req, session):
- """check session validity, reconnecting it to the repository if the
- associated connection expired in the repository side (hence the
- necessity for this method). Return the connected user on success.
+ """check session validity and return the connected user on success.
raise :exc:`InvalidSession` if session is corrupted for a reason or
another and should be closed
also invoked while going from anonymous to logged in
"""
- # with this authentication manager, session is actually a dbapi
- # connection
for retriever in self.authinforetrievers:
if retriever.request_has_auth_info(req):
login = retriever.revalidate_login(req)
@@ -135,8 +131,7 @@
def authenticate(self, req):
"""authenticate user using connection information found in the request,
and return corresponding a :class:`~cubicweb.dbapi.Connection` instance,
- as well as login and authentication information dictionary used to open
- the connection.
+ as well as login used to open the connection.
raise :exc:`cubicweb.AuthenticationError` if authentication failed
(no authentication info found or wrong user/password)
@@ -152,8 +147,7 @@
continue # the next one may succeed
for retriever_ in self.authinforetrievers:
retriever_.authenticated(retriever, req, cnx, login, authinfo)
- return cnx, login, authinfo
-
+ return cnx, login
# false if no authentication info found, eg this is not an
# authentication failure
if 'login' in locals():
@@ -162,7 +156,7 @@
if login:
cnx = self._authenticate(login, authinfo)
cnx.anonymous_connection = True
- return cnx, login, authinfo
+ return cnx, login
raise AuthenticationError()
def _authenticate(self, login, authinfo):
--- a/web/views/basetemplates.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/basetemplates.py Fri Mar 11 09:47:09 2011 +0100
@@ -156,7 +156,7 @@
lang = self._cw.lang
self.write_doctype()
# explictly close the <base> tag to avoid IE 6 bugs while browsing DOM
- w(u'<base href="%s"></base>' % xml_escape(self._cw.base_url()))
+ self._cw.html_headers.define_var('BASE_URL', self._cw.base_url())
w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
% (content_type, self._cw.encoding))
w(u'\n'.join(additional_headers) + u'\n')
--- a/web/views/baseviews.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/baseviews.py Fri Mar 11 09:47:09 2011 +0100
@@ -31,7 +31,7 @@
from rql import nodes
-from logilab.mtconverter import TransformError, xml_escape, xml_escape
+from logilab.mtconverter import TransformError, xml_escape
from cubicweb import NoSelectableObject, tags
from cubicweb.selectors import yes, empty_rset, one_etype_rset, match_kwargs
--- a/web/views/boxes.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/boxes.py Fri Mar 11 09:47:09 2011 +0100
@@ -208,7 +208,6 @@
raise component.EmptyComponent()
self.items = []
-
class RsetBox(component.CtxComponent):
"""helper view class to display an rset in a sidebox"""
__select__ = nonempty_rset() & match_kwargs('title', 'vid')
--- a/web/views/calendar.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/calendar.py Fri Mar 11 09:47:09 2011 +0100
@@ -20,15 +20,28 @@
__docformat__ = "restructuredtext en"
_ = unicode
-from datetime import datetime, date, timedelta
+import copy
+from datetime import timedelta
from logilab.mtconverter import xml_escape
-from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime
+from logilab.common.date import todatetime
+from cubicweb.utils import json_dumps
from cubicweb.interfaces import ICalendarable
from cubicweb.selectors import implements, adaptable
from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+# useful constants & functions ################################################
+
+ONEDAY = timedelta(1)
+
+WEEKDAYS = (_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
+ _("friday"), _("saturday"), _("sunday"))
+MONTHNAMES = ( _('january'), _('february'), _('march'), _('april'), _('may'),
+ _('june'), _('july'), _('august'), _('september'), _('october'),
+ _('november'), _('december')
+ )
+
class ICalendarableAdapter(EntityAdapter):
__needs_bw_compat__ = True
@@ -44,21 +57,10 @@
@property
@implements_adapter_compat('ICalendarable')
def stop(self):
- """return stop state"""
+ """return stop date"""
raise NotImplementedError
-# useful constants & functions ################################################
-
-ONEDAY = timedelta(1)
-
-WEEKDAYS = (_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
- _("friday"), _("saturday"), _("sunday"))
-MONTHNAMES = ( _('january'), _('february'), _('march'), _('april'), _('may'),
- _('june'), _('july'), _('august'), _('september'), _('october'),
- _('november'), _('december')
- )
-
# Calendar views ##############################################################
try:
@@ -146,9 +148,6 @@
self.w('<br/>%s'%self._cw.format_date(icalendarable.start
or icalendarable.stop))
-class CalendarLargeItemView(CalendarItemView):
- __regid__ = 'calendarlargeitem'
-
class _TaskEntry(object):
def __init__(self, task, color, index=0):
@@ -170,413 +169,91 @@
return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar()
-class OneMonthCal(EntityView):
- """At some point, this view will probably replace ampm calendars"""
- __regid__ = 'onemonthcal'
- __select__ = adaptable('ICalendarable')
-
- paginable = False
- title = _('one month')
-
- def call(self):
- self._cw.add_js('cubicweb.ajax.js')
- self._cw.add_css('cubicweb.calendar.css')
- # XXX: restrict courses directy with RQL
- _today = datetime.today()
-
- if 'year' in self._cw.form:
- year = int(self._cw.form['year'])
- else:
- year = _today.year
- if 'month' in self._cw.form:
- month = int(self._cw.form['month'])
- else:
- month = _today.month
-
- first_day_of_month = date(year, month, 1)
- firstday = first_day_of_month - timedelta(first_day_of_month.weekday())
- if month >= 12:
- last_day_of_month = date(year + 1, 1, 1) - timedelta(1)
- else:
- last_day_of_month = date(year, month + 1, 1) - timedelta(1)
- # date range exclude last day so we should at least add one day, hence
- # the 7
- lastday = last_day_of_month + timedelta(7 - last_day_of_month.weekday())
- month_dates = list(date_range(firstday, lastday))
- dates = {}
- task_max = 0
- for row in xrange(self.cw_rset.rowcount):
- task = self.cw_rset.get_entity(row, 0)
- if len(self.cw_rset[row]) > 1 and self.cw_rset.description[row][1] == 'CWUser':
- user = self.cw_rset.get_entity(row, 1)
- else:
- user = None
- the_dates = []
- icalendarable = task.cw_adapt_to('ICalendarable')
- tstart = icalendarable.start
- if tstart:
- tstart = todate(icalendarable.start)
- if tstart > lastday:
- continue
- the_dates = [tstart]
- tstop = icalendarable.stop
- if tstop:
- tstop = todate(tstop)
- if tstop < firstday:
- continue
- the_dates = [tstop]
- if tstart and tstop:
- if tstart.isocalendar() == tstop.isocalendar():
- if firstday <= tstart <= lastday:
- the_dates = [tstart]
- else:
- the_dates = date_range(max(tstart, firstday),
- min(tstop + ONEDAY, lastday))
- if not the_dates:
- continue
-
- for d in the_dates:
- d_tasks = dates.setdefault((d.year, d.month, d.day), {})
- t_users = d_tasks.setdefault(task, set())
- t_users.add( user )
- if len(d_tasks) > task_max:
- task_max = len(d_tasks)
-
- days = []
- nrows = max(3, task_max)
- # colors here are class names defined in cubicweb.css
- colors = [ "col%x" % i for i in range(12) ]
- next_color_index = 0
-
- visited_tasks = {} # holds a description of a task
- task_colors = {} # remember a color assigned to a task
- for mdate in month_dates:
- d_tasks = dates.get((mdate.year, mdate.month, mdate.day), {})
- rows = [None] * nrows
- # every task that is "visited" for the first time
- # require a special treatment, so we put them in
- # 'postpone'
- postpone = []
- for task in d_tasks:
- if task in visited_tasks:
- task_descr = visited_tasks[ task ]
- rows[task_descr.index] = task_descr
- else:
- postpone.append(task)
- for task in postpone:
- # to every 'new' task we must affect a color
- # (which must be the same for every user concerned
- # by the task)
- for i, t in enumerate(rows):
- if t is None:
- if task in task_colors:
- color = task_colors[task]
- else:
- color = colors[next_color_index]
- next_color_index = (next_color_index+1)%len(colors)
- task_colors[task] = color
- task_descr = _TaskEntry(task, color, i)
- rows[i] = task_descr
- visited_tasks[task] = task_descr
- break
- else:
- raise RuntimeError("is it possible we got it wrong?")
-
- days.append( rows )
-
- curdate = first_day_of_month
- self.w(u'<div id="onemonthcalid">')
- # build schedule
- self.w(u'<table class="omcalendar">')
- prevlink, nextlink = self._prevnext_links(curdate) # XXX
- self.w(u'<tr><th><a href="%s"><<</a></th><th colspan="5">%s %s</th>'
- u'<th><a href="%s">>></a></th></tr>' %
- (xml_escape(prevlink), self._cw._(curdate.strftime('%B').lower()),
- curdate.year, xml_escape(nextlink)))
-
- # output header
- self.w(u'<tr><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th></tr>' %
- tuple(self._cw._(day) for day in WEEKDAYS))
- # build calendar
- for mdate, task_rows in zip(month_dates, days):
- if mdate.weekday() == 0:
- self.w(u'<tr>')
- self._build_calendar_cell(mdate, task_rows, curdate)
- if mdate.weekday() == 6:
- self.w(u'</tr>')
- self.w(u'</table></div>')
-
- def _prevnext_links(self, curdate):
- prevdate = curdate - timedelta(31)
- nextdate = curdate + timedelta(31)
- rql = self.cw_rset.printable_rql()
- prevlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql,
- vid='onemonthcal',
- year=prevdate.year,
- month=prevdate.month)
- nextlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql,
- vid='onemonthcal',
- year=nextdate.year,
- month=nextdate.month)
- return prevlink, nextlink
-
- def _build_calendar_cell(self, celldate, rows, curdate):
- curmonth = curdate.month
- classes = ""
- if celldate.month != curmonth:
- classes += " outOfRange"
- if celldate == date.today():
- classes += " today"
- self.w(u'<td class="cell%s">' % classes)
- self.w(u'<div class="calCellTitle%s">' % classes)
- self.w(u'<div class="day">%s</div>' % celldate.day)
-
- if len(self.cw_rset.column_types(0)) == 1:
- etype = list(self.cw_rset.column_types(0))[0]
- url = self._cw.build_url(vid='creation', etype=etype,
- schedule=True,
- start=self._cw.format_date(celldate), stop=self._cw.format_date(celldate),
- __redirectrql=self.cw_rset.printable_rql(),
- __redirectparams=self._cw.build_url_params(year=curdate.year, month=curmonth),
- __redirectvid=self.__regid__
- )
- self.w(u'<div class="cmd"><a href="%s">%s</a></div>' % (xml_escape(url), self._cw._(u'add')))
- self.w(u' ')
- self.w(u'</div>')
- self.w(u'<div class="cellContent">')
- for task_descr in rows:
- if task_descr:
- task = task_descr.task
- self.w(u'<div class="task %s">' % task_descr.color)
- task.view('calendaritem', w=self.w )
- url = task.absolute_url(vid='edition',
- __redirectrql=self.cw_rset.printable_rql(),
- __redirectparams=self._cw.build_url_params(year=curdate.year, month=curmonth),
- __redirectvid=self.__regid__
- )
-
- self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
- task.view('tooltip', w=self.w )
- self.w(u'</div>')
- else:
- self.w(u'<div class="task">')
- self.w(u" ")
- self.w(u'</div>')
- self.w(u'</div>')
- self.w(u'</td>')
-
-
-class OneWeekCal(EntityView):
- """At some point, this view will probably replace ampm calendars"""
- __regid__ = 'oneweekcal'
+class CalendarView(EntityView):
+ __regid__ = 'calendar'
__select__ = adaptable('ICalendarable')
paginable = False
- title = _('one week')
+ title = _('calendar')
+
+ fullcalendar_options = {
+ 'firstDay': 1,
+ 'header': {'left': 'prev,next today',
+ 'center': 'title',
+ 'right': 'month,agendaWeek,agendaDay',
+ },
+ 'editable': True,
+ 'defaultView': 'month',
+ 'timeFormat': {'month': '',
+ '': 'H:mm'},
+ 'firstHour': 8,
+ 'axisFormat': 'H:mm',
+ 'columnFormat': {'month': 'dddd',
+ 'agendaWeek': 'dddd yyyy/M/dd',
+ 'agendaDay': 'dddd yyyy/M/dd'}
+ }
+
def call(self):
- self._cw.add_js( ('cubicweb.ajax.js', 'cubicweb.calendar.js') )
- self._cw.add_css('cubicweb.calendar.css')
- # XXX: restrict directly with RQL
- _today = datetime.today()
- if 'year' in self._cw.form:
- year = int(self._cw.form['year'])
- else:
- year = _today.year
- if 'week' in self._cw.form:
- week = int(self._cw.form['week'])
- else:
- week = _today.isocalendar()[1]
- # week - 1 since we get week number > 0 while we want it to start from 0
- first_day_of_week = todate(strptime('%s-%s-1' % (year, week - 1), '%Y-%U-%w'))
- lastday = first_day_of_week + timedelta(6)
- firstday = first_day_of_week
- dates = [[] for i in range(7)]
- task_colors = {} # remember a color assigned to a task
- # colors here are class names defined in cubicweb.css
- colors = [ "col%x" % i for i in range(12) ]
- next_color_index = 0
- done_tasks = set()
- for row in xrange(self.cw_rset.rowcount):
- task = self.cw_rset.get_entity(row, 0)
- if task.eid in done_tasks:
- continue
- done_tasks.add(task.eid)
- the_dates = []
- icalendarable = task.cw_adapt_to('ICalendarable')
- tstart = icalendarable.start
- tstop = icalendarable.stop
- if tstart:
- tstart = todate(tstart)
- if tstart > lastday:
- continue
- the_dates = [tstart]
- if tstop:
- tstop = todate(tstop)
- if tstop < firstday:
- continue
- the_dates = [tstop]
- if tstart and tstop:
- the_dates = date_range(max(tstart, firstday),
- min(tstop + ONEDAY, lastday))
- if not the_dates:
- continue
+ self._cw.demote_to_html()
+ self._cw.add_css(('fullcalendar.css', 'cubicweb.calendar.css'))
+ self._cw.add_js(('jquery.ui.js', 'fullcalendar.min.js', 'jquery.qtip.min.js'))
+ self.add_onload()
+ # write calendar div to load jquery fullcalendar object
+ self.w(u'<div id="calendar"></div>')
- if task not in task_colors:
- task_colors[task] = colors[next_color_index]
- next_color_index = (next_color_index+1) % len(colors)
-
- for d in the_dates:
- day = d.weekday()
- task_descr = _TaskEntry(task, task_colors[task])
- dates[day].append(task_descr)
-
- self.w(u'<div id="oneweekcalid">')
- # build schedule
- self.w(u'<table class="omcalendar" id="week">')
- prevlink, nextlink = self._prevnext_links(first_day_of_week) # XXX
- self.w(u'<tr><th class="transparent"></th>')
- self.w(u'<th><a href="%s"><<</a></th><th colspan="5">%s %s %s</th>'
- u'<th><a href="%s">>></a></th></tr>' %
- (xml_escape(prevlink), first_day_of_week.year,
- self._cw._(u'week'), first_day_of_week.isocalendar()[1],
- xml_escape(nextlink)))
-
- # output header
- self.w(u'<tr>')
- self.w(u'<th class="transparent"></th>') # column for hours
- _today = date.today()
- for i, day in enumerate(WEEKDAYS):
- wdate = first_day_of_week + timedelta(i)
- if wdate.isocalendar() == _today.isocalendar():
- self.w(u'<th class="today">%s<br/>%s</th>' % (self._cw._(day), self._cw.format_date(wdate)))
- else:
- self.w(u'<th>%s<br/>%s</th>' % (self._cw._(day), self._cw.format_date(wdate)))
- self.w(u'</tr>')
-
- # build week calendar
- self.w(u'<tr>')
- self.w(u'<td style="width:5em;">') # column for hours
- extra = ""
- for h in range(8, 20):
- self.w(u'<div class="hour" %s>'%extra)
- self.w(u'%02d:00'%h)
- self.w(u'</div>')
- self.w(u'</td>')
- for i, day in enumerate(WEEKDAYS):
- wdate = first_day_of_week + timedelta(i)
- classes = ""
- if wdate.isocalendar() == _today.isocalendar():
- classes = " today"
- self.w(u'<td class="column %s" id="%s">' % (classes, day))
- if len(self.cw_rset.column_types(0)) == 1:
- etype = list(self.cw_rset.column_types(0))[0]
- url = self._cw.build_url(vid='creation', etype=etype,
- schedule=True,
- __redirectrql=self.cw_rset.printable_rql(),
- __redirectparams=self._cw.build_url_params(year=year, week=week),
- __redirectvid=self.__regid__
- )
- extra = ' ondblclick="addCalendarItem(event, hmin=8, hmax=20, year=%s, month=%s, day=%s, duration=2, baseurl=\'%s\')"' % (
- wdate.year, wdate.month, wdate.day, xml_escape(url))
- else:
- extra = ""
- self.w(u'<div class="columndiv"%s>'% extra)
- for h in range(8, 20):
- self.w(u'<div class="hourline" style="top:%sex;">'%((h-7)*8))
- self.w(u'</div>')
- if dates[i]:
- self._build_calendar_cell(wdate, dates[i])
- self.w(u'</div>')
- self.w(u'</td>')
- self.w(u'</tr>')
- self.w(u'</table></div>')
- self.w(u'<div id="coord"></div>')
- self.w(u'<div id="debug"> </div>')
-
- def _build_calendar_cell(self, date, task_descrs):
- inday_tasks = [t for t in task_descrs if t.is_one_day_task() and t.in_working_hours()]
- wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()]
- inday_tasks.sort(key=lambda t:t.start)
- sorted_tasks = []
- for i, t in enumerate(wholeday_tasks):
- t.index = i
- ncols = len(wholeday_tasks)
- while inday_tasks:
- t = inday_tasks.pop(0)
- for i, c in enumerate(sorted_tasks):
- if not c or c[-1].stop <= t.start:
- c.append(t)
- t.index = i+ncols
- break
- else:
- t.index = len(sorted_tasks) + ncols
- sorted_tasks.append([t])
- ncols += len(sorted_tasks)
- if ncols == 0:
- return
-
- inday_tasks = []
- for tasklist in sorted_tasks:
- inday_tasks += tasklist
- width = 100.0/ncols
- for task_desc in wholeday_tasks + inday_tasks:
- task = task_desc.task
- start_hour = 8
- start_min = 0
- stop_hour = 20
- stop_min = 0
- if task_desc.start:
- if date < todate(task_desc.start) < date + ONEDAY:
- start_hour = max(8, task_desc.start.hour)
- start_min = task_desc.start.minute
- if task_desc.stop:
- if date < todate(task_desc.stop) < date + ONEDAY:
- stop_hour = min(20, task_desc.stop.hour)
- if stop_hour < 20:
- stop_min = task_desc.stop.minute
-
- height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8)
- top = 100.0*(start_hour+start_min/60.0-8)/(20-8)
- left = width*task_desc.index
- style = "height: %s%%; width: %s%%; top: %s%%; left: %s%%; " % \
- (height, width, top, left)
- self.w(u'<div class="task %s" style="%s">' % \
- (task_desc.color, style))
- task.view('calendaritem', dates=False, w=self.w)
- url = task.absolute_url(vid='edition',
- __redirectrql=self.cw_rset.printable_rql(),
- __redirectparams=self._cw.build_url_params(year=date.year, week=date.isocalendar()[1]),
- __redirectvid=self.__regid__
- )
-
- self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
- task.view('tooltip', w=self.w)
- self.w(u'</div>')
- if task_desc.start is None:
- self.w(u'<div class="bottommarker">')
- self.w(u'<div class="bottommarkerline" style="margin: 0px 3px 0px 3px; height: 1px;">')
- self.w(u'</div>')
- self.w(u'<div class="bottommarkerline" style="margin: 0px 2px 0px 2px; height: 1px;">')
- self.w(u'</div>')
- self.w(u'<div class="bottommarkerline" style="margin: 0px 1px 0px 1px; height: 3ex; color: white; font-size: x-small; vertical-align: center; text-align: center;">')
- self.w(u'end')
- self.w(u'</div>')
- self.w(u'</div>')
- self.w(u'</div>')
+ def add_onload(self):
+ fullcalendar_options = self.fullcalendar_options.copy()
+ fullcalendar_options['events'] = self.get_events()
+ fullcalendar_options['buttonText'] = {'today': self._cw._('today'),
+ 'month': self._cw._('month'),
+ 'week': self._cw._('week'),
+ 'day': self._cw._('day')}
+ # js callback to add a tooltip and to put html in event's title
+ js = """
+ var options = %s;
+ options.eventRender = function(event, $element) {
+ // add a tooltip for each event
+ var div = '<div class="tooltip">'+ event.description+ '</div>';
+ $element.append(div);
+ // allow to have html tags in event's title
+ $element.find('span.fc-event-title').html($element.find('span.fc-event-title').text());
+ };
+ $("#calendar").fullCalendar(options);
+ """ #"
+ self._cw.add_onload(js % json_dumps(fullcalendar_options))
- def _prevnext_links(self, curdate):
- prevdate = curdate - timedelta(7)
- nextdate = curdate + timedelta(7)
- rql = self.cw_rset.printable_rql()
- prevlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql,
- vid='oneweekcal',
- year=prevdate.year,
- week=prevdate.isocalendar()[1])
- nextlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql,
- vid='oneweekcal',
- year=nextdate.year,
- week=nextdate.isocalendar()[1])
- return prevlink, nextlink
+ def get_events(self):
+ events = []
+ for entity in self.cw_rset.entities():
+ icalendarable = entity.cw_adapt_to('ICalendarable')
+ event = {'eid': entity.eid,
+ 'title': entity.view('calendaritem'),
+ 'url': xml_escape(entity.absolute_url()),
+ 'className': 'calevent',
+ 'description': entity.view('tooltip'),
+ }
+ start_date = icalendarable.start
+ if not start_date:
+ start_date = icalendarable.stop
+ event['start'] = start_date.strftime('%Y-%m-%dT%H:%M')
+ event['allDay'] = True
+ if icalendarable.stop:
+ event['end'] = icalendarable.stop.strftime('%Y-%m-%dT%H:%M')
+ event['allDay'] = False
+ events.append(event)
+ return events
+
+class OneMonthCal(CalendarView):
+ __regid__ = 'onemonthcal'
+
+ title = _('one month')
+
+class OneWeekCal(CalendarView):
+ __regid__ = 'oneweekcal'
+
+ title = _('one week')
+ fullcalendar_options = CalendarView.fullcalendar_options.copy()
+ fullcalendar_options['defaultView'] = 'agendaWeek'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/cwsources.py Fri Mar 11 09:47:09 2011 +0100
@@ -0,0 +1,234 @@
+# 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 <http://www.gnu.org/licenses/>.
+"""Specific views for data sources and related entities (eg CWSource,
+CWSourceHostConfig, CWSourceSchemaConfig).
+"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from itertools import repeat, chain
+
+from cubicweb.selectors import is_instance, score_entity, match_user_groups
+from cubicweb.view import EntityView, StartupView
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
+from cubicweb.web import uicfg
+from cubicweb.web.views import tabs, actions
+
+
+_abaa = uicfg.actionbox_appearsin_addmenu
+_abaa.tag_object_of(('CWSourceSchemaConfig', 'cw_schema', '*'), False)
+_abaa.tag_object_of(('CWSourceSchemaConfig', 'cw_for_source', '*'), False)
+
+# source primary views #########################################################
+
+_pvs = uicfg.primaryview_section
+_pvs.tag_object_of(('*', 'cw_for_source', 'CWSource'), 'hidden')
+
+
+class CWSourcePrimaryView(tabs.TabbedPrimaryView):
+ __select__ = is_instance('CWSource')
+ tabs = [_('cwsource-main'), _('cwsource-mapping')]
+ default_tab = 'cwsource-main'
+
+
+class CWSourceMainTab(tabs.PrimaryTab):
+ __regid__ = 'cwsource-main'
+ __select__ = tabs.PrimaryTab.__select__ & is_instance('CWSource')
+
+
+MAPPED_SOURCE_TYPES = set( ('pyrorql', 'datafeed') )
+
+class CWSourceMappingTab(EntityView):
+ __regid__ = 'cwsource-mapping'
+ __select__ = (tabs.PrimaryTab.__select__ & is_instance('CWSource')
+ & match_user_groups('managers')
+ & score_entity(lambda x:x.type in MAPPED_SOURCE_TYPES))
+
+ def entity_call(self, entity):
+ _ = self._cw._
+ self.w('<h3>%s</h3>' % _('Entity and relation supported by this source'))
+ eschema = self._cw.vreg.schema.eschema('CWSourceSchemaConfig')
+ if eschema.has_perm(self._cw, 'add'):
+ self.w(u'<a href="%s" class="addButton right">%s</a>' % (
+ self._cw.build_url('add/%s' % eschema),
+ self._cw._('add a CWSourceSchemaConfig')))
+ self.w(u'<div class="clear"></div>')
+ rset = self._cw.execute(
+ 'Any X, SCH, XO ORDERBY ET WHERE X options XO, X cw_for_source S, S eid %(s)s, '
+ 'X cw_schema SCH, SCH is ET', {'s': entity.eid})
+ self.wview('table', rset, 'noresult')
+ # self.w('<h3>%s</h3>' % _('Relations that should not be crossed'))
+ # self.w('<p>%s</p>' % _(
+ # 'By default, when a relation is not supported by a source, it is '
+ # 'supposed that a local relation may point to an entity from the '
+ # 'external source. Relations listed here won\'t have this '
+ # '"crossing" behaviour.'))
+ # self.wview('list', entity.related('cw_dont_cross'), 'noresult')
+ # self.w('<h3>%s</h3>' % _('Relations that can be crossed'))
+ # self.w('<p>%s</p>' % _(
+ # 'By default, when a relation is supported by a source, it is '
+ # 'supposed that a local relation can\'t point to an entity from the '
+ # 'external source. Relations listed here may have this '
+ # '"crossing" behaviour anyway.'))
+ # self.wview('list', entity.related('cw_may_cross'), 'noresult')
+ checker = MAPPING_CHECKERS.get(entity.type, MappingChecker)(entity)
+ checker.check()
+ if (checker.errors or checker.warnings or checker.infos):
+ self.w('<h2>%s</h2>' % _('Detected problems'))
+ errors = zip(repeat(_('error')), checker.errors)
+ warnings = zip(repeat(_('warning')), checker.warnings)
+ infos = zip(repeat(_('warning')), checker.infos)
+ self.wview('pyvaltable', pyvalue=chain(errors, warnings, infos))
+
+
+class MappingChecker(object):
+ def __init__(self, cwsource):
+ self.cwsource = cwsource
+ self.errors = []
+ self.warnings = []
+ self.infos = []
+ self.schema = cwsource._cw.vreg.schema
+
+ def init(self):
+ # supported entity types
+ self.sentities = set()
+ # supported relations
+ self.srelations = {}
+ # avoid duplicated messages
+ self.seen = set()
+ # first get mapping as dict/sets
+ for schemacfg in self.cwsource.reverse_cw_for_source:
+ self.init_schemacfg(schemacfg)
+
+ def init_schemacfg(self, schemacfg):
+ cwerschema = schemacfg.schema
+ if cwerschema.__regid__ == 'CWEType':
+ self.sentities.add(cwerschema.name)
+ elif cwerschema.__regid__ == 'CWRType':
+ assert not cwerschema.name in self.srelations
+ self.srelations[cwerschema.name] = None
+ else: # CWAttribute/CWRelation
+ self.srelations.setdefault(cwerschema.rtype.name, []).append(
+ (cwerschema.stype.name, cwerschema.otype.name) )
+
+ def check(self):
+ self.init()
+ error = self.errors.append
+ warning = self.warnings.append
+ info = self.infos.append
+ for etype in self.sentities:
+ eschema = self.schema[etype]
+ for rschema, ttypes, role in eschema.relation_definitions():
+ if rschema in META_RTYPES:
+ continue
+ ttypes = [ttype for ttype in ttypes if ttype in self.sentities]
+ if not rschema in self.srelations:
+ for ttype in ttypes:
+ rdef = rschema.role_rdef(etype, ttype, role)
+ self.seen.add(rdef)
+ if rdef.role_cardinality(role) in '1+':
+ error(_('relation %(type)s with %(etype)s as %(role)s '
+ 'and target type %(target)s is mandatory but '
+ 'not supported') %
+ {'rtype': rschema, 'etype': etype, 'role': role,
+ 'target': ttype})
+ elif ttype in self.sentities:
+ warning(_('%s could be supported') % rdef)
+ elif not ttypes:
+ warning(_('relation %(rtype)s with %(etype)s as %(role)s is '
+ 'supported but no target type supported') %
+ {'rtype': rschema, 'role': role, 'etype': etype})
+ for rtype in self.srelations:
+ rschema = self.schema[rtype]
+ for subj, obj in rschema.rdefs:
+ if subj in self.sentities and obj in self.sentities:
+ break
+ else:
+ error(_('relation %s is supported but none if its definitions '
+ 'matches supported entities') % rtype)
+ self.custom_check()
+
+ def custom_check(self):
+ pass
+
+
+class PyroRQLMappingChecker(MappingChecker):
+ """pyrorql source mapping checker"""
+
+ def init(self):
+ self.dontcross = set()
+ self.maycross = set()
+ super(PyroRQLMappingChecker, self).init()
+
+ def init_schemacfg(self, schemacfg):
+ options = schemacfg.options or ()
+ if 'dontcross' in options:
+ self.dontcross.add(schemacfg.schema.name)
+ else:
+ super(PyroRQLMappingChecker, self).init_schemacfg(schemacfg)
+ if 'maycross' in options:
+ self.maycross.add(schemacfg.schema.name)
+
+ def custom_check(self):
+ error = self.errors.append
+ info = self.infos.append
+ for etype in self.sentities:
+ eschema = self.schema[etype]
+ for rschema, ttypes, role in eschema.relation_definitions():
+ if rschema in META_RTYPES:
+ continue
+ if not rschema in self.srelations:
+ if rschema not in self.dontcross:
+ if role == 'subject' and rschema.inlined:
+ error(_('inlined relation %(rtype)s of %(etype)s '
+ 'should be supported') %
+ {'rtype': rschema, 'etype': etype})
+ elif (rschema not in self.seen and rschema not in self.maycross):
+ info(_('you may want to specify something for %s') %
+ rschema)
+ self.seen.add(rschema)
+ elif rschema in self.maycross and rschema.inlined:
+ error(_('you should un-inline relation %s which is '
+ 'supported and may be crossed ') % rschema)
+
+MAPPING_CHECKERS = {
+ 'pyrorql': PyroRQLMappingChecker,
+ }
+
+# sources management view ######################################################
+
+class ManageSourcesAction(actions.ManagersAction):
+ __regid__ = 'cwsource'
+ title = _('data sources')
+ category = 'manage'
+
+class CWSourceManagementView(StartupView):
+ __regid__ = 'cw.source-management'
+ rql = ('Any S, ST, SN ORDERBY SN WHERE S is CWSource, S name SN, S type ST')
+ title = _('data sources management')
+
+ def call(self, **kwargs):
+ self.w('<h1>%s</h1>' % self._cw._(self.title))
+ eschema = self._cw.vreg.schema.eschema('CWSource')
+ if eschema.has_perm(self._cw, 'add'):
+ self.w(u'<a href="%s" class="addButton right">%s</a>' % (
+ self._cw.build_url('add/%s' % eschema),
+ self._cw._('add a CWSource')))
+ self.w(u'<div class="clear"></div>')
+ self.wview('table', self._cw.execute(self.rql), displaycols=range(2))
--- a/web/views/cwuser.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/cwuser.py Fri Mar 11 09:47:09 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -24,10 +24,11 @@
from logilab.mtconverter import xml_escape
+from cubicweb.schema import display_name
from cubicweb.selectors import one_line_rset, is_instance, match_user_groups
-from cubicweb.view import EntityView
+from cubicweb.view import EntityView, StartupView
from cubicweb.web import action, uicfg, formwidgets
-from cubicweb.web.views import tabs
+from cubicweb.web.views import tabs, tableview, actions
_pvs = uicfg.primaryview_section
_pvs.tag_attribute(('CWUser', 'login'), 'hidden')
@@ -157,3 +158,50 @@
entity = self.cw_rset.complete_entity(row, col)
self.w(u'<a href="%s" class="%s">%s</a>' % (
entity.absolute_url(), entity.name, entity.printable_value('name')))
+
+
+# user / groups management views ###############################################
+
+class ManageUsersAction(actions.ManagersAction):
+ __regid__ = 'cwuser' # see rewrite rule /cwuser
+ title = _('users and groups')
+ category = 'manage'
+
+
+class CWUserManagementView(StartupView):
+ __regid__ = 'cw.user-management'
+ rql = ('Any U, F, S, U, L ORDERBY L WHERE U is CWUser, U login L, U firstname F, U surname S')
+ title = _('users and groups management')
+
+ def call(self, **kwargs):
+ self.w('<h1>%s</h1>' % self._cw._(self.title))
+ for etype in ('CWUser', 'CWGroup'):
+ eschema = self._cw.vreg.schema.eschema(etype)
+ if eschema.has_perm(self._cw, 'add'):
+ self.w(u'<a href="%s" class="addButton right">%s</a>' % (
+ self._cw.build_url('add/%s' % eschema),
+ self._cw._('add a %s' % etype).capitalize()))
+ self.w(u'<div class="clear"></div>')
+ self.wview('cw.user-table', self._cw.execute(self.rql))
+
+
+class CWUserTable(tableview.EditableTableView):
+ __regid__ = 'cw.user-table'
+ __select__ = is_instance('CWUser')
+
+ def call(self, **kwargs):
+ headers = (display_name(self._cw, 'CWUser', 'plural'),
+ self._cw._('firstname'), self._cw._('surname'),
+ display_name(self._cw, 'CWGroup', 'plural'))
+ super(CWUserTable, self).call(
+ paginate=True, cellvids={3: 'cw.user-table.group-cell'},
+ headers=headers, **kwargs)
+
+
+class CWUserGroupCell(EntityView):
+ __regid__ = 'cw.user-table.group-cell'
+ __select__ = is_instance('CWUser')
+
+ def cell_call(self, row, col, **kwargs):
+ entity = self.cw_rset.get_entity(row, col)
+ self.w(entity.view('reledit', rtype='in_group', role='subject'))
--- a/web/views/debug.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/debug.py Fri Mar 11 09:47:09 2011 +0100
@@ -27,7 +27,7 @@
from cubicweb import BadConnectionId
from cubicweb.selectors import none_rset, match_user_groups
from cubicweb.view import StartupView
-from cubicweb.web.views import actions
+from cubicweb.web.views import actions, tabs
def dict_to_html(w, dict):
# XHTML doesn't allow emtpy <ul> nodes
@@ -39,12 +39,24 @@
w(u'</ul>')
-
class SiteInfoAction(actions.ManagersAction):
__regid__ = 'siteinfo'
__select__ = match_user_groups('users','managers')
- title = _('info')
- order = 30
+ title = _('siteinfo')
+ category = 'manage'
+ order = 1000
+
+
+class SiteInfoView(tabs.TabsMixin, StartupView):
+ __regid__ = 'siteinfo'
+ title = _('Site information')
+ tabs = [_('info'), _('registry'), _('gc')]
+ default_tab = 'info'
+
+ def call(self, **kwargs):
+ """The default view representing the instance's management"""
+ self.w(u'<h1>%s</h1>' % self._cw._(self.title))
+ self.render_tabs(self.tabs, self.default_tab)
class ProcessInformationView(StartupView):
@@ -61,7 +73,7 @@
_ = req._
w = self.w
# generic instance information
- w(u'<h1>%s</h1>' % _('Instance'))
+ w(u'<h2>%s</h2>' % _('Instance'))
w(u'<table>')
w(u'<tr><th align="left">%s</th><td>%s</td></tr>' % (
_('config type'), self._cw.vreg.config.name))
@@ -82,7 +94,7 @@
w(u'</table>')
# repository information
repo = req.vreg.config.repository(None)
- w(u'<h1>%s</h1>' % _('Repository'))
+ w(u'<h2>%s</h2>' % _('Repository'))
w(u'<h3>%s</h3>' % _('resources usage'))
w(u'<table>')
stats = repo.stats()
@@ -107,7 +119,7 @@
else:
w(u'<p>%s</p>' % _('no repository sessions found'))
# web server information
- w(u'<h1>%s</h1>' % _('Web server'))
+ w(u'<h2>%s</h2>' % _('Web server'))
w(u'<table>')
w(u'<tr><th align="left">%s</th><td>%s</td></tr>' % (
_('base url'), req.base_url()))
@@ -146,7 +158,7 @@
cache_max_age = 0
def call(self, **kwargs):
- self.w(u'<h1>%s</h1>' % self._cw._("Registry's content"))
+ self.w(u'<h2>%s</h2>' % self._cw._("Registry's content"))
keys = sorted(self._cw.vreg)
url = xml_escape(self._cw.url())
self.w(u'<p>%s</p>\n' % ' - '.join('<a href="%s#%s">%s</a>'
@@ -154,7 +166,7 @@
for key in keys:
if key in ('boxes', 'contentnavigation'): # those are bw compat registries
continue
- self.w(u'<h2 id="%s">%s</h2>' % (key, key))
+ self.w(u'<h3 id="%s">%s</h3>' % (key, key))
if self._cw.vreg[key]:
values = sorted(self._cw.vreg[key].iteritems())
self.wview('pyvaltable', pyvalue=[(key, xml_escape(repr(val)))
@@ -186,7 +198,7 @@
lookupclasses += (InternalSession, Session)
except ImportError:
pass # no server part installed
- self.w(u'<h1>%s</h1>' % _('Garbage collection information'))
+ self.w(u'<h2>%s</h2>' % _('Garbage collection information'))
counters, ocounters, garbage = gc_info(lookupclasses,
viewreferrersclasses=())
self.w(u'<h3>%s</h3>' % self._cw._('Looked up classes'))
--- a/web/views/old_calendar.py Thu Mar 10 15:18:22 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,575 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views"""
-
-__docformat__ = "restructuredtext en"
-_ = unicode
-
-from datetime import date, time, timedelta
-
-from logilab.mtconverter import xml_escape
-from logilab.common.date import (ONEDAY, ONEWEEK, days_in_month, previous_month,
- next_month, first_day, last_day, date_range)
-
-from cubicweb.interfaces import ICalendarViews
-from cubicweb.selectors import implements, adaptable
-from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
-
-class ICalendarViewsAdapter(EntityAdapter):
- """calendar views interface"""
- __needs_bw_compat__ = True
- __regid__ = 'ICalendarViews'
- __select__ = implements(ICalendarViews, warn=False) # XXX for bw compat, should be abstract
-
- @implements_adapter_compat('ICalendarViews')
- def matching_dates(self, begin, end):
- """
- :param begin: day considered as begin of the range (`DateTime`)
- :param end: day considered as end of the range (`DateTime`)
-
- :return:
- a list of dates (`DateTime`) in the range [`begin`, `end`] on which
- this entity apply
- """
- raise NotImplementedError
-
-
-# used by i18n tools
-WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
- _("friday"), _("saturday"), _("sunday")]
-MONTHNAMES = [ _('january'), _('february'), _('march'), _('april'), _('may'),
- _('june'), _('july'), _('august'), _('september'), _('october'),
- _('november'), _('december')
- ]
-
-class _CalendarView(EntityView):
- """base calendar view containing helpful methods to build calendar views"""
- __select__ = adaptable('ICalendarViews')
- paginable = False
-
- # Navigation building methods / views ####################################
-
- PREV = u'<a href="%s"><<</a>  <a href="%s"><</a>'
- NEXT = u'<a href="%s">></a>  <a href="%s">>></a>'
- NAV_HEADER = u"""<table class="calendarPageHeader">
-<tr><td class="prev">%s</td><td class="next">%s</td></tr>
-</table>
-""" % (PREV, NEXT)
-
- def nav_header(self, date, smallshift=3, bigshift=9):
- """prints shortcut links to go to previous/next steps (month|week)"""
- prev1 = previous_month(date, smallshift)
- next1 = next_month(date, smallshift)
- prev2 = previous_month(date, bigshift)
- next2 = next_month(date, bigshift)
- rql = self.cw_rset.printable_rql()
- return self.NAV_HEADER % (
- xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev2.year,
- month=prev2.month)),
- xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev1.year,
- month=prev1.month)),
- xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next1.year,
- month=next1.month)),
- xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next2.year,
- month=next2.month)))
-
-
- # Calendar building methods ##############################################
-
- def build_calendars(self, schedule, begin, end):
- """build several HTML calendars at once, one for each month
- between begin and end
- """
- return [self.build_calendar(schedule, date)
- for date in date_range(begin, end, incmonth=1)]
-
- def build_calendar(self, schedule, first_day):
- """method responsible for building *one* HTML calendar"""
- # FIXME iterates between [first_day-first_day.day_of_week ;
- # last_day+6-last_day.day_of_week]
- umonth = self._cw.format_date(first_day, '%B %Y') # localized month name
- rows = []
- current_row = [NO_CELL] * first_day.weekday()
- for daynum in xrange(0, days_in_month(first_day)):
- # build cell day
- day = first_day + timedelta(daynum)
- events = schedule.get(day)
- if events:
- events = [u'\n'.join(event) for event in events.values()]
- current_row.append(CELL % (daynum+1, '\n'.join(events)))
- else:
- current_row.append(EMPTY_CELL % (daynum+1))
- # store & reset current row on Sundays
- if day.weekday() == 6:
- rows.append(u'<tr>%s%s</tr>' % (WEEKNUM_CELL % day.isocalendar()[1], ''.join(current_row)))
- current_row = []
- current_row.extend([NO_CELL] * (6-day.weekday()))
- rql = self.cw_rset.printable_rql()
- if day.weekday() != 6:
- rows.append(u'<tr>%s%s</tr>' % (WEEKNUM_CELL % day.isocalendar()[1], ''.join(current_row)))
- url = self._cw.build_url(rql=rql, vid='calendarmonth',
- year=first_day.year, month=first_day.month)
- monthlink = u'<a href="%s">%s</a>' % (xml_escape(url), umonth)
- return CALENDAR(self._cw) % (monthlink, '\n'.join(rows))
-
- def _mk_schedule(self, begin, end, itemvid='calendaritem'):
- """private method that gathers information from resultset
- and builds calendars according to it
-
- :param begin: begin of date range
- :param end: end of date rangs
- :param itemvid: which view to call to render elements in cells
-
- returns { day1 : { hour : [views] },
- day2 : { hour : [views] } ... }
- """
- # put this here since all sub views are calling this method
- self._cw.add_css('cubicweb.calendar.css')
- schedule = {}
- for row in xrange(len(self.cw_rset.rows)):
- entity = self.cw_rset.get_entity(row, 0)
- infos = u'<div class="event">'
- infos += self._cw.view(itemvid, self.cw_rset, row=row)
- infos += u'</div>'
- for date_ in entity.cw_adapt_to('ICalendarViews').matching_dates(begin, end):
- day = date(date_.year, date_.month, date_.day)
- try:
- dt = time(date_.hour, date_.minute, date_.second)
- except AttributeError:
- # date instance
- dt = time(0, 0, 0)
- schedule.setdefault(day, {})
- schedule[day].setdefault(dt, []).append(infos)
- return schedule
-
-
- @staticmethod
- def get_date_range(day, shift=4):
- """returns a couple (begin, end)
-
- <begin> is the first day of current_month - shift
- <end> is the last day of current_month + (shift+1)
- """
- begin = first_day(previous_month(day, shift))
- end = last_day(next_month(day, shift))
- return begin, end
-
- def _build_ampm_cells(self, events):
- """create a view without any hourly details.
-
- :param events: dictionnary with all events classified by hours
- """
- # split events according am/pm
- am_events = [event for e_time, e_list in events.iteritems()
- if 0 <= e_time.hour < 12
- for event in e_list]
- pm_events = [event for e_time, e_list in events.iteritems()
- if 12 <= e_time.hour < 24
- for event in e_list]
- # format each am/pm cell
- if am_events:
- am_content = AMPM_CONTENT % ("amCell", "am", '\n'.join(am_events))
- else:
- am_content = AMPM_EMPTY % ("amCell", "am")
- if pm_events:
- pm_content = AMPM_CONTENT % ("pmCell", "pm", '\n'.join(pm_events))
- else:
- pm_content = AMPM_EMPTY % ("pmCell", "pm")
- return am_content, pm_content
-
-
-
-class YearCalendarView(_CalendarView):
- __regid__ = 'calendaryear'
- title = _('calendar (year)')
-
- def call(self, year=None, month=None):
- """this view renders a 3x3 calendars' table"""
- year = year or int(self._cw.form.get('year', date.today().year))
- month = month or int(self._cw.form.get('month', date.today().month))
- center_date = date(year, month, 1)
- begin, end = self.get_date_range(day=center_date)
- schedule = self._mk_schedule(begin, end)
- self.w(self.nav_header(center_date))
- calendars = tuple(self.build_calendars(schedule, begin, end))
- self.w(SMALL_CALENDARS_PAGE % calendars)
-
-
-class SemesterCalendarView(_CalendarView):
- """this view renders three semesters as three rows of six columns,
- one column per month
- """
- __regid__ = 'calendarsemester'
- title = _('calendar (semester)')
-
- def call(self, year=None, month=None):
- year = year or int(self._cw.form.get('year', date.today().year))
- month = month or int(self._cw.form.get('month', date.today().month))
- begin = previous_month(date(year, month, 1), 2)
- end = next_month(date(year, month, 1), 3)
- schedule = self._mk_schedule(begin, end)
- self.w(self.nav_header(date(year, month, 1), 1, 6))
- self.w(u'<table class="semesterCalendar">')
- self.build_calendars(schedule, begin, end)
- self.w(u'</table>')
- self.w(self.nav_header(date(year, month, 1), 1, 6))
-
- def build_calendars(self, schedule, begin, end):
- self.w(u'<tr>')
- rql = self.cw_rset.printable_rql()
- for cur_month in date_range(begin, end, incmonth=1):
- umonth = u'%s %s' % (self._cw.format_date(cur_month, '%B'), cur_month.year)
- url = self._cw.build_url(rql=rql, vid=self.__regid__,
- year=cur_month.year, month=cur_month.month)
- self.w(u'<th colspan="2"><a href="%s">%s</a></th>' % (xml_escape(url),
- umonth))
- self.w(u'</tr>')
- _ = self._cw._
- for day_num in xrange(31):
- self.w(u'<tr>')
- for cur_month in date_range(begin, end, incmonth=1):
- if day_num >= days_in_month(cur_month):
- self.w(u'%s%s' % (NO_CELL, NO_CELL))
- else:
- day = date(cur_month.year, cur_month.month, day_num+1)
- events = schedule.get(day)
- self.w(u'<td>%s %s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1))
- self.format_day_events(day, events)
- self.w(u'</tr>')
-
- def format_day_events(self, day, events):
- if events:
- events = ['\n'.join(event) for event in events.values()]
- self.w(WEEK_CELL % '\n'.join(events))
- else:
- self.w(WEEK_EMPTY_CELL)
-
-
-class MonthCalendarView(_CalendarView):
- """this view renders a 3x1 calendars' table"""
- __regid__ = 'calendarmonth'
- title = _('calendar (month)')
-
- def call(self, year=None, month=None):
- year = year or int(self._cw.form.get('year', date.today().year))
- month = month or int(self._cw.form.get('month', date.today().month))
- center_date = date(year, month, 1)
- begin, end = self.get_date_range(day=center_date, shift=1)
- schedule = self._mk_schedule(begin, end)
- calendars = self.build_calendars(schedule, begin, end)
- self.w(self.nav_header(center_date, 1, 3))
- self.w(BIG_CALENDARS_PAGE % tuple(calendars))
- self.w(self.nav_header(center_date, 1, 3))
-
-
-class WeekCalendarView(_CalendarView):
- """this view renders a calendar for week events"""
- __regid__ = 'calendarweek'
- title = _('calendar (week)')
-
- def call(self, year=None, week=None):
- year = year or int(self._cw.form.get('year', date.today().year))
- week = week or int(self._cw.form.get('week', date.today().isocalendar()[1]))
- day0 = date(year, 1, 1)
- first_day_of_week = day0 - day0.weekday()*ONEDAY + ONEWEEK
- begin, end = first_day_of_week- ONEWEEK, first_day_of_week + 2*ONEWEEK
- schedule = self._mk_schedule(begin, end, itemvid='calendarlargeitem')
- self.w(self.nav_header(first_day_of_week))
- self.w(u'<table class="weekCalendar">')
- _weeks = [(first_day_of_week-ONEWEEK, first_day_of_week-ONEDAY),
- (first_day_of_week, first_day_of_week+6*ONEDAY),
- (first_day_of_week+ONEWEEK, first_day_of_week+13*ONEDAY)]
- self.build_calendar(schedule, _weeks)
- self.w(u'</table>')
- self.w(self.nav_header(first_day_of_week))
-
- def build_calendar(self, schedule, weeks):
- rql = self.cw_rset.printable_rql()
- _ = self._cw._
- for monday, sunday in weeks:
- umonth = self._cw.format_date(monday, '%B %Y')
- url = self._cw.build_url(rql=rql, vid='calendarmonth',
- year=monday.year, month=monday.month)
- monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
- self.w(u'<tr><th colspan="3">%s %s (%s)</th></tr>' \
- % (_('week'), monday.isocalendar()[1], monthlink))
- for day in date_range(monday, sunday+ONEDAY):
- self.w(u'<tr>')
- self.w(u'<td>%s</td>' % _(WEEKDAYS[day.weekday()]))
- self.w(u'<td>%s</td>' % (day.strftime('%Y-%m-%d')))
- events = schedule.get(day)
- if events:
- events = ['\n'.join(event) for event in events.values()]
- self.w(WEEK_CELL % '\n'.join(events))
- else:
- self.w(WEEK_EMPTY_CELL)
- self.w(u'</tr>')
-
- def nav_header(self, date, smallshift=1, bigshift=3):
- """prints shortcut links to go to previous/next steps (month|week)"""
- prev1 = date - ONEWEEK * smallshift
- prev2 = date - ONEWEEK * bigshift
- next1 = date + ONEWEEK * smallshift
- next2 = date + ONEWEEK * bigshift
- rql = self.cw_rset.printable_rql()
- return self.NAV_HEADER % (
- xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev2.year, week=prev2.isocalendar()[1])),
- xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev1.year, week=prev1.isocalendar()[1])),
- xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next1.year, week=next1.isocalendar()[1])),
- xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next2.year, week=next2.isocalendar()[1])))
-
-
-
-class AMPMYearCalendarView(YearCalendarView):
- __regid__ = 'ampmcalendaryear'
- title = _('am/pm calendar (year)')
-
- def build_calendar(self, schedule, first_day):
- """method responsible for building *one* HTML calendar"""
- umonth = self._cw.format_date(first_day, '%B %Y') # localized month name
- rows = [] # each row is: (am,pm), (am,pm) ... week_title
- current_row = [(NO_CELL, NO_CELL, NO_CELL)] * first_day.weekday()
- rql = self.cw_rset.printable_rql()
- for daynum in xrange(0, days_in_month(first_day)):
- # build cells day
- day = first_day + timedelta(daynum)
- events = schedule.get(day)
- if events:
- current_row.append((AMPM_DAY % (daynum+1),) + self._build_ampm_cells(events))
- else:
- current_row.append((AMPM_DAY % (daynum+1),
- AMPM_EMPTY % ("amCell", "am"),
- AMPM_EMPTY % ("pmCell", "pm")))
- # store & reset current row on Sundays
- if day.weekday() == 6:
- url = self._cw.build_url(rql=rql, vid='ampmcalendarweek',
- year=day.year, week=day.isocalendar()[1])
- weeklink = '<a href="%s">%s</a>' % (xml_escape(url),
- day.isocalendar()[1])
- current_row.append(WEEKNUM_CELL % weeklink)
- rows.append(current_row)
- current_row = []
- current_row.extend([(NO_CELL, NO_CELL, NO_CELL)] * (6-day.weekday()))
- url = self._cw.build_url(rql=rql, vid='ampmcalendarweek',
- year=day.year, week=day.isocalendar()[1])
- weeklink = '<a href="%s">%s</a>' % (xml_escape(url), day.isocalendar()[1])
- current_row.append(WEEKNUM_CELL % weeklink)
- rows.append(current_row)
- # build two rows for each week: am & pm
- formatted_rows = []
- for row in rows:
- week_title = row.pop()
- day_row = [day for day, am, pm in row]
- am_row = [am for day, am, pm in row]
- pm_row = [pm for day, am, pm in row]
- formatted_rows.append('<tr>%s%s</tr>'% (week_title, '\n'.join(day_row)))
- formatted_rows.append('<tr class="amRow"><td> </td>%s</tr>'% '\n'.join(am_row))
- formatted_rows.append('<tr class="pmRow"><td> </td>%s</tr>'% '\n'.join(pm_row))
- # tigh everything together
- url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth',
- year=first_day.year, month=first_day.month)
- monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
- return CALENDAR(self._cw) % (monthlink, '\n'.join(formatted_rows))
-
-
-
-class AMPMSemesterCalendarView(SemesterCalendarView):
- """this view renders a 3x1 calendars' table"""
- __regid__ = 'ampmcalendarsemester'
- title = _('am/pm calendar (semester)')
-
- def build_calendars(self, schedule, begin, end):
- self.w(u'<tr>')
- rql = self.cw_rset.printable_rql()
- for cur_month in date_range(begin, end, incmonth=1):
- umonth = u'%s %s' % (self._cw.format_date(cur_month, '%B'), cur_month.year)
- url = self._cw.build_url(rql=rql, vid=self.__regid__,
- year=cur_month.year, month=cur_month.month)
- self.w(u'<th colspan="3"><a href="%s">%s</a></th>' % (xml_escape(url),
- umonth))
- self.w(u'</tr>')
- _ = self._cw._
- for day_num in xrange(31):
- self.w(u'<tr>')
- for cur_month in date_range(begin, end, incmonth=1):
- if day_num >= days_in_month(cur_month):
- self.w(u'%s%s%s' % (NO_CELL, NO_CELL, NO_CELL))
- else:
- day = date(cur_month.year, cur_month.month, day_num+1)
- events = schedule.get(day)
- self.w(u'<td>%s %s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(),
- day_num+1))
- self.format_day_events(day, events)
- self.w(u'</tr>')
-
- def format_day_events(self, day, events):
- if events:
- self.w(u'\n'.join(self._build_ampm_cells(events)))
- else:
- self.w(u'%s %s'% (AMPM_EMPTY % ("amCell", "am"),
- AMPM_EMPTY % ("pmCell", "pm")))
-
-
-class AMPMMonthCalendarView(MonthCalendarView):
- """this view renders a 3x1 calendars' table"""
- __regid__ = 'ampmcalendarmonth'
- title = _('am/pm calendar (month)')
-
- def build_calendar(self, schedule, first_day):
- """method responsible for building *one* HTML calendar"""
- umonth = self._cw.format_date(first_day, '%B %Y') # localized month name
- rows = [] # each row is: (am,pm), (am,pm) ... week_title
- current_row = [(NO_CELL, NO_CELL, NO_CELL)] * first_day.weekday()
- rql = self.cw_rset.printable_rql()
- for daynum in xrange(0, days_in_month(first_day)):
- # build cells day
- day = first_day + timedelta(daynum)
- events = schedule.get(day)
- if events:
- current_row.append((AMPM_DAY % (daynum+1),) + self._build_ampm_cells(events))
- else:
- current_row.append((AMPM_DAY % (daynum+1),
- AMPM_EMPTY % ("amCell", "am"),
- AMPM_EMPTY % ("pmCell", "pm")))
- # store & reset current row on Sundays
- if day.weekday() == 6:
- url = self._cw.build_url(rql=rql, vid='ampmcalendarweek',
- year=day.year, week=day.isocalendar()[1])
- weeklink = '<a href="%s">%s</a>' % (xml_escape(url),
- day.isocalendar()[1])
- current_row.append(WEEKNUM_CELL % weeklink)
- rows.append(current_row)
- current_row = []
- current_row.extend([(NO_CELL, NO_CELL, NO_CELL)] * (6-day.weekday()))
- url = self._cw.build_url(rql=rql, vid='ampmcalendarweek',
- year=day.year, week=day.isocalendar()[1])
- weeklink = '<a href="%s">%s</a>' % (xml_escape(url),
- day.isocalendar()[1])
- current_row.append(WEEKNUM_CELL % weeklink)
- rows.append(current_row)
- # build two rows for each week: am & pm
- formatted_rows = []
- for row in rows:
- week_title = row.pop()
- day_row = [day for day, am, pm in row]
- am_row = [am for day, am, pm in row]
- pm_row = [pm for day, am, pm in row]
- formatted_rows.append('<tr>%s%s</tr>'% (week_title, '\n'.join(day_row)))
- formatted_rows.append('<tr class="amRow"><td> </td>%s</tr>'% '\n'.join(am_row))
- formatted_rows.append('<tr class="pmRow"><td> </td>%s</tr>'% '\n'.join(pm_row))
- # tigh everything together
- url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth',
- year=first_day.year, month=first_day.month)
- monthlink = '<a href="%s">%s</a>' % (xml_escape(url),
- umonth)
- return CALENDAR(self._cw) % (monthlink, '\n'.join(formatted_rows))
-
-
-
-class AMPMWeekCalendarView(WeekCalendarView):
- """this view renders a 3x1 calendars' table"""
- __regid__ = 'ampmcalendarweek'
- title = _('am/pm calendar (week)')
-
- def build_calendar(self, schedule, weeks):
- rql = self.cw_rset.printable_rql()
- w = self.w
- _ = self._cw._
- for monday, sunday in weeks:
- umonth = self._cw.format_date(monday, '%B %Y')
- url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth',
- year=monday.year, month=monday.month)
- monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
- w(u'<tr>%s</tr>' % (
- WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink)))
- w(u'<tr><th>%s</th><th> </th></tr>'% _(u'Date'))
- for day in date_range(monday, sunday+ONEDAY):
- events = schedule.get(day)
- style = day.weekday() % 2 and "even" or "odd"
- w(u'<tr class="%s">' % style)
- if events:
- hours = events.keys()
- hours.sort()
- w(AMPM_DAYWEEK % (
- len(hours), _(WEEKDAYS[day.weekday()]),
- self._cw.format_date(day)))
- w(AMPM_WEEK_CELL % (
- hours[0].hour, hours[0].minute,
- '\n'.join(events[hours[0]])))
- w(u'</tr>')
- for hour in hours[1:]:
- w(u'<tr class="%s">%s</tr>'% (
- style, AMPM_WEEK_CELL % (hour.hour, hour.minute,
- '\n'.join(events[hour]))))
- else:
- w(AMPM_DAYWEEK_EMPTY % (
- _(WEEKDAYS[day.weekday()]),
- self._cw.format_date(day)))
- w(WEEK_EMPTY_CELL)
- w(u'</tr>')
-
-
-SMALL_CALENDARS_PAGE = u"""<table class="smallCalendars">
-<tr><td class="calendar">%s</td><td class="calendar">%s</td><td class="calendar">%s</td></tr>
-<tr><td class="calendar">%s</td><td class="calendar">%s</td><td class="calendar">%s</td></tr>
-<tr><td class="calendar">%s</td><td class="calendar">%s</td><td class="calendar">%s</td></tr>
-</table>
-"""
-
-BIG_CALENDARS_PAGE = u"""<table class="bigCalendars">
-<tr><td class="calendar">%s</td></tr>
-<tr><td class="calendar">%s</td></tr>
-<tr><td class="calendar">%s</td></tr>
-</table>
-"""
-
-WEEKNUM_CELL = u'<td class="weeknum">%s</td>'
-
-def CALENDAR(req):
- _ = req._
- WEEKNUM_HEADER = u'<th class="weeknum">%s</th>' % _('week')
- CAL_HEADER = WEEKNUM_HEADER + u' \n'.join([u'<th class="weekday">%s</th>' % _(day)[0].upper()
- for day in WEEKDAYS])
- return u"""<table>
-<tr><th class="month" colspan="8">%%s</th></tr>
-<tr>
- %s
-</tr>
-%%s
-</table>
-""" % (CAL_HEADER,)
-
-
-DAY_TEMPLATE = """<tr><td class="weekday">%(daylabel)s</td><td>%(dmydate)s</td><td>%(dayschedule)s</td>
-"""
-
-NO_CELL = u'<td class="noday"></td>'
-EMPTY_CELL = u'<td class="cellEmpty"><span class="cellTitle">%s</span></td>'
-CELL = u'<td class="cell"><span class="cellTitle">%s</span><div class="cellContent">%s</div></td>'
-
-AMPM_DAY = u'<td class="cellDay">%d</td>'
-AMPM_EMPTY = u'<td class="%sEmpty"><span class="cellTitle">%s</span></td>'
-AMPM_CONTENT = u'<td class="%s"><span class="cellTitle">%s</span><div class="cellContent">%s</div></td>'
-
-WEEK_TITLE = u'<th class="weekTitle" colspan="2">%s %s (%s)</th>'
-WEEK_EMPTY_CELL = u'<td class="weekEmptyCell"> </td>'
-WEEK_CELL = u'<td class="weekCell"><div class="cellContent">%s</div></td>'
-
-AMPM_DAYWEEK_EMPTY = u'<td>%s %s</td>'
-AMPM_DAYWEEK = u'<td rowspan="%d">%s %s</td>'
-AMPM_WEEK_CELL = u'<td class="ampmWeekCell"><div class="cellContent">%02d:%02d - %s</div></td>'
--- a/web/views/primary.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/primary.py Fri Mar 11 09:47:09 2011 +0100
@@ -52,10 +52,8 @@
"""
return []
- def cell_call(self, row, col):
- self.cw_row = row
- self.cw_col = col
- entity = self.cw_rset.complete_entity(row, col)
+ def entity_call(self, entity):
+ entity.complete()
self.render_entity(entity)
def render_entity(self, entity):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/rdf.py Fri Mar 11 09:47:09 2011 +0100
@@ -0,0 +1,100 @@
+# 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 <http://www.gnu.org/licenses/>.
+"""base xml and rss views"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from yams import xy
+
+from cubicweb.schema import VIRTUAL_RTYPES
+from cubicweb.view import EntityView
+from cubicweb.web.views.xmlrss import SERIALIZERS
+
+try:
+ import rdflib
+except ImportError:
+ rdflib = None
+
+if rdflib is not None:
+ RDF = rdflib.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
+ CW = rdflib.Namespace('http://ns.cubicweb.org/cubicweb/0.0/')
+ from rdflib import Literal, URIRef, Namespace
+
+ def urijoin(item):
+ base, ext = item
+ return URIRef(Namespace(base)[ext])
+
+ SKIP_RTYPES = VIRTUAL_RTYPES | set(['cwuri', 'is', 'is_instance_of'])
+
+ class RDFView(EntityView):
+ """rdf view for entities"""
+ __regid__ = 'rdf'
+ title = _('rdf')
+ templatable = False
+ content_type = 'text/xml' # +rdf
+
+ def call(self):
+ graph = rdflib.Graph()
+ graph.bind('cw', CW)
+ for prefix, xmlns in xy.XY.prefixes.items():
+ graph.bind(prefix, rdflib.Namespace(xmlns))
+ for i in xrange(self.cw_rset.rowcount):
+ entity = self.cw_rset.complete_entity(i, 0)
+ self.entity2graph(graph, entity)
+ self.w(graph.serialize().decode('utf-8'))
+
+ def entity2graph(self, graph, entity):
+ cwuri = URIRef(entity.cwuri)
+ add = graph.add
+ add( (cwuri, RDF.type, CW[entity.e_schema.type]) )
+ try:
+ for item in xy.xeq(entity.e_schema.type):
+ add( (cwuri, RDF.type, urijoin(item)) )
+ except xy.UnsupportedVocabulary:
+ pass
+ for rschema, eschemas, role in entity.e_schema.relation_definitions('relation'):
+ rtype = rschema.type
+ if rtype in SKIP_RTYPES or rtype.endswith('_permission'):
+ continue
+ for eschema in eschemas:
+ if eschema.final:
+ try:
+ value = entity.cw_attr_cache[rtype]
+ except KeyError:
+ continue # assuming rtype is Bytes
+ if value is not None:
+ add( (cwuri, CW[rtype], Literal(value)) )
+ try:
+ for item in xy.xeq('%s %s' % (entity.e_schema.type, rtype)):
+ add( (cwuri, urijoin(item[1]), Literal(value)) )
+ except xy.UnsupportedVocabulary:
+ pass
+ else:
+ for related in entity.related(rtype, role, entities=True):
+ if role == 'subject':
+ add( (cwuri, CW[rtype], URIRef(related.cwuri)) )
+ try:
+ for item in xy.xeq('%s %s' % (entity.e_schema.type, rtype)):
+ add( (cwuri, urijoin(item), URIRef(related.cwuri)) )
+ except xy.UnsupportedVocabulary:
+ pass
+ else:
+ add( (URIRef(related.cwuri), CW[rtype], cwuri) )
+
+
--- a/web/views/reledit.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/reledit.py Fri Mar 11 09:47:09 2011 +0100
@@ -56,12 +56,15 @@
_cancelclick = "cw.reledit.cleanupAfterCancel('%s')"
# ui side actions/buttons
- _addzone = u'<img title="%(msg)s" src="data/plus.png" alt="%(msg)s"/>'
+ _addzone = u'<img title="%(msg)s" src="%(logo)s" alt="%(msg)s"/>'
_addmsg = _('click to add a value')
- _deletezone = u'<img title="%(msg)s" src="data/cancel.png" alt="%(msg)s"/>'
+ _addlogo = 'plus.png'
+ _deletezone = u'<img title="%(msg)s" src="%(logo)s" alt="%(msg)s"/>'
_deletemsg = _('click to delete this value')
- _editzone = u'<img title="%(msg)s" src="data/pen_icon.png" alt="%(msg)s"/>'
+ _deletelogo = 'cancel.png'
+ _editzone = u'<img title="%(msg)s" src="%(logo)s" alt="%(msg)s"/>'
_editzonemsg = _('click to edit this field')
+ _editlogo = 'pen_icon.png'
# renderer
_form_renderer_id = 'base'
@@ -81,7 +84,7 @@
self._cw.add_js(('cubicweb.reledit.js', 'cubicweb.edition.js', 'cubicweb.ajax.js'))
entity = self.cw_rset.get_entity(row, col)
rschema = self._cw.vreg.schema[rtype]
- self._rules = rctrl.etype_get(entity.e_schema, rschema, role, '*')
+ self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
if rvid is not None or default_value is not None:
warn('[3.9] specifying rvid/default_value on select is deprecated, '
'reledit_ctrl rtag to control this' % self, DeprecationWarning)
@@ -210,14 +213,18 @@
# NOTE: should be sufficient given a well built schema/security
return rschema.has_perm(self._cw, 'delete', **kwargs)
+ def _build_zone(self, zonedef, msg, logo):
+ return zonedef % {'msg': xml_escape(self._cw._(msg)),
+ 'logo': xml_escape(self._cw.data_url(logo))}
+
def _build_edit_zone(self):
- return self._editzone % {'msg' : xml_escape(self._cw._(self._editzonemsg))}
+ return self._build_zone(self._editzone, self._editzonemsg, self._editlogo)
def _build_delete_zone(self):
- return self._deletezone % {'msg': xml_escape(self._cw._(self._deletemsg))}
+ return self._build_zone(self._deletezone, self._deletezonemsg, self._deletelogo)
def _build_add_zone(self):
- return self._addzone % {'msg': xml_escape(self._cw._(self._addmsg))}
+ return self._build_zone(self._addzone, self._addzonemsg, self._addlogo)
def _build_divid(self, rtype, role, entity_eid):
""" builds an id for the root div of a reledit widget """
--- a/web/views/schema.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/schema.py Fri Mar 11 09:47:09 2011 +0100
@@ -145,7 +145,7 @@
__regid__ = 'schema'
title = _('instance schema')
tabs = [_('schema-diagram'), _('schema-entity-types'),
- _('schema-relation-types'), _('schema-security')]
+ _('schema-relation-types')]
default_tab = 'schema-diagram'
def call(self):
@@ -183,110 +183,6 @@
self.wview('table', self._cw.execute(
'Any X ORDERBY N WHERE X is CWRType, X name N, X final FALSE'))
-
-class SchemaPermissionsTab(SecurityViewMixIn, StartupView):
- __regid__ = 'schema-security'
- __select__ = StartupView.__select__ & match_user_groups('managers')
-
- def call(self, display_relations=True):
- skiptypes = skip_types(self._cw)
- schema = self._cw.vreg.schema
- # compute entities
- entities = sorted(eschema for eschema in schema.entities()
- if not (eschema.final or eschema in skiptypes))
- # compute relations
- if display_relations:
- relations = sorted(rschema for rschema in schema.relations()
- if not (rschema.final
- or rschema in skiptypes
- or rschema in META_RTYPES))
- else:
- relations = []
- # index
- _ = self._cw._
- url = xml_escape(self._cw.build_url('schema'))
- self.w(u'<div id="schema_security">')
- self.w(u'<h2 class="schema">%s</h2>' % _('Index'))
- self.w(u'<h3 id="entities">%s</h3>' % _('Entity types'))
- ents = []
- for eschema in sorted(entities):
- ents.append(u'<a class="grey" href="%s#%s">%s</a>' % (
- url, eschema.type, eschema.type))
- self.w(u', '.join(ents))
- self.w(u'<h3 id="relations">%s</h3>' % _('Relation types'))
- rels = []
- for rschema in sorted(relations):
- rels.append(u'<a class="grey" href="%s#%s">%s</a>' % (
- url , rschema.type, rschema.type))
- self.w(u', '.join(rels))
- # permissions tables
- self.display_entities(entities)
- if relations:
- self.display_relations(relations)
- self.w(u'</div>')
-
- def has_non_default_perms(self, rdef):
- """return true if the given *attribute* relation definition has custom
- permission
- """
- for action in rdef.ACTIONS:
- def_rqlexprs = []
- def_groups = []
- for perm in DEFAULT_ATTRPERMS[action]:
- if not isinstance(perm, basestring):
- def_rqlexprs.append(perm.expression)
- else:
- def_groups.append(perm)
- rqlexprs = [rql.expression for rql in rdef.get_rqlexprs(action)]
- groups = rdef.get_groups(action)
- if groups != frozenset(def_groups) or \
- frozenset(rqlexprs) != frozenset(def_rqlexprs):
- return True
- return False
-
- def display_entities(self, entities):
- _ = self._cw._
- url = xml_escape(self._cw.build_url('schema'))
- self.w(u'<h2 id="entities" class="schema">%s</h2>' % _('Permissions for entity types'))
- for eschema in entities:
- self.w(u'<h3 id="%s" class="schema"><a href="%s">%s (%s)</a> ' % (
- eschema.type, self._cw.build_url('cwetype/%s' % eschema.type),
- eschema.type, _(eschema.type)))
- self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
- url, self._cw.uiprops['UP_ICON'], _('up')))
- self.w(u'</h3>')
- self.w(u'<div style="margin: 0px 1.5em">')
- self.permissions_table(eschema)
- # display entity attributes only if they have some permissions modified
- modified_attrs = []
- for attr, etype in eschema.attribute_definitions():
- rdef = eschema.rdef(attr)
- if attr not in META_RTYPES and self.has_non_default_perms(rdef):
- modified_attrs.append(rdef)
- if modified_attrs:
- self.w(u'<h4>%s</h4>' % _('Attributes with non default permissions:'))
- self.w(u'</div>')
- self.w(u'<div style="margin: 0px 6em">')
- for rdef in modified_attrs:
- attrtype = str(rdef.rtype)
- self.w(u'<h4 class="schema">%s (%s)</h4> ' % (attrtype, _(attrtype)))
- self.permissions_table(rdef)
- self.w(u'</div>')
-
- def display_relations(self, relations):
- _ = self._cw._
- url = xml_escape(self._cw.build_url('schema'))
- self.w(u'<h2 id="relations" class="schema">%s</h2>' % _('Permissions for relations'))
- for rschema in relations:
- self.w(u'<h3 id="%s" class="schema"><a href="%s">%s (%s)</a> ' % (
- rschema.type, self._cw.build_url('cwrtype/%s' % rschema.type),
- rschema.type, _(rschema.type)))
- self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
- url, self._cw.uiprops['UP_ICON'], _('up')))
- self.w(u'</h3>')
- self.grouped_permissions_table(rschema)
-
-
# CWEType ######################################################################
# register msgid generated in entity relations tables
@@ -794,13 +690,14 @@
__select__ = facet.AttributeFacet.__select__ & is_instance('CWEType', 'CWRType')
rtype = 'final'
+
class ViewSchemaAction(action.Action):
__regid__ = 'schema'
__select__ = yes()
title = _("site schema")
- category = 'siteactions'
order = 30
+ category = 'manage'
def url(self):
return self._cw.build_url(self.__regid__)
--- a/web/views/sessions.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/sessions.py Fri Mar 11 09:47:09 2011 +0100
@@ -69,8 +69,8 @@
raise :exc:`cubicweb.AuthenticationError` if authentication failed
(no authentication info found or wrong user/password)
"""
- cnx, login, authinfo = self.authmanager.authenticate(req)
- session = DBAPISession(cnx, login, authinfo)
+ cnx, login = self.authmanager.authenticate(req)
+ session = DBAPISession(cnx, login)
self._sessions[session.sessionid] = session
# associate the connection to the current request
req.set_session(session)
--- a/web/views/startup.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/startup.py Fri Mar 11 09:47:09 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,14 +15,15 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Set of HTML startup views. A startup view is global, e.g. doesn't
-apply to a result set.
+"""Set of HTML startup views. A startup view is global, e.g. doesn't apply to a
+result set.
"""
__docformat__ = "restructuredtext en"
_ = unicode
from logilab.common.textutils import unormalize
+from logilab.common.deprecation import deprecated
from logilab.mtconverter import xml_escape
from cubicweb.view import StartupView
@@ -35,101 +36,59 @@
title = _('manage')
http_cache_manager = httpcache.EtagHTTPCacheManager
add_etype_links = ()
-
- def display_folders(self):
- return False
+ skip_startup_views = set( ('index', 'manage', 'schema', 'owl', 'changelog',
+ 'systempropertiesform', 'propertiesform',
+ 'cw.user-management', 'cw.source-management',
+ 'siteinfo', 'info', 'registry', 'gc',
+ 'tree') )
def call(self, **kwargs):
"""The default view representing the instance's management"""
self._cw.add_css('cubicweb.manageview.css')
self.w(u'<h1>%s</h1>' % self._cw.property_value('ui.site-title'))
- if not self.display_folders():
- self._main_index()
- else:
- self.w(u'<table><tr>\n')
- self.w(u'<td style="width:40%">')
- self._main_index()
- self.w(u'</td><td style="width:60%">')
- self.folders()
- self.w(u'</td>')
- self.w(u'</tr></table>\n')
+ self.entities()
+ self.manage_actions()
+ self.startup_views()
- def _main_index(self):
- req = self._cw
- manager = req.user.matching_groups('managers')
- if not manager and 'Card' in self._cw.vreg.schema:
- rset = self._cw.execute('Card X WHERE X wikiid "index"')
- else:
- rset = None
- if rset:
- self.wview('inlined', rset, row=0)
- else:
- self.entities()
+ def manage_actions(self):
+ allactions = self._cw.vreg['actions'].possible_actions(self._cw)
+ if allactions.get('manage'):
self.w(u'<div class="hr"> </div>')
- self.startup_views()
- if manager and 'Card' in self._cw.vreg.schema:
- self.w(u'<div class="hr"> </div>')
- if rset:
- href = rset.get_entity(0, 0).absolute_url(vid='edition')
- label = self._cw._('edit the index page')
- else:
- href = req.build_url('view', vid='creation', etype='Card', wikiid='index')
- label = self._cw._('create an index page')
- self.w(u'<br/><a href="%s">%s</a>\n' % (xml_escape(href), label))
-
- def folders(self):
- self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by category'))
- self._cw.vreg['views'].select('tree', self._cw).render(w=self.w, maxlevel=1)
-
- def create_links(self):
- self.w(u'<ul class="createLink">')
- for etype in self.add_etype_links:
- eschema = self._cw.vreg.schema.eschema(etype)
- if eschema.has_perm(self._cw, 'add'):
+ self.w(u'<h2>%s</h2>\n' % self._cw._('Manage'))
+ self.w(u'<ul class="manageActions">')
+ for action in allactions['manage']:
self.w(u'<li><a href="%s">%s</a></li>' % (
- self._cw.build_url('add/%s' % eschema),
- self._cw.__('add a %s' % eschema).capitalize()))
- self.w(u'</ul>')
+ action.url(), self._cw._(action.title)))
+ self.w(u'</ul>')
def startup_views(self):
- self.w(u'<h2>%s</h2>\n' % self._cw._('Startup views'))
- self.startupviews_table()
-
- def startupviews_table(self):
- views = self._cw.vreg['views'].possible_views(self._cw, None)
+ views = [v for v in self._cw.vreg['views'].possible_views(self._cw, None)
+ if v.category == 'startupview'
+ and v.__regid__ not in self.skip_startup_views]
if not views:
return
+ self.w(u'<div class="hr"> </div>')
+ self.w(u'<h2>%s</h2>\n' % self._cw._('Startup views'))
self.w(u'<ul class="startup">')
for v in sorted(views, key=lambda x: self._cw._(x.title)):
- if v.category != 'startupview' or v.__regid__ in ('index', 'tree', 'manage'):
- continue
self.w('<li><a href="%s">%s</a></li>' % (
xml_escape(v.url()), xml_escape(self._cw._(v.title).capitalize())))
self.w(u'</ul>')
def entities(self):
schema = self._cw.vreg.schema
- self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by entity type'))
- manager = self._cw.user.matching_groups('managers')
- self.w(u'<table class="startup">')
- if manager:
- self.w(u'<tr><th colspan="4">%s</th></tr>\n' % self._cw._('application entities'))
- self.entity_types_table(eschema for eschema in schema.entities()
- if uicfg.indexview_etype_section.get(eschema) == 'application')
- if manager:
- self.w(u'<tr><th colspan="4">%s</th></tr>\n' % self._cw._('system entities'))
- self.entity_types_table(eschema for eschema in schema.entities()
- if uicfg.indexview_etype_section.get(eschema) == 'system')
- if 'CWAttribute' in schema: # check schema support
- self.w(u'<tr><th colspan="4">%s</th></tr>\n' % self._cw._('schema entities'))
- self.entity_types_table(eschema for eschema in schema.entities()
- if uicfg.indexview_etype_section.get(eschema) == 'schema')
- self.w(u'</table>')
+ eschemas = [eschema for eschema in schema.entities()
+ if uicfg.indexview_etype_section.get(eschema) == 'application']
+ if eschemas:
+ self.w(u'<div class="hr"> </div>')
+ self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by entity type'))
+ self.w(u'<table class="startup">')
+ self.entity_types_table(eschemas)
+ self.w(u'</table>')
def entity_types_table(self, eschemas):
- newline = 0
infos = sorted(self.entity_types(eschemas),
- key=lambda (l,a,e):unormalize(l))
+ key=lambda (l,a,e): unormalize(l))
q, r = divmod(len(infos), 2)
if r:
infos.append( (None, ' ', ' ') )
@@ -140,10 +99,9 @@
self.w(u'<td class="addcol">%s</td><td>%s</td>\n' % (addlink2, etypelink2))
self.w(u'</tr>\n')
-
def entity_types(self, eschemas):
- """return a list of formatted links to get a list of entities of
- a each entity's types
+ """return an iterator on formatted links to get a list of entities of
+ each entity types
"""
req = self._cw
for eschema in eschemas:
@@ -161,6 +119,18 @@
xml_escape(url), label, nb)
if eschema.has_perm(req, 'add'):
yield (label, etypelink, self.add_entity_link(etype))
+ else:
+ yield (label, etypelink, u'')
+
+ def create_links(self):
+ self.w(u'<ul class="createLink">')
+ for etype in self.add_etype_links:
+ eschema = self.schema.eschema(etype)
+ if eschema.has_perm(self._cw, 'add'):
+ self.w(u'<li><a href="%s">%s</a></li>' % (
+ self._cw.build_url('add/%s' % eschema),
+ self._cw.__('add a %s' % eschema).capitalize()))
+ self.w(u'</ul>')
def add_entity_link(self, etype):
"""creates a [+] link for adding an entity"""
@@ -168,11 +138,21 @@
return u'[<a href="%s" title="%s">+</a>]' % (
xml_escape(url), self._cw.__('add a %s' % etype))
+ @deprecated('[3.11] display_folders method is deprecated, backport it if needed')
+ def display_folders(self):
+ return False
+
+ @deprecated('[3.11] folders method is deprecated, backport it if needed')
+ def folders(self):
+ self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by category'))
+ self._cw.vreg['views'].select('tree', self._cw).render(w=self.w, maxlevel=1)
+
class IndexView(ManageView):
__regid__ = 'index'
title = _('view_index')
+ @deprecated('[3.11] display_folders method is deprecated, backport it if needed')
def display_folders(self):
return 'Folder' in self._cw.vreg.schema and self._cw.execute('Any COUNT(X) WHERE X is Folder')[0][0]
--- a/web/views/tabs.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/tabs.py Fri Mar 11 09:47:09 2011 +0100
@@ -24,27 +24,27 @@
from logilab.mtconverter import xml_escape
from cubicweb import NoSelectableObject, role
+from cubicweb import tags, uilib, utils
from cubicweb.selectors import partial_has_related_entities
from cubicweb.view import EntityView
-from cubicweb import tags, uilib
-from cubicweb.utils import make_uid
from cubicweb.web.views import primary
class LazyViewMixin(object):
- """provides two convenience methods for the tab machinery
- can also be used to lazy-load arbitrary views
+ """provides two convenience methods for the tab machinery.
+
+ Can also be used to lazy-load arbitrary views.
"""
def _prepare_bindings(self, vid, reloadable):
self._cw.add_onload(u"""
jQuery('#lazy-%(vid)s').bind('%(event)s', function(event) {
- load_now('#lazy-%(vid)s', '#%(vid)s-hole', %(reloadable)s);
+ loadNow('#lazy-%(vid)s', '#%(vid)s-hole', %(reloadable)s);
});""" % {'event': 'load_%s' % vid, 'vid': vid,
'reloadable' : str(reloadable).lower()})
def lazyview(self, vid, rql=None, eid=None, rset=None, tabid=None,
reloadable=False, show_spinbox=True, w=None):
- """ a lazy version of wview """
+ """a lazy version of wview"""
w = w or self.w
self._cw.add_js('cubicweb.lazy.js')
urlparams = self._cw.form.copy()
@@ -60,8 +60,9 @@
w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
tabid, xml_escape(self._cw.build_url('json', **urlparams))))
if show_spinbox:
- w(u'<img src="data/loading.gif" id="%s-hole" alt="%s"/>'
- % (tabid, self._cw._('(loading ...)')))
+ w(u'<img src="%s" id="%s-hole" alt="%s"/>'
+ % (xml_escape(self._cw.data_url('loading.gif')),
+ tabid, self._cw._('(loading ...)')))
else:
w(u'<div id="%s-hole"></div>' % tabid)
w(u'<noscript><p><a class="style: hidden" id="seo-%s" href="%s">%s</a></p></noscript>'
@@ -71,16 +72,14 @@
self._prepare_bindings(tabid, reloadable)
def forceview(self, vid):
- """trigger an event that will force immediate loading of the view
- on dom readyness
+ """trigger an event that will force immediate loading of the view on dom
+ readyness
"""
- self._cw.add_js('cubicweb.lazy.js')
- self._cw.add_onload("trigger_load('%s');" % vid)
+ self._cw.add_onload(uilib.js.triggerLoad(vid))
class TabsMixin(LazyViewMixin):
- """a tab mixin
- """
+ """a tab mixin to easily get jQuery based, lazy, ajax tabs"""
@property
def cookie_name(self):
@@ -105,11 +104,11 @@
active_tab = uilib.domid(default_tab)
viewsvreg = self._cw.vreg['views']
for tab in tabs:
- try:
+ if isinstance(tab, basestring):
+ tabid, tabkwargs = tab, {}
+ else:
tabid, tabkwargs = tab
tabkwargs = tabkwargs.copy()
- except ValueError:
- tabid, tabkwargs = tab, {}
tabkwargs.setdefault('rset', self.cw_rset)
vid = tabkwargs.get('vid', tabid)
domid = uilib.domid(tabid)
@@ -129,20 +128,19 @@
entity.view(default, w=self.w)
return
self._cw.add_css('ui.tabs.css')
- self._cw.add_js(('ui.core.js', 'ui.tabs.js',
- 'cubicweb.ajax.js', 'cubicweb.tabs.js', 'cubicweb.lazy.js'))
+ self._cw.add_js(('ui.core.js', 'ui.tabs.js', 'cubicweb.ajax.js'))
# prune tabs : not all are to be shown
tabs, active_tab = self.prune_tabs(tabs, default)
# build the html structure
w = self.w
- uid = entity and entity.eid or make_uid('tab')
+ uid = entity and entity.eid or utils.make_uid('tab')
w(u'<div id="entity-tabs-%s">' % uid)
w(u'<ul>')
active_tab_idx = None
for i, (tabid, domid, tabkwargs) in enumerate(tabs):
w(u'<li>')
w(u'<a href="#%s">' % domid)
- w(u'<span onclick="set_tab(\'%s\', \'%s\')">' % (domid, self.cookie_name))
+ w(u'<span onclick="%s">' % xml_escape(unicode(uilib.js.setTab(domid, self.cookie_name))))
w(tabkwargs.pop('label', self._cw._(tabid)))
w(u'</span>')
w(u'</a>')
@@ -158,12 +156,12 @@
tabkwargs.setdefault('rset', self.cw_rset)
self.lazyview(**tabkwargs)
w(u'</div>')
- # call the set_tab() JS function *after* each tab is generated
+ # call the setTab() JS function *after* each tab is generated
# because the callback binding needs to be done before
# XXX make work history: true
self._cw.add_onload(u"""
jQuery('#entity-tabs-%(eeid)s > ul').tabs( { selected: %(tabindex)s });
- set_tab('%(domid)s', '%(cookiename)s');
+ setTab('%(domid)s', '%(cookiename)s');
""" % {'tabindex' : active_tab_idx,
'domid' : active_tab,
'eeid' : (entity and entity.eid or uid),
--- a/web/views/urlpublishing.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/urlpublishing.py Fri Mar 11 09:47:09 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -156,7 +156,7 @@
<etype>[[/<attribute name>]/<attribute value>]*
"""
- priority = 2
+ priority = 3
def evaluate_path(self, req, parts):
if not (0 < len(parts) < 4):
@@ -214,7 +214,8 @@
URL rewrite rule definitions are stored in URLRewriter objects
"""
- priority = 3
+ priority = 2
+
def evaluate_path(self, req, parts):
# uri <=> req._twreq.path or req._twreq.uri
uri = req.url_unquote('/' + '/'.join(parts))
@@ -236,6 +237,7 @@
<any evaluator path>/<action>
"""
priority = 4
+
def evaluate_path(self, req, parts):
if len(parts) < 2:
raise PathDontMatch()
--- a/web/views/urlrewrite.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/urlrewrite.py Fri Mar 11 09:47:09 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -90,12 +90,14 @@
('/index', dict(vid='index')),
('/myprefs', dict(vid='propertiesform')),
('/siteconfig', dict(vid='systempropertiesform')),
- ('/siteinfo', dict(vid='info')),
+ ('/siteinfo', dict(vid='siteinfo')),
('/manage', dict(vid='manage')),
('/notfound', dict(vid='404')),
('/error', dict(vid='error')),
('/sparql', dict(vid='sparql')),
('/processinfo', dict(vid='processinfo')),
+ (rgx('/cwuser', re.I), dict(vid='cw.user-management')),
+ (rgx('/cwsource', re.I), dict(vid='cw.source-management')),
# XXX should be case insensitive as 'create', but I would like to find another way than
# relying on the etype_selector
(rgx('/schema/([^/]+?)/?'), dict(vid='primary', rql=r'Any X WHERE X is CWEType, X name "\1"')),
--- a/web/views/xmlrss.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/xmlrss.py Fri Mar 11 09:47:09 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -20,6 +20,7 @@
__docformat__ = "restructuredtext en"
_ = unicode
+from base64 import b64encode
from time import timezone
from logilab.mtconverter import xml_escape
@@ -31,6 +32,18 @@
from cubicweb.uilib import simple_sgml_tag
from cubicweb.web import httpcache, component
+def encode_bytes(value):
+ return '<![CDATA[%s]]>' % b64encode(value.getvalue())
+
+# see cubicweb.sobjects.parser.DEFAULT_CONVERTERS
+SERIALIZERS = {
+ 'String': xml_escape,
+ 'Bytes': encode_bytes,
+ 'Date': lambda x: x.strftime('%Y-%m-%d'),
+ 'Datetime': lambda x: x.strftime('%Y-%m-%d %H:%M:%S'),
+ 'Time': lambda x: x.strftime('%H:%M:%S'),
+ 'Interval': lambda x: x.days * 60*60*24 + x.seconds,
+ }
# base xml views ##############################################################
@@ -61,24 +74,52 @@
def cell_call(self, row, col):
""" element as an item for an xml feed """
entity = self.cw_rset.complete_entity(row, col)
- self.w(u'<%s>\n' % (entity.e_schema))
+ self.w(u'<%s eid="%s" cwuri="%s">\n'
+ % (entity.e_schema, entity.eid, xml_escape(entity.cwuri)))
for rschema, attrschema in entity.e_schema.attribute_definitions():
attr = rschema.type
- if attr == 'eid':
- value = entity.eid
+ if attr in ('eid', 'cwuri'):
+ continue
else:
try:
value = entity.cw_attr_cache[attr]
except KeyError:
# Bytes
continue
- if value is not None:
- if attrschema == 'Bytes':
- from base64 import b64encode
- value = '<![CDATA[%s]]>' % b64encode(value.getvalue())
- elif isinstance(value, basestring):
- value = xml_escape(value)
+ if value is None:
+ self.w(u' <%s/>\n' % attr)
+ else:
+ try:
+ value = SERIALIZERS[attrschema](value)
+ except KeyError:
+ pass
self.w(u' <%s>%s</%s>\n' % (attr, value, attr))
+ for relstr in self._cw.list_form_param('relation'):
+ try:
+ rtype, role = relstr.split('-')
+ except ValueError:
+ self.error('badly formated relation name %r', relstr)
+ continue
+ if role == 'subject':
+ getrschema = entity.e_schema.subjrels
+ elif role == 'object':
+ getrschema = entity.e_schema.objrels
+ else:
+ self.error('badly formated relation name %r', relstr)
+ continue
+ if not rtype in getrschema:
+ self.error('unexisting relation %r', relstr)
+ continue
+ self.w(u' <%s role="%s">\n' % (rtype, role))
+ for related in entity.related(rtype, role, entities=True):
+ # XXX put unique attributes as xml attribute, they are much
+ # probably used to search existing entities in client data feed,
+ # and putting it here may avoid an extra request to get those
+ # attributes values
+ self.w(u' <%s eid="%s" cwuri="%s"/>\n'
+ % (related.e_schema, related.eid,
+ xml_escape(related.cwuri)))
+ self.w(u' </%s>\n' % rtype)
self.w(u'</%s>\n' % (entity.e_schema))
@@ -234,7 +275,6 @@
if entity.creator:
self._marker('dc:creator', entity.dc_creator())
-
def _marker(self, marker, value):
if value:
self.w(u' <%s>%s</%s>\n' % (marker, xml_escape(value), marker))