--- a/.hgignore Tue Jul 19 15:59:02 2016 +0200
+++ b/.hgignore Tue Jul 19 16:13:12 2016 +0200
@@ -1,6 +1,8 @@
\.svn
^build$
^dist$
+\.egg-info$
+^.tox$
\.pyc$
\.pyo$
\.bak$
@@ -11,11 +13,16 @@
^doc/book/en/apidoc$
\.old$
syntax: regexp
-.*/data/database/.*\.sqlite
-.*/data/database/.*\.config
+.*/data.*/database/.*\.sqlite
+.*/data.*/database/.*\.config
.*/data/database/tmpdb.*
.*/data/ldapdb/.*
+.*/data/uicache/
+.*/data/cubes/.*/i18n/.*\.po
^doc/html/
^doc/doctrees/
^doc/book/en/devweb/js_api/
+^doc/_build
+^doc/js_api/
data/pgdb/
+data.*/pgdb.*
--- a/.hgtags Tue Jul 19 15:59:02 2016 +0200
+++ b/.hgtags Tue Jul 19 16:13:12 2016 +0200
@@ -520,6 +520,28 @@
f66a4895759e0913b1203943fc2cd7be1a821e05 3.20.14
f66a4895759e0913b1203943fc2cd7be1a821e05 debian/3.20.14-1
f66a4895759e0913b1203943fc2cd7be1a821e05 centos/3.20.14-1
+887c6eef807781560adcd4ecd2dea9011f5a6681 3.21.0
+887c6eef807781560adcd4ecd2dea9011f5a6681 debian/3.21.0-1
+887c6eef807781560adcd4ecd2dea9011f5a6681 centos/3.21.0-1
+a8a0de0298a58306d63dbc998ad60c48bf18c80a 3.21.1
+a8a0de0298a58306d63dbc998ad60c48bf18c80a debian/3.21.1-1
+a8a0de0298a58306d63dbc998ad60c48bf18c80a centos/3.21.1-1
+a5428e1ab36491a8e6d66ce09d23b708b97e1337 3.21.2
+a5428e1ab36491a8e6d66ce09d23b708b97e1337 debian/3.21.2-1
+a5428e1ab36491a8e6d66ce09d23b708b97e1337 centos/3.21.2-1
+9edfe9429209848e31d1998df48da7a84db0c819 3.21.3
+9edfe9429209848e31d1998df48da7a84db0c819 debian/3.21.3-1
+9edfe9429209848e31d1998df48da7a84db0c819 centos/3.21.3-1
+d3b92d3a7db098b25168beef9b3ee7b36263a652 3.21.4
+d3b92d3a7db098b25168beef9b3ee7b36263a652 debian/3.21.4-1
+d3b92d3a7db098b25168beef9b3ee7b36263a652 centos/3.21.4-1
+e0572a786e6b4b0965d405dd95cf5bce754005a2 3.21.5
+e0572a786e6b4b0965d405dd95cf5bce754005a2 debian/3.21.5-1
+e0572a786e6b4b0965d405dd95cf5bce754005a2 centos/3.21.5-1
+228b6d2777e44d7bc158d0b4579d09960acea926 debian/3.21.5-2
+b3cbbb7690b6e193570ffe4846615d372868a923 3.21.6
+b3cbbb7690b6e193570ffe4846615d372868a923 debian/3.21.6-1
+b3cbbb7690b6e193570ffe4846615d372868a923 centos/3.21.6-1
636a83e65870433c2560f3c49d55ca628bc96e11 3.20.15
636a83e65870433c2560f3c49d55ca628bc96e11 debian/3.20.15-1
636a83e65870433c2560f3c49d55ca628bc96e11 centos/3.20.15-1
--- a/MANIFEST.in Tue Jul 19 15:59:02 2016 +0200
+++ b/MANIFEST.in Tue Jul 19 16:13:12 2016 +0200
@@ -6,9 +6,18 @@
include man/cubicweb-ctl.1
include doc/*.rst
+include doc/Makefile
recursive-include doc/book *
recursive-include doc/tools *.py
recursive-include doc/tutorials *.rst *.py
+include doc/api/*.rst
+recursive-include doc/_themes *
+recursive-include doc/_static *
+include doc/_templates/*.html
+include doc/changes/*.rst
+recursive-include doc/dev .txt *.rst
+recursive-include doc/images *.png *.svg
+include doc/conf.py
recursive-include misc *.py *.png *.display
@@ -25,18 +34,18 @@
recursive-include sobjects/test/data bootstrap_cubes *.py
recursive-include hooks/test/data bootstrap_cubes *.py
recursive-include server/test/data bootstrap_cubes *.py source* *.conf.in *.ldif
-recursive-include devtools/test/data bootstrap_cubes *.py *.txt *.js
+recursive-include devtools/test/data bootstrap_cubes *.py *.txt *.js *.po.ref
recursive-include web/test/data bootstrap_cubes pouet.css *.py
+recursive-include etwist/test/data *.py
recursive-include web/test/jstests *.js *.html *.css *.json
recursive-include web/test/windmill *.py
-recursive-include skeleton *.py *.css *.js *.po compat *.in *.tmpl
+recursive-include skeleton *.py *.css *.js *.po compat *.in *.tmpl rules
prune doc/book/en/.static
prune doc/book/fr/.static
prune doc/html/_sources/
prune misc/cwfs
-prune goa
-prune doc/book/en/devweb/js_api
+prune doc/js_api
global-exclude *.pyc
--- a/README Tue Jul 19 15:59:02 2016 +0200
+++ b/README Tue Jul 19 16:13:12 2016 +0200
@@ -14,7 +14,7 @@
Install
-------
-More details at http://docs.cubicweb.org/admin/setup
+More details at http://docs.cubicweb.org/book/admin/setup
Getting started
---------------
--- a/__pkginfo__.py Tue Jul 19 15:59:02 2016 +0200
+++ b/__pkginfo__.py Tue Jul 19 16:13:12 2016 +0200
@@ -22,7 +22,7 @@
modname = distname = "cubicweb"
-numversion = (3, 20, 16)
+numversion = (3, 21, 6)
version = '.'.join(str(num) for num in numversion)
description = "a repository of entities / relations for knowledge management"
@@ -42,7 +42,7 @@
'logilab-common': '>= 0.63.1',
'logilab-mtconverter': '>= 0.8.0',
'rql': '>= 0.31.2, < 0.34',
- 'yams': '>= 0.40.0, < 0.42',
+ 'yams': '>= 0.40.0',
#gettext # for xgettext, msgcat, etc...
# web dependencies
'lxml': '',
@@ -55,7 +55,6 @@
__recommends__ = {
'docutils': '>= 0.6',
- 'Pyro': '>= 3.9.1, < 4.0.0',
'Pillow': '', # for captcha
'pycrypto': '', # for crypto extensions
'fyzz': '>= 0.1.0', # for sparql
@@ -115,8 +114,6 @@
[join('share', 'cubicweb', 'cubes', 'shared', 'data'),
[join(_data_dir, fname) for fname in listdir(_data_dir)
if not isdir(join(_data_dir, fname))]],
- [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'timeline'),
- [join(_data_dir, 'timeline', fname) for fname in listdir(join(_data_dir, 'timeline'))]],
[join('share', 'cubicweb', 'cubes', 'shared', 'data', 'images'),
[join(_data_dir, 'images', fname) for fname in listdir(join(_data_dir, 'images'))]],
[join('share', 'cubicweb', 'cubes', 'shared', 'data', 'jquery-treeview'),
--- a/_exceptions.py Tue Jul 19 15:59:02 2016 +0200
+++ b/_exceptions.py Tue Jul 19 16:13:12 2016 +0200
@@ -82,6 +82,8 @@
self.session = session
assert 'rtypes' in kwargs or 'cstrname' in kwargs
self.kwargs = kwargs
+ # fill cache while the session is open
+ self.rtypes
@cachedproperty
def rtypes(self):
@@ -100,6 +102,12 @@
return None, self.rtypes
+class ViolatedConstraint(RepositoryError):
+ def __init__(self, cnx, cstrname):
+ self.cnx = cnx
+ self.cstrname = cstrname
+
+
# security exceptions #########################################################
class Unauthorized(SecurityError):
--- a/_gcdebug.py Tue Jul 19 15:59:02 2016 +0200
+++ b/_gcdebug.py Tue Jul 19 16:13:12 2016 +0200
@@ -19,6 +19,10 @@
import gc, types, weakref
from cubicweb.schema import CubicWebRelationSchema, CubicWebEntitySchema
+try:
+ from cubicweb.web.request import _NeedAuthAccessMock
+except ImportError:
+ _NeedAuthAccessMock = None
listiterator = type(iter([]))
@@ -30,6 +34,8 @@
types.ModuleType, types.FunctionType, types.MethodType,
types.MemberDescriptorType, types.GetSetDescriptorType,
)
+if _NeedAuthAccessMock is not None:
+ IGNORE_CLASSES = IGNORE_CLASSES + (_NeedAuthAccessMock,)
def _get_counted_class(obj, classes):
for cls in classes:
@@ -63,7 +69,8 @@
ocounters[key] = 1
if isinstance(obj, viewreferrersclasses):
print ' ', obj, referrers(obj, showobjs, maxlevel)
- return counters, ocounters, gc.garbage
+ garbage = [repr(obj) for obj in gc.garbage]
+ return counters, ocounters, garbage
def referrers(obj, showobj=False, maxlevel=1):
--- a/appobject.py Tue Jul 19 15:59:02 2016 +0200
+++ b/appobject.py Tue Jul 19 16:13:12 2016 +0200
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""
-.. _appobject:
The `AppObject` class
---------------------
@@ -27,7 +26,6 @@
We can find a certain number of attributes and methods defined in this class and
common to all the application objects.
-.. autoclass:: AppObject
"""
__docformat__ = "restructuredtext en"
--- a/bin/clone_deps.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-#!/usr/bin/python
-import sys
-
-from subprocess import call as sbp_call, Popen, PIPE
-from urllib import urlopen
-import os
-from os import path as osp, pardir, chdir
-
-
-def find_mercurial():
- print "trying to find mercurial from the command line ..."
- print '-' * 20
- tryhg = sbp_call(['hg', '--version'])
- if tryhg:
- print 'mercurial seems to be unavailable, please install it'
- raise
- print '-' * 20
- def hg_call(args):
- return sbp_call(['hg'] + args)
-
- return hg_call
-
-
-BASE_URL = 'http://www.logilab.org/hg/'
-
-to_clone = ['fyzz', 'yams', 'rql',
- 'logilab/common', 'logilab/constraint', 'logilab/database',
- 'logilab/devtools', 'logilab/mtconverter',
- 'cubes/blog', 'cubes/calendar', 'cubes/card', 'cubes/comment',
- 'cubes/datafeed', 'cubes/email', 'cubes/file', 'cubes/folder',
- 'cubes/forgotpwd', 'cubes/keyword', 'cubes/link', 'cubes/localperms',
- 'cubes/mailinglist', 'cubes/nosylist', 'cubes/person',
- 'cubes/preview', 'cubes/registration', 'cubes/rememberme',
- 'cubes/tag', 'cubes/vcsfile', 'cubes/zone']
-
-# a couple of functions to be used to explore available
-# repositories and cubes
-def list_repos(repos_root):
- assert repos_root.startswith('http://')
- hgwebdir_repos = (repo.strip()
- for repo in urlopen(repos_root + '?style=raw').readlines()
- if repo.strip())
- prefix = osp.commonprefix(hgwebdir_repos)
- return (repo[len(prefix):].strip('/')
- for repo in hgwebdir_repos)
-
-def list_all_cubes(base_url=BASE_URL):
- all_repos = list_repos(base_url)
- #search for cubes
- for repo in all_repos:
- if repo.startswith('cubes'):
- to_clone.append(repo)
-
-def get_latest_debian_tag(path):
- proc = Popen(['hg', '-R', path, 'tags'], stdout=PIPE)
- out, _err = proc.communicate()
- for line in out.splitlines():
- if 'debian-version' in line:
- return line.split()[0]
-
-def main():
- if len(sys.argv) == 1:
- base_url = BASE_URL
- elif len(sys.argv) == 2:
- base_url = sys.argv[1]
- else:
- sys.stderr.write('usage %s [base_url]\n' % sys.argv[0])
- sys.exit(1)
- hg_call = find_mercurial()
- print len(to_clone), 'repositories will be cloned'
- base_dir = osp.normpath(osp.join(osp.dirname(__file__), pardir, pardir))
- chdir(base_dir)
- not_updated = []
- for repo in to_clone:
- url = base_url + repo
- if '/' not in repo:
- target_path = repo
- else:
- assert repo.count('/') == 1, repo
- directory, repo = repo.split('/')
- if not osp.isdir(directory):
- os.mkdir(directory)
- open(osp.join(directory, '__init__.py'), 'w').close()
- target_path = osp.join(directory, repo)
- if osp.exists(target_path):
- print target_path, 'seems already cloned. Skipping it.'
- else:
- hg_call(['clone', '-U', url, target_path])
- tag = get_latest_debian_tag(target_path)
- if tag:
- print 'updating to', tag
- hg_call(['update', '-R', target_path, tag])
- else:
- not_updated.append(target_path)
- print """
-CubicWeb dependencies and standard set of cubes have been fetched and
-update to the latest stable version.
-
-You should ensure your PYTHONPATH contains `%(basedir)s`.
-You might want to read the environment configuration section of the documentation
-at http://docs.cubicweb.org/admin/setup.html#environment-configuration
-
-You can find more cubes at http://www.cubicweb.org.
-Clone them from `%(baseurl)scubes/` into the `%(basedir)s%(sep)scubes%(sep)s` directory.
-
-To get started you may read http://docs.cubicweb.org/tutorials/base/index.html.
-""" % {'basedir': os.getcwd(), 'baseurl': base_url, 'sep': os.sep}
- if not_updated:
- sys.stderr.write('WARNING: The following repositories were not updated (no debian tag found):\n')
- for path in not_updated:
- sys.stderr.write('\t-%s\n' % path)
-
-if __name__ == '__main__':
- main()
-
-
-
--- a/cubicweb.spec Tue Jul 19 15:59:02 2016 +0200
+++ b/cubicweb.spec Tue Jul 19 16:13:12 2016 +0200
@@ -7,7 +7,7 @@
%endif
Name: cubicweb
-Version: 3.20.16
+Version: 3.21.6
Release: logilab.1%{?dist}
Summary: CubicWeb is a semantic web application framework
Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz
--- a/cwconfig.py Tue Jul 19 15:59:02 2016 +0200
+++ b/cwconfig.py Tue Jul 19 16:13:12 2016 +0200
@@ -279,7 +279,7 @@
('default-text-format',
{'type' : 'choice',
'choices': ('text/plain', 'text/rest', 'text/html', 'text/markdown'),
- 'default': 'text/html', # use fckeditor in the web ui
+ 'default': 'text/plain',
'help': _('default text format for rich text fields.'),
'group': 'ui',
}),
@@ -835,7 +835,7 @@
# set by upgrade command
verbosity = 0
-
+ cmdline_options = None
options = CubicWebNoAppConfiguration.options + (
('log-file',
{'type' : 'string',
@@ -843,6 +843,13 @@
'help': 'file where output logs should be written',
'group': 'main', 'level': 2,
}),
+ ('statsd-endpoint',
+ {'type' : 'string',
+ 'default': '',
+ 'help': 'UDP address of the statsd endpoint; it must be formatted'
+ 'like <ip>:<port>; disabled is unset.',
+ 'group': 'main', 'level': 2,
+ }),
# email configuration
('smtp-host',
{'type' : 'string',
@@ -870,6 +877,18 @@
the repository',
'group': 'email', 'level': 1,
}),
+ ('logstat-interval',
+ {'type' : 'int',
+ 'default': 0,
+ 'help': 'interval (in seconds) at which stats are dumped in the logstat file; set 0 to disable',
+ 'group': 'main', 'level': 2,
+ }),
+ ('logstat-file',
+ {'type' : 'string',
+ 'default': Method('default_stats_file'),
+ 'help': 'file where stats for the instance should be written',
+ 'group': 'main', 'level': 2,
+ }),
)
@classmethod
@@ -953,6 +972,13 @@
log_path = os.path.join(_INSTALL_PREFIX, 'var', 'log', 'cubicweb', '%s-%s.log')
return log_path % (self.appid, self.name)
+ def default_stats_file(self):
+ """return default path to the stats file of the instance'server"""
+ logfile = self.default_log_file()
+ if logfile.endswith('.log'):
+ logfile = logfile[:-4]
+ return logfile + '.stats'
+
def default_pid_file(self):
"""return default path to the pid file of the instance'server"""
if self.mode == 'system':
@@ -1010,7 +1036,7 @@
# or site_cubicweb files
self.load_file_configuration(self.main_config_file())
# configuration initialization hook
- self.load_configuration()
+ self.load_configuration(**(self.cmdline_options or {}))
def add_cubes(self, cubes):
"""add given cubes to the list of used cubes"""
@@ -1077,9 +1103,9 @@
infos.append('cubicweb-%s' % str(self.cubicweb_version()))
return md5(';'.join(infos)).hexdigest()
- def load_configuration(self):
+ def load_configuration(self, **kw):
"""load instance's configuration files"""
- super(CubicWebConfiguration, self).load_configuration()
+ super(CubicWebConfiguration, self).load_configuration(**kw)
if self.apphome and not self.creating:
# init gettext
self._gettext_init()
@@ -1102,6 +1128,17 @@
logconfig = join(self.apphome, 'logging.conf')
if exists(logconfig):
logging.config.fileConfig(logconfig)
+ # set the statsd address, if any
+ if self.get('statsd-endpoint'):
+ try:
+ address, port = self.get('statsd-endpoint').split(':')
+ port = int(port)
+ except:
+ self.error('statsd-endpoint: invalid address format ({}); '
+ 'it should be "ip:port"'.format(self.get('statsd-endpoint')))
+ else:
+ import statsd_logger
+ statsd_logger.setup('cubicweb.%s' % self.appid, (address, port))
def available_languages(self, *args):
"""return available translation for an instance, by looking for
--- a/cwctl.py Tue Jul 19 15:59:02 2016 +0200
+++ b/cwctl.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -25,7 +25,7 @@
# possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
# completion). So import locally in command helpers.
import sys
-from warnings import warn
+from warnings import warn, filterwarnings
from os import remove, listdir, system, pathsep
from os.path import exists, join, isfile, isdir, dirname, abspath
from urlparse import urlparse
@@ -401,7 +401,7 @@
if 'type' in odict
and odict.get('level') <= self.config.config_level)
for section in sections:
- if section not in ('main', 'email', 'pyro', 'web'):
+ if section not in ('main', 'email', 'web'):
print '\n' + underline_title('%s options' % section)
config.input_config(section, self.config.config_level)
# write down configuration
@@ -520,7 +520,12 @@
'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
'help': 'debug if -D is set, error otherwise',
}),
- )
+ ('param',
+ {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
+ 'default': {},
+ 'help': 'override <key> configuration file option with <value>.',
+ }),
+ )
def start_instance(self, appid):
"""start the instance's server"""
@@ -534,6 +539,8 @@
"- '{ctl} pyramid {appid}' (requires the pyramid cube)\n")
raise ExecutionError(msg.format(ctl='cubicweb-ctl', appid=appid))
config = cwcfg.config_for(appid, debugmode=self['debug'])
+ # override config file values with cmdline options
+ config.cmdline_options = self.config.param
init_cmdline_log_threshold(config, self['loglevel'])
if self['profile']:
config.global_set_option('profile', self.config.profile)
@@ -900,9 +907,7 @@
('repo-uri',
{'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
'help': 'URI of the CubicWeb repository to connect to. URI can be \
-pyro://[host:port] the Pyro name server host; if the pyro nameserver is not set, \
-it will be detected by using a broadcast query, a ZMQ URL or \
-inmemory:// (default) use an in-memory repository. THIS OPTION IS DEPRECATED, \
+a ZMQ URL or inmemory:// (default) use an in-memory repository. THIS OPTION IS DEPRECATED, \
directly give URI as instance id instead',
'group': 'remote'
}),
@@ -953,7 +958,7 @@
if self.config.repo_uri:
warn('[3.16] --repo-uri option is deprecated, directly give the URI as instance id',
DeprecationWarning)
- if urlparse(self.config.repo_uri).scheme in ('pyro', 'inmemory'):
+ if urlparse(self.config.repo_uri).scheme == 'inmemory':
appuri = '%s/%s' % (self.config.repo_uri.rstrip('/'), appuri)
from cubicweb.utils import parse_repo_uri
@@ -1135,6 +1140,7 @@
import os
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0)
+ filterwarnings('default', category=DeprecationWarning)
cwcfg.load_cwctl_plugins()
try:
CWCTL.run(args)
--- a/cwvreg.py Tue Jul 19 15:59:02 2016 +0200
+++ b/cwvreg.py Tue Jul 19 16:13:12 2016 +0200
@@ -15,179 +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/>.
-""".. RegistryStore:
-
-The `RegistryStore`
--------------------
-
-The `RegistryStore` can be seen as a two-level dictionary. It contains
-all dynamically loaded objects (subclasses of :ref:`appobject`) to
-build a |cubicweb| application. Basically:
-
-* the first level key returns a *registry*. This key corresponds to the
- `__registry__` attribute of application object classes
-
-* the second level key returns a list of application objects which
- share the same identifier. This key corresponds to the `__regid__`
- attribute of application object classes.
-
-A *registry* holds a specific kind of application objects. There is
-for instance a registry for entity classes, another for views, etc...
-
-The `RegistryStore` has two main responsibilities:
-
-- being the access point to all registries
-
-- handling the registration process at startup time, and during automatic
- reloading in debug mode.
-
-.. _AppObjectRecording:
-
-Details of the recording process
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. index::
- vregistry: registration_callback
-
-On startup, |cubicweb| loads application objects defined in its library
-and in cubes used by the instance. Application objects from the
-library are loaded first, then those provided by cubes are loaded in
-dependency order (e.g. if your cube depends on an other, objects from
-the dependency will be loaded first). The layout of the modules or packages
-in a cube is explained in :ref:`cubelayout`.
-
-For each module:
-
-* by default all objects are registered automatically
-
-* if some objects have to replace other objects, or have to be
- included only if some condition is met, you'll have to define a
- `registration_callback(vreg)` function in your module and explicitly
- register **all objects** in this module, using the api defined
- below.
-
-.. Note::
- Once the function `registration_callback(vreg)` is implemented in a module,
- all the objects from this module have to be explicitly registered as it
- disables the automatic objects registration.
-
-
-API for objects registration
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here are the registration methods that you can use in the `registration_callback`
-to register your objects to the `RegistryStore` instance given as argument (usually
-named `vreg`):
-
-.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_all
-.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_and_replace
-.. automethod:: cubicweb.cwvreg.CWRegistryStore.register
-.. automethod:: cubicweb.cwvreg.CWRegistryStore.unregister
-
-Examples:
-
-.. sourcecode:: python
-
- # web/views/basecomponents.py
- def registration_callback(vreg):
- # register everything in the module except SeeAlsoComponent
- vreg.register_all(globals().itervalues(), __name__, (SeeAlsoVComponent,))
- # conditionally register SeeAlsoVComponent
- if 'see_also' in vreg.schema:
- vreg.register(SeeAlsoVComponent)
-
-In this example, we register all application object classes defined in the module
-except `SeeAlsoVComponent`. This class is then registered only if the 'see_also'
-relation type is defined in the instance'schema.
-
-.. sourcecode:: python
-
- # goa/appobjects/sessions.py
- def registration_callback(vreg):
- vreg.register(SessionsCleaner)
- # replace AuthenticationManager by GAEAuthenticationManager
- vreg.register_and_replace(GAEAuthenticationManager, AuthenticationManager)
- # replace PersistentSessionManager by GAEPersistentSessionManager
- vreg.register_and_replace(GAEPersistentSessionManager, PersistentSessionManager)
-
-In this example, we explicitly register classes one by one:
-
-* the `SessionCleaner` class
-* the `GAEAuthenticationManager` to replace the `AuthenticationManager`
-* the `GAEPersistentSessionManager` to replace the `PersistentSessionManager`
-
-If at some point we register a new appobject class in this module, it won't be
-registered at all without modification to the `registration_callback`
-implementation. The previous example will register it though, thanks to the call
-to the `register_all` method.
-
-
-.. _Selection:
-
-Runtime objects selection
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Now that we have all application objects loaded, the question is : when
-I want some specific object, for instance the primary view for a given
-entity, how do I get the proper object ? This is what we call the
-**selection mechanism**.
-
-As explained in the :ref:`Concepts` section:
-
-* each application object has a **selector**, defined by its
- `__select__` class attribute
-
-* this selector is responsible to return a **score** for a given context
-
- - 0 score means the object doesn't apply to this context
-
- - else, the higher the score, the better the object suits the context
-
-* the object with the highest score is selected.
-
-.. Note::
-
- When no single object has the highest score, an exception is raised in development
- mode to let you know that the engine was not able to identify the view to
- apply. This error is silenced in production mode and one of the objects with
- the highest score is picked.
-
- In such cases you would need to review your design and make sure
- your selectors or appobjects are properly defined. Such an error is
- typically caused by either forgetting to change the __regid__ in a
- derived class, or by having copy-pasted some code.
-
-For instance, if you are selecting the primary (`__regid__ =
-'primary'`) view (`__registry__ = 'views'`) for a result set
-containing a `Card` entity, two objects will probably be selectable:
-
-* the default primary view (`__select__ = is_instance('Any')`), meaning
- that the object is selectable for any kind of entity type
-
-* the specific `Card` primary view (`__select__ = is_instance('Card')`,
- meaning that the object is selectable for Card entities
-
-Other primary views specific to other entity types won't be selectable in this
-case. Among selectable objects, the `is_instance('Card')` selector will return a higher
-score since it's more specific, so the correct view will be selected as expected.
-
-.. _SelectionAPI:
-
-API for objects selections
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here is the selection API you'll get on every registry. Some of them, as the
-'etypes' registry, containing entity classes, extend it. In those methods,
-`*args, **kwargs` is what we call the **context**. Those arguments are given to
-selectors that will inspect their content and return a score accordingly.
-
-.. automethod:: cubicweb.vregistry.Registry.select
-
-.. automethod:: cubicweb.vregistry.Registry.select_or_none
-
-.. automethod:: cubicweb.vregistry.Registry.possible_objects
-
-.. automethod:: cubicweb.vregistry.Registry.object_by_id
+"""
+Cubicweb registries
"""
__docformat__ = "restructuredtext en"
@@ -229,6 +58,7 @@
sys.modules.pop('cubicweb.web.uicfg', None)
sys.modules.pop('cubicweb.web.uihelper', None)
+
def require_appobject(obj):
"""return appobjects required by the given object by searching for
`appobject_selectable` predicate
@@ -241,11 +71,16 @@
class CWRegistry(Registry):
def __init__(self, vreg):
+ """
+ :param vreg: the :py:class:`CWRegistryStore` managing this registry.
+ """
super(CWRegistry, self).__init__(True)
self.vreg = vreg
@property
def schema(self):
+ """The :py:class:`cubicweb.schema.CubicWebSchema`
+ """
return self.vreg.schema
def poss_visible_objects(self, *args, **kwargs):
@@ -269,7 +104,7 @@
def selected(self, winner, args, kwargs):
"""overriden to avoid the default 'instanciation' behaviour, ie
- winner(*args, **kwargs)
+ `winner(*args, **kwargs)`
"""
return winner
--- a/dataimport.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1175 +0,0 @@
-# -*- coding: utf-8 -*-
-# copyright 2003-2014 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/>.
-"""This module provides tools to import tabular data.
-
-
-Example of use (run this with `cubicweb-ctl shell instance import-script.py`):
-
-.. sourcecode:: python
-
- from cubicweb.dataimport import *
- # define data generators
- GENERATORS = []
-
- USERS = [('Prenom', 'firstname', ()),
- ('Nom', 'surname', ()),
- ('Identifiant', 'login', ()),
- ]
-
- def gen_users(ctl):
- for row in ctl.iter_and_commit('utilisateurs'):
- entity = mk_entity(row, USERS)
- entity['upassword'] = 'motdepasse'
- ctl.check('login', entity['login'], None)
- entity = ctl.store.create_entity('CWUser', **entity)
- email = ctl.store.create_entity('EmailAddress', address=row['email'])
- ctl.store.relate(entity.eid, 'use_email', email.eid)
- ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x':entity['eid']})
-
- CHK = [('login', check_doubles, 'Utilisateurs Login',
- 'Deux utilisateurs ne devraient pas avoir le même login.'),
- ]
-
- GENERATORS.append( (gen_users, CHK) )
-
- # create controller
- ctl = CWImportController(RQLObjectStore(cnx))
- ctl.askerror = 1
- ctl.generators = GENERATORS
- ctl.data['utilisateurs'] = lazytable(ucsvreader(open('users.csv')))
- # run
- ctl.run()
-
-.. BUG file with one column are not parsable
-.. TODO rollback() invocation is not possible yet
-"""
-__docformat__ = "restructuredtext en"
-
-import csv
-import sys
-import threading
-import traceback
-import warnings
-import cPickle
-import os.path as osp
-import inspect
-from base64 import b64encode
-from collections import defaultdict
-from copy import copy
-from datetime import date, datetime, time
-from time import asctime
-from StringIO import StringIO
-
-from logilab.common import shellutils, attrdict
-from logilab.common.date import strptime
-from logilab.common.decorators import cached
-from logilab.common.deprecation import deprecated
-
-from cubicweb import QueryError
-from cubicweb.utils import make_uid
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES
-from cubicweb.server.edition import EditedEntity
-from cubicweb.server.sqlutils import SQL_PREFIX
-from cubicweb.server.utils import eschema_eid
-
-
-def count_lines(stream_or_filename):
- if isinstance(stream_or_filename, basestring):
- f = open(stream_or_filename)
- else:
- f = stream_or_filename
- f.seek(0)
- i = 0 # useful is f is an empty file
- for i, line in enumerate(f):
- pass
- f.seek(0)
- return i+1
-
-def ucsvreader_pb(stream_or_path, encoding='utf-8', delimiter=',', quotechar='"',
- skipfirst=False, withpb=True, skip_empty=True, separator=None,
- quote=None):
- """same as :func:`ucsvreader` but a progress bar is displayed as we iter on rows"""
- if separator is not None:
- delimiter = separator
- warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
- if quote is not None:
- quotechar = quote
- warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
- if isinstance(stream_or_path, basestring):
- if not osp.exists(stream_or_path):
- raise Exception("file doesn't exists: %s" % stream_or_path)
- stream = open(stream_or_path)
- else:
- stream = stream_or_path
- rowcount = count_lines(stream)
- if skipfirst:
- rowcount -= 1
- if withpb:
- pb = shellutils.ProgressBar(rowcount, 50)
- for urow in ucsvreader(stream, encoding, delimiter, quotechar,
- skipfirst=skipfirst, skip_empty=skip_empty):
- yield urow
- if withpb:
- pb.update()
- print ' %s rows imported' % rowcount
-
-def ucsvreader(stream, encoding='utf-8', delimiter=',', quotechar='"',
- skipfirst=False, ignore_errors=False, skip_empty=True,
- separator=None, quote=None):
- """A csv reader that accepts files with any encoding and outputs unicode
- strings
-
- if skip_empty (the default), lines without any values specified (only
- separators) will be skipped. This is useful for Excel exports which may be
- full of such lines.
- """
- if separator is not None:
- delimiter = separator
- warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
- if quote is not None:
- quotechar = quote
- warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
- it = iter(csv.reader(stream, delimiter=delimiter, quotechar=quotechar))
- if not ignore_errors:
- if skipfirst:
- it.next()
- for row in it:
- decoded = [item.decode(encoding) for item in row]
- if not skip_empty or any(decoded):
- yield decoded
- else:
- if skipfirst:
- try:
- row = it.next()
- except csv.Error:
- pass
- # Safe version, that can cope with error in CSV file
- while True:
- try:
- row = it.next()
- # End of CSV, break
- except StopIteration:
- break
- # Error in CSV, ignore line and continue
- except csv.Error:
- continue
- decoded = [item.decode(encoding) for item in row]
- if not skip_empty or any(decoded):
- yield decoded
-
-
-def callfunc_every(func, number, iterable):
- """yield items of `iterable` one by one and call function `func`
- every `number` iterations. Always call function `func` at the end.
- """
- for idx, item in enumerate(iterable):
- yield item
- if not idx % number:
- func()
- func()
-
-def lazytable(reader):
- """The first row is taken to be the header of the table and
- used to output a dict for each row of data.
-
- >>> data = lazytable(ucsvreader(open(filename)))
- """
- header = reader.next()
- for row in reader:
- yield dict(zip(header, row))
-
-def lazydbtable(cu, table, headers, orderby=None):
- """return an iterator on rows of a sql table. On each row, fetch columns
- defined in headers and return values as a dictionary.
-
- >>> data = lazydbtable(cu, 'experimentation', ('id', 'nickname', 'gps'))
- """
- sql = 'SELECT %s FROM %s' % (','.join(headers), table,)
- if orderby:
- sql += ' ORDER BY %s' % ','.join(orderby)
- cu.execute(sql)
- while True:
- row = cu.fetchone()
- if row is None:
- break
- yield dict(zip(headers, row))
-
-def mk_entity(row, map):
- """Return a dict made from sanitized mapped values.
-
- ValueError can be raised on unexpected values found in checkers
-
- >>> row = {'myname': u'dupont'}
- >>> map = [('myname', u'name', (call_transform_method('title'),))]
- >>> mk_entity(row, map)
- {'name': u'Dupont'}
- >>> row = {'myname': u'dupont', 'optname': u''}
- >>> map = [('myname', u'name', (call_transform_method('title'),)),
- ... ('optname', u'MARKER', (optional,))]
- >>> mk_entity(row, map)
- {'name': u'Dupont', 'optname': None}
- """
- res = {}
- assert isinstance(row, dict)
- assert isinstance(map, list)
- for src, dest, funcs in map:
- try:
- res[dest] = row[src]
- except KeyError:
- continue
- try:
- for func in funcs:
- res[dest] = func(res[dest])
- if res[dest] is None:
- break
- except ValueError as err:
- raise ValueError('error with %r field: %s' % (src, err)), None, sys.exc_info()[-1]
- return res
-
-# user interactions ############################################################
-
-def tell(msg):
- print msg
-
-def confirm(question):
- """A confirm function that asks for yes/no/abort and exits on abort."""
- answer = shellutils.ASK.ask(question, ('Y', 'n', 'abort'), 'Y')
- if answer == 'abort':
- sys.exit(1)
- return answer == 'Y'
-
-
-class catch_error(object):
- """Helper for @contextmanager decorator."""
-
- def __init__(self, ctl, key='unexpected error', msg=None):
- self.ctl = ctl
- self.key = key
- self.msg = msg
-
- def __enter__(self):
- return self
-
- def __exit__(self, type, value, traceback):
- if type is not None:
- if issubclass(type, (KeyboardInterrupt, SystemExit)):
- return # re-raise
- if self.ctl.catcherrors:
- self.ctl.record_error(self.key, None, type, value, traceback)
- return True # silent
-
-
-# base sanitizing/coercing functions ###########################################
-
-def optional(value):
- """checker to filter optional field
-
- If value is undefined (ex: empty string), return None that will
- break the checkers validation chain
-
- General use is to add 'optional' check in first condition to avoid
- ValueError by further checkers
-
- >>> MAPPER = [(u'value', 'value', (optional, int))]
- >>> row = {'value': u'XXX'}
- >>> mk_entity(row, MAPPER)
- {'value': None}
- >>> row = {'value': u'100'}
- >>> mk_entity(row, MAPPER)
- {'value': 100}
- """
- if value:
- return value
- return None
-
-def required(value):
- """raise ValueError if value is empty
-
- This check should be often found in last position in the chain.
- """
- if value:
- return value
- raise ValueError("required")
-
-def todatetime(format='%d/%m/%Y'):
- """return a transformation function to turn string input value into a
- `datetime.datetime` instance, using given format.
-
- Follow it by `todate` or `totime` functions from `logilab.common.date` if
- you want a `date`/`time` instance instead of `datetime`.
- """
- def coerce(value):
- return strptime(value, format)
- return coerce
-
-def call_transform_method(methodname, *args, **kwargs):
- """return value returned by calling the given method on input"""
- def coerce(value):
- return getattr(value, methodname)(*args, **kwargs)
- return coerce
-
-def call_check_method(methodname, *args, **kwargs):
- """check value returned by calling the given method on input is true,
- else raise ValueError
- """
- def check(value):
- if getattr(value, methodname)(*args, **kwargs):
- return value
- raise ValueError('%s not verified on %r' % (methodname, value))
- return check
-
-# base integrity checking functions ############################################
-
-def check_doubles(buckets):
- """Extract the keys that have more than one item in their bucket."""
- return [(k, len(v)) for k, v in buckets.items() if len(v) > 1]
-
-def check_doubles_not_none(buckets):
- """Extract the keys that have more than one item in their bucket."""
- return [(k, len(v)) for k, v in buckets.items()
- if k is not None and len(v) > 1]
-
-# sql generator utility functions #############################################
-
-
-def _import_statements(sql_connect, statements, nb_threads=3,
- dump_output_dir=None,
- support_copy_from=True, encoding='utf-8'):
- """
- Import a bunch of sql statements, using different threads.
- """
- try:
- chunksize = (len(statements) / nb_threads) + 1
- threads = []
- for i in xrange(nb_threads):
- chunks = statements[i*chunksize:(i+1)*chunksize]
- thread = threading.Thread(target=_execmany_thread,
- args=(sql_connect, chunks,
- dump_output_dir,
- support_copy_from,
- encoding))
- thread.start()
- threads.append(thread)
- for t in threads:
- t.join()
- except Exception:
- print 'Error in import statements'
-
-def _execmany_thread_not_copy_from(cu, statement, data, table=None,
- columns=None, encoding='utf-8'):
- """ Execute thread without copy from
- """
- cu.executemany(statement, data)
-
-def _execmany_thread_copy_from(cu, statement, data, table,
- columns, encoding='utf-8'):
- """ Execute thread with copy from
- """
- try:
- buf = _create_copyfrom_buffer(data, columns, encoding=encoding)
- except ValueError:
- _execmany_thread_not_copy_from(cu, statement, data)
- else:
- if columns is None:
- cu.copy_from(buf, table, null='NULL')
- else:
- cu.copy_from(buf, table, null='NULL', columns=columns)
-
-def _execmany_thread(sql_connect, statements, dump_output_dir=None,
- support_copy_from=True, encoding='utf-8'):
- """
- Execute sql statement. If 'INSERT INTO', try to use 'COPY FROM' command,
- or fallback to execute_many.
- """
- if support_copy_from:
- execmany_func = _execmany_thread_copy_from
- else:
- execmany_func = _execmany_thread_not_copy_from
- cnx = sql_connect()
- cu = cnx.cursor()
- try:
- for statement, data in statements:
- table = None
- columns = None
- try:
- if not statement.startswith('INSERT INTO'):
- cu.executemany(statement, data)
- continue
- table = statement.split()[2]
- if isinstance(data[0], (tuple, list)):
- columns = None
- else:
- columns = list(data[0])
- execmany_func(cu, statement, data, table, columns, encoding)
- except Exception:
- print 'unable to copy data into table %s' % table
- # Error in import statement, save data in dump_output_dir
- if dump_output_dir is not None:
- pdata = {'data': data, 'statement': statement,
- 'time': asctime(), 'columns': columns}
- filename = make_uid()
- try:
- with open(osp.join(dump_output_dir,
- '%s.pickle' % filename), 'w') as fobj:
- fobj.write(cPickle.dumps(pdata))
- except IOError:
- print 'ERROR while pickling in', dump_output_dir, filename+'.pickle'
- pass
- cnx.rollback()
- raise
- finally:
- cnx.commit()
- cu.close()
-
-
-def _copyfrom_buffer_convert_None(value, **opts):
- '''Convert None value to "NULL"'''
- return 'NULL'
-
-def _copyfrom_buffer_convert_number(value, **opts):
- '''Convert a number into its string representation'''
- return str(value)
-
-def _copyfrom_buffer_convert_string(value, **opts):
- '''Convert string value.
-
- Recognized keywords:
- :encoding: resulting string encoding (default: utf-8)
- :replace_sep: character used when input contains characters
- that conflict with the column separator.
- '''
- encoding = opts.get('encoding','utf-8')
- replace_sep = opts.get('replace_sep', None)
- # Remove separators used in string formatting
- for _char in (u'\t', u'\r', u'\n'):
- if _char in value:
- # If a replace_sep is given, replace
- # the separator
- # (and thus avoid empty buffer)
- if replace_sep is None:
- raise ValueError('conflicting separator: '
- 'you must provide the replace_sep option')
- value = value.replace(_char, replace_sep)
- value = value.replace('\\', r'\\')
- if isinstance(value, unicode):
- value = value.encode(encoding)
- return value
-
-def _copyfrom_buffer_convert_date(value, **opts):
- '''Convert date into "YYYY-MM-DD"'''
- # Do not use strftime, as it yields issue with date < 1900
- # (http://bugs.python.org/issue1777412)
- return '%04d-%02d-%02d' % (value.year, value.month, value.day)
-
-def _copyfrom_buffer_convert_datetime(value, **opts):
- '''Convert date into "YYYY-MM-DD HH:MM:SS.UUUUUU"'''
- # Do not use strftime, as it yields issue with date < 1900
- # (http://bugs.python.org/issue1777412)
- return '%s %s' % (_copyfrom_buffer_convert_date(value, **opts),
- _copyfrom_buffer_convert_time(value, **opts))
-
-def _copyfrom_buffer_convert_time(value, **opts):
- '''Convert time into "HH:MM:SS.UUUUUU"'''
- return '%02d:%02d:%02d.%06d' % (value.hour, value.minute,
- value.second, value.microsecond)
-
-# (types, converter) list.
-_COPYFROM_BUFFER_CONVERTERS = [
- (type(None), _copyfrom_buffer_convert_None),
- ((long, int, float), _copyfrom_buffer_convert_number),
- (basestring, _copyfrom_buffer_convert_string),
- (datetime, _copyfrom_buffer_convert_datetime),
- (date, _copyfrom_buffer_convert_date),
- (time, _copyfrom_buffer_convert_time),
-]
-
-def _create_copyfrom_buffer(data, columns=None, **convert_opts):
- """
- Create a StringIO buffer for 'COPY FROM' command.
- Deals with Unicode, Int, Float, Date... (see ``converters``)
-
- :data: a sequence/dict of tuples
- :columns: list of columns to consider (default to all columns)
- :converter_opts: keyword arguements given to converters
- """
- # Create a list rather than directly create a StringIO
- # to correctly write lines separated by '\n' in a single step
- rows = []
- if columns is None:
- if isinstance(data[0], (tuple, list)):
- columns = range(len(data[0]))
- elif isinstance(data[0], dict):
- columns = data[0].keys()
- else:
- raise ValueError('Could not get columns: you must provide columns.')
- for row in data:
- # Iterate over the different columns and the different values
- # and try to convert them to a correct datatype.
- # If an error is raised, do not continue.
- formatted_row = []
- for col in columns:
- try:
- value = row[col]
- except KeyError:
- warnings.warn(u"Column %s is not accessible in row %s"
- % (col, row), RuntimeWarning)
- # XXX 'value' set to None so that the import does not end in
- # error.
- # Instead, the extra keys are set to NULL from the
- # database point of view.
- value = None
- for types, converter in _COPYFROM_BUFFER_CONVERTERS:
- if isinstance(value, types):
- value = converter(value, **convert_opts)
- break
- else:
- raise ValueError("Unsupported value type %s" % type(value))
- # We push the value to the new formatted row
- # if the value is not None and could be converted to a string.
- formatted_row.append(value)
- rows.append('\t'.join(formatted_row))
- return StringIO('\n'.join(rows))
-
-
-# object stores #################################################################
-
-class ObjectStore(object):
- """Store objects in memory for *faster* validation (development mode)
-
- But it will not enforce the constraints of the schema and hence will miss some problems
-
- >>> store = ObjectStore()
- >>> user = store.create_entity('CWUser', login=u'johndoe')
- >>> group = store.create_entity('CWUser', name=u'unknown')
- >>> store.relate(user.eid, 'in_group', group.eid)
- """
- def __init__(self):
- self.items = []
- self.eids = {}
- self.types = {}
- self.relations = set()
- self.indexes = {}
-
- def create_entity(self, etype, **data):
- data = attrdict(data)
- data['eid'] = eid = len(self.items)
- self.items.append(data)
- self.eids[eid] = data
- self.types.setdefault(etype, []).append(eid)
- return data
-
- def relate(self, eid_from, rtype, eid_to, **kwargs):
- """Add new relation"""
- relation = eid_from, rtype, eid_to
- self.relations.add(relation)
- return relation
-
- def commit(self):
- """this commit method does nothing by default"""
- return
-
- def flush(self):
- """The method is provided so that all stores share a common API"""
- pass
-
- @property
- def nb_inserted_entities(self):
- return len(self.eids)
- @property
- def nb_inserted_types(self):
- return len(self.types)
- @property
- def nb_inserted_relations(self):
- return len(self.relations)
-
-class RQLObjectStore(ObjectStore):
- """ObjectStore that works with an actual RQL repository (production mode)"""
-
- def __init__(self, cnx, commit=None):
- if commit is not None:
- warnings.warn('[3.19] commit argument should not be specified '
- 'as the cnx object already provides it.',
- DeprecationWarning, stacklevel=2)
- super(RQLObjectStore, self).__init__()
- self._cnx = cnx
- self._commit = commit or cnx.commit
-
- def commit(self):
- return self._commit()
-
- def rql(self, *args):
- return self._cnx.execute(*args)
-
- @property
- def session(self):
- warnings.warn('[3.19] deprecated property.', DeprecationWarning,
- stacklevel=2)
- return self._cnx.repo._get_session(self._cnx.sessionid)
-
- def create_entity(self, *args, **kwargs):
- entity = self._cnx.create_entity(*args, **kwargs)
- self.eids[entity.eid] = entity
- self.types.setdefault(args[0], []).append(entity.eid)
- return entity
-
- def relate(self, eid_from, rtype, eid_to, **kwargs):
- eid_from, rtype, eid_to = super(RQLObjectStore, self).relate(
- eid_from, rtype, eid_to, **kwargs)
- self.rql('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
- {'x': int(eid_from), 'y': int(eid_to)})
-
- @deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
- def find_entities(self, *args, **kwargs):
- return self._cnx.find(*args, **kwargs).entities()
-
- @deprecated("[3.19] use cnx.find(*args, **kwargs).one() instead")
- def find_one_entity(self, *args, **kwargs):
- return self._cnx.find(*args, **kwargs).one()
-
-# the import controller ########################################################
-
-class CWImportController(object):
- """Controller of the data import process.
-
- >>> ctl = CWImportController(store)
- >>> ctl.generators = list_of_data_generators
- >>> ctl.data = dict_of_data_tables
- >>> ctl.run()
- """
-
- def __init__(self, store, askerror=0, catcherrors=None, tell=tell,
- commitevery=50):
- self.store = store
- self.generators = None
- self.data = {}
- self.errors = None
- self.askerror = askerror
- if catcherrors is None:
- catcherrors = askerror
- self.catcherrors = catcherrors
- self.commitevery = commitevery # set to None to do a single commit
- self._tell = tell
-
- def check(self, type, key, value):
- self._checks.setdefault(type, {}).setdefault(key, []).append(value)
-
- def check_map(self, entity, key, map, default):
- try:
- entity[key] = map[entity[key]]
- except KeyError:
- self.check(key, entity[key], None)
- entity[key] = default
-
- def record_error(self, key, msg=None, type=None, value=None, tb=None):
- tmp = StringIO()
- if type is None:
- traceback.print_exc(file=tmp)
- else:
- traceback.print_exception(type, value, tb, file=tmp)
- # use a list to avoid counting a <nb lines> errors instead of one
- errorlog = self.errors.setdefault(key, [])
- if msg is None:
- errorlog.append(tmp.getvalue().splitlines())
- else:
- errorlog.append( (msg, tmp.getvalue().splitlines()) )
-
- def run(self):
- self.errors = {}
- if self.commitevery is None:
- self.tell('Will commit all or nothing.')
- else:
- self.tell('Will commit every %s iterations' % self.commitevery)
- for func, checks in self.generators:
- self._checks = {}
- func_name = func.__name__
- self.tell("Run import function '%s'..." % func_name)
- try:
- func(self)
- except Exception:
- if self.catcherrors:
- self.record_error(func_name, 'While calling %s' % func.__name__)
- else:
- self._print_stats()
- raise
- for key, func, title, help in checks:
- buckets = self._checks.get(key)
- if buckets:
- err = func(buckets)
- if err:
- self.errors[title] = (help, err)
- try:
- txuuid = self.store.commit()
- if txuuid is not None:
- self.tell('Transaction commited (txuuid: %s)' % txuuid)
- except QueryError as ex:
- self.tell('Transaction aborted: %s' % ex)
- self._print_stats()
- if self.errors:
- if self.askerror == 2 or (self.askerror and confirm('Display errors ?')):
- from pprint import pformat
- for errkey, error in self.errors.items():
- self.tell("\n%s (%s): %d\n" % (error[0], errkey, len(error[1])))
- self.tell(pformat(sorted(error[1])))
-
- def _print_stats(self):
- nberrors = sum(len(err) for err in self.errors.itervalues())
- self.tell('\nImport statistics: %i entities, %i types, %i relations and %i errors'
- % (self.store.nb_inserted_entities,
- self.store.nb_inserted_types,
- self.store.nb_inserted_relations,
- nberrors))
-
- def get_data(self, key):
- return self.data.get(key)
-
- def index(self, name, key, value, unique=False):
- """create a new index
-
- If unique is set to True, only first occurence will be kept not the following ones
- """
- if unique:
- try:
- if value in self.store.indexes[name][key]:
- return
- except KeyError:
- # we're sure that one is the first occurence; so continue...
- pass
- self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
-
- def tell(self, msg):
- self._tell(msg)
-
- def iter_and_commit(self, datakey):
- """iter rows, triggering commit every self.commitevery iterations"""
- if self.commitevery is None:
- return self.get_data(datakey)
- else:
- return callfunc_every(self.store.commit,
- self.commitevery,
- self.get_data(datakey))
-
-
-class NoHookRQLObjectStore(RQLObjectStore):
- """ObjectStore that works with an actual RQL repository (production mode)"""
-
- def __init__(self, cnx, metagen=None, baseurl=None):
- super(NoHookRQLObjectStore, self).__init__(cnx)
- self.source = cnx.repo.system_source
- self.rschema = cnx.repo.schema.rschema
- self.add_relation = self.source.add_relation
- if metagen is None:
- metagen = MetaGenerator(cnx, baseurl)
- self.metagen = metagen
- self._nb_inserted_entities = 0
- self._nb_inserted_types = 0
- self._nb_inserted_relations = 0
- # deactivate security
- cnx.read_security = False
- cnx.write_security = False
-
- def create_entity(self, etype, **kwargs):
- for k, v in kwargs.iteritems():
- kwargs[k] = getattr(v, 'eid', v)
- entity, rels = self.metagen.base_etype_dicts(etype)
- # make a copy to keep cached entity pristine
- entity = copy(entity)
- entity.cw_edited = copy(entity.cw_edited)
- entity.cw_clear_relation_cache()
- entity.cw_edited.update(kwargs, skipsec=False)
- entity_source, extid = self.metagen.init_entity(entity)
- cnx = self._cnx
- self.source.add_entity(cnx, entity)
- self.source.add_info(cnx, entity, entity_source, extid)
- kwargs = dict()
- if inspect.getargspec(self.add_relation).keywords:
- kwargs['subjtype'] = entity.cw_etype
- for rtype, targeteids in rels.iteritems():
- # targeteids may be a single eid or a list of eids
- inlined = self.rschema(rtype).inlined
- try:
- for targeteid in targeteids:
- self.add_relation(cnx, entity.eid, rtype, targeteid,
- inlined, **kwargs)
- except TypeError:
- self.add_relation(cnx, entity.eid, rtype, targeteids,
- inlined, **kwargs)
- self._nb_inserted_entities += 1
- return entity
-
- def relate(self, eid_from, rtype, eid_to, **kwargs):
- assert not rtype.startswith('reverse_')
- self.add_relation(self._cnx, eid_from, rtype, eid_to,
- self.rschema(rtype).inlined)
- if self.rschema(rtype).symmetric:
- self.add_relation(self._cnx, eid_to, rtype, eid_from,
- self.rschema(rtype).inlined)
- self._nb_inserted_relations += 1
-
- @property
- def nb_inserted_entities(self):
- return self._nb_inserted_entities
- @property
- def nb_inserted_types(self):
- return self._nb_inserted_types
- @property
- def nb_inserted_relations(self):
- return self._nb_inserted_relations
-
-
-class MetaGenerator(object):
- META_RELATIONS = (META_RTYPES
- - VIRTUAL_RTYPES
- - set(('eid', 'cwuri',
- 'is', 'is_instance_of', 'cw_source')))
-
- def __init__(self, cnx, baseurl=None, source=None):
- self._cnx = cnx
- if baseurl is None:
- config = cnx.vreg.config
- baseurl = config['base-url'] or config.default_base_url()
- if not baseurl[-1] == '/':
- baseurl += '/'
- self.baseurl = baseurl
- if source is None:
- source = cnx.repo.system_source
- self.source = source
- self.create_eid = cnx.repo.system_source.create_eid
- self.time = datetime.now()
- # attributes/relations shared by all entities of the same type
- self.etype_attrs = []
- self.etype_rels = []
- # attributes/relations specific to each entity
- self.entity_attrs = ['cwuri']
- #self.entity_rels = [] XXX not handled (YAGNI?)
- schema = cnx.vreg.schema
- rschema = schema.rschema
- for rtype in self.META_RELATIONS:
- # skip owned_by / created_by if user is the internal manager
- if cnx.user.eid == -1 and rtype in ('owned_by', 'created_by'):
- continue
- if rschema(rtype).final:
- self.etype_attrs.append(rtype)
- else:
- self.etype_rels.append(rtype)
-
- @cached
- def base_etype_dicts(self, etype):
- entity = self._cnx.vreg['etypes'].etype_class(etype)(self._cnx)
- # entity are "surface" copied, avoid shared dict between copies
- del entity.cw_extra_kwargs
- entity.cw_edited = EditedEntity(entity)
- for attr in self.etype_attrs:
- genfunc = self.generate(attr)
- if genfunc:
- entity.cw_edited.edited_attribute(attr, genfunc(entity))
- rels = {}
- for rel in self.etype_rels:
- genfunc = self.generate(rel)
- if genfunc:
- rels[rel] = genfunc(entity)
- return entity, rels
-
- def init_entity(self, entity):
- entity.eid = self.create_eid(self._cnx)
- extid = entity.cw_edited.get('cwuri')
- for attr in self.entity_attrs:
- if attr in entity.cw_edited:
- # already set, skip this attribute
- continue
- genfunc = self.generate(attr)
- if genfunc:
- entity.cw_edited.edited_attribute(attr, genfunc(entity))
- if isinstance(extid, unicode):
- extid = extid.encode('utf-8')
- return self.source, extid
-
- def generate(self, rtype):
- return getattr(self, 'gen_%s' % rtype, None)
-
- def gen_cwuri(self, entity):
- assert self.baseurl, 'baseurl is None while generating cwuri'
- return u'%s%s' % (self.baseurl, entity.eid)
-
- def gen_creation_date(self, entity):
- return self.time
-
- def gen_modification_date(self, entity):
- return self.time
-
- def gen_created_by(self, entity):
- return self._cnx.user.eid
-
- def gen_owned_by(self, entity):
- return self._cnx.user.eid
-
-
-###########################################################################
-## SQL object store #######################################################
-###########################################################################
-class SQLGenObjectStore(NoHookRQLObjectStore):
- """Controller of the data import process. This version is based
- on direct insertions throught SQL command (COPY FROM or execute many).
-
- >>> store = SQLGenObjectStore(cnx)
- >>> store.create_entity('Person', ...)
- >>> store.flush()
- """
-
- def __init__(self, cnx, dump_output_dir=None, nb_threads_statement=3):
- """
- Initialize a SQLGenObjectStore.
-
- Parameters:
-
- - cnx: connection on the cubicweb instance
- - dump_output_dir: a directory to dump failed statements
- for easier recovery. Default is None (no dump).
- - nb_threads_statement: number of threads used
- for SQL insertion (default is 3).
- """
- super(SQLGenObjectStore, self).__init__(cnx)
- ### hijack default source
- self.source = SQLGenSourceWrapper(
- self.source, cnx.vreg.schema,
- dump_output_dir=dump_output_dir,
- nb_threads_statement=nb_threads_statement)
- ### XXX This is done in super().__init__(), but should be
- ### redone here to link to the correct source
- self.add_relation = self.source.add_relation
- self.indexes_etypes = {}
-
- def flush(self):
- """Flush data to the database"""
- self.source.flush()
-
- def relate(self, subj_eid, rtype, obj_eid, **kwargs):
- if subj_eid is None or obj_eid is None:
- return
- # XXX Could subjtype be inferred ?
- self.source.add_relation(self._cnx, subj_eid, rtype, obj_eid,
- self.rschema(rtype).inlined, **kwargs)
- if self.rschema(rtype).symmetric:
- self.source.add_relation(self._cnx, obj_eid, rtype, subj_eid,
- self.rschema(rtype).inlined, **kwargs)
-
- def drop_indexes(self, etype):
- """Drop indexes for a given entity type"""
- if etype not in self.indexes_etypes:
- cu = self._cnx.cnxset.cu
- def index_to_attr(index):
- """turn an index name to (database) attribute name"""
- return index.replace(etype.lower(), '').replace('idx', '').strip('_')
- indices = [(index, index_to_attr(index))
- for index in self.source.dbhelper.list_indices(cu, etype)
- # Do not consider 'cw_etype_pkey' index
- if not index.endswith('key')]
- self.indexes_etypes[etype] = indices
- for index, attr in self.indexes_etypes[etype]:
- self._cnx.system_sql('DROP INDEX %s' % index)
-
- def create_indexes(self, etype):
- """Recreate indexes for a given entity type"""
- for index, attr in self.indexes_etypes.get(etype, []):
- sql = 'CREATE INDEX %s ON cw_%s(%s)' % (index, etype, attr)
- self._cnx.system_sql(sql)
-
-
-###########################################################################
-## SQL Source #############################################################
-###########################################################################
-
-class SQLGenSourceWrapper(object):
-
- def __init__(self, system_source, schema,
- dump_output_dir=None, nb_threads_statement=3):
- self.system_source = system_source
- self._sql = threading.local()
- # Explicitely backport attributes from system source
- self._storage_handler = self.system_source._storage_handler
- self.preprocess_entity = self.system_source.preprocess_entity
- self.sqlgen = self.system_source.sqlgen
- self.uri = self.system_source.uri
- self.eid = self.system_source.eid
- # Directory to write temporary files
- self.dump_output_dir = dump_output_dir
- # Allow to execute code with SQLite backend that does
- # not support (yet...) copy_from
- # XXX Should be dealt with in logilab.database
- spcfrom = system_source.dbhelper.dbapi_module.support_copy_from
- self.support_copy_from = spcfrom
- self.dbencoding = system_source.dbhelper.dbencoding
- self.nb_threads_statement = nb_threads_statement
- # initialize thread-local data for main thread
- self.init_thread_locals()
- self._inlined_rtypes_cache = {}
- self._fill_inlined_rtypes_cache(schema)
- self.schema = schema
- self.do_fti = False
-
- def _fill_inlined_rtypes_cache(self, schema):
- cache = self._inlined_rtypes_cache
- for eschema in schema.entities():
- for rschema in eschema.ordered_relations():
- if rschema.inlined:
- cache[eschema.type] = SQL_PREFIX + rschema.type
-
- def init_thread_locals(self):
- """initializes thread-local data"""
- self._sql.entities = defaultdict(list)
- self._sql.relations = {}
- self._sql.inlined_relations = {}
- # keep track, for each eid of the corresponding data dict
- self._sql.eid_insertdicts = {}
-
- def flush(self):
- print 'starting flush'
- _entities_sql = self._sql.entities
- _relations_sql = self._sql.relations
- _inlined_relations_sql = self._sql.inlined_relations
- _insertdicts = self._sql.eid_insertdicts
- try:
- # try, for each inlined_relation, to find if we're also creating
- # the host entity (i.e. the subject of the relation).
- # In that case, simply update the insert dict and remove
- # the need to make the
- # UPDATE statement
- for statement, datalist in _inlined_relations_sql.iteritems():
- new_datalist = []
- # for a given inlined relation,
- # browse each couple to be inserted
- for data in datalist:
- keys = list(data)
- # For inlined relations, it exists only two case:
- # (rtype, cw_eid) or (cw_eid, rtype)
- if keys[0] == 'cw_eid':
- rtype = keys[1]
- else:
- rtype = keys[0]
- updated_eid = data['cw_eid']
- if updated_eid in _insertdicts:
- _insertdicts[updated_eid][rtype] = data[rtype]
- else:
- # could not find corresponding insert dict, keep the
- # UPDATE query
- new_datalist.append(data)
- _inlined_relations_sql[statement] = new_datalist
- _import_statements(self.system_source.get_connection,
- _entities_sql.items()
- + _relations_sql.items()
- + _inlined_relations_sql.items(),
- dump_output_dir=self.dump_output_dir,
- nb_threads=self.nb_threads_statement,
- support_copy_from=self.support_copy_from,
- encoding=self.dbencoding)
- finally:
- _entities_sql.clear()
- _relations_sql.clear()
- _insertdicts.clear()
- _inlined_relations_sql.clear()
-
- def add_relation(self, cnx, subject, rtype, object,
- inlined=False, **kwargs):
- if inlined:
- _sql = self._sql.inlined_relations
- data = {'cw_eid': subject, SQL_PREFIX + rtype: object}
- subjtype = kwargs.get('subjtype')
- if subjtype is None:
- # Try to infer it
- targets = [t.type for t in
- self.schema.rschema(rtype).subjects()]
- if len(targets) == 1:
- subjtype = targets[0]
- else:
- raise ValueError('You should give the subject etype for '
- 'inlined relation %s'
- ', as it cannot be inferred: '
- 'this type is given as keyword argument '
- '``subjtype``'% rtype)
- statement = self.sqlgen.update(SQL_PREFIX + subjtype,
- data, ['cw_eid'])
- else:
- _sql = self._sql.relations
- data = {'eid_from': subject, 'eid_to': object}
- statement = self.sqlgen.insert('%s_relation' % rtype, data)
- if statement in _sql:
- _sql[statement].append(data)
- else:
- _sql[statement] = [data]
-
- def add_entity(self, cnx, entity):
- with self._storage_handler(entity, 'added'):
- attrs = self.preprocess_entity(entity)
- rtypes = self._inlined_rtypes_cache.get(entity.cw_etype, ())
- if isinstance(rtypes, str):
- rtypes = (rtypes,)
- for rtype in rtypes:
- if rtype not in attrs:
- attrs[rtype] = None
- sql = self.sqlgen.insert(SQL_PREFIX + entity.cw_etype, attrs)
- self._sql.eid_insertdicts[entity.eid] = attrs
- self._append_to_entities(sql, attrs)
-
- def _append_to_entities(self, sql, attrs):
- self._sql.entities[sql].append(attrs)
-
- def _handle_insert_entity_sql(self, cnx, sql, attrs):
- # We have to overwrite the source given in parameters
- # as here, we directly use the system source
- attrs['asource'] = self.system_source.uri
- self._append_to_entities(sql, attrs)
-
- def _handle_is_relation_sql(self, cnx, sql, attrs):
- self._append_to_entities(sql, attrs)
-
- def _handle_is_instance_of_sql(self, cnx, sql, attrs):
- self._append_to_entities(sql, attrs)
-
- def _handle_source_relation_sql(self, cnx, sql, attrs):
- self._append_to_entities(sql, attrs)
-
- # add_info is _copypasted_ from the one in NativeSQLSource. We want it
- # there because it will use the _handlers of the SQLGenSourceWrapper, which
- # are not like the ones in the native source.
- def add_info(self, cnx, entity, source, extid):
- """add type and source info for an eid into the system table"""
- # begin by inserting eid/type/source/extid into the entities table
- if extid is not None:
- assert isinstance(extid, str)
- extid = b64encode(extid)
- attrs = {'type': entity.cw_etype, 'eid': entity.eid, 'extid': extid,
- 'asource': source.uri}
- self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
- # insert core relations: is, is_instance_of and cw_source
- try:
- self._handle_is_relation_sql(cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)',
- (entity.eid, eschema_eid(cnx, entity.e_schema)))
- except IndexError:
- # during schema serialization, skip
- pass
- else:
- for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
- self._handle_is_relation_sql(cnx,
- 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)',
- (entity.eid, eschema_eid(cnx, eschema)))
- if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10
- self._handle_is_relation_sql(cnx, '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.cw_etype):
- self.index_entity(cnx, entity=entity)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/__init__.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,35 @@
+# copyright 2003-2015 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/>.
+"""Package containing various utilities to import data into cubicweb."""
+
+
+def callfunc_every(func, number, iterable):
+ """yield items of `iterable` one by one and call function `func`
+ every `number` iterations. Always call function `func` at the end.
+ """
+ for idx, item in enumerate(iterable):
+ yield item
+ if not idx % number:
+ func()
+ func()
+
+# import for backward compat
+from cubicweb.dataimport.stores import *
+from cubicweb.dataimport.pgstore import *
+from cubicweb.dataimport.csv import *
+from cubicweb.dataimport.deprecated import *
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/csv.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,114 @@
+# copyright 2003-2015 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/>.
+"""Functions to help importing CSV data"""
+
+from __future__ import absolute_import
+
+import csv as csvmod
+import warnings
+import os.path as osp
+
+from logilab.common import shellutils
+
+
+def count_lines(stream_or_filename):
+ if isinstance(stream_or_filename, basestring):
+ f = open(stream_or_filename)
+ else:
+ f = stream_or_filename
+ f.seek(0)
+ i = 0 # useful is f is an empty file
+ for i, line in enumerate(f):
+ pass
+ f.seek(0)
+ return i+1
+
+
+def ucsvreader_pb(stream_or_path, encoding='utf-8', delimiter=',', quotechar='"',
+ skipfirst=False, withpb=True, skip_empty=True, separator=None,
+ quote=None):
+ """same as :func:`ucsvreader` but a progress bar is displayed as we iter on rows"""
+ if separator is not None:
+ delimiter = separator
+ warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
+ if quote is not None:
+ quotechar = quote
+ warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
+ if isinstance(stream_or_path, basestring):
+ if not osp.exists(stream_or_path):
+ raise Exception("file doesn't exists: %s" % stream_or_path)
+ stream = open(stream_or_path)
+ else:
+ stream = stream_or_path
+ rowcount = count_lines(stream)
+ if skipfirst:
+ rowcount -= 1
+ if withpb:
+ pb = shellutils.ProgressBar(rowcount, 50)
+ for urow in ucsvreader(stream, encoding, delimiter, quotechar,
+ skipfirst=skipfirst, skip_empty=skip_empty):
+ yield urow
+ if withpb:
+ pb.update()
+ print ' %s rows imported' % rowcount
+
+
+def ucsvreader(stream, encoding='utf-8', delimiter=',', quotechar='"',
+ skipfirst=False, ignore_errors=False, skip_empty=True,
+ separator=None, quote=None):
+ """A csv reader that accepts files with any encoding and outputs unicode
+ strings
+
+ if skip_empty (the default), lines without any values specified (only
+ separators) will be skipped. This is useful for Excel exports which may be
+ full of such lines.
+ """
+ if separator is not None:
+ delimiter = separator
+ warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
+ if quote is not None:
+ quotechar = quote
+ warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
+ it = iter(csvmod.reader(stream, delimiter=delimiter, quotechar=quotechar))
+ if not ignore_errors:
+ if skipfirst:
+ it.next()
+ for row in it:
+ decoded = [item.decode(encoding) for item in row]
+ if not skip_empty or any(decoded):
+ yield decoded
+ else:
+ if skipfirst:
+ try:
+ row = it.next()
+ except csvmod.Error:
+ pass
+ # Safe version, that can cope with error in CSV file
+ while True:
+ try:
+ row = it.next()
+ # End of CSV, break
+ except StopIteration:
+ break
+ # Error in CSV, ignore line and continue
+ except csvmod.Error:
+ continue
+ decoded = [item.decode(encoding) for item in row]
+ if not skip_empty or any(decoded):
+ yield decoded
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/deprecated.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,460 @@
+# copyright 2003-2015 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/>.
+"""Old and deprecated dataimport API that provides tools to import tabular data.
+
+
+Example of use (run this with `cubicweb-ctl shell instance import-script.py`):
+
+.. sourcecode:: python
+
+ from cubicweb.dataimport import *
+ # define data generators
+ GENERATORS = []
+
+ USERS = [('Prenom', 'firstname', ()),
+ ('Nom', 'surname', ()),
+ ('Identifiant', 'login', ()),
+ ]
+
+ def gen_users(ctl):
+ for row in ctl.iter_and_commit('utilisateurs'):
+ entity = mk_entity(row, USERS)
+ entity['upassword'] = 'motdepasse'
+ ctl.check('login', entity['login'], None)
+ entity = ctl.store.prepare_insert_entity('CWUser', **entity)
+ email = ctl.store.prepare_insert_entity('EmailAddress', address=row['email'])
+ ctl.store.prepare_insert_relation(entity, 'use_email', email)
+ ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x': entity})
+
+ CHK = [('login', check_doubles, 'Utilisateurs Login',
+ 'Deux utilisateurs ne devraient pas avoir le meme login.'),
+ ]
+
+ GENERATORS.append( (gen_users, CHK) )
+
+ # create controller
+ ctl = CWImportController(RQLObjectStore(cnx))
+ ctl.askerror = 1
+ ctl.generators = GENERATORS
+ ctl.data['utilisateurs'] = lazytable(ucsvreader(open('users.csv')))
+ # run
+ ctl.run()
+
+.. BUG file with one column are not parsable
+.. TODO rollback() invocation is not possible yet
+"""
+
+import sys
+import traceback
+from StringIO import StringIO
+
+from logilab.common import attrdict, shellutils
+from logilab.common.date import strptime
+from logilab.common.deprecation import deprecated, class_deprecated
+
+from cubicweb import QueryError
+from cubicweb.dataimport import callfunc_every
+
+
+@deprecated('[3.21] deprecated')
+def lazytable(reader):
+ """The first row is taken to be the header of the table and
+ used to output a dict for each row of data.
+
+ >>> data = lazytable(ucsvreader(open(filename)))
+ """
+ header = reader.next()
+ for row in reader:
+ yield dict(zip(header, row))
+
+
+@deprecated('[3.21] deprecated')
+def lazydbtable(cu, table, headers, orderby=None):
+ """return an iterator on rows of a sql table. On each row, fetch columns
+ defined in headers and return values as a dictionary.
+
+ >>> data = lazydbtable(cu, 'experimentation', ('id', 'nickname', 'gps'))
+ """
+ sql = 'SELECT %s FROM %s' % (','.join(headers), table,)
+ if orderby:
+ sql += ' ORDER BY %s' % ','.join(orderby)
+ cu.execute(sql)
+ while True:
+ row = cu.fetchone()
+ if row is None:
+ break
+ yield dict(zip(headers, row))
+
+
+@deprecated('[3.21] deprecated')
+def tell(msg):
+ print msg
+
+
+@deprecated('[3.21] deprecated')
+def confirm(question):
+ """A confirm function that asks for yes/no/abort and exits on abort."""
+ answer = shellutils.ASK.ask(question, ('Y', 'n', 'abort'), 'Y')
+ if answer == 'abort':
+ sys.exit(1)
+ return answer == 'Y'
+
+
+class catch_error(object):
+ """Helper for @contextmanager decorator."""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.21] deprecated'
+
+ def __init__(self, ctl, key='unexpected error', msg=None):
+ self.ctl = ctl
+ self.key = key
+ self.msg = msg
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ if type is not None:
+ if issubclass(type, (KeyboardInterrupt, SystemExit)):
+ return # re-raise
+ if self.ctl.catcherrors:
+ self.ctl.record_error(self.key, None, type, value, traceback)
+ return True # silent
+
+@deprecated('[3.21] deprecated')
+def mk_entity(row, map):
+ """Return a dict made from sanitized mapped values.
+
+ ValueError can be raised on unexpected values found in checkers
+
+ >>> row = {'myname': u'dupont'}
+ >>> map = [('myname', u'name', (call_transform_method('title'),))]
+ >>> mk_entity(row, map)
+ {'name': u'Dupont'}
+ >>> row = {'myname': u'dupont', 'optname': u''}
+ >>> map = [('myname', u'name', (call_transform_method('title'),)),
+ ... ('optname', u'MARKER', (optional,))]
+ >>> mk_entity(row, map)
+ {'name': u'Dupont', 'optname': None}
+ """
+ res = {}
+ assert isinstance(row, dict)
+ assert isinstance(map, list)
+ for src, dest, funcs in map:
+ try:
+ res[dest] = row[src]
+ except KeyError:
+ continue
+ try:
+ for func in funcs:
+ res[dest] = func(res[dest])
+ if res[dest] is None:
+ break
+ except ValueError as err:
+ raise ValueError('error with %r field: %s' % (src, err)), None, sys.exc_info()[-1]
+ return res
+
+
+# base sanitizing/coercing functions ###########################################
+
+@deprecated('[3.21] deprecated')
+def optional(value):
+ """checker to filter optional field
+
+ If value is undefined (ex: empty string), return None that will
+ break the checkers validation chain
+
+ General use is to add 'optional' check in first condition to avoid
+ ValueError by further checkers
+
+ >>> MAPPER = [(u'value', 'value', (optional, int))]
+ >>> row = {'value': u'XXX'}
+ >>> mk_entity(row, MAPPER)
+ {'value': None}
+ >>> row = {'value': u'100'}
+ >>> mk_entity(row, MAPPER)
+ {'value': 100}
+ """
+ if value:
+ return value
+ return None
+
+
+@deprecated('[3.21] deprecated')
+def required(value):
+ """raise ValueError if value is empty
+
+ This check should be often found in last position in the chain.
+ """
+ if value:
+ return value
+ raise ValueError("required")
+
+
+@deprecated('[3.21] deprecated')
+def todatetime(format='%d/%m/%Y'):
+ """return a transformation function to turn string input value into a
+ `datetime.datetime` instance, using given format.
+
+ Follow it by `todate` or `totime` functions from `logilab.common.date` if
+ you want a `date`/`time` instance instead of `datetime`.
+ """
+ def coerce(value):
+ return strptime(value, format)
+ return coerce
+
+
+@deprecated('[3.21] deprecated')
+def call_transform_method(methodname, *args, **kwargs):
+ """return value returned by calling the given method on input"""
+ def coerce(value):
+ return getattr(value, methodname)(*args, **kwargs)
+ return coerce
+
+
+@deprecated('[3.21] deprecated')
+def call_check_method(methodname, *args, **kwargs):
+ """check value returned by calling the given method on input is true,
+ else raise ValueError
+ """
+ def check(value):
+ if getattr(value, methodname)(*args, **kwargs):
+ return value
+ raise ValueError('%s not verified on %r' % (methodname, value))
+ return check
+
+
+# base integrity checking functions ############################################
+
+@deprecated('[3.21] deprecated')
+def check_doubles(buckets):
+ """Extract the keys that have more than one item in their bucket."""
+ return [(k, len(v)) for k, v in buckets.items() if len(v) > 1]
+
+
+@deprecated('[3.21] deprecated')
+def check_doubles_not_none(buckets):
+ """Extract the keys that have more than one item in their bucket."""
+ return [(k, len(v)) for k, v in buckets.items()
+ if k is not None and len(v) > 1]
+
+
+class ObjectStore(object):
+ """Store objects in memory for *faster* validation (development mode)
+
+ But it will not enforce the constraints of the schema and hence will miss some problems
+
+ >>> store = ObjectStore()
+ >>> user = store.prepare_insert_entity('CWUser', login=u'johndoe')
+ >>> group = store.prepare_insert_entity('CWUser', name=u'unknown')
+ >>> store.prepare_insert_relation(user, 'in_group', group)
+ """
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.21] use the new importer API'
+
+ def __init__(self):
+ self.items = []
+ self.eids = {}
+ self.types = {}
+ self.relations = set()
+ self.indexes = {}
+
+ def prepare_insert_entity(self, etype, **data):
+ """Given an entity type, attributes and inlined relations, return an eid for the entity that
+ would be inserted with a real store.
+ """
+ data = attrdict(data)
+ data['eid'] = eid = len(self.items)
+ self.items.append(data)
+ self.eids[eid] = data
+ self.types.setdefault(etype, []).append(eid)
+ return eid
+
+ def prepare_update_entity(self, etype, eid, **kwargs):
+ """Given an entity type and eid, updates the corresponding fake entity with specified
+ attributes and inlined relations.
+ """
+ assert eid in self.types[etype], 'Trying to update with wrong type {}'.format(etype)
+ data = self.eids[eid]
+ data.update(kwargs)
+
+ def prepare_insert_relation(self, eid_from, rtype, eid_to, **kwargs):
+ """Store into the `relations` attribute that a relation ``rtype`` exists between entities
+ with eids ``eid_from`` and ``eid_to``.
+ """
+ relation = eid_from, rtype, eid_to
+ self.relations.add(relation)
+ return relation
+
+ def flush(self):
+ """Nothing to flush for this store."""
+ pass
+
+ def commit(self):
+ """Nothing to commit for this store."""
+ return
+
+ def finish(self):
+ """Nothing to do once import is terminated for this store."""
+ pass
+
+ @property
+ def nb_inserted_entities(self):
+ return len(self.eids)
+
+ @property
+ def nb_inserted_types(self):
+ return len(self.types)
+
+ @property
+ def nb_inserted_relations(self):
+ return len(self.relations)
+
+ @deprecated('[3.21] use prepare_insert_entity instead')
+ def create_entity(self, etype, **data):
+ self.prepare_insert_entity(etype, **data)
+ return attrdict(data)
+
+ @deprecated('[3.21] use prepare_insert_relation instead')
+ def relate(self, eid_from, rtype, eid_to, **kwargs):
+ self.prepare_insert_relation(eid_from, rtype, eid_to, **kwargs)
+
+
+class CWImportController(object):
+ """Controller of the data import process.
+
+ >>> ctl = CWImportController(store)
+ >>> ctl.generators = list_of_data_generators
+ >>> ctl.data = dict_of_data_tables
+ >>> ctl.run()
+ """
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.21] use the new importer API'
+
+ def __init__(self, store, askerror=0, catcherrors=None, tell=tell,
+ commitevery=50):
+ self.store = store
+ self.generators = None
+ self.data = {}
+ self.errors = None
+ self.askerror = askerror
+ if catcherrors is None:
+ catcherrors = askerror
+ self.catcherrors = catcherrors
+ self.commitevery = commitevery # set to None to do a single commit
+ self._tell = tell
+
+ def check(self, type, key, value):
+ self._checks.setdefault(type, {}).setdefault(key, []).append(value)
+
+ def check_map(self, entity, key, map, default):
+ try:
+ entity[key] = map[entity[key]]
+ except KeyError:
+ self.check(key, entity[key], None)
+ entity[key] = default
+
+ def record_error(self, key, msg=None, type=None, value=None, tb=None):
+ tmp = StringIO()
+ if type is None:
+ traceback.print_exc(file=tmp)
+ else:
+ traceback.print_exception(type, value, tb, file=tmp)
+ # use a list to avoid counting a <nb lines> errors instead of one
+ errorlog = self.errors.setdefault(key, [])
+ if msg is None:
+ errorlog.append(tmp.getvalue().splitlines())
+ else:
+ errorlog.append( (msg, tmp.getvalue().splitlines()) )
+
+ def run(self):
+ self.errors = {}
+ if self.commitevery is None:
+ self.tell('Will commit all or nothing.')
+ else:
+ self.tell('Will commit every %s iterations' % self.commitevery)
+ for func, checks in self.generators:
+ self._checks = {}
+ func_name = func.__name__
+ self.tell("Run import function '%s'..." % func_name)
+ try:
+ func(self)
+ except Exception:
+ if self.catcherrors:
+ self.record_error(func_name, 'While calling %s' % func.__name__)
+ else:
+ self._print_stats()
+ raise
+ for key, func, title, help in checks:
+ buckets = self._checks.get(key)
+ if buckets:
+ err = func(buckets)
+ if err:
+ self.errors[title] = (help, err)
+ try:
+ txuuid = self.store.commit()
+ if txuuid is not None:
+ self.tell('Transaction commited (txuuid: %s)' % txuuid)
+ except QueryError as ex:
+ self.tell('Transaction aborted: %s' % ex)
+ self._print_stats()
+ if self.errors:
+ if self.askerror == 2 or (self.askerror and confirm('Display errors ?')):
+ from pprint import pformat
+ for errkey, error in self.errors.items():
+ self.tell("\n%s (%s): %d\n" % (error[0], errkey, len(error[1])))
+ self.tell(pformat(sorted(error[1])))
+
+ def _print_stats(self):
+ nberrors = sum(len(err) for err in self.errors.itervalues())
+ self.tell('\nImport statistics: %i entities, %i types, %i relations and %i errors'
+ % (self.store.nb_inserted_entities,
+ self.store.nb_inserted_types,
+ self.store.nb_inserted_relations,
+ nberrors))
+
+ def get_data(self, key):
+ return self.data.get(key)
+
+ def index(self, name, key, value, unique=False):
+ """create a new index
+
+ If unique is set to True, only first occurence will be kept not the following ones
+ """
+ if unique:
+ try:
+ if value in self.store.indexes[name][key]:
+ return
+ except KeyError:
+ # we're sure that one is the first occurence; so continue...
+ pass
+ self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
+
+ def tell(self, msg):
+ self._tell(msg)
+
+ def iter_and_commit(self, datakey):
+ """iter rows, triggering commit every self.commitevery iterations"""
+ if self.commitevery is None:
+ return self.get_data(datakey)
+ else:
+ return callfunc_every(self.store.commit,
+ self.commitevery,
+ self.get_data(datakey))
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/importer.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,417 @@
+# copyright 2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact@logilab.fr
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+"""Data import of external entities.
+
+Main entry points:
+
+.. autoclass:: ExtEntitiesImporter
+.. autoclass:: ExtEntity
+
+Utilities:
+
+.. autofunction:: cwuri2eid
+.. autoclass:: RelationMapping
+.. autofunction:: cubicweb.dataimport.importer.use_extid_as_cwuri
+"""
+
+from collections import defaultdict
+import logging
+
+from logilab.mtconverter import xml_escape
+
+
+def cwuri2eid(cnx, etypes, source_eid=None):
+ """Return a dictionary mapping cwuri to eid for entities of the given entity types and / or
+ source.
+ """
+ assert source_eid or etypes, 'no entity types nor source specified'
+ rql = 'Any U, X WHERE X cwuri U'
+ args = {}
+ if len(etypes) == 1:
+ rql += ', X is %s' % etypes[0]
+ elif etypes:
+ rql += ', X is IN (%s)' % ','.join(etypes)
+ if source_eid is not None:
+ rql += ', X cw_source S, S eid %(s)s'
+ args['s'] = source_eid
+ return dict(cnx.execute(rql, args))
+
+
+def use_extid_as_cwuri(extid2eid):
+ """Return a generator of :class:`ExtEntity` objects that will set `cwuri`
+ using entity's extid if the entity does not exist yet and has no `cwuri`
+ defined.
+
+ `extid2eid` is an extid to eid dictionary coming from an
+ :class:`ExtEntitiesImporter` instance.
+
+ Example usage:
+
+ .. code-block:: python
+
+ importer = SKOSExtEntitiesImporter(cnx, store, import_log)
+ set_cwuri = use_extid_as_cwuri(importer.extid2eid)
+ importer.import_entities(set_cwuri(extentities))
+ """
+ def use_extid_as_cwuri_filter(extentities):
+ for extentity in extentities:
+ if extentity.extid not in extid2eid:
+ extentity.values.setdefault('cwuri', set([unicode(extentity.extid)]))
+ yield extentity
+ return use_extid_as_cwuri_filter
+
+
+class RelationMapping(object):
+ """Read-only mapping from relation type to set of related (subject, object) eids.
+
+ If `source` is specified, only returns relations implying entities from
+ this source.
+ """
+
+ def __init__(self, cnx, source=None):
+ self.cnx = cnx
+ self._rql_template = 'Any S,O WHERE S {} O'
+ self._kwargs = {}
+ if source is not None:
+ self._rql_template += ', S cw_source SO, O cw_source SO, SO eid %(s)s'
+ self._kwargs['s'] = source.eid
+
+ def __getitem__(self, rtype):
+ """Return a set of (subject, object) eids already related by `rtype`"""
+ rql = self._rql_template.format(rtype)
+ return set(tuple(x) for x in self.cnx.execute(rql, self._kwargs))
+
+
+class ExtEntity(object):
+ """Transitional representation of an entity for use in data importer.
+
+ An external entity has the following properties:
+
+ * ``extid`` (external id), an identifier for the ext entity,
+
+ * ``etype`` (entity type), a string which must be the name of one entity type in the schema
+ (eg. ``'Person'``, ``'Animal'``, ...),
+
+ * ``values``, a dictionary whose keys are attribute or relation names from the schema (eg.
+ ``'first_name'``, ``'friend'``), and whose values are *sets*
+
+ For instance:
+
+ .. code-block:: python
+
+ ext_entity.extid = 'http://example.org/person/debby'
+ ext_entity.etype = 'Person'
+ ext_entity.values = {'first_name': set([u"Deborah", u"Debby"]),
+ 'friend': set(['http://example.org/person/john'])}
+
+ """
+
+ def __init__(self, etype, extid, values=None):
+ self.etype = etype
+ self.extid = extid
+ if values is None:
+ values = {}
+ self.values = values
+ self._schema = None
+
+ def __repr__(self):
+ return '<%s %s %s>' % (self.etype, self.extid, self.values)
+
+ def iter_rdefs(self):
+ """Yield (key, rtype, role) defined in `.values` dict, with:
+
+ * `key` is the original key in `.values` (i.e. the relation type or a 2-uple (relation type,
+ role))
+
+ * `rtype` is a yams relation type, expected to be found in the schema (attribute or
+ relation)
+
+ * `role` is the role of the entity in the relation, 'subject' or 'object'
+
+ Iteration is done on a copy of the keys so values may be inserted/deleted during it.
+ """
+ for key in list(self.values):
+ if isinstance(key, tuple):
+ rtype, role = key
+ assert role in ('subject', 'object'), key
+ yield key, rtype, role
+ else:
+ yield key, key, 'subject'
+
+ def prepare(self, schema):
+ """Prepare an external entity for later insertion:
+
+ * ensure attributes and inlined relations have a single value
+ * turn set([value]) into value and remove key associated to empty set
+ * remove non inlined relations and return them as a [(e1key, relation, e2key)] list
+
+ Return a list of non inlined relations that may be inserted later, each relations defined by
+ a 3-tuple (subject extid, relation type, object extid).
+
+ Take care the importer may call this method several times.
+ """
+ assert self._schema is None, 'prepare() has already been called for %s' % self
+ self._schema = schema
+ eschema = schema.eschema(self.etype)
+ deferred = []
+ entity_dict = self.values
+ for key, rtype, role in self.iter_rdefs():
+ rschema = schema.rschema(rtype)
+ if rschema.final or (rschema.inlined and role == 'subject'):
+ assert len(entity_dict[key]) <= 1, \
+ "more than one value for %s: %s (%s)" % (rtype, entity_dict[key], self.extid)
+ if entity_dict[key]:
+ entity_dict[rtype] = entity_dict[key].pop()
+ if key != rtype:
+ del entity_dict[key]
+ if (rschema.final and eschema.has_metadata(rtype, 'format')
+ and not rtype + '_format' in entity_dict):
+ entity_dict[rtype + '_format'] = u'text/plain'
+ else:
+ del entity_dict[key]
+ else:
+ for target_extid in entity_dict.pop(key):
+ if role == 'subject':
+ deferred.append((self.extid, rtype, target_extid))
+ else:
+ deferred.append((target_extid, rtype, self.extid))
+ return deferred
+
+ def is_ready(self, extid2eid):
+ """Return True if the ext entity is ready, i.e. has all the URIs used in inlined relations
+ currently existing.
+ """
+ assert self._schema, 'prepare() method should be called first on %s' % self
+ # as .prepare has been called, we know that .values only contains subject relation *type* as
+ # key (no more (rtype, role) tuple)
+ schema = self._schema
+ entity_dict = self.values
+ for rtype in entity_dict:
+ rschema = schema.rschema(rtype)
+ if not rschema.final:
+ # .prepare() should drop other cases from the entity dict
+ assert rschema.inlined
+ if not entity_dict[rtype] in extid2eid:
+ return False
+ # entity is ready, replace all relation's extid by eids
+ for rtype in entity_dict:
+ rschema = schema.rschema(rtype)
+ if rschema.inlined:
+ entity_dict[rtype] = extid2eid[entity_dict[rtype]]
+ return True
+
+
+class ExtEntitiesImporter(object):
+ """This class is responsible for importing externals entities, that is instances of
+ :class:`ExtEntity`, into CubicWeb entities.
+
+ :param schema: the CubicWeb's instance schema
+ :param store: a CubicWeb `Store`
+ :param extid2eid: optional {extid: eid} dictionary giving information on existing entities. It
+ will be completed during import. You may want to use :func:`cwuri2eid` to build it.
+ :param existing_relation: optional {rtype: set((subj eid, obj eid))} mapping giving information on
+ existing relations of a given type. You may want to use :class:`RelationMapping` to build it.
+ :param etypes_order_hint: optional ordered iterable on entity types, giving an hint on the order in
+ which they should be attempted to be imported
+ :param import_log: optional object implementing the :class:`SimpleImportLog` interface to record
+ events occuring during the import
+ :param raise_on_error: optional boolean flag - default to false, indicating whether errors should
+ be raised or logged. You usually want them to be raised during test but to be logged in
+ production.
+
+ Instances of this class are meant to import external entities through :meth:`import_entities`
+ which handles a stream of :class:`ExtEntity`. One may then plug arbitrary filters into the
+ external entities stream.
+
+ .. automethod:: import_entities
+
+ """
+
+ def __init__(self, schema, store, extid2eid=None, existing_relations=None,
+ etypes_order_hint=(), import_log=None, raise_on_error=False):
+ self.schema = schema
+ self.store = store
+ self.extid2eid = extid2eid if extid2eid is not None else {}
+ self.existing_relations = (existing_relations if existing_relations is not None
+ else defaultdict(set))
+ self.etypes_order_hint = etypes_order_hint
+ if import_log is None:
+ import_log = SimpleImportLog('<unspecified>')
+ self.import_log = import_log
+ self.raise_on_error = raise_on_error
+ # set of created/updated eids
+ self.created = set()
+ self.updated = set()
+
+ def import_entities(self, ext_entities):
+ """Import given external entities (:class:`ExtEntity`) stream (usually a generator)."""
+ # {etype: [etype dict]} of entities that are in the import queue
+ queue = {}
+ # order entity dictionaries then create/update them
+ deferred = self._import_entities(ext_entities, queue)
+ # create deferred relations that don't exist already
+ missing_relations = self.prepare_insert_deferred_relations(deferred)
+ self._warn_about_missing_work(queue, missing_relations)
+
+ def _import_entities(self, ext_entities, queue):
+ extid2eid = self.extid2eid
+ deferred = {} # non inlined relations that may be deferred
+ self.import_log.record_debug('importing entities')
+ for ext_entity in self.iter_ext_entities(ext_entities, deferred, queue):
+ try:
+ eid = extid2eid[ext_entity.extid]
+ except KeyError:
+ self.prepare_insert_entity(ext_entity)
+ else:
+ if ext_entity.values:
+ self.prepare_update_entity(ext_entity, eid)
+ return deferred
+
+ def iter_ext_entities(self, ext_entities, deferred, queue):
+ """Yield external entities in an order which attempts to satisfy
+ schema constraints (inlined / cardinality) and to optimize the import.
+ """
+ schema = self.schema
+ extid2eid = self.extid2eid
+ for ext_entity in ext_entities:
+ # check data in the transitional representation and prepare it for
+ # later insertion in the database
+ for subject_uri, rtype, object_uri in ext_entity.prepare(schema):
+ deferred.setdefault(rtype, set()).add((subject_uri, object_uri))
+ if not ext_entity.is_ready(extid2eid):
+ queue.setdefault(ext_entity.etype, []).append(ext_entity)
+ continue
+ yield ext_entity
+ # check for some entities in the queue that may now be ready. We'll have to restart
+ # search for ready entities until no one is generated
+ new = True
+ while new:
+ new = False
+ for etype in self.etypes_order_hint:
+ if etype in queue:
+ new_queue = []
+ for ext_entity in queue[etype]:
+ if ext_entity.is_ready(extid2eid):
+ yield ext_entity
+ # may unlock entity previously handled within this loop
+ new = True
+ else:
+ new_queue.append(ext_entity)
+ if new_queue:
+ queue[etype][:] = new_queue
+ else:
+ del queue[etype]
+
+ def prepare_insert_entity(self, ext_entity):
+ """Call the store to prepare insertion of the given external entity"""
+ eid = self.store.prepare_insert_entity(ext_entity.etype, **ext_entity.values)
+ self.extid2eid[ext_entity.extid] = eid
+ self.created.add(eid)
+ return eid
+
+ def prepare_update_entity(self, ext_entity, eid):
+ """Call the store to prepare update of the given external entity"""
+ self.store.prepare_update_entity(ext_entity.etype, eid, **ext_entity.values)
+ self.updated.add(eid)
+
+ def prepare_insert_deferred_relations(self, deferred):
+ """Call the store to insert deferred relations (not handled during insertion/update for
+ entities). Return a list of relations `[(subj ext id, obj ext id)]` that may not be inserted
+ because the target entities don't exists yet.
+ """
+ prepare_insert_relation = self.store.prepare_insert_relation
+ rschema = self.schema.rschema
+ extid2eid = self.extid2eid
+ missing_relations = []
+ for rtype, relations in deferred.items():
+ self.import_log.record_debug('importing %s %s relations' % (len(relations), rtype))
+ symmetric = rschema(rtype).symmetric
+ existing = self.existing_relations[rtype]
+ for subject_uri, object_uri in relations:
+ try:
+ subject_eid = extid2eid[subject_uri]
+ object_eid = extid2eid[object_uri]
+ except KeyError:
+ missing_relations.append((subject_uri, rtype, object_uri))
+ continue
+ if (subject_eid, object_eid) not in existing:
+ prepare_insert_relation(subject_eid, rtype, object_eid)
+ existing.add((subject_eid, object_eid))
+ if symmetric:
+ existing.add((object_eid, subject_eid))
+ return missing_relations
+
+ def _warn_about_missing_work(self, queue, missing_relations):
+ error = self.import_log.record_error
+ if queue:
+ msgs = ["can't create some entities, is there some cycle or "
+ "missing data?"]
+ for ext_entities in queue.values():
+ for ext_entity in ext_entities:
+ msgs.append(str(ext_entity))
+ map(error, msgs)
+ if self.raise_on_error:
+ raise Exception('\n'.join(msgs))
+ if missing_relations:
+ msgs = ["can't create some relations, is there missing data?"]
+ for subject_uri, rtype, object_uri in missing_relations:
+ msgs.append("%s %s %s" % (subject_uri, rtype, object_uri))
+ map(error, msgs)
+ if self.raise_on_error:
+ raise Exception('\n'.join(msgs))
+
+
+class SimpleImportLog(object):
+ """Fake CWDataImport log using a simple text format.
+
+ Useful to display logs in the UI instead of storing them to the
+ database.
+ """
+
+ def __init__(self, filename):
+ self.logs = []
+ self.filename = filename
+
+ def record_debug(self, msg, path=None, line=None):
+ self._log(logging.DEBUG, msg, path, line)
+
+ def record_info(self, msg, path=None, line=None):
+ self._log(logging.INFO, msg, path, line)
+
+ def record_warning(self, msg, path=None, line=None):
+ self._log(logging.WARNING, msg, path, line)
+
+ def record_error(self, msg, path=None, line=None):
+ self._log(logging.ERROR, msg, path, line)
+
+ def record_fatal(self, msg, path=None, line=None):
+ self._log(logging.FATAL, msg, path, line)
+
+ def _log(self, severity, msg, path, line):
+ encodedmsg = u'%s\t%s\t%s\t%s' % (severity, self.filename,
+ line or u'', msg)
+ self.logs.append(encodedmsg)
+
+
+class HTMLImportLog(SimpleImportLog):
+ """Fake CWDataImport log using a simple HTML format."""
+ def __init__(self, filename):
+ super(HTMLImportLog, self).__init__(xml_escape(filename))
+
+ def _log(self, severity, msg, path, line):
+ encodedmsg = u'%s\t%s\t%s\t%s<br/>' % (severity, self.filename,
+ line or u'', xml_escape(msg))
+ self.logs.append(encodedmsg)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/pgstore.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,447 @@
+# copyright 2003-2015 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/>.
+"""Postgres specific store"""
+
+import warnings
+import cPickle
+import os.path as osp
+from StringIO import StringIO
+from time import asctime
+from datetime import date, datetime, time
+from collections import defaultdict
+from base64 import b64encode
+
+from cubicweb.utils import make_uid
+from cubicweb.server.utils import eschema_eid
+from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.dataimport.stores import NoHookRQLObjectStore
+
+
+def _execmany_thread_not_copy_from(cu, statement, data, table=None,
+ columns=None, encoding='utf-8'):
+ """ Execute thread without copy from
+ """
+ cu.executemany(statement, data)
+
+def _execmany_thread_copy_from(cu, statement, data, table,
+ columns, encoding='utf-8'):
+ """ Execute thread with copy from
+ """
+ try:
+ buf = _create_copyfrom_buffer(data, columns, encoding=encoding)
+ except ValueError:
+ _execmany_thread_not_copy_from(cu, statement, data)
+ else:
+ if columns is None:
+ cu.copy_from(buf, table, null='NULL')
+ else:
+ cu.copy_from(buf, table, null='NULL', columns=columns)
+
+def _execmany_thread(sql_connect, statements, dump_output_dir=None,
+ support_copy_from=True, encoding='utf-8'):
+ """
+ Execute sql statement. If 'INSERT INTO', try to use 'COPY FROM' command,
+ or fallback to execute_many.
+ """
+ if support_copy_from:
+ execmany_func = _execmany_thread_copy_from
+ else:
+ execmany_func = _execmany_thread_not_copy_from
+ cnx = sql_connect()
+ cu = cnx.cursor()
+ try:
+ for statement, data in statements:
+ table = None
+ columns = None
+ try:
+ if not statement.startswith('INSERT INTO'):
+ cu.executemany(statement, data)
+ continue
+ table = statement.split()[2]
+ if isinstance(data[0], (tuple, list)):
+ columns = None
+ else:
+ columns = list(data[0])
+ execmany_func(cu, statement, data, table, columns, encoding)
+ except Exception:
+ print 'unable to copy data into table %s' % table
+ # Error in import statement, save data in dump_output_dir
+ if dump_output_dir is not None:
+ pdata = {'data': data, 'statement': statement,
+ 'time': asctime(), 'columns': columns}
+ filename = make_uid()
+ try:
+ with open(osp.join(dump_output_dir,
+ '%s.pickle' % filename), 'w') as fobj:
+ fobj.write(cPickle.dumps(pdata))
+ except IOError:
+ print 'ERROR while pickling in', dump_output_dir, filename+'.pickle'
+ pass
+ cnx.rollback()
+ raise
+ finally:
+ cnx.commit()
+ cu.close()
+
+
+def _copyfrom_buffer_convert_None(value, **opts):
+ '''Convert None value to "NULL"'''
+ return 'NULL'
+
+def _copyfrom_buffer_convert_number(value, **opts):
+ '''Convert a number into its string representation'''
+ return str(value)
+
+def _copyfrom_buffer_convert_string(value, **opts):
+ '''Convert string value.
+
+ Recognized keywords:
+ :encoding: resulting string encoding (default: utf-8)
+ '''
+ encoding = opts.get('encoding','utf-8')
+ escape_chars = ((u'\\', ur'\\'), (u'\t', u'\\t'), (u'\r', u'\\r'),
+ (u'\n', u'\\n'))
+ for char, replace in escape_chars:
+ value = value.replace(char, replace)
+ if isinstance(value, unicode):
+ value = value.encode(encoding)
+ return value
+
+def _copyfrom_buffer_convert_date(value, **opts):
+ '''Convert date into "YYYY-MM-DD"'''
+ # Do not use strftime, as it yields issue with date < 1900
+ # (http://bugs.python.org/issue1777412)
+ return '%04d-%02d-%02d' % (value.year, value.month, value.day)
+
+def _copyfrom_buffer_convert_datetime(value, **opts):
+ '''Convert date into "YYYY-MM-DD HH:MM:SS.UUUUUU"'''
+ # Do not use strftime, as it yields issue with date < 1900
+ # (http://bugs.python.org/issue1777412)
+ return '%s %s' % (_copyfrom_buffer_convert_date(value, **opts),
+ _copyfrom_buffer_convert_time(value, **opts))
+
+def _copyfrom_buffer_convert_time(value, **opts):
+ '''Convert time into "HH:MM:SS.UUUUUU"'''
+ return '%02d:%02d:%02d.%06d' % (value.hour, value.minute,
+ value.second, value.microsecond)
+
+# (types, converter) list.
+_COPYFROM_BUFFER_CONVERTERS = [
+ (type(None), _copyfrom_buffer_convert_None),
+ ((long, int, float), _copyfrom_buffer_convert_number),
+ (basestring, _copyfrom_buffer_convert_string),
+ (datetime, _copyfrom_buffer_convert_datetime),
+ (date, _copyfrom_buffer_convert_date),
+ (time, _copyfrom_buffer_convert_time),
+]
+
+def _create_copyfrom_buffer(data, columns=None, **convert_opts):
+ """
+ Create a StringIO buffer for 'COPY FROM' command.
+ Deals with Unicode, Int, Float, Date... (see ``converters``)
+
+ :data: a sequence/dict of tuples
+ :columns: list of columns to consider (default to all columns)
+ :converter_opts: keyword arguements given to converters
+ """
+ # Create a list rather than directly create a StringIO
+ # to correctly write lines separated by '\n' in a single step
+ rows = []
+ if columns is None:
+ if isinstance(data[0], (tuple, list)):
+ columns = range(len(data[0]))
+ elif isinstance(data[0], dict):
+ columns = data[0].keys()
+ else:
+ raise ValueError('Could not get columns: you must provide columns.')
+ for row in data:
+ # Iterate over the different columns and the different values
+ # and try to convert them to a correct datatype.
+ # If an error is raised, do not continue.
+ formatted_row = []
+ for col in columns:
+ try:
+ value = row[col]
+ except KeyError:
+ warnings.warn(u"Column %s is not accessible in row %s"
+ % (col, row), RuntimeWarning)
+ # XXX 'value' set to None so that the import does not end in
+ # error.
+ # Instead, the extra keys are set to NULL from the
+ # database point of view.
+ value = None
+ for types, converter in _COPYFROM_BUFFER_CONVERTERS:
+ if isinstance(value, types):
+ value = converter(value, **convert_opts)
+ break
+ else:
+ raise ValueError("Unsupported value type %s" % type(value))
+ # We push the value to the new formatted row
+ # if the value is not None and could be converted to a string.
+ formatted_row.append(value)
+ rows.append('\t'.join(formatted_row))
+ return StringIO('\n'.join(rows))
+
+
+class SQLGenObjectStore(NoHookRQLObjectStore):
+ """Controller of the data import process. This version is based
+ on direct insertions throught SQL command (COPY FROM or execute many).
+
+ >>> store = SQLGenObjectStore(cnx)
+ >>> store.create_entity('Person', ...)
+ >>> store.flush()
+ """
+
+ def __init__(self, cnx, dump_output_dir=None, nb_threads_statement=1):
+ """
+ Initialize a SQLGenObjectStore.
+
+ Parameters:
+
+ - cnx: connection on the cubicweb instance
+ - dump_output_dir: a directory to dump failed statements
+ for easier recovery. Default is None (no dump).
+ """
+ super(SQLGenObjectStore, self).__init__(cnx)
+ ### hijack default source
+ self.source = SQLGenSourceWrapper(
+ self.source, cnx.vreg.schema,
+ dump_output_dir=dump_output_dir)
+ ### XXX This is done in super().__init__(), but should be
+ ### redone here to link to the correct source
+ self.add_relation = self.source.add_relation
+ self.indexes_etypes = {}
+ if nb_threads_statement != 1:
+ warn('[3.21] SQLGenObjectStore is no longer threaded', DeprecationWarning)
+
+ def flush(self):
+ """Flush data to the database"""
+ self.source.flush()
+
+ def relate(self, subj_eid, rtype, obj_eid, **kwargs):
+ if subj_eid is None or obj_eid is None:
+ return
+ # XXX Could subjtype be inferred ?
+ self.source.add_relation(self._cnx, subj_eid, rtype, obj_eid,
+ self.rschema(rtype).inlined, **kwargs)
+ if self.rschema(rtype).symmetric:
+ self.source.add_relation(self._cnx, obj_eid, rtype, subj_eid,
+ self.rschema(rtype).inlined, **kwargs)
+
+ def drop_indexes(self, etype):
+ """Drop indexes for a given entity type"""
+ if etype not in self.indexes_etypes:
+ cu = self._cnx.cnxset.cu
+ def index_to_attr(index):
+ """turn an index name to (database) attribute name"""
+ return index.replace(etype.lower(), '').replace('idx', '').strip('_')
+ indices = [(index, index_to_attr(index))
+ for index in self.source.dbhelper.list_indices(cu, etype)
+ # Do not consider 'cw_etype_pkey' index
+ if not index.endswith('key')]
+ self.indexes_etypes[etype] = indices
+ for index, attr in self.indexes_etypes[etype]:
+ self._cnx.system_sql('DROP INDEX %s' % index)
+
+ def create_indexes(self, etype):
+ """Recreate indexes for a given entity type"""
+ for index, attr in self.indexes_etypes.get(etype, []):
+ sql = 'CREATE INDEX %s ON cw_%s(%s)' % (index, etype, attr)
+ self._cnx.system_sql(sql)
+
+
+###########################################################################
+## SQL Source #############################################################
+###########################################################################
+
+class SQLGenSourceWrapper(object):
+
+ def __init__(self, system_source, schema,
+ dump_output_dir=None):
+ self.system_source = system_source
+ # Explicitely backport attributes from system source
+ self._storage_handler = self.system_source._storage_handler
+ self.preprocess_entity = self.system_source.preprocess_entity
+ self.sqlgen = self.system_source.sqlgen
+ self.uri = self.system_source.uri
+ self.eid = self.system_source.eid
+ # Directory to write temporary files
+ self.dump_output_dir = dump_output_dir
+ # Allow to execute code with SQLite backend that does
+ # not support (yet...) copy_from
+ # XXX Should be dealt with in logilab.database
+ spcfrom = system_source.dbhelper.dbapi_module.support_copy_from
+ self.support_copy_from = spcfrom
+ self.dbencoding = system_source.dbhelper.dbencoding
+ self.init_statement_lists()
+ self._inlined_rtypes_cache = {}
+ self._fill_inlined_rtypes_cache(schema)
+ self.schema = schema
+ self.do_fti = False
+
+ def _fill_inlined_rtypes_cache(self, schema):
+ cache = self._inlined_rtypes_cache
+ for eschema in schema.entities():
+ for rschema in eschema.ordered_relations():
+ if rschema.inlined:
+ cache[eschema.type] = SQL_PREFIX + rschema.type
+
+ def init_statement_lists(self):
+ self._sql_entities = defaultdict(list)
+ self._sql_relations = {}
+ self._sql_inlined_relations = {}
+ self._sql_eids = defaultdict(list)
+ # keep track, for each eid of the corresponding data dict
+ self._sql_eid_insertdicts = {}
+
+ def flush(self):
+ print 'starting flush'
+ _entities_sql = self._sql_entities
+ _relations_sql = self._sql_relations
+ _inlined_relations_sql = self._sql_inlined_relations
+ _insertdicts = self._sql_eid_insertdicts
+ try:
+ # try, for each inlined_relation, to find if we're also creating
+ # the host entity (i.e. the subject of the relation).
+ # In that case, simply update the insert dict and remove
+ # the need to make the
+ # UPDATE statement
+ for statement, datalist in _inlined_relations_sql.iteritems():
+ new_datalist = []
+ # for a given inlined relation,
+ # browse each couple to be inserted
+ for data in datalist:
+ keys = list(data)
+ # For inlined relations, it exists only two case:
+ # (rtype, cw_eid) or (cw_eid, rtype)
+ if keys[0] == 'cw_eid':
+ rtype = keys[1]
+ else:
+ rtype = keys[0]
+ updated_eid = data['cw_eid']
+ if updated_eid in _insertdicts:
+ _insertdicts[updated_eid][rtype] = data[rtype]
+ else:
+ # could not find corresponding insert dict, keep the
+ # UPDATE query
+ new_datalist.append(data)
+ _inlined_relations_sql[statement] = new_datalist
+ _execmany_thread(self.system_source.get_connection,
+ self._sql_eids.items()
+ + _entities_sql.items()
+ + _relations_sql.items()
+ + _inlined_relations_sql.items(),
+ dump_output_dir=self.dump_output_dir,
+ support_copy_from=self.support_copy_from,
+ encoding=self.dbencoding)
+ finally:
+ _entities_sql.clear()
+ _relations_sql.clear()
+ _insertdicts.clear()
+ _inlined_relations_sql.clear()
+
+ def add_relation(self, cnx, subject, rtype, object,
+ inlined=False, **kwargs):
+ if inlined:
+ _sql = self._sql_inlined_relations
+ data = {'cw_eid': subject, SQL_PREFIX + rtype: object}
+ subjtype = kwargs.get('subjtype')
+ if subjtype is None:
+ # Try to infer it
+ targets = [t.type for t in
+ self.schema.rschema(rtype).subjects()]
+ if len(targets) == 1:
+ subjtype = targets[0]
+ else:
+ raise ValueError('You should give the subject etype for '
+ 'inlined relation %s'
+ ', as it cannot be inferred: '
+ 'this type is given as keyword argument '
+ '``subjtype``'% rtype)
+ statement = self.sqlgen.update(SQL_PREFIX + subjtype,
+ data, ['cw_eid'])
+ else:
+ _sql = self._sql_relations
+ data = {'eid_from': subject, 'eid_to': object}
+ statement = self.sqlgen.insert('%s_relation' % rtype, data)
+ if statement in _sql:
+ _sql[statement].append(data)
+ else:
+ _sql[statement] = [data]
+
+ def add_entity(self, cnx, entity):
+ with self._storage_handler(cnx, entity, 'added'):
+ attrs = self.preprocess_entity(entity)
+ rtypes = self._inlined_rtypes_cache.get(entity.cw_etype, ())
+ if isinstance(rtypes, str):
+ rtypes = (rtypes,)
+ for rtype in rtypes:
+ if rtype not in attrs:
+ attrs[rtype] = None
+ sql = self.sqlgen.insert(SQL_PREFIX + entity.cw_etype, attrs)
+ self._sql_eid_insertdicts[entity.eid] = attrs
+ self._append_to_entities(sql, attrs)
+
+ def _append_to_entities(self, sql, attrs):
+ self._sql_entities[sql].append(attrs)
+
+ def _handle_insert_entity_sql(self, cnx, sql, attrs):
+ # We have to overwrite the source given in parameters
+ # as here, we directly use the system source
+ attrs['asource'] = self.system_source.uri
+ self._sql_eids[sql].append(attrs)
+
+ def _handle_is_relation_sql(self, cnx, sql, attrs):
+ self._append_to_entities(sql, attrs)
+
+ def _handle_is_instance_of_sql(self, cnx, sql, attrs):
+ self._append_to_entities(sql, attrs)
+
+ def _handle_source_relation_sql(self, cnx, sql, attrs):
+ self._append_to_entities(sql, attrs)
+
+ # add_info is _copypasted_ from the one in NativeSQLSource. We want it
+ # there because it will use the _handlers of the SQLGenSourceWrapper, which
+ # are not like the ones in the native source.
+ def add_info(self, cnx, entity, source, extid):
+ """add type and source info for an eid into the system table"""
+ # begin by inserting eid/type/source/extid into the entities table
+ if extid is not None:
+ assert isinstance(extid, str)
+ extid = b64encode(extid)
+ attrs = {'type': entity.cw_etype, 'eid': entity.eid, 'extid': extid,
+ 'asource': source.uri}
+ self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
+ # insert core relations: is, is_instance_of and cw_source
+ try:
+ self._handle_is_relation_sql(cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)',
+ (entity.eid, eschema_eid(cnx, entity.e_schema)))
+ except IndexError:
+ # during schema serialization, skip
+ pass
+ else:
+ for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
+ self._handle_is_relation_sql(cnx,
+ 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)',
+ (entity.eid, eschema_eid(cnx, eschema)))
+ if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10
+ self._handle_is_relation_sql(cnx, '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.cw_etype):
+ self.index_entity(cnx, entity=entity)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/stores.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,323 @@
+# copyright 2003-2015 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/>.
+"""
+Stores are responsible to insert properly formatted entities and relations into the database. They
+have the following API::
+
+ >>> user_eid = store.prepare_insert_entity('CWUser', login=u'johndoe')
+ >>> group_eid = store.prepare_insert_entity('CWUser', name=u'unknown')
+ >>> store.relate(user_eid, 'in_group', group_eid)
+ >>> store.flush()
+ >>> store.commit()
+ >>> store.finish()
+
+Some store **requires a flush** to copy data in the database, so if you want to have store
+independant code you should explicitly call it. (There may be multiple flushes during the
+process, or only one at the end if there is no memory issue). This is different from the
+commit which validates the database transaction. At last, the `finish()` method should be called in
+case the store requires additional work once everything is done.
+
+* ``prepare_insert_entity(<entity type>, **kwargs) -> eid``: given an entity
+ type, attributes and inlined relations, return the eid of the entity to be
+ inserted, *with no guarantee that anything has been inserted in database*,
+
+* ``prepare_update_entity(<entity type>, eid, **kwargs) -> None``: given an
+ entity type and eid, promise for update given attributes and inlined
+ relations *with no guarantee that anything has been inserted in database*,
+
+* ``prepare_insert_relation(eid_from, rtype, eid_to) -> None``: indicate that a
+ relation ``rtype`` should be added between entities with eids ``eid_from``
+ and ``eid_to``. Similar to ``prepare_insert_entity()``, *there is no
+ guarantee that the relation will be inserted in database*,
+
+* ``flush() -> None``: flush any temporary data to database. May be called
+ several times during an import,
+
+* ``commit() -> None``: commit the database transaction,
+
+* ``finish() -> None``: additional stuff to do after import is terminated.
+
+.. autoclass:: cubicweb.dataimport.stores.RQLObjectStore
+.. autoclass:: cubicweb.dataimport.stores.NoHookRQLObjectStore
+.. autoclass:: cubicweb.dataimport.stores.MetaGenerator
+"""
+import inspect
+import warnings
+from datetime import datetime
+from copy import copy
+
+from logilab.common.deprecation import deprecated
+from logilab.common.decorators import cached
+
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES
+from cubicweb.server.edition import EditedEntity
+
+
+class RQLObjectStore(object):
+ """Store that works by making RQL queries, hence with all the cubicweb's machinery activated.
+ """
+
+ def __init__(self, cnx, commit=None):
+ if commit is not None:
+ warnings.warn('[3.19] commit argument should not be specified '
+ 'as the cnx object already provides it.',
+ DeprecationWarning, stacklevel=2)
+ self._cnx = cnx
+ self._commit = commit or cnx.commit
+ # XXX 3.21 deprecated attributes
+ self.eids = {}
+ self.types = {}
+
+ def rql(self, *args):
+ """Execute a RQL query. This is NOT part of the store API."""
+ return self._cnx.execute(*args)
+
+ def prepare_insert_entity(self, *args, **kwargs):
+ """Given an entity type, attributes and inlined relations, returns the inserted entity's
+ eid.
+ """
+ entity = self._cnx.create_entity(*args, **kwargs)
+ self.eids[entity.eid] = entity
+ self.types.setdefault(args[0], []).append(entity.eid)
+ return entity.eid
+
+ def prepare_update_entity(self, etype, eid, **kwargs):
+ """Given an entity type and eid, updates the corresponding entity with specified attributes
+ and inlined relations.
+ """
+ entity = self._cnx.entity_from_eid(eid)
+ assert entity.cw_etype == etype, 'Trying to update with wrong type {}'.format(etype)
+ # XXX some inlined relations may already exists
+ entity.cw_set(**kwargs)
+
+ def prepare_insert_relation(self, eid_from, rtype, eid_to, **kwargs):
+ """Insert into the database a relation ``rtype`` between entities with eids ``eid_from``
+ and ``eid_to``.
+ """
+ self.rql('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+ {'x': int(eid_from), 'y': int(eid_to)})
+
+ def flush(self):
+ """Nothing to flush for this store."""
+ pass
+
+ def commit(self):
+ """Commit the database transaction."""
+ return self._commit()
+
+ @property
+ def session(self):
+ warnings.warn('[3.19] deprecated property.', DeprecationWarning, stacklevel=2)
+ return self._cnx.repo._get_session(self._cnx.sessionid)
+
+ @deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
+ def find_entities(self, *args, **kwargs):
+ return self._cnx.find(*args, **kwargs).entities()
+
+ @deprecated("[3.19] use cnx.find(*args, **kwargs).one() instead")
+ def find_one_entity(self, *args, **kwargs):
+ return self._cnx.find(*args, **kwargs).one()
+
+ @deprecated('[3.21] use prepare_insert_entity instead')
+ def create_entity(self, *args, **kwargs):
+ eid = self.prepare_insert_entity(*args, **kwargs)
+ return self._cnx.entity_from_eid(eid)
+
+ @deprecated('[3.21] use prepare_insert_relation instead')
+ def relate(self, eid_from, rtype, eid_to, **kwargs):
+ self.prepare_insert_relation(eid_from, rtype, eid_to, **kwargs)
+
+
+class NoHookRQLObjectStore(RQLObjectStore):
+ """Store that works by accessing low-level CubicWeb's source API, with all hooks deactivated. It
+ must be given a metadata generator object to handle metadata which are usually handled by hooks
+ (see :class:`MetaGenerator`).
+ """
+
+ def __init__(self, cnx, metagen=None):
+ super(NoHookRQLObjectStore, self).__init__(cnx)
+ self.source = cnx.repo.system_source
+ self.rschema = cnx.repo.schema.rschema
+ self.add_relation = self.source.add_relation
+ if metagen is None:
+ metagen = MetaGenerator(cnx)
+ self.metagen = metagen
+ self._nb_inserted_entities = 0
+ self._nb_inserted_types = 0
+ self._nb_inserted_relations = 0
+ # deactivate security
+ cnx.read_security = False
+ cnx.write_security = False
+
+ def prepare_insert_entity(self, etype, **kwargs):
+ """Given an entity type, attributes and inlined relations, returns the inserted entity's
+ eid.
+ """
+ for k, v in kwargs.iteritems():
+ kwargs[k] = getattr(v, 'eid', v)
+ entity, rels = self.metagen.base_etype_dicts(etype)
+ # make a copy to keep cached entity pristine
+ entity = copy(entity)
+ entity.cw_edited = copy(entity.cw_edited)
+ entity.cw_clear_relation_cache()
+ entity.cw_edited.update(kwargs, skipsec=False)
+ entity_source, extid = self.metagen.init_entity(entity)
+ cnx = self._cnx
+ self.source.add_info(cnx, entity, entity_source, extid)
+ self.source.add_entity(cnx, entity)
+ kwargs = dict()
+ if inspect.getargspec(self.add_relation).keywords:
+ kwargs['subjtype'] = entity.cw_etype
+ for rtype, targeteids in rels.iteritems():
+ # targeteids may be a single eid or a list of eids
+ inlined = self.rschema(rtype).inlined
+ try:
+ for targeteid in targeteids:
+ self.add_relation(cnx, entity.eid, rtype, targeteid,
+ inlined, **kwargs)
+ except TypeError:
+ self.add_relation(cnx, entity.eid, rtype, targeteids,
+ inlined, **kwargs)
+ self._nb_inserted_entities += 1
+ return entity.eid
+
+ # XXX: prepare_update_entity is inherited from RQLObjectStore, it should be reimplemented to
+ # actually skip hooks as prepare_insert_entity
+
+ def prepare_insert_relation(self, eid_from, rtype, eid_to, **kwargs):
+ """Insert into the database a relation ``rtype`` between entities with eids ``eid_from``
+ and ``eid_to``.
+ """
+ assert not rtype.startswith('reverse_')
+ self.add_relation(self._cnx, eid_from, rtype, eid_to,
+ self.rschema(rtype).inlined)
+ if self.rschema(rtype).symmetric:
+ self.add_relation(self._cnx, eid_to, rtype, eid_from,
+ self.rschema(rtype).inlined)
+ self._nb_inserted_relations += 1
+
+ @property
+ @deprecated('[3.21] deprecated')
+ def nb_inserted_entities(self):
+ return self._nb_inserted_entities
+
+ @property
+ @deprecated('[3.21] deprecated')
+ def nb_inserted_types(self):
+ return self._nb_inserted_types
+
+ @property
+ @deprecated('[3.21] deprecated')
+ def nb_inserted_relations(self):
+ return self._nb_inserted_relations
+
+
+class MetaGenerator(object):
+ """Class responsible for generating standard metadata for imported entities. You may want to
+ derive it to add application specific's metadata.
+
+ Parameters:
+ * `cnx`: connection to the repository
+ * `baseurl`: optional base URL to be used for `cwuri` generation - default to config['base-url']
+ * `source`: optional source to be used as `cw_source` for imported entities
+ """
+ META_RELATIONS = (META_RTYPES
+ - VIRTUAL_RTYPES
+ - set(('eid', 'cwuri',
+ 'is', 'is_instance_of', 'cw_source')))
+
+ def __init__(self, cnx, baseurl=None, source=None):
+ self._cnx = cnx
+ if baseurl is None:
+ config = cnx.vreg.config
+ baseurl = config['base-url'] or config.default_base_url()
+ if not baseurl[-1] == '/':
+ baseurl += '/'
+ self.baseurl = baseurl
+ if source is None:
+ source = cnx.repo.system_source
+ self.source = source
+ self.create_eid = cnx.repo.system_source.create_eid
+ self.time = datetime.now()
+ # attributes/relations shared by all entities of the same type
+ self.etype_attrs = []
+ self.etype_rels = []
+ # attributes/relations specific to each entity
+ self.entity_attrs = ['cwuri']
+ #self.entity_rels = [] XXX not handled (YAGNI?)
+ schema = cnx.vreg.schema
+ rschema = schema.rschema
+ for rtype in self.META_RELATIONS:
+ # skip owned_by / created_by if user is the internal manager
+ if cnx.user.eid == -1 and rtype in ('owned_by', 'created_by'):
+ continue
+ if rschema(rtype).final:
+ self.etype_attrs.append(rtype)
+ else:
+ self.etype_rels.append(rtype)
+
+ @cached
+ def base_etype_dicts(self, etype):
+ entity = self._cnx.vreg['etypes'].etype_class(etype)(self._cnx)
+ # entity are "surface" copied, avoid shared dict between copies
+ del entity.cw_extra_kwargs
+ entity.cw_edited = EditedEntity(entity)
+ for attr in self.etype_attrs:
+ genfunc = self.generate(attr)
+ if genfunc:
+ entity.cw_edited.edited_attribute(attr, genfunc(entity))
+ rels = {}
+ for rel in self.etype_rels:
+ genfunc = self.generate(rel)
+ if genfunc:
+ rels[rel] = genfunc(entity)
+ return entity, rels
+
+ def init_entity(self, entity):
+ entity.eid = self.create_eid(self._cnx)
+ extid = entity.cw_edited.get('cwuri')
+ for attr in self.entity_attrs:
+ if attr in entity.cw_edited:
+ # already set, skip this attribute
+ continue
+ genfunc = self.generate(attr)
+ if genfunc:
+ entity.cw_edited.edited_attribute(attr, genfunc(entity))
+ if isinstance(extid, unicode):
+ extid = extid.encode('utf-8')
+ return self.source, extid
+
+ def generate(self, rtype):
+ return getattr(self, 'gen_%s' % rtype, None)
+
+ def gen_cwuri(self, entity):
+ assert self.baseurl, 'baseurl is None while generating cwuri'
+ return u'%s%s' % (self.baseurl, entity.eid)
+
+ def gen_creation_date(self, entity):
+ return self.time
+
+ def gen_modification_date(self, entity):
+ return self.time
+
+ def gen_created_by(self, entity):
+ return self._cnx.user.eid
+
+ def gen_owned_by(self, entity):
+ return self._cnx.user.eid
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/data-massimport/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,154 @@
+# copyright 2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact@logilab.fr
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""cubicweb-geonames schema"""
+
+from yams.buildobjs import (EntityType, RelationDefinition, SubjectRelation,
+ String, Int, BigInt, Float, Date)
+from cubicweb.schemas.base import ExternalUri
+
+"""
+See geonames readme.txt for more details.
+"""
+
+class TestLocation(EntityType):
+ """
+ Entity type for location of Geonames.
+ See cities1000.zip, cities5000.zip, cities15000.zip and allCountries.txt
+ """
+ name = String(maxsize=1024, indexed=True, fulltextindexed=True)
+ geonameid = Int(required=True, unique=True, indexed=True)
+
+class Location(EntityType):
+ """
+ Entity type for location of Geonames.
+ See cities1000.zip, cities5000.zip, cities15000.zip and allCountries.txt
+ """
+ name = String(maxsize=1024, indexed=True, fulltextindexed=True)
+ geonameid = Int(indexed=True)
+ asciiname = String(maxsize=200, fulltextindexed=True)
+ alternatenames = String(fulltextindexed=True)
+ names = SubjectRelation('LocationName', cardinality='**')
+ latitude = Float(indexed=True)
+ longitude = Float(indexed=True)
+ feature_class = String(maxsize=1, indexed=True)
+ feature_code = SubjectRelation('FeatureCode', cardinality='?*', inlined=True)
+ country = SubjectRelation('Country', cardinality='?*', inlined=True)
+ alternate_country_code = String(maxsize=60)
+ main_administrative_region = SubjectRelation('AdministrativeRegion', cardinality='?*', inlined=True)
+ second_administrative_region = SubjectRelation('AdministrativeRegion', cardinality='?*', inlined=True)
+ admin_code_1 = String(maxsize=124)
+ admin_code_2 = String(maxsize=124)
+ admin_code_3 = String(maxsize=20)
+ admin_code_4 = String(maxsize=20)
+ population = BigInt(indexed=True)
+ elevation = Int(indexed=True)
+ gtopo30 = Int(indexed=True)
+ timezone = SubjectRelation('TimeZone', cardinality='?*', inlined=True)
+ geonames_date = Date()
+
+class LocationName(EntityType):
+ """
+ Name of a Location
+ """
+ name = String(maxsize=1024, indexed=True, fulltextindexed=True)
+ language = SubjectRelation('Language', cardinality='?*', inlined=True)
+ alternatenamesid = Int(indexed=True)
+
+class FeatureCode(EntityType):
+ """
+ Entity type for feature codes of Geonames.
+ See featureCodes_en.txt
+ """
+ name = String(maxsize=1024, indexed=True, fulltextindexed=True)
+ main_code = String(maxsize=1, indexed=True)
+ code = String(maxsize=12)
+ description = String(maxsize=1024, fulltextindexed=True)
+
+class AdministrativeRegion(EntityType):
+ """
+ Entity type for administrative regions of Geonames.
+ See admin1CodesASCII.txt and admin2Codes.txt
+ """
+ name = String(maxsize=1024, indexed=True, fulltextindexed=True)
+ code = String(maxsize=64, indexed=True)
+ country = SubjectRelation('Country', cardinality='?*', inlined=True)
+ geonameid = Int(indexed=True)
+ asciiname = String(maxsize=200, fulltextindexed=True)
+
+class Language(EntityType):
+ """
+ Entity type for languages of Geonames.
+ See admin1CodesASCII.txt and admin2Codes.txt
+ """
+ name = String(maxsize=1024, indexed=True, fulltextindexed=True)
+ iso_639_3 = String(maxsize=3, indexed=True)
+ iso_639_2 = String(maxsize=64, indexed=True)
+ iso_639_1 = String(maxsize=3, indexed=True)
+
+class Continent(EntityType):
+ """
+ Entity type for continents of geonames.
+ """
+ name = String(maxsize=1024, indexed=True, fulltextindexed=True)
+ code = String(maxsize=2, indexed=True)
+ geonameid = Int(indexed=True)
+
+class Country(EntityType):
+ """
+ Entity type for countries of geonames.
+ See countryInfo.txt
+ """
+ name = String(maxsize=1024, indexed=True, fulltextindexed=True)
+ code = String(maxsize=2, indexed=True)
+ code3 = String(maxsize=3, indexed=True)
+ codenum = Int(indexed=True)
+ fips = String(maxsize=2)
+ capital = String(maxsize=1024, fulltextindexed=True)
+ area = Float(indexed=True)
+ population = BigInt(indexed=True)
+ continent_code = String(maxsize=3)
+ continent = SubjectRelation('Continent', cardinality='?*', inlined=True)
+ tld = String(maxsize=64)
+ currency = String(maxsize=1024, fulltextindexed=True)
+ currency_code = String(maxsize=64)
+ geonameid = Int(indexed=True)
+ phone = String(maxsize=64)
+ postal_code = String(maxsize=200)
+ postal_code_regex = String(maxsize=200)
+ languages_code = String(maxsize=200)
+ neighbours_code = String(maxsize=200)
+ equivalent_fips = String(maxsize=2)
+
+class TimeZone(EntityType):
+ """
+ Entity type for timezone of geonames.
+ See timeZones.txt
+ """
+ code = String(maxsize=1024, indexed=True)
+ gmt = Float()
+ dst = Float()
+ raw_offset = Float()
+
+class used_language(RelationDefinition):
+ subject = 'Country'
+ object = 'Language'
+ cardinality = '**'
+
+class neighbour_of(RelationDefinition):
+ subject = 'Country'
+ object = 'Country'
+ cardinality = '**'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/data/geonames.csv Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,4000 @@
+3038814 Costa de Xurius Costa de Xurius 42.5 1.48333 T SLP AD 00 0 1316 Europe/Andorra 1993-12-23
+3038815 Font de la Xona Font de la Xona 42.55003 1.44986 H SPNG AD 04 0 2082 Europe/Andorra 2010-01-11
+3038816 Xixerella Xixerella 42.55327 1.48736 P PPL AD 04 0 1520 Europe/Andorra 2009-04-24
+3038817 Xixerella Xixerella Xixerella 42.55 1.48333 A ADMD AD 00 0 1548 Europe/Andorra 2011-11-05
+3038818 Riu Xic Riu Xic 42.56667 1.68333 H STM AD 00 0 2340 Europe/Andorra 1993-12-23
+3038819 Pas del Xic Pas del Xic 42.5 1.58333 R TRL AD 00 0 1888 Europe/Andorra 1993-12-23
+3038820 Roc del Xeig Roc del Xeig 42.56667 1.48333 T RK AD 00 0 1508 Europe/Andorra 1993-12-23
+3038821 Collada del Xeig Collada del Xeig 42.56667 1.48333 T PK AD 00 0 1508 Europe/Andorra 1993-12-23
+3038822 Fonts Vives Fonts Vives 42.5 1.56667 H SPNG AD 00 0 1776 Europe/Andorra 1993-12-23
+3038823 Roc de Vista Roc de Vista 42.5 1.48333 T RK AD 00 0 1316 Europe/Andorra 1993-12-23
+3038824 Obaga de Vista Obaga de Vista 42.48333 1.45 T SLP AD 00 0 1195 Europe/Andorra 1993-12-23
+3038825 Coll de Vista Coll de Vista 42.46667 1.58333 T SPUR AD 00 0 2367 Europe/Andorra 1993-12-23
+3038826 Coll de Vista Coll de Vista Coll de Vista 42.48333 1.43333 T PASS AD 00 0 1938 Europe/Andorra 2011-11-05
+3038827 Visanseny Visanseny Visanceny,Visanseny 42.56667 1.61667 L LCTY AD AD 00 0 1920 Europe/Andorra 2011-11-05
+3038828 Roc de la Vinya Roc de la Vinya 42.53333 1.56667 T RK AD 00 0 1418 Europe/Andorra 1993-12-23
+3038829 Canal de la Vinya Canal de la Vinya 42.51667 1.51667 H STM AD 00 0 1265 Europe/Andorra 1993-12-23
+3038830 Bosc de Villar Bosc de Villar 42.6 1.55 V FRST AD 00 0 2298 Europe/Andorra 1993-12-23
+3038831 Torrent de Vila Torrent de Vila 42.53333 1.56667 H STM AD 00 0 1418 Europe/Andorra 1993-12-23
+3038832 Vila Vila Casas Vila,Vila 42.53222 1.57392 P PPL AD 03 0 1418 Europe/Andorra 2011-11-05
+3038833 Basers de Vicenç Basers de Vicenc 42.48333 1.48333 T CLF AD 00 0 981 Europe/Andorra 1993-12-23
+3038834 Pla de Viàs Pla de Vias 42.58333 1.66667 T UPLD AD 00 0 2159 Europe/Andorra 1993-12-23
+3038835 Vial del Cardaire Vial del Cardaire 42.56667 1.5 L LCTY AD 00 0 1636 Europe/Andorra 1993-12-23
+3038836 Font del Vi Font del Vi 42.51667 1.48333 H SPNG AD 00 0 1839 Europe/Andorra 1993-12-23
+3038837 Font Verda Font Verda 42.55 1.56667 H SPNG AD 00 0 1828 Europe/Andorra 1993-12-23
+3038838 Costa Verda Costa Verda 42.48333 1.66667 T SLP AD 00 0 2524 Europe/Andorra 1993-12-23
+3038839 Costa Verda Costa Verda 42.48333 1.56667 T SLP AD 00 0 2231 Europe/Andorra 1993-12-23
+3038840 Serrat de Ventader Serrat de Ventader 42.48333 1.43333 T MT AD 00 0 1938 Europe/Andorra 1993-12-23
+3038841 Bony de Vellatocina Bony de Vellatocina 42.61667 1.5 T PK AD 00 0 2390 Europe/Andorra 1993-12-23
+3038842 Pleta Vella Pleta Vella 42.51667 1.61667 L GRAZ AD 00 0 2254 Europe/Andorra 1993-12-23
+3038843 Solà Vell Sola Vell 42.46667 1.48333 T SLP AD 00 0 1134 Europe/Andorra 1993-12-23
+3038844 Port Vell Port Vell 42.65 1.55 T PASS AD 00 0 2181 Europe/Andorra 1993-12-23
+3038845 Port Negre del Pallars Port Negre del Pallars Port Negre del Pallars,Port Vell 42.56667 1.45 T PASS AD 00 0 2137 Europe/Andorra 2011-11-05
+3038846 Bancal Vedeller Bancal Vedeller 42.6 1.46667 R TRL AD 00 0 2421 Europe/Andorra 1993-12-23
+3038847 Vedat del Xeig Vedat del Xeig 42.56667 1.48333 L LCTY AD 00 0 1508 Europe/Andorra 1993-12-23
+3038848 Vedat dels Plans Vedat dels Plans 42.53333 1.5 A ADMD AD 00 0 1357 Europe/Andorra 1993-12-23
+3038849 Vedat de Coll de les Cases Vedat de Coll de les Cases 42.56667 1.5 L LCTY AD 00 0 1636 Europe/Andorra 1993-12-23
+3038850 Serra del Vedat Serra del Vedat 42.46667 1.48333 T RDGE AD 00 0 1134 Europe/Andorra 1993-12-23
+3038851 Planades del Vedat Planades del Vedat 42.53333 1.5 T UPLD AD 00 0 1357 Europe/Andorra 1993-12-23
+3038852 Veda de Sorteny Veda de Sorteny 42.61667 1.55 L LCTY AD 00 0 2007 Europe/Andorra 1993-12-23
+3038853 Veda del Castellar Veda del Castellar 42.63333 1.51667 A ADMD AD 00 0 1894 Europe/Andorra 1993-12-23
+3038854 Canal de les Veces Canal de les Veces 42.51667 1.51667 H STM AD 00 0 1265 Europe/Andorra 1993-12-23
+3038855 Coma de Varilles Coma de Varilles 42.63333 1.53333 T VAL AD 00 0 2072 Europe/Andorra 1993-12-23
+3038856 Portella de les Vaques Portella de les Vaques 42.56667 1.45 T PASS AD 00 0 2137 Europe/Andorra 1993-12-23
+3038857 Pas de les Vaques Pas de les Vaques 42.58333 1.7 T PASS AD 00 0 2584 Europe/Andorra 1993-12-23
+3038858 Collada de les Vaques Collada de les Vaques 42.56667 1.56667 T PASS AD 00 0 2089 Europe/Andorra 1993-12-23
+3038859 Roc del Vaquer Roc del Vaquer 42.63333 1.48333 T CLF AD 00 0 2283 Europe/Andorra 1993-12-23
+3038860 Valls de la Coma Valls de la Coma 42.6 1.63333 L LCTY AD 00 0 1893 Europe/Andorra 1993-12-23
+3038861 Vall d'Incles Vall d'Incles 42.58386 1.66331 L LCTY AD 02 0 1867 Europe/Andorra 2010-01-11
+3038862 Vall de Soldeu Vall de Soldeu 42.55 1.68333 L LCTY AD 00 0 2254 Europe/Andorra 1993-12-23
+3038863 Riu de la Vall del Riu Riu de la Vall del Riu 42.56667 1.61667 H STM AD 00 0 1920 Europe/Andorra 1993-12-23
+3038864 Estany Gran de la Vall del Riu Estany Gran de la Vall del Riu 42.601 1.59368 H LK AD 00 0 2467 Europe/Andorra 2011-04-19
+3038865 Cascada de la Vall del Riu Cascada de la Vall del Riu 42.56667 1.61667 H FLLS AD 00 0 1920 Europe/Andorra 1993-12-23
+3038866 Canals de la Vall del Riu Canals de la Vall del Riu 42.58333 1.6 H RVN AD 00 0 1828 Europe/Andorra 1993-12-23
+3038867 Camà de la Vall del Riu Cami de la Vall del Riu 42.58333 1.61667 R TRL AD 00 0 1707 Europe/Andorra 1993-12-23
+3038868 Bosc de la Vall del Riu Bosc de la Vall del Riu 42.58333 1.61667 V FRST AD 00 0 1707 Europe/Andorra 1993-12-23
+3038869 Vall del Riu Vall del Riu 42.6 1.6 A ADMD AD 00 0 2143 Europe/Andorra 1993-12-23
+3038870 Coll de Vall Civera Coll de Vall Civera Coll de Valcibera,Coll de Vall Civera,Collado de Vall-Civera,Port de Vall Civera 42.48333 1.66667 T PASS AD 00 0 2524 Europe/Andorra 2011-11-05
+3038871 Obac de la Vall Obac de la Vall 42.46667 1.5 T SLP AD 00 0 1383 Europe/Andorra 1993-12-23
+3038872 Riu Valira d’Orient Riu Valira d'Orient Rio Balira del Orien,Riu Valira d'Orient,Riu Valira d’Orient,RÃo Balira del Orien 42.51667 1.53333 H STM AD 00 0 1460 Europe/Andorra 2011-11-05
+3038873 Riu Valira del Nord Riu Valira del Nord Riu Valira d'Ordino,Riu Valira del Nord,Riu Valira del Nort,Riu Valira d’Ordino,Valira del Nord,Valira del Norte 42.34645 1.44271 H STM AD 00 0 682 Europe/Andorra 2011-11-05
+3038874 Estanys de la Val del Riu Estanys de la Val del Riu 42.60138 1.59418 H LKS AD 00 0 2467 Europe/Andorra 2011-04-19
+3038875 Vaca Morta Vaca Morta 42.56667 1.76667 L LCTY AD 00 0 1674 Europe/Andorra 1993-12-23
+3038876 Riu d’ Urina Riu d' Urina 42.56667 1.58333 H STM AD 00 0 1919 Europe/Andorra 1993-12-23
+3038877 Serrat de la Uïna Serrat de la Uina 42.56667 1.51667 T RDGE AD 00 0 1500 Europe/Andorra 1993-12-23
+3038878 Obaga de la Tuta Obaga de la Tuta 42.48333 1.45 T SLP AD 00 0 1195 Europe/Andorra 1993-12-23
+3038879 Cova de la Tuta Cova de la Tuta 42.48333 1.45 S CAVE AD 00 0 1195 Europe/Andorra 1993-12-23
+3038880 Roca del Tut Roca del Tut 42.55 1.46667 T RK AD 00 0 1585 Europe/Andorra 1993-12-23
+3038881 Coll de Turer Coll de Turer 42.56667 1.46667 T PK AD 00 0 1673 Europe/Andorra 1993-12-23
+3038882 Font del Tudó Font del Tudo 42.43333 1.53333 H SPNG AD 00 0 2108 Europe/Andorra 1993-12-23
+3038883 Estany de les Truites Estany de les Truites 42.58333 1.45 H LK AD 00 0 2156 Europe/Andorra 1993-12-23
+3038884 Serra de Tristaina Serra de Tristaina Serra de Tristaina 42.65 1.48333 T RDGE AD 00 0 2341 Europe/Andorra 2011-11-05
+3038885 Riu de Tristaina Riu de Tristaina 42.61667 1.55 H STM AD 00 0 2007 Europe/Andorra 1993-12-23
+3038886 Pic de Tristaina Pic de Tristaina Pic de Triatagne,Pic de Tristagne,Pic de Tristaina,Pic des Tri-Stagnes,Tristaina 42.65265 1.49242 T PK AD AD,FR 00 0 2384 Europe/Andorra 2007-04-29
+3038887 Clot de Tres Torrents Clot de Tres Torrents 42.53333 1.53333 H RVN AD 00 0 1521 Europe/Andorra 1993-12-23
+3038888 Bony de Tres Corts Bony de Tres Corts 42.53333 1.53333 T SPUR AD 00 0 1521 Europe/Andorra 1993-12-23
+3038889 Canal dels Trèmols Canal dels Tremols 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3038890 Travenc Travenc 42.6 1.68333 L LCTY AD 00 0 2089 Europe/Andorra 1993-12-23
+3038891 Pleta de la Trava Pleta de la Trava 42.48333 1.61667 L GRAZ AD 00 0 2217 Europe/Andorra 1993-12-23
+3038892 Coll de la Trava Coll de la Trava 42.48333 1.46667 T SPUR AD 00 0 1148 Europe/Andorra 1993-12-23
+3038893 Canal de la Trava Canal de la Trava 42.56667 1.53333 H RVN AD 00 0 1669 Europe/Andorra 1993-12-23
+3038894 Bosc de la Trava Bosc de la Trava 42.48333 1.63333 V FRST AD 00 0 2296 Europe/Andorra 1993-12-23
+3038895 Font dels Traginers Font dels Traginers 42.43333 1.51667 H SPNG AD 00 0 2031 Europe/Andorra 1993-12-23
+3038896 Pla de les Toves Pla de les Toves 42.46667 1.45 T UPLD AD 00 0 1562 Europe/Andorra 1993-12-23
+3038897 Torrent del Tossalroi Torrent del Tossalroi 42.46667 1.51667 H STM AD 00 0 1985 Europe/Andorra 1993-12-23
+3038898 Tossal Gran Tossal Gran 42.48333 1.48333 L LCTY AD 00 0 981 Europe/Andorra 1993-12-23
+3038899 Tossalet i Vinyals Tossalet i Vinyals 42.48333 1.48333 L LCTY AD 00 0 981 Europe/Andorra 1993-12-23
+3038900 Bosc del Tossal de les Poselles Bosc del Tossal de les Poselles 42.53333 1.5 V FRST AD 00 0 1357 Europe/Andorra 1993-12-23
+3038901 Tossal Tossal 42.46667 1.48333 L LCTY AD 00 0 1134 Europe/Andorra 1993-12-23
+3038902 Tosquers Tosquers 42.56667 1.48333 T SLP AD 00 0 1508 Europe/Andorra 1993-12-23
+3038903 Canal del Tosquer Canal del Tosquer 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3038904 Bosc del Tosquer Bosc del Tosquer 42.56667 1.53333 V FRST AD 00 0 1669 Europe/Andorra 1993-12-23
+3038905 Bosc del Tosquer Bosc del Tosquer 42.53333 1.6 V FRST AD 00 0 1888 Europe/Andorra 1993-12-23
+3038906 Bosc de la Tosca Bosc de la Tosca 42.43333 1.45 V FRST AD 00 0 877 Europe/Andorra 1993-12-23
+3038907 Tosa d’Incles Tosa d'Incles 42.58333 1.68333 A ADMD AD 00 0 2294 Europe/Andorra 1993-12-23
+3038908 Cap de Tosa d’Entor Cap de Tosa d'Entor 42.6 1.65 T PK AD 00 0 2131 Europe/Andorra 1993-12-23
+3038909 Tosa de les Mussoles Tosa de les Mussoles 42.61667 1.68333 L LCTY AD 00 0 2406 Europe/Andorra 1993-12-23
+3038910 Pic de la Tosa de Juclar Pic de la Tosa de Juclar 42.6 1.71667 T PK AD 00 0 2516 Europe/Andorra 1993-12-23
+3038911 Collada de la Tosa de Caraup Collada de la Tosa de Caraup 42.6 1.65 T SPUR AD 00 0 2131 Europe/Andorra 1993-12-23
+3038912 Planell de la Tosa Planell de la Tosa 42.53333 1.45 T UPLD AD 00 0 2130 Europe/Andorra 1993-12-23
+3038913 Canals de la Tosa Canals de la Tosa 42.58333 1.7 H RVN AD 00 0 2584 Europe/Andorra 1993-12-23
+3038914 Canal de la Tosa Canal de la Tosa 42.5 1.56667 H STM AD 00 0 1776 Europe/Andorra 1993-12-23
+3038915 Bosc de la Tosa Bosc de la Tosa 42.55 1.58333 V FRST AD 00 0 1499 Europe/Andorra 1993-12-23
+3038916 Canal Torta Canal Torta 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3038917 Canal Torta Canal Torta 42.53333 1.53333 H STM AD 00 0 1521 Europe/Andorra 1993-12-23
+3038918 Torrent Tort Torrent Tort 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3038919 Torretinyà Torretinya 42.45 1.51667 L LCTY AD 00 0 1790 Europe/Andorra 1993-12-23
+3038920 Prat d’en Torres Prat d'en Torres 42.6 1.51667 L GRAZ AD 00 0 1445 Europe/Andorra 1993-12-23
+3038921 Canals de Torrent Pregó Canals de Torrent Prego 42.48333 1.48333 H STM AD 00 0 981 Europe/Andorra 1993-12-23
+3038922 Canal de Torrentinyà Canal de Torrentinya 42.45 1.5 H STM AD 00 0 1614 Europe/Andorra 1993-12-23
+3038923 Torrentill Torrentill 42.46667 1.5 H STM AD 00 0 1383 Europe/Andorra 1993-12-23
+3038924 Camà de Torrent Forcat Cami de Torrent Forcat 42.46667 1.51667 R TRL AD 00 0 1985 Europe/Andorra 1993-12-23
+3038925 Canal del Torrent Cauber Canal del Torrent Cauber 42.6 1.51667 H RVN AD 00 0 1445 Europe/Andorra 1993-12-23
+3038926 Torre dels Soldats Torre dels Soldats Pic Monturull,Torre dels Soldats 42.45 1.58333 T PK AD 00 0 2537 Europe/Andorra 2011-11-05
+3038927 Torrapuis Torrapuis 42.53333 1.53333 L LCTY AD 00 0 1521 Europe/Andorra 1993-12-23
+3038928 Pic de Torradella Pic de Torradella 42.6 1.61667 T PK AD 00 0 2271 Europe/Andorra 1993-12-23
+3038929 Pala de la Torradella Pala de la Torradella 42.58333 1.61667 T SLP AD 00 0 1707 Europe/Andorra 1993-12-23
+3038930 Obaga de Torradella Obaga de Torradella 42.6 1.63333 T SLP AD 00 0 1893 Europe/Andorra 1993-12-23
+3038931 Canals de Torradella Canals de Torradella 42.6 1.63333 H STM AD 00 0 1893 Europe/Andorra 1993-12-23
+3038932 Borda del Torner Borda del Torner 42.58333 1.48333 S FRM AD 00 0 1809 Europe/Andorra 1993-12-23
+3038933 Planell de la Tora Planell de la Tora 42.55 1.6 T UPLD AD 00 0 2210 Europe/Andorra 1993-12-23
+3038934 Clots de la Tora Clots de la Tora 42.46667 1.58333 H RVN AD 00 0 2367 Europe/Andorra 1993-12-23
+3038935 Prat del Toni Prat del Toni 42.55 1.61667 L GRAZ AD 00 0 2206 Europe/Andorra 1993-12-23
+3038936 Molà del Tomàs Moli del Tomas 42.58333 1.65 S ML AD 00 0 1767 Europe/Andorra 1993-12-23
+3038937 Cortals de Tolse Cortals de Tolse 42.45 1.48333 S CRRL AD 00 0 1111 Europe/Andorra 1993-12-23
+3038938 Bosc de Tolse Bosc de Tolse 42.45 1.48333 V FRST AD 00 0 1111 Europe/Andorra 1993-12-23
+3038939 Tolse Tolse 42.45 1.48333 S HUTS AD 00 0 1111 Europe/Andorra 1993-12-23
+3038940 Basera dels Tolls de l’Olla Basera dels Tolls de l'Olla 42.48333 1.45 T CLF AD 00 0 1195 Europe/Andorra 1993-12-23
+3038941 Tolls de l’Olla Tolls de l'Olla 42.46667 1.56667 L LCTY AD 00 0 2365 Europe/Andorra 1993-12-23
+3038942 Torrent del Tirader Torrent del Tirader 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3038943 Canal del Timbarro Canal del Timbarro 42.5 1.46667 H STM AD 00 0 1678 Europe/Andorra 1993-12-23
+3038944 Terres de Serra Terres de Serra 42.48333 1.5 L LCTY AD 00 0 1631 Europe/Andorra 1993-12-23
+3038945 Terres de Sant Miguel Terres de Sant Miguel 42.5 1.58333 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3038946 Terres del Torrent Pregó Terres del Torrent Prego 42.55 1.58333 L LCTY AD 00 0 1499 Europe/Andorra 1993-12-23
+3038947 Terres del Solà Terres del Sola 42.55 1.55 L LCTY AD 00 0 2097 Europe/Andorra 1993-12-23
+3038948 Terres de Jacques Terres de Jacques 42.56667 1.61667 L LCTY AD 00 0 1920 Europe/Andorra 1993-12-23
+3038949 Obaga de les Terres de Casamanya Obaga de les Terres de Casamanya 42.56667 1.55 T SLP AD 00 0 1996 Europe/Andorra 1993-12-23
+3038950 Canal del Terrer Roig Canal del Terrer Roig 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3038951 Terrer Roig Terrer Roig 42.58333 1.45 L LCTY AD 00 0 2156 Europe/Andorra 1993-12-23
+3038952 Clots dels Terregalls Clots dels Terregalls 42.61667 1.61667 H STMH AD 00 0 2352 Europe/Andorra 1993-12-23
+3038953 Terregalls Terregalls 42.5 1.51667 L LCTY AD 00 0 1410 Europe/Andorra 1993-12-23
+3038954 Terra del Pou Terra del Pou 42.46667 1.48333 L LCTY AD 00 0 1134 Europe/Andorra 1993-12-23
+3038955 Terra Bogada Terra Bogada 42.46667 1.5 L LCTY AD 00 0 1383 Europe/Andorra 1993-12-23
+3038956 Canal del Teixó Canal del Teixo 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3038957 Coma de Teix Coma de Teix 42.46667 1.46667 T SLP AD 00 0 1340 Europe/Andorra 1993-12-23
+3038958 Font de les Taules Font de les Taules 42.55 1.55 H SPNG AD 00 0 2097 Europe/Andorra 1993-12-23
+3038959 Coma Tarterosa Coma Tarterosa 42.48333 1.45 H RVN AD 00 0 1195 Europe/Andorra 1993-12-23
+3038960 Bosc de la Tarterosa Bosc de la Tarterosa 42.6 1.65 V FRST AD 00 0 2131 Europe/Andorra 1993-12-23
+3038961 Tarter Gran Tarter Gran 42.58333 1.45 L LCTY AD 00 0 2156 Europe/Andorra 1993-12-23
+3038962 Tartera Gran Tartera Gran 42.48333 1.46667 L LCTY AD 00 0 1148 Europe/Andorra 1993-12-23
+3038963 Pont del Tarter Pont del Tarter 42.58333 1.65 S BDG AD 00 0 1767 Europe/Andorra 1993-12-23
+3038964 Coll de la TÃ pia Coll de la Tapia 42.46667 1.48333 T PASS AD 00 0 1134 Europe/Andorra 1993-12-23
+3038965 Canal Tancada Canal Tancada 42.48333 1.55 H STM AD 00 0 2233 Europe/Andorra 1993-12-23
+3038966 Roc del Tampuent Roc del Tampuent 42.58333 1.48333 T RK AD 00 0 1809 Europe/Andorra 1993-12-23
+3038967 Basers de les Tallades Basers de les Tallades 42.5 1.46667 T CLF AD 00 0 1678 Europe/Andorra 1993-12-23
+3038968 Bosc de la Tallada Nova Bosc de la Tallada Nova 42.58333 1.48333 V FRST AD 00 0 1809 Europe/Andorra 1993-12-23
+3038969 Camà de la Tallada Cami de la Tallada 42.48333 1.56667 R TRL AD 00 0 2231 Europe/Andorra 1993-12-23
+3038970 Canal del Tabanell Canal del Tabanell 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3038971 Bosc del Tabanell Bosc del Tabanell 42.58333 1.5 V FRST AD 00 0 1595 Europe/Andorra 1993-12-23
+3038972 Suriguera Suriguera 42.58333 1.61667 T SLP AD 00 0 1707 Europe/Andorra 1993-12-23
+3038973 Sudornar Sudornar 42.58333 1.45 T SLP AD 00 0 2156 Europe/Andorra 1993-12-23
+3038974 Font del Sucre Font del Sucre 42.51667 1.46667 H SPNG AD 00 0 1840 Europe/Andorra 1993-12-23
+3038975 Costa de la Sucarana Costa de la Sucarana 42.6 1.5 T SLP AD 00 0 1923 Europe/Andorra 1993-12-23
+3038976 Canya de la Sucarana Canya de la Sucarana 42.6 1.5 T GRGE AD 00 0 1923 Europe/Andorra 1993-12-23
+3038977 Borda del Sucarana Borda del Sucarana 42.55 1.58333 S HUTS AD 00 0 1499 Europe/Andorra 1993-12-23
+3038978 Riu de Sorteny Riu de Sorteny 42.62341 1.54869 H STM AD 00 0 1950 Europe/Andorra 2011-04-19
+3038979 Pla de Sorteny Pla de Sorteny 42.61952 1.58872 T UPLD AD 00 0 2524 Europe/Andorra 2011-04-19
+3038980 Marrades de Sorteny Marrades de Sorteny 42.61667 1.55 R TRL AD 00 0 2007 Europe/Andorra 1993-12-23
+3038981 Sorteny Sorteny 42.61667 1.58333 A ADMD AD 00 0 2374 Europe/Andorra 1993-12-23
+3038982 Riu de les Soronelles Riu de les Soronelles 42.53333 1.65 H STM AD 00 0 2508 Europe/Andorra 1993-12-23
+3038983 Collada de les Soronelles Collada de les Soronelles 42.53333 1.65 T PASS AD 00 0 2508 Europe/Andorra 1993-12-23
+3038984 Pic de les Sorobilles Pic de les Sorobilles 42.56667 1.53333 T PK AD 00 0 1669 Europe/Andorra 1993-12-23
+3038985 Riu de Sornàs Riu de Sornas 42.56667 1.53333 H STM AD 00 0 1669 Europe/Andorra 1993-12-23
+3038986 Clot de Sornàs Clot de Sornas 42.56667 1.55 H RVN AD 00 0 1996 Europe/Andorra 1993-12-23
+3038987 Sornàs Sornas Sornas,Sornàs 42.5654 1.5287 P PPL AD 05 0 1514 Europe/Andorra 2011-11-05
+3038988 Riu de Soriguera Riu de Soriguera Riu de Soriguera,Riu de Suriguera 42.58333 1.61667 H STM AD AD 00 0 1707 Europe/Andorra 2011-11-05
+3038989 Solana del Soriguer Solana del Soriguer 42.56667 1.55 T SLP AD 00 0 1996 Europe/Andorra 1993-12-23
+3038990 Clot Sord Clot Sord 42.6 1.65 T CRQ AD 00 0 2131 Europe/Andorra 1993-12-23
+3038991 Solà de Soquer Sola de Soquer 42.45 1.48333 T SLP AD 00 0 1111 Europe/Andorra 1993-12-23
+3038992 Riu de Soplanàs Riu de Soplanas Riu de Soplanars,Riu de Soplanas,Riu de Soplanàs 42.58333 1.63333 H STM AD AD 00 0 1722 Europe/Andorra 2011-11-05
+3038993 Borda de Som Borda de Som 42.56667 1.58333 S HUTS AD 00 0 1919 Europe/Andorra 1993-12-23
+3038994 Roc de Solobre Roc de Solobre 42.48333 1.51667 T RKS AD 00 0 2061 Europe/Andorra 1993-12-23
+3038995 Bosc de Solobre Bosc de Solobre 42.48333 1.5 V FRST AD 00 0 1631 Europe/Andorra 1993-12-23
+3038996 Bosc del Soleador Bosc del Soleador 42.6 1.51667 V FRST AD 00 0 1445 Europe/Andorra 1993-12-23
+3038997 Camà de Soldeu Cami de Soldeu 42.58333 1.66667 R TRL AD 00 0 2159 Europe/Andorra 1993-12-23
+3038998 Bosc de Soldeu Bosc de Soldeu Bois de Soldeu,Bosc de Soldeu,Bosque de Soldeu 42.57107 1.65402 V FRST AD 00 0 1988 Europe/Andorra 2011-11-05
+3038999 Soldeu Soldeu 42.57688 1.66769 P PPL AD 02 0 2159 Europe/Andorra 2007-04-16
+3039000 Solana del Solanyó Solana del Solanyo 42.53333 1.55 T SLP AD 00 0 1344 Europe/Andorra 1993-12-23
+3039001 Riu del Solanyò Riu del Solanyo 42.53333 1.55 H STM AD 00 0 1344 Europe/Andorra 1993-12-23
+3039002 Bosc del Solanyó Bosc del Solanyo 42.55 1.45 V FRST AD 00 0 1788 Europe/Andorra 1993-12-23
+3039003 Barranc del Solanyó Barranc del Solanyo 42.55 1.45 H STM AD 00 0 1788 Europe/Andorra 1993-12-23
+3039004 Solans Solans 42.53333 1.6 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3039005 Solanet de RÃ mio Solanet de Ramio 42.5 1.56667 L LCTY AD 00 0 1776 Europe/Andorra 1993-12-23
+3039006 Terregalls del Solanet Terregalls del Solanet 42.63333 1.6 T RDGE AD 00 0 2635 Europe/Andorra 1993-12-23
+3039007 Bosc del Solanet Bosc del Solanet 42.55 1.48333 V FRST AD 00 0 1548 Europe/Andorra 1993-12-23
+3039008 Solanes de la Peguera Solanes de la Peguera 42.45 1.51667 A ADMD AD 00 0 1790 Europe/Andorra 1993-12-23
+3039009 Serra de les Solanelles Serra de les Solanelles 42.55 1.66667 T RDGE AD 00 0 2224 Europe/Andorra 1993-12-23
+3039010 Riu de les Solanelles Riu de les Solanelles 42.55 1.68333 H STM AD 00 0 2254 Europe/Andorra 1993-12-23
+3039011 Collet de les Solanelles Collet de les Solanelles 42.55 1.61667 T PASS AD 00 0 2206 Europe/Andorra 1993-12-23
+3039012 Collada de les Solanelles Collada de les Solanelles 42.55 1.66667 T PASS AD 00 0 2224 Europe/Andorra 1993-12-23
+3039013 Clot de les Solanelles Clot de les Solanelles 42.55 1.63333 H RVN AD 00 0 2336 Europe/Andorra 1993-12-23
+3039014 Canal de les Solanelles Canal de les Solanelles 42.55 1.63333 H STM AD 00 0 2336 Europe/Andorra 1993-12-23
+3039015 Cabana de les Solanelles Cabana de les Solanelles 42.53333 1.66667 S HUT AD 00 0 2489 Europe/Andorra 1993-12-23
+3039016 Solanelles Solanelles 42.55 1.66667 L LCTY AD 00 0 2224 Europe/Andorra 1993-12-23
+3039017 Barranc de la Solana del Pas de la Casa Barranc de la Solana del Pas de la Casa 42.53333 1.73333 H STM AD 00 0 2300 Europe/Andorra 1993-12-23
+3039018 Solana del Lloser d’Ordino Solana del Lloser d'Ordino 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3039019 Riu de la Solana del Forn Riu de la Solana del Forn 42.56154 1.67948 H STM AD 00 0 2094 Europe/Andorra 2011-04-19
+3039020 Obagot de la Solana del Forn Obagot de la Solana del Forn 42.55 1.66667 T SLP AD 00 0 2224 Europe/Andorra 1993-12-23
+3039021 Solana del Forn Solana del Forn 42.55 1.66667 A ADMD AD 00 0 2224 Europe/Andorra 1993-12-23
+3039022 Solana de l’Estall Serrer Solana de l'Estall Serrer 42.5 1.61667 A ADMD AD 00 0 2560 Europe/Andorra 1993-12-23
+3039023 Solana de les Aubes Solana de les Aubes 42.56667 1.56667 L LCTY AD 00 0 2089 Europe/Andorra 1993-12-23
+3039024 Solana de la Baronia Solana de la Baronia 42.53333 1.61667 A ADMD AD 00 0 2237 Europe/Andorra 1993-12-23
+3039025 Prats de la Solana Prats de la Solana 42.56667 1.78333 L GRAZ AD 00 0 1680 Europe/Andorra 1993-12-23
+3039026 Camà de la Solana Cami de la Solana 42.43333 1.45 R TRL AD 00 0 877 Europe/Andorra 1993-12-23
+3039027 Bosc de la Solana Bosc de la Solana 42.51667 1.46667 V FRST AD 00 0 1840 Europe/Andorra 1993-12-23
+3039028 Solà de Soldeu Sola de Soldeu 42.58333 1.66667 A ADMD AD 00 0 2159 Europe/Andorra 1993-12-23
+3039029 Serra del Solà de Sispony Serra del Sola de Sispony 42.53333 1.48333 T MT AD 00 0 1677 Europe/Andorra 1993-12-23
+3039030 Pic del Solà d’Erts Pic del Sola d'Erts 42.56667 1.5 T PK AD 00 0 1636 Europe/Andorra 1993-12-23
+3039031 Solà d’Erts Sola d'Erts 42.56667 1.5 A ADMD AD 00 0 1636 Europe/Andorra 1993-12-23
+3039032 Solà de Riambert Sola de Riambert 42.58333 1.53333 A ADMD AD 00 0 1924 Europe/Andorra 1993-12-23
+3039033 Serra del Solà de Rà mio Serra del Sola de Ramio 42.5 1.58333 T MT AD 00 0 1888 Europe/Andorra 1993-12-23
+3039034 Solà de Rà mio Sola de Ramio 42.5 1.58333 A ADMD AD 00 0 1888 Europe/Andorra 1993-12-23
+3039035 Bosc del Solà d’Envalira Bosc del Sola d'Envalira 42.55 1.68333 V FRST AD 00 0 2254 Europe/Andorra 1993-12-23
+3039036 Carena del Solà d’Engordany Carena del Sola d'Engordany 42.51667 1.55 T RDGE AD 00 0 1322 Europe/Andorra 1993-12-23
+3039037 Solá d’Engordany Sola d'Engordany 42.51667 1.53333 A ADMD AD 00 0 1460 Europe/Andorra 1993-12-23
+3039038 Solà d’Enclar Sola d'Enclar 42.51667 1.48333 A ADMD AD 00 0 1839 Europe/Andorra 1993-12-23
+3039039 Solà d’Encamp Sola d'Encamp 42.53333 1.58333 A ADMD AD 00 0 1571 Europe/Andorra 1993-12-23
+3039040 Cresta del Solà de Nadal Cresta del Sola de Nadal 42.51667 1.51667 T SPUR AD 00 0 1265 Europe/Andorra 1993-12-23
+3039041 Aspres del Solà de Nadal Aspres del Sola de Nadal 42.51667 1.51667 V VINS AD 00 0 1265 Europe/Andorra 1993-12-23
+3039042 Solà de Mossers Sola de Mossers 42.45 1.46667 A ADMD AD 00 0 935 Europe/Andorra 1993-12-23
+3039043 Solà de Mereig Sola de Mereig 42.56667 1.58333 L LCTY AD 00 0 1919 Europe/Andorra 1993-12-23
+3039044 Solà del Tarter Sola del Tarter 42.58333 1.65 A ADMD AD 00 0 1767 Europe/Andorra 1993-12-23
+3039045 Solà dels Sulls Sola dels Sulls 42.48333 1.56667 A ADMD AD 00 0 2231 Europe/Andorra 1993-12-23
+3039046 Solà dels Plans Sola dels Plans 42.58333 1.63333 A ADMD AD 00 0 1722 Europe/Andorra 1993-12-23
+3039047 Serra del Sola del Quart Mitger Serra del Sola del Quart Mitger 42.55 1.55 T RDGE AD 00 0 2097 Europe/Andorra 1993-12-23
+3039048 Solà del Quart Mitger Sola del Quart Mitger 42.55 1.53333 A ADMD AD 00 0 1593 Europe/Andorra 1993-12-23
+3039049 Solà del Pui Sola del Pui 42.55 1.51667 A ADMD AD 00 0 1397 Europe/Andorra 1993-12-23
+3039050 Cap del Solà de les Comes Cap del Sola de les Comes 42.58333 1.5 T PK AD 00 0 1595 Europe/Andorra 1993-12-23
+3039051 Canal del Solà de les Comes Canal del Sola de les Comes 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3039052 Solà de la Moixella Sola de la Moixella 42.45 1.48333 A ADMD AD 00 0 1111 Europe/Andorra 1993-12-23
+3039053 Balmes del Solà de l’Allau Balmes del Sola de l'Allau 42.6 1.53333 T CLF AD 00 0 1695 Europe/Andorra 1993-12-23
+3039054 Solà de l’Aldosa Sola de l'Aldosa 42.58333 1.61667 A ADMD AD 00 0 1707 Europe/Andorra 1993-12-23
+3039055 Solà de la Farga Sola de la Farga 42.48333 1.6 A ADMD AD 00 0 2250 Europe/Andorra 1993-12-23
+3039056 Canals del Solà de Juclar Canals del Sola de Juclar 42.6 1.7 H RVN AD 00 0 2354 Europe/Andorra 1993-12-23
+3039057 Solà d’Arcavell Sola d'Arcavell Sola d'Arcabell,Sola d'Arcavell,Sola d’Arcabell,Solà d’Arcavell 42.43333 1.48333 A ADMD AD 00 0 1228 Europe/Andorra 2011-11-05
+3039058 Solà d’Andorra Sola d'Andorra 42.51667 1.5 A ADMD AD 00 0 1688 Europe/Andorra 1993-12-23
+3039059 Solà d’Aixovall Sola d'Aixovall 42.48333 1.48333 A ADMD AD 00 0 981 Europe/Andorra 1993-12-23
+3039060 Riu del Solà Riu del Sola 42.55 1.46667 H STM AD 00 0 1585 Europe/Andorra 1993-12-23
+3039061 Clot del Solà Clot del Sola 42.46667 1.46667 H RVN AD 00 0 1340 Europe/Andorra 1993-12-23
+3039062 Carrera del Solà Carrera del Sola 42.53333 1.58333 R TRL AD 00 0 1571 Europe/Andorra 1993-12-23
+3039063 Costa del Sodorn Costa del Sodorn 42.5 1.46667 T SLP AD 00 0 1678 Europe/Andorra 1993-12-23
+3039064 Pla del Socarrat Pla del Socarrat 42.55 1.6 T UPLD AD 00 0 2210 Europe/Andorra 1993-12-23
+3039065 Camà del Socarrat Cami del Socarrat 42.58333 1.63333 R TRL AD 00 0 1722 Europe/Andorra 1993-12-23
+3039066 Bosc del Socarrat Bosc del Socarrat 42.58333 1.65 V FRST AD 00 0 1767 Europe/Andorra 1993-12-23
+3039067 Vial de la Socarrada de Coll Carnisser Vial de la Socarrada de Coll Carnisser 42.58333 1.46667 R RD AD 00 0 1643 Europe/Andorra 1993-12-23
+3039068 Pala de Sobre l’Estany Pala de Sobre l'Estany 42.61667 1.71667 T CLF AD 00 0 2352 Europe/Andorra 1993-12-23
+3039069 Pala de Sobre les Basses Pala de Sobre les Basses 42.58333 1.7 T SLP AD 00 0 2584 Europe/Andorra 1993-12-23
+3039070 Basers de Sobre la Pleta Basers de Sobre la Pleta 42.6 1.46667 T CLF AD 00 0 2421 Europe/Andorra 1993-12-23
+3039071 Sobre dels Camps de la Cortinada Sobre dels Camps de la Cortinada 42.58333 1.5 A ADMD AD 00 0 1595 Europe/Andorra 1993-12-23
+3039072 Planells Sobirans Planells Sobirans 42.58333 1.46667 T UPLD AD 00 0 1643 Europe/Andorra 1993-12-23
+3039073 Riu del Sisqueró Riu del Sisquero 42.6 1.7 H STM AD 00 0 2354 Europe/Andorra 1993-12-23
+3039074 Camà del Sisquero Cami del Sisquero 42.6 1.68333 R TRL AD 00 0 2089 Europe/Andorra 1993-12-23
+3039075 Solà de Sispony Sola de Sispony 42.53333 1.48333 T SLP AD 00 0 1677 Europe/Andorra 1993-12-23
+3039076 Camà de Sispony Cami de Sispony 42.53333 1.46667 R TRL AD 00 0 1846 Europe/Andorra 1993-12-23
+3039077 Sispony Sispony 42.53355 1.51481 P PPL AD 04 0 1252 Europe/Andorra 2011-04-19
+3039078 Pletes del Siscaró Pletes del Siscaro 42.58333 1.7 L GRAZ AD 00 0 2584 Europe/Andorra 1993-12-23
+3039079 Estanys del Siscaro Estanys del Siscaro 42.58333 1.7 H LKS AD 00 0 2584 Europe/Andorra 1993-12-23
+3039080 Canals del Siscaró Canals del Siscaro 42.58333 1.7 H RVN AD 00 0 2584 Europe/Andorra 1993-12-23
+3039081 Pic de Siscarou Pic de Siscarou Pic de Siscarou,Pico de Siscarou,Siscaro,Siscaró 42.6 1.73333 T PK AD 00 0 2383 Europe/Andorra 2011-11-05
+3039082 Siscaró Siscaro 42.58333 1.71667 A ADMD AD 00 0 2553 Europe/Andorra 1993-12-23
+3039083 Marrades del Siscar Marrades del Siscar 42.58333 1.71667 R TRL AD 00 0 2553 Europe/Andorra 1993-12-23
+3039084 Canals del Siscar Canals del Siscar 42.6 1.71667 H RVN AD 00 0 2516 Europe/Andorra 1993-12-23
+3039085 Basses del Siscar Basses del Siscar 42.58333 1.71667 H LKS AD 00 0 2553 Europe/Andorra 1993-12-23
+3039086 Sincloset Sincloset 42.48333 1.5 T MT AD 00 0 1631 Europe/Andorra 1993-12-23
+3039087 Port de Siguer Port de Siguer Port de Siguer 42.65 1.56667 T PASS AD 00 0 2471 Europe/Andorra 2011-11-05
+3039088 Bosc del Sigarró Bosc del Sigarro 42.51667 1.6 V FRST AD 00 0 2085 Europe/Andorra 1993-12-23
+3039089 Canal de la Sicalma Canal de la Sicalma 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3039090 Portella de Satut Portella de Satut Port de Setut,Portella de Satut,Portella de Setut 42.46667 1.63333 T PK AD 00 0 2619 Europe/Andorra 2011-11-05
+3039091 Cabana de Setut Cabana de Setut 42.48333 1.63333 S HUT AD 00 0 2296 Europe/Andorra 1993-12-23
+3039092 Basses de Setut Basses de Setut 42.48333 1.65 H LKS AD 00 0 2658 Europe/Andorra 1993-12-23
+3039093 Setut Setut 42.46667 1.65 A ADMD AD 00 0 2700 Europe/Andorra 1993-12-23
+3039094 Carretera de Setúria Carretera de Seturia 42.55 1.48333 R RD AD 00 0 1548 Europe/Andorra 1993-12-23
+3039095 Camà de Setúria Cami de Seturia 42.55 1.46667 R TRL AD 00 0 1585 Europe/Andorra 1993-12-23
+3039096 Bordes de Setúria Bordes de Seturia 42.53288 1.43718 S HUTS AD 00 0 1972 Europe/Andorra 2011-04-19
+3039097 Setúria Seturia 42.55 1.43333 A ADMD AD 00 0 1949 Europe/Andorra 1993-12-23
+3039098 Tarteres de la Serrera Tarteres de la Serrera 42.61667 1.58333 T TAL AD 00 0 2374 Europe/Andorra 1993-12-23
+3039099 Riu de la Serrera Riu de la Serrera 42.61667 1.56667 H STM AD 00 0 2228 Europe/Andorra 1993-12-23
+3039100 Pleta de la Serrera Pleta de la Serrera 42.61667 1.58333 L GRAZ AD 00 0 2374 Europe/Andorra 1993-12-23
+3039101 Pic de la Serrera Pic de la Serrera Pic de Serrere,Pic de Serrère,Pic de la Serrera,Serrera 42.63333 1.6 T PK AD 00 0 2635 Europe/Andorra 2011-11-05
+3039102 Pas de la Serrera Pas de la Serrera 42.61667 1.58333 T PASS AD 00 0 2374 Europe/Andorra 1993-12-23
+3039103 Clots de la Serrera Clots de la Serrera 42.61667 1.6 H STMH AD 00 0 2528 Europe/Andorra 1993-12-23
+3039104 Aspres de la Serrera Aspres de la Serrera 42.61667 1.6 V VINS AD 00 0 2528 Europe/Andorra 1993-12-23
+3039105 Canal de Serrats Canal de Serrats 42.53333 1.5 H STM AD 00 0 1357 Europe/Andorra 1993-12-23
+3039106 Camà del Serrat Pla Cami del Serrat Pla 42.43333 1.46667 R TRL AD 00 0 1113 Europe/Andorra 1993-12-23
+3039107 Bosc del Serrat Llong Bosc del Serrat Llong 42.51667 1.48333 V FRST AD 00 0 1839 Europe/Andorra 1993-12-23
+3039108 Planell del Serrat del Tronc Planell del Serrat del Tronc 42.58333 1.61667 T UPLD AD 00 0 1707 Europe/Andorra 1993-12-23
+3039109 Cabana del Serrat de la Barracota Cabana del Serrat de la Barracota 42.48333 1.63333 S HUT AD 00 0 2296 Europe/Andorra 1993-12-23
+3039110 Pic de Serra Seca Pic de Serra Seca 42.51667 1.7 T PK AD 00 0 2435 Europe/Andorra 1993-12-23
+3039111 Canal de Serra Plana Canal de Serra Plana 42.48333 1.48333 H STM AD 00 0 981 Europe/Andorra 1993-12-23
+3039112 Riu de Serrana Riu de Serrana 42.55 1.51667 H STM AD 00 0 1397 Europe/Andorra 1993-12-23
+3039113 Pic de Serra Mitjana Pic de Serra Mitjana 42.47479 1.61523 T PK AD 00 0 2418 Europe/Andorra 2011-04-19
+3039114 Estany de Serra Mitjana Estany de Serra Mitjana 42.46667 1.6 H LK AD 00 0 2449 Europe/Andorra 1993-12-23
+3039115 Canal Ampla de Serra Mitjana Canal Ampla de Serra Mitjana 42.46667 1.61667 H STM AD 00 0 2448 Europe/Andorra 1993-12-23
+3039116 Canal de la Serradora Canal de la Serradora 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3039117 Canal Serradora Canal Serradora 42.5 1.56667 H STM AD 00 0 1776 Europe/Andorra 1993-12-23
+3039118 Cap de la Serra dels Isards Cap de la Serra dels Isards 42.58333 1.58333 T PK AD 00 0 1993 Europe/Andorra 1993-12-23
+3039119 Serrat dels Serradells Serrat dels Serradells 42.61667 1.53333 T SPUR AD 00 0 1609 Europe/Andorra 1993-12-23
+3039120 Cortal de la Serra Cortal de la Serra 42.53333 1.5 S CRRL AD 00 0 1357 Europe/Andorra 1993-12-23
+3039121 Cap de la Serra Cap de la Serra 42.61667 1.6 T RDGE AD 00 0 2528 Europe/Andorra 1993-12-23
+3039122 Roc de la Senyoreta Roc de la Senyoreta 42.45 1.48333 T RK AD 00 0 1111 Europe/Andorra 1993-12-23
+3039123 Bosc de la Senyoreta Bosc de la Senyoreta 42.45 1.48333 V FRST AD 00 0 1111 Europe/Andorra 1993-12-23
+3039124 Senyal Negre Senyal Negre 42.65 1.55 T MT AD 00 0 2181 Europe/Andorra 1993-12-23
+3039125 Senyal de Missa Senyal de Missa 42.46667 1.48333 T PK AD 00 0 1134 Europe/Andorra 1993-12-23
+3039126 Roc de Senders Roc de Senders 42.5 1.53333 T RK AD 00 0 1574 Europe/Andorra 1993-12-23
+3039127 Sella Sella 42.56667 1.6 T CLF AD 00 0 1655 Europe/Andorra 1993-12-23
+3039128 Riu del Seig Riu del Seig 42.56667 1.61667 H STM AD 00 0 1920 Europe/Andorra 1993-12-23
+3039129 Canal del Seig Canal del Seig 42.61667 1.53333 H STM AD 00 0 1609 Europe/Andorra 1993-12-23
+3039130 Solà de Segudet Sola de Segudet 42.56667 1.53333 T SLP AD 00 0 1669 Europe/Andorra 1993-12-23
+3039131 Riu de Segudet Riu de Segudet 42.5583 1.54304 H STM AD 00 0 1722 Europe/Andorra 2011-04-19
+3039132 Segudet Segudet Segudet 42.55755 1.53858 P PPL AD 05 0 1556 Europe/Andorra 2011-11-05
+3039133 Estany Segon Estany Segon 42.61667 1.73333 H LK AD 00 0 2508 Europe/Andorra 1993-12-23
+3039134 Torrent del Segalars Torrent del Segalars 42.55 1.58333 H STM AD 00 0 1499 Europe/Andorra 1993-12-23
+3039135 Camà de Sedornet Cami de Sedornet 42.58333 1.51667 R TRL AD 00 0 1722 Europe/Andorra 1993-12-23
+3039136 Bordes de Sedornet Bordes de Sedornet 42.58333 1.51667 S HUTS AD 00 0 1722 Europe/Andorra 1993-12-23
+3039137 Costa Seda Costa Seda 42.48333 1.5 T SLP AD 00 0 1631 Europe/Andorra 1993-12-23
+3039138 Planells Secants Planells Secants 42.43333 1.53333 T UPLD AD 00 0 2108 Europe/Andorra 1993-12-23
+3039139 Riu Sec Riu Sec 42.51667 1.46667 H STM AD 00 0 1840 Europe/Andorra 1993-12-23
+3039140 Estany Sec Estany Sec 42.46667 1.61667 H LK AD 00 0 2448 Europe/Andorra 1993-12-23
+3039141 Borda del Savoiano Borda del Savoiano 42.56667 1.51667 S HUTS AD 00 0 1500 Europe/Andorra 1993-12-23
+3039142 Serra de la Sauvata Serra de la Sauvata 42.56667 1.58333 T RDGE AD 00 0 1919 Europe/Andorra 1993-12-23
+3039143 Costa de la Sauvata Costa de la Sauvata 42.56667 1.58333 T SLP AD 00 0 1919 Europe/Andorra 1993-12-23
+3039144 Roc del Sastre Roc del Sastre Roc del Sastra,Roc del Sastre 42.58333 1.61667 T SPUR AD AD 00 0 1707 Europe/Andorra 2011-11-05
+3039145 Pont Sassanat Pont Sassanat 42.5 1.55 S BDG AD 00 0 1566 Europe/Andorra 1993-12-23
+3039146 Saquet Saquet 42.55 1.6 L LCTY AD 00 0 2210 Europe/Andorra 1993-12-23
+3039147 Roc de Sant Vicenç Roc de Sant Vicenc 42.5 1.5 T RK AD 00 0 1135 Europe/Andorra 1993-12-23
+3039148 Collet de Sant Vicenç Collet de Sant Vicenc 42.5 1.48333 T PASS AD 00 0 1316 Europe/Andorra 1993-12-23
+3039149 Solà de Santserra Sola de Santserra 42.48333 1.46667 T SLP AD 00 0 1148 Europe/Andorra 1993-12-23
+3039150 Sant Romà de Vila Sant Roma de Vila 42.53333 1.56667 S CH AD 00 0 1418 Europe/Andorra 1993-12-23
+3039151 Sant Romà de les Bons Sant Roma de les Bons 42.53333 1.58333 S CH AD 00 0 1571 Europe/Andorra 1993-12-23
+3039152 Bosc de Sant Romà Bosc de Sant Roma 42.45 1.5 V FRST AD 00 0 1614 Europe/Andorra 1993-12-23
+3039153 Sant Romà Sant Roma 42.45 1.5 S CH AD 00 0 1614 Europe/Andorra 1993-12-23
+3039154 Sant Pere Sant Pere Sant Pere 42.57952 1.65362 P PPL AD 02 0 1767 Europe/Andorra 2011-11-05
+3039155 Sant Miquel d'Engolasters Sant Miquel d'Engolasters Sant Miquel d'Engolasters 42.51094 1.56008 S CH AD AD 07 0 1661 Europe/Andorra 2007-04-05
+3039156 Sant Miquel de Fontaneda Sant Miquel de Fontaneda Sant Miquel,Sant Miquel de Fontaneda 42.45 1.46667 S CH AD AD 00 0 935 Europe/Andorra 2011-11-05
+3039157 Solà de Sant Miquel Sola de Sant Miquel 42.58333 1.66667 T SLP AD 00 0 2159 Europe/Andorra 1993-12-23
+3039158 Roc de Sant Miquel Roc de Sant Miquel 42.58333 1.66667 T CLF AD 00 0 2159 Europe/Andorra 1993-12-23
+3039159 Drecera de Sant Martà Drecera de Sant Marti 42.48333 1.5 R TRL AD 00 0 1631 Europe/Andorra 1993-12-23
+3039160 Sant Martà Sant Marti 42.48333 1.5 T UPLD AD 00 0 1631 Europe/Andorra 1993-12-23
+3039161 Sant Martà Sant Marti 42.48333 1.48333 S RUIN AD 00 0 981 Europe/Andorra 1993-12-23
+3039162 Parròquia de Sant Julià de Lòria Parroquia de Sant Julia de Loria Parroquia de Sant Julia de Loria,Parroquia de Sant Julià de Lòria,Sant Julia,Sant Julia de Loria,Sant Julià ,Sant Julià de Lòria 42.46247 1.48247 A ADM1 AD 06 9448 966 Europe/Andorra 2010-08-13
+3039163 Sant Julià de Lòria Sant Julia de Loria San Julia,San Julià ,San-Dzhulija-de-Lorija,San-Khulija-de-Lorija,Sant Julia de Loria,Sant Julià de Lòria,sheng hu li ya-de luo li ya,Сан-ДжулиÑ-де-ЛориÑ,Сан-ХулиÑ-де-ЛориÑ,サン・ジュリア・デ・ãƒãƒªã‚¢æ•™åŒº,圣胡利娅-德洛里亚,圣胡利娅ï¼å¾·æ´›é‡Œäºš 42.46372 1.49129 P PPLA AD 06 8022 1045 Europe/Andorra 2008-10-15
+3039164 Riu de Sant Josep Riu de Sant Josep 42.56419 1.75244 H STM AD 00 0 1836 Europe/Andorra 2011-04-19
+3039165 Clot de Sant Josep Clot de Sant Josep 42.56667 1.7 T CRQ AD 00 0 2375 Europe/Andorra 1993-12-23
+3039166 Sant Joan de Caselles Sant Joan de Caselles San Joan de Casettas,Sant Joan de Casellas,Sant Joan de Caselles 42.56988 1.60922 P PPL AD 02 0 1724 Europe/Andorra 2011-11-05
+3039167 Serrat de Sant Jaume Serrat de Sant Jaume 42.53333 1.6 T RDGE AD 00 0 1888 Europe/Andorra 1993-12-23
+3039168 Sant Jaume Sant Jaume 42.58333 1.63333 S CH AD 00 0 1722 Europe/Andorra 1993-12-23
+3039169 Sant Jaume Sant Jaume Sant Jaume,Sant Joums 42.53333 1.6 S CH AD AD 00 0 1888 Europe/Andorra 2011-11-05
+3039170 Sant Esteve Sant Esteve 42.43333 1.48333 S CH AD 00 0 1228 Europe/Andorra 1993-12-23
+3039171 Sant Esteve Sant Esteve 42.43333 1.45 S CH AD 00 0 877 Europe/Andorra 1993-12-23
+3039172 Sant Cristòfol Sant Cristofol 42.45 1.5 S SHRN AD 00 0 1614 Europe/Andorra 1993-12-23
+3039173 Sant Cristòfol Sant Cristofol 42.53333 1.53333 S CH AD 00 0 1521 Europe/Andorra 1993-12-23
+3039174 Solà de Sant Cerni Sola de Sant Cerni 42.46667 1.5 T SLP AD 00 0 1383 Europe/Andorra 1993-12-23
+3039175 Eglèsia de Sant Cerni Eglesia de Sant Cerni 42.56667 1.6 S CH AD 00 0 1655 Europe/Andorra 1993-12-23
+3039176 Sant Cerni Sant Cerni 42.46981 1.50133 S CH AD 00 0 1383 Europe/Andorra 2011-04-19
+3039177 Sant Antoni de la Grella Sant Antoni de la Grella 42.53333 1.53333 S CH AD 00 0 1521 Europe/Andorra 1993-12-23
+3039178 Túnels de Sant Antoni Tunels de Sant Antoni 42.51667 1.51667 R TNLS AD 00 0 1265 Europe/Andorra 1993-12-23
+3039179 Pont de Sant Antoni Pont de Sant Antoni 42.51667 1.51667 S BDG AD 00 0 1265 Europe/Andorra 1993-12-23
+3039180 Pont de Santa Creu Pont de Santa Creu 42.56667 1.6 S BDG AD 00 0 1655 Europe/Andorra 1993-12-23
+3039181 Santa Coloma Santa Coloma Santa Coloma 42.5 1.5 P PPL AD 07 0 1135 Europe/Andorra 2011-11-05
+3039182 Santa Caterina Santa Caterina 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3039183 Cortal del Sansa Cortal del Sansa 42.5 1.51667 S CRRL AD 00 0 1410 Europe/Andorra 1993-12-23
+3039184 Portella de Sanfons Portella de Sanfons Portella de Sanfons 42.56667 1.43333 T PASS AD 00 0 2402 Europe/Andorra 2011-11-05
+3039185 Pic de Sanfons Pic de Sanfons Pic de Sanfons 42.58333 1.43333 T PK AD 00 0 2412 Europe/Andorra 2011-11-05
+3039186 Roc de la Salve Roc de la Salve 42.53333 1.6 T RK AD 00 0 1888 Europe/Andorra 1993-12-23
+3039187 Bosc de la Salvata Bosc de la Salvata 42.45 1.51667 V FRST AD 00 0 1790 Europe/Andorra 1993-12-23
+3039188 Solana del Saltader Solana del Saltader 42.56667 1.53333 T SLP AD 00 0 1669 Europe/Andorra 1993-12-23
+3039189 Clot del Saltader Clot del Saltader 42.56667 1.46667 H RVN AD 00 0 1673 Europe/Andorra 1993-12-23
+3039190 Roc del Salt Roc del Salt 42.58333 1.58333 T RK AD 00 0 1993 Europe/Andorra 1993-12-23
+3039191 Clot del Salt Clot del Salt 42.58333 1.6 H RVN AD 00 0 1828 Europe/Andorra 1993-12-23
+3039192 Rius de les Salses Rius de les Salses 42.63333 1.5 H STM AD 00 0 1979 Europe/Andorra 1993-12-23
+3039193 Pont de les Salines Pont de les Salines 42.61667 1.53333 S BDG AD 00 0 1609 Europe/Andorra 1993-12-23
+3039194 Costa de les Salines Costa de les Salines 42.61667 1.53333 T SLP AD 00 0 1609 Europe/Andorra 1993-12-23
+3039195 Bosc de les Salines Bosc de les Salines 42.61667 1.53333 V FRST AD 00 0 1609 Europe/Andorra 1993-12-23
+3039196 Basses de les Salamandres Basses de les Salamandres 42.6 1.66667 H LKS AD 00 0 1858 Europe/Andorra 1993-12-23
+3039197 Costa de la Salamandra Costa de la Salamandra 42.55 1.43333 T SLP AD 00 0 1949 Europe/Andorra 1993-12-23
+3039198 Costa Salamandra Costa Salamandra 42.5 1.48333 T SLP AD 00 0 1316 Europe/Andorra 1993-12-23
+3039199 Roca de la Sabina Roca de la Sabina 42.56667 1.46667 T SLP AD 00 0 1673 Europe/Andorra 1993-12-23
+3039200 Cortal del Sabater Cortal del Sabater 42.45 1.48333 S CRRL AD 00 0 1111 Europe/Andorra 1993-12-23
+3039201 Bosc del Sabater Bosc del Sabater 42.45 1.48333 V FRST AD 00 0 1111 Europe/Andorra 1993-12-23
+3039202 Borda del Sabater Borda del Sabater 42.45 1.48333 S FRM AD 00 0 1111 Europe/Andorra 1993-12-23
+3039203 Canal dels Rulls Canal dels Rulls 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3039204 Canal de Ruixol Canal de Ruixol 42.58333 1.51667 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3039205 Torrent del Ruïder Torrent del Ruider 42.58333 1.48333 H STM AD 00 0 1809 Europe/Andorra 1993-12-23
+3039206 Roc del Ruïder Roc del Ruider 42.58333 1.48333 T SPUR AD 00 0 1809 Europe/Andorra 1993-12-23
+3039207 Pala del Ruf Pala del Ruf 42.58333 1.45 T SLP AD 00 0 2156 Europe/Andorra 1993-12-23
+3039208 Basses del Ruf Basses del Ruf 42.58333 1.45 H LKS AD 00 0 2156 Europe/Andorra 1993-12-23
+3039209 Serrat de Rudielles Serrat de Rudielles 42.5 1.45 T MT AD 00 0 1840 Europe/Andorra 1993-12-23
+3039210 Canal de Rudielles Canal de Rudielles 42.5 1.45 H STM AD 00 0 1840 Europe/Andorra 1993-12-23
+3039211 Vial de Rubials Vial de Rubials 42.48333 1.45 R RD AD 00 0 1195 Europe/Andorra 1993-12-23
+3039212 Torrent de Rubials Torrent de Rubials 42.48333 1.45 H STM AD 00 0 1195 Europe/Andorra 1993-12-23
+3039213 Solana de Rubials Solana de Rubials 42.48333 1.45 T SLP AD 00 0 1195 Europe/Andorra 1993-12-23
+3039214 Rua del Terrer Roi Rua del Terrer Roi 42.43333 1.5 L LCTY AD 00 0 1804 Europe/Andorra 1993-12-23
+3039215 Bosc de Roures Bosc de Roures 42.6 1.51667 V FRST AD 00 0 1445 Europe/Andorra 1993-12-23
+3039216 Collet de Roques Negres Collet de Roques Negres 42.45 1.45 T PASS AD 00 0 1482 Europe/Andorra 1993-12-23
+3039217 Roques Negres Roques Negres 42.45 1.45 L LCTY AD 00 0 1482 Europe/Andorra 1993-12-23
+3039218 Serrat de Roques Grosses Serrat de Roques Grosses 42.58333 1.6 T RDGE AD 00 0 1828 Europe/Andorra 1993-12-23
+3039219 Canal de Roques Blanques Canal de Roques Blanques 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3039220 Fontanal de les Roques Fontanal de les Roques 42.53333 1.46667 H SPNG AD 00 0 1846 Europe/Andorra 1993-12-23
+3039221 Pleta de les Romes Pleta de les Romes 42.65 1.55 L GRAZ AD 00 0 2181 Europe/Andorra 1993-12-23
+3039222 Font Roja Font Roja 42.55 1.45 H SPNG AD 00 0 1788 Europe/Andorra 1993-12-23
+3039223 Font Roja Font Roja 42.48333 1.5 H SPNG AD 00 0 1631 Europe/Andorra 1993-12-23
+3039224 Bassa Roja Bassa Roja 42.6 1.66667 H LK AD 00 0 1858 Europe/Andorra 1993-12-23
+3039225 Grau Roig Grau Roig 42.58333 1.46667 T SLP AD 00 0 1643 Europe/Andorra 1993-12-23
+3039226 Borda de Roig Borda de Roig 42.56667 1.58333 S HUT AD 00 0 1919 Europe/Andorra 1993-12-23
+3039227 Fonts Roges Fonts Roges 42.56667 1.46667 H SPNG AD 00 0 1673 Europe/Andorra 1993-12-23
+3039228 Fonts Roges Fonts Roges 42.55 1.65 H SPNG AD 00 0 2432 Europe/Andorra 1993-12-23
+3039229 Canals Roges Canals Roges 42.58333 1.71667 H RVN AD 00 0 2553 Europe/Andorra 1993-12-23
+3039230 Basses Roges Basses Roges 42.46667 1.55 H LKS AD 00 0 2341 Europe/Andorra 1993-12-23
+3039231 Font Rodona Font Rodona 42.55 1.41667 H SPNG AD 00 0 2105 Europe/Andorra 1993-12-23
+3039232 Costa Rodona Costa Rodona 42.65 1.48333 T SLP AD 00 0 2341 Europe/Andorra 1993-12-23
+3039233 Costa Rodona Costa Rodona 42.58333 1.45 T SLP AD 00 0 2156 Europe/Andorra 1993-12-23
+3039234 Costa Rodona Costa Rodona 42.55 1.71667 T SLP AD 00 0 2192 Europe/Andorra 1993-12-23
+3039235 Boïga Rodona Boiga Rodona 42.43333 1.53333 V CULT AD 00 0 2108 Europe/Andorra 1993-12-23
+3039236 Bosc del Ródol Bosc del Rodol 42.48882 1.57683 V FRST AD 00 0 2246 Europe/Andorra 2011-04-19
+3039237 Turó Rodó Turo Rodo 42.56667 1.55 T PK AD 00 0 1996 Europe/Andorra 1993-12-23
+3039238 Roc Rodó Roc Rodo 42.56667 1.48333 T SPUR AD 00 0 1508 Europe/Andorra 1993-12-23
+3039239 Roc Rodó Roc Rodo 42.56667 1.43333 T SPUR AD 00 0 2402 Europe/Andorra 1993-12-23
+3039240 Pla de Rodó Pla de Rodo 42.55 1.43333 T UPLD AD 00 0 1949 Europe/Andorra 1993-12-23
+3039241 Estany Rodó Estany Rodo 42.51667 1.68333 H LK AD 00 0 2352 Europe/Andorra 1993-12-23
+3039242 Estany Rodó Estany Rodo 42.5 1.65 H LK AD 00 0 2542 Europe/Andorra 1993-12-23
+3039243 Bony Rodó Bony Rodo 42.61667 1.55 T SPUR AD 00 0 2007 Europe/Andorra 1993-12-23
+3039244 Rocs Tous Rocs Tous 42.55 1.58333 L LCTY AD 00 0 1499 Europe/Andorra 1993-12-23
+3039245 Rocs Negres Rocs Negres 42.53333 1.61667 A ADMD AD 00 0 2237 Europe/Andorra 1993-12-23
+3039246 Planell del Roc Gros Planell del Roc Gros 42.58333 1.48333 T UPLD AD 00 0 1809 Europe/Andorra 1993-12-23
+3039247 Bosc del Roc Gros Bosc del Roc Gros 42.58333 1.48333 V FRST AD 00 0 1809 Europe/Andorra 1993-12-23
+3039248 Serra del Roc del Rellotge Serra del Roc del Rellotge 42.61667 1.58333 T MT AD 00 0 2374 Europe/Andorra 1993-12-23
+3039249 Font del Roc del Porquer Font del Roc del Porquer 42.6 1.45 H SPNG AD 00 0 2174 Europe/Andorra 1993-12-23
+3039250 Canal del Roc de la Grael Canal del Roc de la Grael 42.51667 1.53333 H STM AD 00 0 1460 Europe/Andorra 1993-12-23
+3039251 Roca Podrida Roca Podrida 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3039252 Costa de Roca Negra Costa de Roca Negra 42.58333 1.58333 T SLP AD 00 0 1993 Europe/Andorra 1993-12-23
+3039253 Canal de Rocanegra Canal de Rocanegra 42.5 1.46667 H STM AD 00 0 1678 Europe/Andorra 1993-12-23
+3039254 Canal de Roca Major Canal de Roca Major 42.43333 1.5 H RVN AD 00 0 1804 Europe/Andorra 1993-12-23
+3039255 Planell de la Roca Grossa Planell de la Roca Grossa 42.56667 1.7 T UPLD AD 00 0 2375 Europe/Andorra 1993-12-23
+3039256 Planell de Roca Grossa Planell de Roca Grossa 42.55 1.66667 T UPLD AD 00 0 2224 Europe/Andorra 1993-12-23
+3039257 Canal de Rocafort Canal de Rocafort 42.46667 1.48333 H STM AD 00 0 1134 Europe/Andorra 1993-12-23
+3039258 Rocafort Rocafort 42.46667 1.48333 A ADMD AD 00 0 1134 Europe/Andorra 1993-12-23
+3039259 Solana de la Roca de la Sabina Solana de la Roca de la Sabina 42.56667 1.46667 T SLP AD 00 0 1673 Europe/Andorra 1993-12-23
+3039260 Canal de la Roca Blanca Canal de la Roca Blanca 42.51667 1.51667 H STM AD 00 0 1265 Europe/Andorra 1993-12-23
+3039261 Vial de la Roca Vial de la Roca 42.55 1.6 R RD AD 00 0 2210 Europe/Andorra 1993-12-23
+3039262 Pala de la Roca Pala de la Roca 42.55 1.61667 T SLP AD 00 0 2206 Europe/Andorra 1993-12-23
+3039263 Bosc de la Roca Bosc de la Roca 42.55 1.46667 V FRST AD 00 0 1585 Europe/Andorra 1993-12-23
+3039264 Pont del Riu Montaner Pont del Riu Montaner 42.53333 1.51667 S BDG AD 00 0 1361 Europe/Andorra 1993-12-23
+3039265 Camà del Riu del Seig Cami del Riu del Seig 42.56667 1.6 R TRL AD 00 0 1655 Europe/Andorra 1993-12-23
+3039266 Riu del Seig Riu del Seig 42.56667 1.61667 A ADMD AD 00 0 1920 Europe/Andorra 1993-12-23
+3039267 Forat del Riu dels Clots de Massat Forat del Riu dels Clots de Massat 42.55 1.68333 H RVN AD 00 0 2254 Europe/Andorra 1993-12-23
+3039268 Fonts del Riu de les Cebes Fonts del Riu de les Cebes 42.61667 1.58333 H SPNG AD 00 0 2374 Europe/Andorra 1993-12-23
+3039269 Costa del Riu de les Cebes Costa del Riu de les Cebes 42.61667 1.58333 T SLP AD 00 0 2374 Europe/Andorra 1993-12-23
+3039270 Collada del Riu de les Cebes Collada del Riu de les Cebes 42.61667 1.58333 T SPUR AD 00 0 2374 Europe/Andorra 1993-12-23
+3039271 Bony del Riu de les Cebes Bony del Riu de les Cebes 42.61667 1.58333 T RK AD 00 0 2374 Europe/Andorra 1993-12-23
+3039272 Prats del Riu Prats del Riu 42.46667 1.5 L GRAZ AD 00 0 1383 Europe/Andorra 1993-12-23
+3039273 Canal de Rita Canal de Rita 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3039274 Solà del Riguer Sola del Riguer 42.5 1.55 T SLP AD 00 0 1566 Europe/Andorra 1993-12-23
+3039275 Obaga del Riguer Obaga del Riguer 42.48333 1.53333 T SLP AD 00 0 2255 Europe/Andorra 1993-12-23
+3039276 Bordes de Rigoder Bordes de Rigoder 42.53333 1.61667 S HUTS AD 00 0 2237 Europe/Andorra 1993-12-23
+3039277 Bosc de les Ribes Bosc de les Ribes 42.55 1.55 V FRST AD 00 0 2097 Europe/Andorra 1993-12-23
+3039278 Riberal d’Envalira Riberal d'Envalira 42.53333 1.7 L LCTY AD 00 0 2357 Europe/Andorra 1993-12-23
+3039279 Canals de Ribera Canals de Ribera 42.43333 1.48333 H STM AD 00 0 1228 Europe/Andorra 1993-12-23
+3039280 Ribassot Ribassot 42.58333 1.48333 L LCTY AD 00 0 1809 Europe/Andorra 1993-12-23
+3039281 Torrent de Ribassols Torrent de Ribassols 42.58333 1.48333 H STM AD 00 0 1809 Europe/Andorra 1993-12-23
+3039282 Torrent Ribal Torrent Ribal 42.58333 1.48333 H STM AD 00 0 1809 Europe/Andorra 1993-12-23
+3039283 Tosa de Riba Escorjada Tosa de Riba Escorjada 42.55 1.65 T UPLD AD 00 0 2432 Europe/Andorra 1993-12-23
+3039284 Solanelles de Riba Escorjada Solanelles de Riba Escorjada 42.55 1.63333 T SLP AD 00 0 2336 Europe/Andorra 1993-12-23
+3039285 Planells de Riba Escorjada Planells de Riba Escorjada 42.56667 1.63333 T UPLD AD 00 0 2016 Europe/Andorra 1993-12-23
+3039286 Camà de Riba Escorjada Cami de Riba Escorjada 42.56667 1.63333 R TRL AD 00 0 2016 Europe/Andorra 1993-12-23
+3039287 Camà de Riba Escorjada Cami de Riba Escorjada 42.55 1.6 R TRL AD 00 0 2210 Europe/Andorra 1993-12-23
+3039288 Riba Escorjada Riba Escorjada 42.55 1.63333 A ADMD AD 00 0 2336 Europe/Andorra 1993-12-23
+3039289 Bosc de Riba Bosc de Riba 42.55 1.58333 V FRST AD 00 0 1499 Europe/Andorra 1993-12-23
+3039290 Bosc del Riambert Bosc del Riambert 42.56667 1.51667 V FRST AD 00 0 1500 Europe/Andorra 1993-12-23
+3039291 Riu de Rialb Riu de Rialb Riu de Rialb,Riu de Rialp 42.61667 1.53333 H STM AD AD 00 0 1609 Europe/Andorra 2011-11-05
+3039292 Portella de Rialb Portella de Rialb 42.63333 1.53333 T PASS AD 00 0 2072 Europe/Andorra 1993-12-23
+3039293 Basera de Rialb Basera de Rialb 42.65 1.56667 T CLF AD 00 0 2471 Europe/Andorra 1993-12-23
+3039294 Rialb Rialb 42.65 1.55 A ADMD AD 00 0 2181 Europe/Andorra 1993-12-23
+3039295 Pala de Rep Pala de Rep 42.55 1.6 T SLP AD 00 0 2210 Europe/Andorra 1993-12-23
+3039296 Coll del Rep Coll del Rep 42.45 1.46667 T PASS AD 00 0 935 Europe/Andorra 1993-12-23
+3039297 Cap de Rep Cap de Rep Cap de Rep,Cap de l' Ovella,Cap de l’ Ovella 42.54325 1.61092 T PK AD 00 0 2213 Europe/Andorra 2011-11-05
+3039298 Rep Rep 42.53333 1.58333 A ADMD AD 00 0 1571 Europe/Andorra 1993-12-23
+3039299 Roc del Rellotge Roc del Rellotge 42.61667 1.56667 T SPUR AD 00 0 2228 Europe/Andorra 1993-12-23
+3039300 Roc del Rellotge Roc del Rellotge 42.56667 1.61667 T RK AD 00 0 1920 Europe/Andorra 1993-12-23
+3039301 Font de les Reïneres Font de les Reineres 42.6 1.68333 H SPNG AD 00 0 2089 Europe/Andorra 1993-12-23
+3039302 Solana de la RegalÃssia Solana de la Regalissia 42.48333 1.41667 T SLP AD 00 0 1920 Europe/Andorra 1993-12-23
+3039303 Obaga de Redort Obaga de Redort 42.55 1.56667 T SLP AD 00 0 1828 Europe/Andorra 1993-12-23
+3039304 Redort Redort 42.56667 1.55 L LCTY AD 00 0 1996 Europe/Andorra 1993-12-23
+3039305 Clot de la Rectoria Clot de la Rectoria 42.51667 1.5 H RVN AD 00 0 1688 Europe/Andorra 1993-12-23
+3039306 Serrat del Rec d’Areny Serrat del Rec d'Areny 42.58333 1.46667 T SPUR AD 00 0 1643 Europe/Andorra 1993-12-23
+3039307 Bosc de Rèbols Bosc de Rebols 42.46667 1.51667 V FRST AD 00 0 1985 Europe/Andorra 1993-12-23
+3039308 Pont de la Rebollissa Pont de la Rebollissa 42.61667 1.53333 S BDG AD 00 0 1609 Europe/Andorra 1993-12-23
+3039309 Llanesques de les Rebes Llanesques de les Rebes 42.63333 1.61667 T CLF AD 00 0 2541 Europe/Andorra 1993-12-23
+3039310 Clot de les Rebes Clot de les Rebes 42.61667 1.61667 H RVN AD 00 0 2352 Europe/Andorra 1993-12-23
+3039311 Rebaixant del Maià Rebaixant del Maia 42.56667 1.73333 L LCTY AD 00 0 2096 Europe/Andorra 1993-12-23
+3039312 Canal de la Rata Canal de la Rata 42.58333 1.46667 H RVN AD 00 0 1643 Europe/Andorra 1993-12-23
+3039313 Port de Rat Port de Rat Port de Rat,Port de Rat du D'Auzat 42.62127 1.47371 T PASS AD FR,AD 07 0 2442 Europe/Andorra 2007-03-04
+3039314 Bosc de Rasets Bosc de Rasets 42.43333 1.53333 V FRST AD 00 0 2108 Europe/Andorra 1993-12-23
+3039315 Rasa de Perafita Rasa de Perafita 42.48333 1.58333 T UPLD AD 00 0 2349 Europe/Andorra 1993-12-23
+3039316 Costa Rasa Costa Rasa 42.46667 1.45 T SLP AD 00 0 1562 Europe/Andorra 1993-12-23
+3039317 Bosc de la Rasa Bosc de la Rasa 42.46667 1.53333 V FRST AD 00 0 2332 Europe/Andorra 1993-12-23
+3039318 Presa de Ransol Presa de Ransol 42.58333 1.63333 S DAM AD 00 0 1722 Europe/Andorra 1993-12-23
+3039319 Carretera de Ransol Carretera de Ransol 42.58333 1.65 R RD AD 00 0 1767 Europe/Andorra 1993-12-23
+3039320 Ransol Ransol 42.58137 1.63812 P PPL AD 02 0 1727 Europe/Andorra 2007-04-16
+3039321 Roca de RÃ mio Roca de Ramio 42.5 1.56667 T RK AD 00 0 1776 Europe/Andorra 1993-12-23
+3039322 RÃ mio Ramio 42.49702 1.57414 S HUTS AD 00 0 1776 Europe/Andorra 2011-04-19
+3039323 Canal del Ramer Canal del Ramer 42.56667 1.48333 H RVN AD 00 0 1508 Europe/Andorra 1993-12-23
+3039324 Canal de la Ramenada Canal de la Ramenada 42.5 1.51667 H STM AD 00 0 1410 Europe/Andorra 1993-12-23
+3039325 Camà Ral Cami Ral 42.55 1.58333 R TRL AD 00 0 1499 Europe/Andorra 1993-12-23
+3039326 Camà Ral Cami Ral 42.53333 1.58333 R TRL AD 00 0 1571 Europe/Andorra 1993-12-23
+3039327 Radonella Radonella 42.48333 1.45 L LCTY AD 00 0 1195 Europe/Andorra 1993-12-23
+3039328 Rádio Andorra Radio Andorra 42.5282 1.57019 S STNR AD 00 0 1418 Europe/Andorra 2011-04-19
+3039329 Racons Racons 42.56667 1.58333 A ADMD AD 00 0 1919 Europe/Andorra 1993-12-23
+3039330 Pic de la Raconada de la Maiana Pic de la Raconada de la Maiana Pic de la Raconada de la Maiana 42.46667 1.61667 T PK AD 00 0 2448 Europe/Andorra 2011-11-05
+3039331 Clots de la Raconada de la Maiana Clots de la Raconada de la Maiana 42.46667 1.6 T CRQS AD 00 0 2449 Europe/Andorra 1993-12-23
+3039332 Pic de Racofred Pic de Racofred Pic de Racofred,Pic de Racofret 42.6 1.45 T PK AD 00 0 2174 Europe/Andorra 2011-11-05
+3039333 Racó de l’Estany de Cabana Sorda Raco de l'Estany de Cabana Sorda 42.61667 1.66667 L LCTY AD 00 0 2536 Europe/Andorra 1993-12-23
+3039334 Pleta del Racó Pleta del Raco 42.61667 1.56667 L GRAZ AD 00 0 2228 Europe/Andorra 1993-12-23
+3039335 Pleta del Racó Pleta del Raco 42.58333 1.45 L GRAZ AD 00 0 2156 Europe/Andorra 1993-12-23
+3039336 Canal del Racó Canal del Raco 42.56667 1.48333 H RVN AD 00 0 1508 Europe/Andorra 1993-12-23
+3039337 Bassa del Racó Bassa del Raco 42.61667 1.5 H LK AD 00 0 2390 Europe/Andorra 1993-12-23
+3039338 Pleta de la Rabassa Pleta de la Rabassa 42.63333 1.55 L GRAZ AD 00 0 2053 Europe/Andorra 1993-12-23
+3039339 Carretera de la Rabassa Carretera de la Rabassa 42.43333 1.51667 R RD AD 00 0 2031 Europe/Andorra 1993-12-23
+3039340 Canya de la Rabassa Canya de la Rabassa 42.63333 1.55 S CAVE AD 00 0 2053 Europe/Andorra 1993-12-23
+3039341 Bosc de la Rabassa Bosc de la Rabassa 42.4379 1.51425 V FRST AD 00 0 1990 Europe/Andorra 2011-04-19
+3039342 Solana del Querol Solana del Querol 42.58333 1.53333 T SLP AD 00 0 1924 Europe/Andorra 1993-12-23
+3039343 Riu del Querol Riu del Querol 42.58333 1.66667 H STM AD 00 0 2159 Europe/Andorra 1993-12-23
+3039344 Riu del Querol Riu del Querol 42.58333 1.53333 H STM AD 00 0 1924 Europe/Andorra 1993-12-23
+3039345 Pleta del Querol Pleta del Querol 42.6 1.66667 L GRAZ AD 00 0 1858 Europe/Andorra 1993-12-23
+3039346 Estanyó del Querol Estanyo del Querol 42.61303 1.67019 H LK AD 00 0 2355 Europe/Andorra 2011-04-19
+3039347 Serrat de la Quera Serrat de la Quera 42.51667 1.51667 T RDGE AD 00 0 1265 Europe/Andorra 1993-12-23
+3039348 Canal Gran de la Quera Canal Gran de la Quera 42.45 1.46667 H STM AD 00 0 935 Europe/Andorra 1993-12-23
+3039349 Bosc de la Quera Bosc de la Quera 42.51667 1.51667 V FRST AD 00 0 1265 Europe/Andorra 1993-12-23
+3039350 Roc del Quer Roc del Quer 42.61667 1.55 T SPUR AD 00 0 2007 Europe/Andorra 1993-12-23
+3039351 Roc del Quer Roc del Quer 42.55 1.51667 T SPUR AD 00 0 1397 Europe/Andorra 1993-12-23
+3039352 Roc del Quer Roc del Quer 42.58333 1.48333 T RK AD 00 0 1809 Europe/Andorra 1993-12-23
+3039353 Roc del Quer Roc del Quer 42.52798 1.60146 T RK AD 00 0 1888 Europe/Andorra 2011-04-19
+3039354 Roc del Quer Roc del Quer 42.56667 1.6 T CLF AD 00 0 1655 Europe/Andorra 1993-12-23
+3039355 Roc de Quer Roc de Quer 42.48333 1.46667 T RK AD 00 0 1148 Europe/Andorra 1993-12-23
+3039356 Planell del Quer Planell del Quer 42.63333 1.56667 T UPLD AD 00 0 2394 Europe/Andorra 1993-12-23
+3039357 Canal del Quer Canal del Quer 42.63333 1.55 H RVN AD 00 0 2053 Europe/Andorra 1993-12-23
+3039358 Canal del Quer Canal del Quer 42.51667 1.6 H RVN AD 00 0 2085 Europe/Andorra 1993-12-23
+3039359 Bosc del Quer Bosc del Quer 42.51667 1.6 V FRST AD 00 0 2085 Europe/Andorra 1993-12-23
+3039360 Solà del Quart de Nagol Sola del Quart de Nagol 42.48333 1.51667 T SLP AD 00 0 2061 Europe/Andorra 1993-12-23
+3039361 Collet Purgat Collet Purgat 42.46667 1.48333 T PK AD 00 0 1134 Europe/Andorra 1993-12-23
+3039363 Pont de Puntal Pont de Puntal 42.61667 1.55 S BDG AD 00 0 2007 Europe/Andorra 1993-12-23
+3039364 Bosc de Puntal Bosc de Puntal 42.63333 1.55 V FRST AD 00 0 2053 Europe/Andorra 1993-12-23
+3039365 Puntal Puntal 42.63333 1.55 L LCTY AD 00 0 2053 Europe/Andorra 1993-12-23
+3039366 Vial dels Pujols Vial dels Pujols 42.48333 1.48333 R RD AD 00 0 981 Europe/Andorra 1993-12-23
+3039367 Tarteres dels Pujols Tarteres dels Pujols 42.48333 1.48333 T TAL AD 00 0 981 Europe/Andorra 1993-12-23
+3039368 Roc del Pujol Roc del Pujol 42.46667 1.5 T RK AD 00 0 1383 Europe/Andorra 1993-12-23
+3039369 Pujant de Donges Pujant de Donges 42.45 1.5 L LCTY AD 00 0 1614 Europe/Andorra 1993-12-23
+3039370 Puiol del Piu Puiol del Piu 42.56667 1.5 P PPL AD 04 0 1636 Europe/Andorra 1993-12-23
+3039371 Borda del Puigcernal Borda del Puigcernal 42.55 1.58333 S HUT AD 00 0 1499 Europe/Andorra 1993-12-23
+3039372 Pui d’Olivesa Pui d'Olivesa 42.45 1.48333 L LCTY AD 00 0 1111 Europe/Andorra 1993-12-23
+3039373 Pui d’Encamp Pui d'Encamp 42.53333 1.56667 L LCTY AD 00 0 1418 Europe/Andorra 1993-12-23
+3039374 Estany Primer Estany Primer 42.63721 1.49019 H LK AD 05 0 2254 Europe/Andorra 2010-01-12
+3039375 Estany Primer Estany Primer 42.61667 1.71667 H LK AD 00 0 2352 Europe/Andorra 1993-12-23
+3039376 Estany Primer Estany Primer 42.51667 1.68333 H LK AD 00 0 2352 Europe/Andorra 1993-12-23
+3039377 Canal de la Presa Canal de la Presa 42.58333 1.63333 H CNL AD 00 0 1722 Europe/Andorra 1993-12-23
+3039378 Canal de la Premsa Canal de la Premsa 42.46667 1.48333 H STM AD 00 0 1134 Europe/Andorra 1993-12-23
+3039379 Canal Pregona Canal Pregona 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3039380 Canal Pregona Canal Pregona 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3039381 Torrent Pregó Torrent Prego 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3039382 Bosc dels Prats Sobirans Bosc dels Prats Sobirans 42.51667 1.46667 V FRST AD 00 0 1840 Europe/Andorra 1993-12-23
+3039383 Planells dels Prats Nous Planells dels Prats Nous 42.58333 1.48333 T UPLD AD 00 0 1809 Europe/Andorra 1993-12-23
+3039384 Bordes dels Prats Nous Bordes dels Prats Nous 42.58333 1.48333 S HUTS AD 00 0 1809 Europe/Andorra 1993-12-23
+3039385 Riu de Prats Riu de Prats 42.55 1.58333 H STM AD 00 0 1499 Europe/Andorra 1993-12-23
+3039386 Prats Prats 42.56003 1.59396 P PPL AD 02 0 1677 Europe/Andorra 2007-04-16
+3039387 Solana de Prat Primer Solana de Prat Primer 42.48333 1.55 T SLP AD 00 0 2233 Europe/Andorra 1993-12-23
+3039388 Planell de Prat Primer Planell de Prat Primer 42.48333 1.55 T UPLD AD 00 0 2233 Europe/Andorra 1993-12-23
+3039389 Obaga de Prat Primer Obaga de Prat Primer 42.48333 1.55 T SLP AD 00 0 2233 Europe/Andorra 1993-12-23
+3039390 Font de Prat Primer Font de Prat Primer 42.48333 1.55 H SPNG AD 00 0 2233 Europe/Andorra 1993-12-23
+3039391 Collada de Prat Primer Collada de Prat Primer 42.46667 1.55 T PASS AD 00 0 2341 Europe/Andorra 1993-12-23
+3039392 Clots de Prat Primer Clots de Prat Primer 42.48333 1.55 H RVN AD 00 0 2233 Europe/Andorra 1993-12-23
+3039393 Prat Primer Prat Primer 42.48333 1.55 A ADMD AD 00 0 2233 Europe/Andorra 1993-12-23
+3039394 Collada de Prat Porceller Collada de Prat Porceller Collada de Prat Porceller 42.46667 1.45 T PASS AD 00 0 1562 Europe/Andorra 2011-11-05
+3039395 Font del Prat de Roca Font del Prat de Roca 42.56667 1.58333 H SPNG AD 00 0 1919 Europe/Andorra 1993-12-23
+3039396 Font del Prat dels Pollins Font del Prat dels Pollins 42.56667 1.58333 H SPNG AD 00 0 1919 Europe/Andorra 1993-12-23
+3039397 Basers del Prat del Quart Basers del Prat del Quart 42.61667 1.63333 T CLF AD 00 0 2331 Europe/Andorra 1993-12-23
+3039398 Font del Prat del Jep Font del Prat del Jep Font del Prat del Gep,Font del Prat del Jep 42.56667 1.58333 H SPNG AD AD 00 0 1919 Europe/Andorra 2011-11-05
+3039399 Torrent del Prat del Gaspar Torrent del Prat del Gaspar 42.53333 1.56667 H STM AD 00 0 1418 Europe/Andorra 1993-12-23
+3039400 Bosc del Prat de l’Estel Bosc del Prat de l'Estel 42.53333 1.46667 V FRST AD 00 0 1846 Europe/Andorra 1993-12-23
+3039401 Riu del Prat del Comellar Riu del Prat del Comellar 42.58333 1.65 H STM AD 00 0 1767 Europe/Andorra 1993-12-23
+3039402 Solana del Prat del Bosc Solana del Prat del Bosc 42.58333 1.46667 T SLP AD 00 0 1643 Europe/Andorra 1993-12-23
+3039403 Riu del Prat del Bosc Riu del Prat del Bosc 42.55 1.48333 H STM AD 00 0 1548 Europe/Andorra 1993-12-23
+3039404 Pont del Prat del Bosc Pont del Prat del Bosc 42.53333 1.46667 S BDG AD 00 0 1846 Europe/Andorra 1993-12-23
+3039405 Bosc del Prat del Bosc Bosc del Prat del Bosc 42.53333 1.46667 V FRST AD 00 0 1846 Europe/Andorra 1993-12-23
+3039406 Obagues del Prat de la Posella Obagues del Prat de la Posella 42.48333 1.45 T SLP AD 00 0 1195 Europe/Andorra 1993-12-23
+3039407 Basera del Prat de la Farga Basera del Prat de la Farga 42.48333 1.45 T CLF AD 00 0 1195 Europe/Andorra 1993-12-23
+3039408 Basers del Prat de la Creu Basers del Prat de la Creu 42.6 1.63333 T CLF AD 00 0 1893 Europe/Andorra 1993-12-23
+3039409 Obaga dels Pradets Obaga dels Pradets 42.48333 1.43333 T SLP AD 00 0 1938 Europe/Andorra 1993-12-23
+3039410 Roc dels Pous Roc dels Pous 42.55 1.61667 T RK AD 00 0 2206 Europe/Andorra 1993-12-23
+3039411 Bosc dels Pous Bosc dels Pous 42.55 1.48333 V FRST AD 00 0 1548 Europe/Andorra 1993-12-23
+3039412 Serrat del Pouet Serrat del Pouet 42.55 1.51667 T RDGE AD 00 0 1397 Europe/Andorra 1993-12-23
+3039413 Canal del Pouet Canal del Pouet 42.56667 1.53333 H STM AD 00 0 1669 Europe/Andorra 1993-12-23
+3039414 Costa del Pou Costa del Pou 42.58333 1.58333 T SLP AD 00 0 1993 Europe/Andorra 1993-12-23
+3039415 Bosc de les Poselletes Bosc de les Poselletes 42.55 1.45 V FRST AD 00 0 1788 Europe/Andorra 1993-12-23
+3039416 Bony de la Posella Bony de la Posella 42.58333 1.48333 T SPUR AD 00 0 1809 Europe/Andorra 1993-12-23
+3039417 Posada dels Pastors Posada dels Pastors 42.58333 1.48333 L LCTY AD 00 0 1809 Europe/Andorra 1993-12-23
+3039418 Font de la Posada Font de la Posada 42.46667 1.45 H SPNG AD 00 0 1562 Europe/Andorra 1993-12-23
+3039419 Serrat de la Posa Serrat de la Posa 42.55 1.66667 T RDGE AD 00 0 2224 Europe/Andorra 1993-12-23
+3039420 Turó del Port Vell Turo del Port Vell 42.65 1.56667 T PK AD 00 0 2471 Europe/Andorra 1993-12-23
+3039421 Pic del Port Vell Pic del Port Vell Pic del Port Vell 42.57205 1.44341 T PK AD 00 0 2655 2305 Europe/Andorra 2011-02-09
+3039422 Canal del Port Vell Canal del Port Vell 42.56667 1.45 H STM AD 00 0 2137 Europe/Andorra 1993-12-23
+3039423 Clot de Port Negre Clot de Port Negre 42.46667 1.56667 H RVN AD 00 0 2365 Europe/Andorra 1993-12-23
+3039424 Basers de les Portes Basers de les Portes 42.48333 1.5 T CLF AD 00 0 1631 Europe/Andorra 1993-12-23
+3039425 Portella de la Portelleta Portella de la Portelleta 42.46667 1.65 T PASS AD 00 0 2700 Europe/Andorra 1993-12-23
+3039426 Tossa Plana de Lles Tossa Plana de Lles Pic de la Portelleta,Tosal Plane,Tossa Plana de Lles 42.46667 1.66667 T PK AD 00 0 2559 Europe/Andorra 2011-11-05
+3039427 Font de la Portelleta Font de la Portelleta 42.46667 1.66667 H SPNG AD 00 0 2559 Europe/Andorra 1993-12-23
+3039428 Collada de la Portelleta Collada de la Portelleta Collada de la Portelleta 42.46667 1.66667 T PASS AD 00 0 2559 Europe/Andorra 2011-11-05
+3039429 Riu de les Portelles Riu de les Portelles 42.61667 1.63333 H STM AD 00 0 2331 Europe/Andorra 1993-12-23
+3039430 Clots de la Portella de Setut Clots de la Portella de Setut 42.46667 1.63333 T CRQS AD 00 0 2619 Europe/Andorra 1993-12-23
+3039431 Pleta de la Portella Pleta de la Portella 42.56667 1.71667 L GRAZ AD 00 0 2219 Europe/Andorra 1993-12-23
+3039432 Collada de la Portella Collada de la Portella 42.46667 1.63333 T PASS AD 00 0 2619 Europe/Andorra 1993-12-23
+3039433 Riu del Port Dret Riu del Port Dret 42.6 1.46667 H STM AD 00 0 2421 Europe/Andorra 1993-12-23
+3039434 Estany del Port Dret Estany del Port Dret 42.6 1.46667 H LK AD 00 0 2421 Europe/Andorra 1993-12-23
+3039435 Costa del Port Dret Costa del Port Dret 42.58333 1.7 T SLP AD 00 0 2584 Europe/Andorra 1993-12-23
+3039436 Clots del Port Dret Clots del Port Dret 42.56667 1.68333 H RVN AD 00 0 2340 Europe/Andorra 1993-12-23
+3039437 Camà del Port Dret Cami del Port Dret 42.56667 1.66667 R TRL AD 00 0 1938 Europe/Andorra 1993-12-23
+3039439 Camà del Port de Setut Cami del Port de Setut 42.46667 1.63333 R TRL AD 00 0 2619 Europe/Andorra 1993-12-23
+3039440 Basses del Port de Rat Basses del Port de Rat 42.61667 1.48333 H LKS AD 05 0 2470 Europe/Andorra 2010-01-12
+3039441 Font del Port de Cabús Font del Port de Cabus 42.55 1.41667 H SPNG AD 00 0 2105 Europe/Andorra 1993-12-23
+3039442 Planades del Port Planades del Port 42.56667 1.45 T UPLD AD 00 0 2137 Europe/Andorra 1993-12-23
+3039443 Costa del Port Costa del Port 42.56667 1.45 T SLP AD 00 0 2137 Europe/Andorra 1993-12-23
+3039444 Costa del Port Costa del Port 42.53333 1.71667 T SLP AD 00 0 2400 Europe/Andorra 1993-12-23
+3039445 Clots del Port Clots del Port 42.46667 1.58333 T CRQS AD 00 0 2367 Europe/Andorra 1993-12-23
+3039446 Clot del Port Clot del Port 42.48333 1.65 H RVN AD 00 0 2658 Europe/Andorra 1993-12-23
+3039447 Canal del Port Canal del Port 42.56667 1.45 H STM AD 00 0 2137 Europe/Andorra 1993-12-23
+3039448 Camà del Port Cami del Port 42.48333 1.58333 R TRL AD 00 0 2349 Europe/Andorra 1993-12-23
+3039449 Roc del Porquer Roc del Porquer 42.58333 1.45 T SPUR AD 00 0 2156 Europe/Andorra 1993-12-23
+3039450 Oratori del Pont d’Aixovall Oratori del Pont d'Aixovall 42.48333 1.5 S AMTH AD 00 0 1631 Europe/Andorra 1993-12-23
+3039451 Canal del Pont Canal del Pont 42.5 1.55 H STM AD 00 0 1566 Europe/Andorra 1993-12-23
+3039452 Riu Pollós Riu Pollos 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3039453 Bosc de la Pollentia Bosc de la Pollentia 42.53333 1.5 V FRST AD 00 0 1357 Europe/Andorra 1993-12-23
+3039454 Roc del Poll Roc del Poll 42.61667 1.53333 T RK AD 00 0 1609 Europe/Andorra 1993-12-23
+3039455 Roc de Podoïna Roc de Podoina 42.45 1.5 T RK AD 00 0 1614 Europe/Andorra 1993-12-23
+3039456 Canal del Pletiu Canal del Pletiu 42.5 1.58333 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3039457 Serrat de les Pletes Serrat de les Pletes 42.48333 1.43333 T SPUR AD 00 0 1938 Europe/Andorra 1993-12-23
+3039458 Serrat de la Pleta Vella Serrat de la Pleta Vella 42.6 1.55 T RDGE AD 00 0 2298 Europe/Andorra 1993-12-23
+3039459 Canal de Pleta Mosquera Canal de Pleta Mosquera 42.61667 1.51667 H STM AD 00 0 1716 Europe/Andorra 1993-12-23
+3039460 Canal de la Pleta dels Llacs Canal de la Pleta dels Llacs 42.6 1.63333 H STM AD 00 0 1893 Europe/Andorra 1993-12-23
+3039461 Solana de la Pleta del Perro Solana de la Pleta del Perro 42.53333 1.61667 T SLP AD 00 0 2237 Europe/Andorra 1993-12-23
+3039462 Canals de la Pleta del Llomar Canals de la Pleta del Llomar 42.61667 1.56667 H RVN AD 00 0 2228 Europe/Andorra 1993-12-23
+3039463 Bony de la Pleta de Jan Bony de la Pleta de Jan 42.61667 1.63333 T MT AD 00 0 2331 Europe/Andorra 1993-12-23
+3039464 Roc de la Pleta Roc de la Pleta 42.6 1.46667 T RK AD 00 0 2421 Europe/Andorra 1993-12-23
+3039465 Costa de la Pleta Costa de la Pleta 42.58333 1.45 T SLP AD 00 0 2156 Europe/Andorra 1993-12-23
+3039466 Bosc de la Pleta Bosc de la Pleta 42.55 1.45 V FRST AD 00 0 1788 Europe/Andorra 1993-12-23
+3039467 Barraca de la Pleta Barraca de la Pleta 42.56667 1.71667 S HUT AD 00 0 2219 Europe/Andorra 1993-12-23
+3039468 Camà dels Plans Cami dels Plans 42.58333 1.61667 R TRL AD 00 0 1707 Europe/Andorra 1993-12-23
+3039469 Camà dels Plans Cami dels Plans 42.53333 1.51667 R TRL AD 00 0 1361 Europe/Andorra 1993-12-23
+3039470 Bosc dels Plans Bosc dels Plans 42.58333 1.63333 V FRST AD 00 0 1722 Europe/Andorra 1993-12-23
+3039471 Bordes dels Plans Bordes dels Plans 42.53333 1.51667 S FRM AD 00 0 1361 Europe/Andorra 1993-12-23
+3039472 Obaga dels Plannels de la RegalÃssia Obaga dels Plannels de la Regalissia 42.48333 1.41667 T SLP AD 00 0 1920 Europe/Andorra 1993-12-23
+3039473 Planes de Fels Planes de Fels 42.58333 1.53333 L LCTY AD 00 0 1924 Europe/Andorra 1993-12-23
+3039474 Solana de les Planes Solana de les Planes 42.56667 1.53333 T SLP AD 00 0 1669 Europe/Andorra 1993-12-23
+3039475 Riu de les Planes Riu de les Planes 42.63333 1.5 H STM AD 00 0 1979 Europe/Andorra 1993-12-23
+3039476 Pleta de les Planes Pleta de les Planes 42.65 1.5 L GRAZ AD 00 0 2455 Europe/Andorra 1993-12-23
+3039477 Pic de l' Albeille Pic de l' Albeille Pic de les Planes 42.6457 1.49929 T RDGE AD FR,AD 07 0 2542 Europe/Andorra 2007-03-04
+3039478 Les Planes Les Planes 42.56667 1.55 T UPLD AD 00 0 1996 Europe/Andorra 1993-12-23
+3039479 Collet de les Planes Collet de les Planes 42.56667 1.53333 T SPUR AD 00 0 1669 Europe/Andorra 1993-12-23
+3039480 Collada de les Planes Collada de les Planes 42.65 1.5 T PASS AD 00 0 2455 Europe/Andorra 1993-12-23
+3039481 Bosc de les Planes Bosc de les Planes 42.51667 1.6 V FRST AD 00 0 2085 Europe/Andorra 1993-12-23
+3039482 Bony de les Planes Bony de les Planes 42.55 1.51667 T SPUR AD 00 0 1397 Europe/Andorra 1993-12-23
+3039483 Serrat dels Planells Grans Serrat dels Planells Grans 42.53333 1.53333 T RDGE AD 00 0 1521 Europe/Andorra 1993-12-23
+3039484 Cabana dels Planells de Rialb Cabana dels Planells de Rialb 42.65 1.55 S HUT AD 00 0 2181 Europe/Andorra 1993-12-23
+3039485 Planells de Rialb Planells de Rialb 42.65 1.55 L LCTY AD 00 0 2181 Europe/Andorra 1993-12-23
+3039486 Riu dels Planells de Caraup Riu dels Planells de Caraup 42.6 1.63333 H STM AD 00 0 1893 Europe/Andorra 1993-12-23
+3039487 Planells d’ArcalÃs Planells d'Arcalis 42.63333 1.5 L LCTY AD 00 0 1979 Europe/Andorra 1993-12-23
+3039488 Serrat del Planell Lluent Serrat del Planell Lluent 42.56667 1.51667 T RDGE AD 00 0 1500 Europe/Andorra 1993-12-23
+3039489 Bony del Planell Gran Bony del Planell Gran 42.63333 1.55 T SPUR AD 00 0 2053 Europe/Andorra 1993-12-23
+3039490 Bosc de Planavilla Bosc de Planavilla 42.55 1.68333 V FRST AD 00 0 2254 Europe/Andorra 1993-12-23
+3039491 Planavilla Planavilla 42.55 1.68333 L CLG AD 00 0 2254 Europe/Andorra 1993-12-23
+3039492 Bosc de la Planassa Bosc de la Planassa 42.55 1.5 V FRST AD 00 0 1292 Europe/Andorra 1993-12-23
+3039493 Bosc de Plana en Blanca Bosc de Plana en Blanca 42.55 1.56667 V FRST AD 00 0 1828 Europe/Andorra 1993-12-23
+3039494 Tarteres de Plana de Gral Tarteres de Plana de Gral 42.58333 1.48333 T TAL AD 00 0 1809 Europe/Andorra 1993-12-23
+3039495 Tosa Plana Tosa Plana 42.46667 1.6 T UPLD AD 00 0 2449 Europe/Andorra 1993-12-23
+3039496 Torrent de la Plana Torrent de la Plana 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3039497 Serra Plana Serra Plana 42.58333 1.6 T SPUR AD 00 0 1828 Europe/Andorra 1993-12-23
+3039498 Serra Plana Serra Plana 42.56667 1.48333 T MT AD 00 0 1508 Europe/Andorra 1993-12-23
+3039499 Sierra Plana Sierra Plana Serra Plana,Sierra Plana 42.48333 1.43333 T MT AD 00 0 1938 Europe/Andorra 2011-11-05
+3039500 Cortal de la Plana Cortal de la Plana 42.48333 1.53333 S HUT AD 00 0 2255 Europe/Andorra 1993-12-23
+3039501 Coll de la Plana Coll de la Plana 42.45 1.5 T SPUR AD 00 0 1614 Europe/Andorra 1993-12-23
+3039502 Camà de la Plana Cami de la Plana 42.48333 1.55 R TRL AD 00 0 2233 Europe/Andorra 1993-12-23
+3039503 Bosquet de la Plana Bosquet de la Plana 42.5 1.53333 V FRST AD 00 0 1574 Europe/Andorra 1993-12-23
+3039504 Bosc de la Plana Bosc de la Plana 42.55 1.55 V FRST AD 00 0 2097 Europe/Andorra 1993-12-23
+3039505 Bordes de la Plana Bordes de la Plana 42.53333 1.6 S HUTS AD 00 0 1888 Europe/Andorra 1993-12-23
+3039506 Borda de la Plana Borda de la Plana 42.55 1.55 S HUTS AD 00 0 2097 Europe/Andorra 1993-12-23
+3039507 Barranc de la Plana Barranc de la Plana 42.55 1.55 H STM AD 00 0 2097 Europe/Andorra 1993-12-23
+3039508 Serrat del Pla Morell Serrat del Pla Morell 42.53333 1.53333 T RDGE AD 00 0 1521 Europe/Andorra 1993-12-23
+3039509 Torrent de Plamanera Torrent de Plamanera 42.53333 1.56667 H STM AD 00 0 1418 Europe/Andorra 1993-12-23
+3039510 Bosc del Pla de Rodó Bosc del Pla de Rodo 42.55 1.43333 V FRST AD 00 0 1949 Europe/Andorra 1993-12-23
+3039511 Bosc del Pla de Miretes Bosc del Pla de Miretes 42.53333 1.48333 V FRST AD 00 0 1677 Europe/Andorra 1993-12-23
+3039512 Pic del Pla de l’Ingla Pic del Pla de l'Ingla 42.48333 1.63333 T PK AD 00 0 2296 Europe/Andorra 1993-12-23
+3039513 Pla de l’Ingla Pla de l'Ingla 42.48333 1.63333 A ADMD AD 00 0 2296 Europe/Andorra 1993-12-23
+3039514 Riu del Pla de l’Estany Riu del Pla de l'Estany 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3039515 Pleta del Pla de l’Estany Pleta del Pla de l'Estany 42.58333 1.46667 L GRAZ AD 00 0 1643 Europe/Andorra 1993-12-23
+3039516 Pic des Bareytes Pic des Bareytes Pic del Pla de l'Estany,Pic del Pla de l’Estany,Pic des Bareytes 42.6 1.46667 T PK AD 00 0 2421 Europe/Andorra 2011-11-05
+3039517 Estret del Pla de l’Estany Estret del Pla de l'Estany 42.6 1.45 T PASS AD 00 0 2174 Europe/Andorra 1993-12-23
+3039518 Pla de l’Estany Pla de l'Estany 42.6 1.45 A ADMD AD 00 0 2174 Europe/Andorra 1993-12-23
+3039519 Camà del Pla de les Pedres Cami del Pla de les Pedres 42.53333 1.68333 R TRL AD 00 0 2322 Europe/Andorra 1993-12-23
+3039520 Camà del Pla del Bosc Cami del Pla del Bosc 42.53333 1.61667 R TRL AD 00 0 2237 Europe/Andorra 1993-12-23
+3039521 Bosc del Pla de la Cot Bosc del Pla de la Cot 42.53333 1.46667 V FRST AD 00 0 1846 Europe/Andorra 1993-12-23
+3039522 Grau de les Places Grau de les Places 42.55 1.46667 T SLP AD 00 0 1585 Europe/Andorra 1993-12-23
+3039523 Pleta de Pixolell Pleta de Pixolell 42.58333 1.63333 L GRAZ AD 00 0 1722 Europe/Andorra 1993-12-23
+3039524 Costes de Pixolell Costes de Pixolell 42.58333 1.63333 T SLP AD 00 0 1722 Europe/Andorra 1993-12-23
+3039525 Pont de la Pixistella Pont de la Pixistella 42.56667 1.5 S BDG AD 00 0 1636 Europe/Andorra 1993-12-23
+3039526 Obac de la Pixistella Obac de la Pixistella 42.55 1.5 T SLP AD 00 0 1292 Europe/Andorra 1993-12-23
+3039527 Canal de la Pixistella Canal de la Pixistella 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3039528 Bosc de la Pixistella Bosc de la Pixistella 42.55 1.48333 V FRST AD 00 0 1548 Europe/Andorra 1993-12-23
+3039529 Font Pixadera Font Pixadera 42.5 1.56667 H SPNG AD 00 0 1776 Europe/Andorra 1993-12-23
+3039530 Roca del Pisó Roca del Piso 42.53333 1.63333 T RK AD 00 0 2360 Europe/Andorra 1993-12-23
+3039531 Borda del Pirot Borda del Pirot 42.58333 1.65 S HUT AD 00 0 1767 Europe/Andorra 1993-12-23
+3039532 Pirineu Pirineu 42.51667 1.55 L LCTY AD 00 0 1322 Europe/Andorra 1993-12-23
+3039533 Bosc de la Pinosa de Llumeneres Bosc de la Pinosa de Llumeneres 42.46667 1.51667 V FRST AD 00 0 1985 Europe/Andorra 1993-12-23
+3039534 Bosc de la Pinosa Bosc de la Pinosa 42.6 1.68333 V FRST AD 00 0 2089 Europe/Andorra 1993-12-23
+3039535 Bosc de la Pinosa Bosc de la Pinosa 42.45 1.5 V FRST AD 00 0 1614 Europe/Andorra 1993-12-23
+3039536 Serrat Pinós Serrat Pinos 42.55 1.68333 T MT AD 00 0 2254 Europe/Andorra 1993-12-23
+3039537 Collada de Pimes Collada de Pimes 42.42951 1.54658 T PASS AD 00 0 2186 Europe/Andorra 2011-04-19
+3039538 Canal del Pi Gros Canal del Pi Gros 42.5 1.46667 H STM AD 00 0 1678 Europe/Andorra 1993-12-23
+3039539 Borda del Piedro Borda del Piedro 42.58333 1.63333 S HUT AD 00 0 1722 Europe/Andorra 1993-12-23
+3039540 Bosc del Pi de Montsalla Bosc del Pi de Montsalla 42.53333 1.48333 V FRST AD 00 0 1677 Europe/Andorra 1993-12-23
+3039541 Camà del Pi de la Creu Cami del Pi de la Creu 42.55 1.6 R TRL AD 00 0 2210 Europe/Andorra 1993-12-23
+3039542 Canal dels Picons Canal dels Picons 42.56667 1.5 H STM AD 00 0 1636 Europe/Andorra 1993-12-23
+3039543 Bosc del Picó Bosc del Pico 42.5 1.55 V FRST AD 00 0 1566 Europe/Andorra 1993-12-23
+3039544 Basers del Pic de la Cabaneta Basers del Pic de la Cabaneta 42.61667 1.6 T CLF AD 00 0 2528 Europe/Andorra 1993-12-23
+3039545 Font Picadora Font Picadora 42.56667 1.56667 H SPNG AD 00 0 2089 Europe/Andorra 1993-12-23
+3039546 Canal de la Pica Canal de la Pica 42.5 1.5 H STM AD 00 0 1135 Europe/Andorra 1993-12-23
+3039547 Bony de la Pica Bony de la Pica Bony de la Pica,Pic d' Os,Pic d’ Ós,Pico de Ancla,Pico de Anclá 42.5 1.45 T MT AD 00 0 1840 Europe/Andorra 2011-11-05
+3039548 Basses del Pic Basses del Pic 42.61667 1.61667 H LKS AD 00 0 2352 Europe/Andorra 1993-12-23
+3039549 Bosc del Peu dels Pessons Bosc del Peu dels Pessons 42.51667 1.68333 V FRST AD 00 0 2352 Europe/Andorra 1993-12-23
+3039550 Font dels Pets Font dels Pets 42.58333 1.48333 H SPNG AD 00 0 1809 Europe/Andorra 1993-12-23
+3039551 Coll Petit Coll Petit Coll Petit,Collado Pequeno,Collado Pequeño 42.56498 1.44248 T PASS AD 00 0 2408 Europe/Andorra 2011-11-05
+3039552 Riu dels Pessons Riu dels Pessons 42.51667 1.7 H STM AD 00 0 2435 Europe/Andorra 1993-12-23
+3039553 Pic dels Pessons Pic dels Pessons Pic de Pessons,Pic dels Pessons,Pic des Pessons 42.50832 1.6585 T PK AD 00 0 2727 Europe/Andorra 2011-11-05
+3039554 Collada dels Pessons Collada dels Pessons 42.5 1.65 T PASS AD 00 0 2542 Europe/Andorra 1993-12-23
+3039555 Pleta dels Pescadors Pleta dels Pescadors 42.48333 1.65 L GRAZ AD 00 0 2658 Europe/Andorra 1993-12-23
+3039556 Pesada de Pal Pesada de Pal 42.56667 1.48333 L LCTY AD 00 0 1508 Europe/Andorra 1993-12-23
+3039557 Pont de Pesada Pont de Pesada 42.56667 1.48333 S BDG AD 00 0 1508 Europe/Andorra 1993-12-23
+3039558 Canal de Pesada Canal de Pesada 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3039559 Basera de Pesada Basera de Pesada 42.56667 1.48333 T CLF AD 00 0 1508 Europe/Andorra 1993-12-23
+3039560 Pesada Pesada 42.56667 1.48333 T SLP AD 00 0 1508 Europe/Andorra 1993-12-23
+3039561 Roc de Persoma Roc de Persoma 42.48333 1.48333 T RK AD 00 0 981 Europe/Andorra 1993-12-23
+3039562 Plana Perdiguera Plana Perdiguera 42.55 1.45 T UPLD AD 00 0 1788 Europe/Andorra 1993-12-23
+3039563 Pic de Percanela Pic de Percanela 42.58915 1.49666 T PK AD 00 0 2089 Europe/Andorra 2011-04-19
+3039564 Bordes de Percanela Bordes de Percanela 42.5825 1.48809 S FRM AD 00 0 2019 Europe/Andorra 2011-04-19
+3039565 Percanela Percanela Coma de Percanela,Loma de Percanela,Percanela 42.58333 1.48333 A ADMD AD AD 00 0 1809 Europe/Andorra 2011-11-05
+3039566 Camà de Per Baix Cami de Per Baix 42.55 1.68333 R TRL AD 00 0 2254 Europe/Andorra 1993-12-23
+3039567 Riu de Perafita Riu de Perafita 42.49733 1.55811 H STM AD 00 0 1566 Europe/Andorra 2011-04-19
+3039568 Pleta de Perafita Pleta de Perafita 42.48333 1.58333 L GRAZ AD 00 0 2349 Europe/Andorra 1993-12-23
+3039569 Planells de Perafita Planells de Perafita 42.48333 1.58333 T UPLD AD 00 0 2349 Europe/Andorra 1993-12-23
+3039570 Estanys de Perafita Estanys de Perafita 42.47027 1.58958 H LKS AD 00 0 2518 Europe/Andorra 2011-04-19
+3039571 Costa de Perafita Costa de Perafita 42.48333 1.58333 T SLP AD 00 0 2349 Europe/Andorra 1993-12-23
+3039572 Camà de Perafita Cami de Perafita 42.5 1.56667 R TRL AD 00 0 1776 Europe/Andorra 1993-12-23
+3039573 Cabana de Perafita Cabana de Perafita 42.48333 1.58333 S HUT AD 00 0 2349 Europe/Andorra 1993-12-23
+3039574 Perafita Perafita 42.46667 1.58333 A ADMD AD 00 0 2367 Europe/Andorra 1993-12-23
+3039575 Canal de la Pera Canal de la Pera 42.56667 1.48333 H RVN AD 00 0 1508 Europe/Andorra 1993-12-23
+3039576 Roc de la Penya Roc de la Penya 42.53333 1.55 T RK AD 00 0 1344 Europe/Andorra 1993-12-23
+3039577 Costa Pentinada Costa Pentinada 42.48333 1.53333 T SLP AD 00 0 2255 Europe/Andorra 1993-12-23
+3039578 Riu de la Peguera Riu de la Peguera 42.45 1.51667 H STM AD 00 0 1790 Europe/Andorra 1993-12-23
+3039579 Conreu de la Peguera Conreu de la Peguera 42.45 1.53333 V CULT AD 00 0 1859 Europe/Andorra 1993-12-23
+3039580 Carretera de la Peguera Carretera de la Peguera 42.46667 1.55 R RD AD 00 0 2341 Europe/Andorra 1993-12-23
+3039581 Bosc de la Peguera Bosc de la Peguera 42.46321 1.52655 V FRST AD 00 0 2068 Europe/Andorra 2011-04-19
+3039582 Bordes de la Peguera Bordes de la Peguera 42.45 1.53333 S HUTS AD 00 0 1859 Europe/Andorra 1993-12-23
+3039583 Serrat de les Pedrusques Serrat de les Pedrusques 42.6 1.5 T SPUR AD 00 0 1923 Europe/Andorra 1993-12-23
+3039584 Torrent Pedrós Torrent Pedros 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3039585 Bosc de Pedres Blanques Bosc de Pedres Blanques 42.53333 1.48333 V FRST AD 00 0 1677 Europe/Andorra 1993-12-23
+3039586 Pla de les Pedres Pla de les Pedres 42.55 1.66667 T UPLD AD 00 0 2224 Europe/Andorra 1993-12-23
+3039587 Pont Pedregat Pont Pedregat 42.58333 1.48333 S BDG AD 00 0 1809 Europe/Andorra 1993-12-23
+3039588 Canal de Pedra Plana Canal de Pedra Plana 42.51667 1.5 H STM AD 00 0 1688 Europe/Andorra 1993-12-23
+3039589 Corral de Pedra Corral de Pedra 42.56667 1.7 L GRAZ AD 00 0 2375 Europe/Andorra 1993-12-23
+3039590 Corral de Pedra Corral de Pedra 42.56667 1.65 L GRAZ AD 00 0 1988 Europe/Andorra 1993-12-23
+3039591 Canal de la Peca Rodona Canal de la Peca Rodona 42.53333 1.48333 H STM AD 00 0 1677 Europe/Andorra 1993-12-23
+3039592 Bosc de Paulelles Bosc de Paulelles 42.53333 1.53333 V FRST AD 00 0 1521 Europe/Andorra 1993-12-23
+3039593 Borda de Paulelles Borda de Paulelles 42.53333 1.55 S HUT AD 00 0 1344 Europe/Andorra 1993-12-23
+3039594 Passos dels Estanys Passos dels Estanys 42.6 1.6 L LCTY AD 00 0 2143 Europe/Andorra 1993-12-23
+3039595 Passos de la Tarterosa Passos de la Tarterosa 42.6 1.65 L LCTY AD 00 0 2131 Europe/Andorra 1993-12-23
+3039596 Canal del Passatorrents Canal del Passatorrents 42.43333 1.48333 H STM AD 00 0 1228 Europe/Andorra 1993-12-23
+3039597 Coll Passader Coll Passader 42.55 1.5 T PK AD 00 0 1292 Europe/Andorra 1993-12-23
+3039598 Riu del Pas Mal Riu del Pas Mal 42.53333 1.65 H STM AD 00 0 2508 Europe/Andorra 1993-12-23
+3039599 Costa del Pas del Monjo Costa del Pas del Monjo 42.63333 1.56667 T SLP AD 00 0 2394 Europe/Andorra 1993-12-23
+3039600 Pas del Monjo Pas del Monjo 42.63333 1.55 L LCTY AD 00 0 2053 Europe/Andorra 1993-12-23
+3039601 Bosc del Pas de la Clau Bosc del Pas de la Clau 42.51667 1.61667 V FRST AD 00 0 2254 Europe/Andorra 1993-12-23
+3039602 Solana del Pas de la Casa Solana del Pas de la Casa 42.53333 1.73333 T SLP AD 00 0 2300 Europe/Andorra 1993-12-23
+3039603 Riu del Pas de la Casa Riu del Pas de la Casa Riu del Pas de la Casa 42.56667 1.75 H STMX AD 00 0 1923 Europe/Andorra 2011-11-05
+3039604 Pas de la Casa Pas de la Casa Pas de la Kasa,ÐŸÐ°Ñ Ð´Ðµ ла КаÑа 42.54277 1.73361 P PPL AD 03 2363 2050 2230 Europe/Andorra 2008-06-09
+3039605 Pas de la Casa Pas de la Casa 42.53333 1.71667 A ADMD AD 00 0 2400 Europe/Andorra 1993-12-23
+3039606 Partida d’Ensucaranes Partida d'Ensucaranes 42.51667 1.55 L LCTY AD 00 0 1322 Europe/Andorra 1993-12-23
+3039607 Partida de l’Any de la Part Partida de l'Any de la Part 42.55 1.53333 A ADMD AD 00 0 1593 Europe/Andorra 1993-12-23
+3039608 Partida de la Grella Partida de la Grella 42.51667 1.51667 A ADMD AD 00 0 1265 Europe/Andorra 1993-12-23
+3039609 Borda de les Pardines Borda de les Pardines 42.53333 1.6 S FRM AD 00 0 1888 Europe/Andorra 1993-12-23
+3039610 Tossal de la Pardina Tossal de la Pardina 42.5 1.48333 T SPUR AD 00 0 1316 Europe/Andorra 1993-12-23
+3039611 Bosc del Pardal Bosc del Pardal 42.55 1.5 V FRST AD 00 0 1292 Europe/Andorra 1993-12-23
+3039612 Riu de la Palomera Riu de la Palomera 42.56667 1.78333 H STM AD 00 0 1680 Europe/Andorra 1993-12-23
+3039613 Cap de la Palomera Cap de la Palomera Cap de la Palomera 42.58333 1.78333 T PK AD 00 0 1694 Europe/Andorra 2011-11-05
+3039614 Pont de Palomer Pont de Palomer 42.56667 1.48333 S BDG AD 00 0 1508 Europe/Andorra 1993-12-23
+3039615 Pic de Palomer Pic de Palomer 42.56667 1.48333 T PK AD 00 0 1508 Europe/Andorra 1993-12-23
+3039616 Obaga de Palomer Obaga de Palomer 42.56667 1.48333 T SLP AD 00 0 1508 Europe/Andorra 1993-12-23
+3039617 Canal de Palomer Canal de Palomer 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3039618 Callissa de Palomer Callissa de Palomer 42.56667 1.48333 T GRGE AD 00 0 1508 Europe/Andorra 1993-12-23
+3039619 Palomer Palomer 42.56667 1.48333 A ADMD AD 00 0 1508 Europe/Andorra 1993-12-23
+3039620 Feixa Pallola Feixa Pallola 42.43333 1.46667 V CULT AD 00 0 1113 Europe/Andorra 1993-12-23
+3039621 Serrat Pallero Serrat Pallero 42.45 1.45 T SPUR AD 00 0 1482 Europe/Andorra 1993-12-23
+3039622 Prat de Paleta Prat de Paleta 42.48333 1.6 L GRAZ AD 00 0 2250 Europe/Andorra 1993-12-23
+3039623 Serrat de la Palanqueta Serrat de la Palanqueta 42.55 1.6 T RDGE AD 00 0 2210 Europe/Andorra 1993-12-23
+3039624 Riu de la Palanqueta Riu de la Palanqueta 42.55 1.6 H STM AD 00 0 2210 Europe/Andorra 1993-12-23
+3039625 Pont de les Palanques Pont de les Palanques 42.55 1.51667 S BDG AD 00 0 1397 Europe/Andorra 1993-12-23
+3039626 Planell de la Palanca Planell de la Palanca 42.55 1.68333 T UPLD AD 00 0 2254 Europe/Andorra 1993-12-23
+3039627 Pic de la Pala de Coll Carnisser Pic de la Pala de Coll Carnisser 42.6 1.48333 T PK AD 00 0 2441 Europe/Andorra 1993-12-23
+3039628 Pic de la Pala Alta Pic de la Pala Alta 42.6 1.63333 T PK AD 00 0 1893 Europe/Andorra 1993-12-23
+3039629 Solà de Pal Sola de Pal 42.55 1.46667 T SLP AD 00 0 1585 Europe/Andorra 1993-12-23
+3039630 Riu de Pal Riu de Pal 42.56159 1.49544 H STM AD 00 0 1430 Europe/Andorra 2011-04-19
+3039631 Pont de Pal Pont de Pal 42.55 1.46667 S BDG AD 00 0 1585 Europe/Andorra 1993-12-23
+3039632 Carretera de Pal Carretera de Pal 42.55 1.48333 R RD AD 00 0 1548 Europe/Andorra 1993-12-23
+3039633 Bosc de Pal Bosc de Pal 42.53333 1.46667 V FRST AD 00 0 1846 Europe/Andorra 1993-12-23
+3039634 Pal Pal Pal 42.55 1.48333 P PPL AD 04 0 1548 Europe/Andorra 2011-11-05
+3039635 Serra de Padern Serra de Padern 42.53333 1.55 T RDGE AD 00 0 1344 Europe/Andorra 1993-12-23
+3039636 Riu de Padern Riu de Padern 42.52834 1.52213 H STM AD 00 0 1361 Europe/Andorra 2011-04-19
+3039637 Pic de Padern Pic de Padern 42.52429 1.54697 T PK AD 00 0 1290 Europe/Andorra 2011-04-19
+3039638 Bosc de Padern Bosc de Padern 42.53333 1.53333 V FRST AD 00 0 1521 Europe/Andorra 1993-12-23
+3039639 Coll Pa Coll Pa 42.55 1.43333 T PK AD 00 0 1949 Europe/Andorra 1993-12-23
+3039640 Coll Pa Coll Pa 42.48333 1.56667 T PASS AD 00 0 2231 Europe/Andorra 1993-12-23
+3039641 Canal de l’ Ovella Morta Canal de l' Ovella Morta 42.48333 1.58333 H STM AD 00 0 2349 Europe/Andorra 1993-12-23
+3039642 Riu de l’ Ovella Riu de l' Ovella 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3039643 Port de l’ Ovella Port de l' Ovella Port de L'Ovella,Port de L’Ovella,Port de l' Ovella,Port de l’ Ovella 42.55 1.43333 T PASS AD 00 0 1949 Europe/Andorra 2011-11-05
+3039644 Bosc de l’ Ovella Bosc de l' Ovella 42.53333 1.6 V FRST AD 00 0 1888 Europe/Andorra 1993-12-23
+3039645 Font de l’ Óssa Font de l' Ossa 42.51667 1.48333 H SPNG AD 00 0 1839 Europe/Andorra 1993-12-23
+3039646 Cova de l’ Óssa Cova de l' Ossa 42.46667 1.48333 S CAVE AD 00 0 1134 Europe/Andorra 1993-12-23
+3039647 Cova de l’ Óssa Cova de l' Ossa 42.45 1.48333 S CAVE AD 00 0 1111 Europe/Andorra 1993-12-23
+3039648 Canya de l’ Óssa Canya de l' Ossa 42.56667 1.51667 S CAVE AD 00 0 1500 Europe/Andorra 1993-12-23
+3039649 Canal de l’ Óssa Canal de l' Ossa 42.5 1.63333 H STM AD 00 0 2545 Europe/Andorra 1993-12-23
+3039650 Canal de l’ Óssa Canal de l' Ossa 42.58333 1.46667 H RVN AD 00 0 1643 Europe/Andorra 1993-12-23
+3039651 Carretera d’ Ós de CivÃs Carretera d' Os de Civis 42.48333 1.46667 R RD AD 00 0 1148 Europe/Andorra 1993-12-23
+3039652 Canal de l’ Osca de Migdia Canal de l' Osca de Migdia 42.46667 1.46667 H STM AD 00 0 1340 Europe/Andorra 1993-12-23
+3039653 Clots de l’ Ós Clots de l' Os 42.58333 1.68333 H RVN AD 00 0 2294 Europe/Andorra 1993-12-23
+3039654 Clot de l’ Ós Clot de l' Os 42.58333 1.66667 H RVN AD 00 0 2159 Europe/Andorra 1993-12-23
+3039655 Tosa d’ Ortafà Tosa d' Ortafa 42.56667 1.71667 T UPLD AD 00 0 2219 Europe/Andorra 1993-12-23
+3039656 Tarteres d’ Ortafà Tarteres d' Ortafa 42.56667 1.71667 T TAL AD 00 0 2219 Europe/Andorra 1993-12-23
+3039657 Font d’ Ortafà Font d' Ortafa 42.56667 1.71667 H SPNG AD 00 0 2219 Europe/Andorra 1993-12-23
+3039658 Collet d’ Ortafà Collet d' Ortafa 42.56667 1.7 T PASS AD 00 0 2375 Europe/Andorra 1993-12-23
+3039659 Clots d’ Ortafà Clots d' Ortafa 42.55 1.71667 H RVN AD 00 0 2192 Europe/Andorra 1993-12-23
+3039660 Canals d’ Ortafà Canals d' Ortafa 42.56667 1.7 H RVN AD 00 0 2375 Europe/Andorra 1993-12-23
+3039661 Ortafà Ortafa 42.56667 1.71667 A ADMD AD 00 0 2219 Europe/Andorra 1993-12-23
+3039662 Bony de l’ Orri Vell Bony de l' Orri Vell 42.53333 1.63333 T SPUR AD 00 0 2360 Europe/Andorra 1993-12-23
+3039663 Riu dels Orris Riu dels Orris 42.48333 1.63333 H STM AD 00 0 2296 Europe/Andorra 1993-12-23
+3039664 Prats dels Orris Prats dels Orris 42.53333 1.63333 L GRAZ AD 00 0 2360 Europe/Andorra 1993-12-23
+3039665 Pleta dels Orris Pleta dels Orris 42.53333 1.63333 L GRAZ AD 00 0 2360 Europe/Andorra 1993-12-23
+3039666 Barraca de l’ Orri de Rusca Barraca de l' Orri de Rusca 42.55 1.68333 S HUT AD 00 0 2254 Europe/Andorra 1993-12-23
+3039667 Bosc de l’ Orri del Call Bosc de l' Orri del Call 42.6 1.65 V FRST AD 00 0 2131 Europe/Andorra 1993-12-23
+3039668 Serrat de l’ Orri de Gastó Serrat de l' Orri de Gasto 42.48333 1.46667 T SPUR AD 00 0 1148 Europe/Andorra 1993-12-23
+3039669 Serrat de l’ Orri Serrat de l' Orri 42.58333 1.66667 T SLP AD 00 0 2159 Europe/Andorra 1993-12-23
+3039670 Pont de l’ Orri Pont de l' Orri 42.58333 1.66667 S BDG AD 00 0 2159 Europe/Andorra 1993-12-23
+3039671 Obaga de l’ Orri Obaga de l' Orri 42.46667 1.45 T SLP AD 00 0 1562 Europe/Andorra 1993-12-23
+3039672 Borda de l’ Orri Borda de l' Orri 42.55 1.43333 S HUT AD 00 0 1949 Europe/Andorra 1993-12-23
+3039673 Solana dels Oriols Solana dels Oriols 42.56667 1.48333 T SLP AD 00 0 1508 Europe/Andorra 1993-12-23
+3039674 Cap dels Oriols Cap dels Oriols 42.56667 1.46667 T PK AD 00 0 1673 Europe/Andorra 1993-12-23
+3039675 Pont d'Ordino Pont d'Ordino 42.5492 1.52373 S BDG AD 07 0 1397 Europe/Andorra 2007-04-04
+3039676 Parròquia d'Ordino Parroquia d'Ordino Ordino,Parroquia d'Ordino,Parròquia d'Ordino 42.59758 1.52573 A ADM1 AD 05 3467 1695 Europe/Andorra 2008-03-17
+3039677 Coll d'Ordino Coll d'Ordino Coll d'Ordino,Port d'Ordino 42.55615 1.57147 T PASS AD AD 07 0 1883 1879 Europe/Andorra 2007-04-04
+3039678 Ordino Ordino Ordino,ao er di nuo,orudino jiao qu,Ордино,オルディノ教区,奥尔迪诺 42.55623 1.53319 P PPLA AD 05 3066 1340 Europe/Andorra 2009-12-11
+3039679 Font de les Ordigues Font de les Ordigues 42.51667 1.56667 H SPNG AD 00 0 1759 Europe/Andorra 1993-12-23
+3039680 Font de l’ Ordigal Font de l' Ordigal 42.46667 1.46667 H SPNG AD 00 0 1340 Europe/Andorra 1993-12-23
+3039681 Roc de l’ Oral Roc de l' Oral 42.53333 1.56667 T CLF AD 00 0 1418 Europe/Andorra 1993-12-23
+3039682 Planells d’ Olio Planells d' Olio 42.53333 1.6 T UPLD AD 00 0 1888 Europe/Andorra 1993-12-23
+3039683 Obagueta de Cantà Obagueta de Canti 42.56667 1.51667 L LCTY AD 00 0 1500 Europe/Andorra 1993-12-23
+3039684 Riu de les Obagues Riu de les Obagues 42.58333 1.63333 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3039685 Canal de les Obagues Canal de les Obagues 42.58333 1.48333 H STM AD 00 0 1809 Europe/Andorra 1993-12-23
+3039686 Bosc de les Obagues Bosc de les Obagues 42.58333 1.63333 V FRST AD 00 0 1722 Europe/Andorra 1993-12-23
+3039687 Bosc de l’ Obaga Sobirana Bosc de l' Obaga Sobirana 42.55 1.48333 V FRST AD 00 0 1548 Europe/Andorra 1993-12-23
+3039688 Canal de l’ Obaga Fosca Canal de l' Obaga Fosca 42.48333 1.43333 H STM AD 00 0 1938 Europe/Andorra 1993-12-23
+3039689 Clots de l’ Obaga de Torradella Clots de l' Obaga de Torradella 42.6 1.63333 H RVN AD 00 0 1893 Europe/Andorra 1993-12-23
+3039690 Serra de l’ Obaga d’Enclar Serra de l' Obaga d'Enclar 42.51246 1.477 T MT AD 00 0 2069 Europe/Andorra 2011-04-19
+3039691 Obaga d’Enclar Obaga d'Enclar 42.5 1.46667 A ADMD AD 00 0 1678 Europe/Andorra 1993-12-23
+3039692 Canal de l’ Obaga de l’Óssa Canal de l' Obaga de l'Ossa 42.55 1.48333 H STM AD 00 0 1548 Europe/Andorra 1993-12-23
+3039693 Bosc de l’ Obaga de l’Óssa Bosc de l' Obaga de l'Ossa 42.55 1.48333 V FRST AD 00 0 1548 Europe/Andorra 1993-12-23
+3039694 Obaga de l’Estall Serrer Obaga de l'Estall Serrer 42.48333 1.6 A ADMD AD 00 0 2250 Europe/Andorra 1993-12-23
+3039695 Obaga de la Gonarda Obaga de la Gonarda 42.55 1.53333 A ADMD AD 00 0 1593 Europe/Andorra 1993-12-23
+3039696 Obaga de Juclar Obaga de Juclar 42.61667 1.7 L LCTY AD 00 0 2285 Europe/Andorra 1993-12-23
+3039697 Obaga de Juberri Obaga de Juberri 42.43333 1.48333 A ADMD AD 00 0 1228 Europe/Andorra 1993-12-23
+3039698 Bosc de l’ Obaga de Gali Bosc de l' Obaga de Gali 42.55 1.48333 V FRST AD 00 0 1548 Europe/Andorra 1993-12-23
+3039699 Obaga de Fontverd Obaga de Fontverd 42.48333 1.58333 A ADMD AD 00 0 2349 Europe/Andorra 1993-12-23
+3039700 Obaga d’Ansalonga Obaga d'Ansalonga 42.56667 1.51667 A ADMD AD 00 0 1500 Europe/Andorra 1993-12-23
+3039701 Obaga d’Andorra Obaga d'Andorra 42.48333 1.51667 A ADMD AD 00 0 2061 Europe/Andorra 1993-12-23
+3039702 Camà de l’ Obaga Cami de l' Obaga 42.55 1.55 R TRL AD 00 0 2097 Europe/Andorra 1993-12-23
+3039703 Serrat dels Obacs Serrat dels Obacs 42.6 1.5 T SLP AD 00 0 1923 Europe/Andorra 1993-12-23
+3039704 Fonts dels Obacs Fonts dels Obacs 42.6 1.5 H SPNG AD 00 0 1923 Europe/Andorra 1993-12-23
+3039705 Obac d’Incles Obac d'Incles 42.58333 1.68333 A ADMD AD 00 0 2294 Europe/Andorra 1993-12-23
+3039706 Obac de Soldeu Obac de Soldeu 42.56667 1.66667 A ADMD AD 00 0 1938 Europe/Andorra 1993-12-23
+3039707 Obac de Sispony Obac de Sispony 42.51667 1.48333 A ADMD AD 00 0 1839 Europe/Andorra 1993-12-23
+3039708 Bosc de l’ Obac de Salla Bosc de l' Obac de Salla 42.55 1.5 V FRST AD 00 0 1292 Europe/Andorra 1993-12-23
+3039709 Obac d’Envalira Obac d'Envalira 42.55 1.68333 A ADMD AD 00 0 2254 Europe/Andorra 1993-12-23
+3039710 Obac d’Encamp Obac d'Encamp 42.51667 1.6 A ADMD AD 00 0 2085 Europe/Andorra 1993-12-23
+3039711 Obac del Tarter Obac del Tarter 42.56667 1.63333 A ADMD AD 00 0 2016 Europe/Andorra 1993-12-23
+3039712 Obac dels Cortals Obac dels Cortals 42.51667 1.61667 A ADMD AD 00 0 2254 Europe/Andorra 1993-12-23
+3039713 Obac de les Escaldes i Engordany Obac de les Escaldes i Engordany 42.5 1.53333 L LCTY AD 00 0 1574 Europe/Andorra 2007-04-05
+3039714 Obac de la Cebollera Obac de la Cebollera 42.61667 1.58333 L LCTY AD 00 0 2374 Europe/Andorra 1993-12-23
+3039715 Pont de l’ Obac de Fontaneda Pont de l' Obac de Fontaneda 42.45 1.46667 S BDG AD 00 0 935 Europe/Andorra 1993-12-23
+3039716 Obac de Canillo Obac de Canillo 42.56667 1.63333 A ADMD AD 00 0 2016 Europe/Andorra 1993-12-23
+3039717 Obac d’Anyós Obac d'Anyos 42.53333 1.53333 A ADMD AD 00 0 1521 Europe/Andorra 1993-12-23
+3039718 Serrat de l’ Obac Serrat de l' Obac 42.48333 1.5 T MT AD 00 0 1631 Europe/Andorra 1993-12-23
+3039719 Rec de l’ Obac Rec de l' Obac 42.5 1.53333 H CNL AD 00 0 1574 Europe/Andorra 1993-12-23
+3039720 Prats de l’ Obac Prats de l' Obac 42.53333 1.61667 L GRAZ AD 00 0 2237 Europe/Andorra 1993-12-23
+3039721 Font de l’ Obac Font de l' Obac 42.5 1.56667 H SPNG AD 00 0 1776 Europe/Andorra 1993-12-23
+3039722 Coll d’ Obac Coll d' Obac 42.5 1.46667 T PK AD 00 0 1678 Europe/Andorra 1993-12-23
+3039723 Torrent dels Nyerros Torrent dels Nyerros 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3039724 Carretera General Número Un Carretera General Numero Un 42.5 1.53333 R RD AD 00 0 1574 Europe/Andorra 1993-12-23
+3039725 Carretera General Número Tres Carretera General Numero Tres 42.61667 1.48333 R RD AD 00 0 2470 Europe/Andorra 1993-12-23
+3039726 Carretera General Número Duo Carretera General Numero Duo 42.53333 1.73333 R RD AD 00 0 2300 Europe/Andorra 1993-12-23
+3039727 Pleta Nova Pleta Nova 42.61667 1.51667 L GRAZ AD 00 0 1716 Europe/Andorra 1993-12-23
+3039728 Obaga de Nou Fonts Obaga de Nou Fonts 42.46667 1.53333 T SLP AD 00 0 2332 Europe/Andorra 1993-12-23
+3039729 Nou Fonts Nou Fonts 42.46667 1.53333 T RDGE AD 00 0 2332 Europe/Andorra 1993-12-23
+3039730 Estany de la Nou Estany de la Nou 42.47539 1.5756 H LK AD 00 0 2349 Europe/Andorra 2011-04-19
+3039731 Pic d’ Ascobes Pic d' Ascobes Pic d' Ascobes,Pic de Noe,Pic de Noé,Pic d’ Ascobes 42.61667 1.73333 T PK AD 00 0 2508 Europe/Andorra 2011-11-05
+3039732 Basera del Niu de l’Aliga Basera del Niu de l'Aliga 42.5 1.45 T CLF AD 00 0 1840 Europe/Andorra 1993-12-23
+3039733 Collades de Nier Collades de Nier 42.61667 1.53333 T PASS AD 00 0 1609 Europe/Andorra 1993-12-23
+3039734 Bosc de les Neres Bosc de les Neres 42.53333 1.56667 V FRST AD 00 0 1418 Europe/Andorra 1993-12-23
+3039735 Bony de les Neres Bony de les Neres Bony de las Neras,Bony de les Neres,Bony de los Neros,Pic de las Neras 42.54761 1.56693 T MT AD 00 0 1828 Europe/Andorra 2011-11-05
+3039736 Roques Negres Roques Negres 42.5 1.46667 T RKS AD 00 0 1678 Europe/Andorra 1993-12-23
+3039737 Rocs Negres Rocs Negres 42.55 1.61667 T RKS AD 00 0 2206 Europe/Andorra 1993-12-23
+3039738 Canals Negres Canals Negres 42.56667 1.68333 H RVN AD 00 0 2340 Europe/Andorra 1993-12-23
+3039739 Serrat Negre Serrat Negre 42.58333 1.48333 T SPUR AD 00 0 1809 Europe/Andorra 1993-12-23
+3039740 Riu Negre Riu Negre 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3039741 Port de Comallempla Port de Comallempla Port Negre,Port de Comallempla 42.56667 1.45 T PASS AD 00 0 2137 Europe/Andorra 2011-11-05
+3039742 Pic Negre Pic Negre Pic Negre 42.56667 1.45 T PK AD 00 0 2137 Europe/Andorra 2011-11-05
+3039743 Pic Negre Pic Negre Pic Negre 42.45 1.56667 T PK AD 00 0 2558 Europe/Andorra 2011-11-05
+3039744 Forat Negre Forat Negre 42.5 1.5 H RVN AD 00 0 1135 Europe/Andorra 1993-12-23
+3039745 Estany Negre Estany Negre 42.58333 1.43333 H LK AD 00 0 2412 Europe/Andorra 1993-12-23
+3039746 Bosc Negre Bosc Negre 42.56667 1.55 V FRST AD 00 0 1996 Europe/Andorra 1993-12-23
+3039747 Bosc Negre Bosc Negre 42.53333 1.6 V FRST AD 00 0 1888 Europe/Andorra 1993-12-23
+3039748 Bosc Negre Bosc Negre 42.51667 1.48333 V FRST AD 00 0 1839 Europe/Andorra 1993-12-23
+3039749 Bosc Negre Bosc Negre 42.5 1.56667 V FRST AD 00 0 1776 Europe/Andorra 1993-12-23
+3039750 Bosc Negre Bosc Negre 42.48333 1.51667 V FRST AD 00 0 2061 Europe/Andorra 1993-12-23
+3039751 Bony Negre Bony Negre 42.56667 1.46667 T SPUR AD 00 0 1673 Europe/Andorra 1993-12-23
+3039752 Roca Negra Roca Negra 42.58333 1.58333 T RK AD 00 0 1993 Europe/Andorra 1993-12-23
+3039753 Torrent del Nedó Torrent del Nedo 42.46667 1.5 H STM AD 00 0 1383 Europe/Andorra 1993-12-23
+3039754 Font de la Navina Font de la Navina 42.55 1.56667 H SPNG AD 00 0 1828 Europe/Andorra 1993-12-23
+3039755 Llosers de Naudà Llosers de Naudi 42.56667 1.56667 S MNQR AD 00 0 2089 Europe/Andorra 1993-12-23
+3039756 Carretera de Nagol Carretera de Nagol 42.46667 1.48333 R RD AD 00 0 1134 Europe/Andorra 1993-12-23
+3039757 Nagol Nagol Nagol 42.47146 1.50314 P PPL AD 06 0 1383 Europe/Andorra 2011-11-05
+3039758 Solà de Nadal Sola de Nadal 42.51667 1.51667 T SLP AD 00 0 1265 Europe/Andorra 1993-12-23
+3039759 Obaga de les Mussoles Obaga de les Mussoles 42.55 1.63333 T SLP AD 00 0 2336 Europe/Andorra 1993-12-23
+3039760 Pla Mussola Pla Mussola 42.53333 1.56667 T UPLD AD 00 0 1418 Europe/Andorra 1993-12-23
+3039761 Camà de la Muntanya Cami de la Muntanya 42.5 1.56667 R TRL AD 00 0 1776 Europe/Andorra 1993-12-23
+3039762 Pleta de les Mules Pleta de les Mules 42.43333 1.53333 L GRAZ AD 00 0 2108 Europe/Andorra 1993-12-23
+3039763 Canal del Mulassar Canal del Mulassar 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3039764 Bosc del Mulassar Bosc del Mulassar 42.56667 1.53333 V FRST AD 00 0 1669 Europe/Andorra 1993-12-23
+3039765 Basers del Motxo Basers del Motxo 42.6 1.63333 T CLF AD 00 0 1893 Europe/Andorra 1993-12-23
+3039766 Riu de Mossers Riu de Mossers 42.45 1.46667 H STM AD 00 0 935 Europe/Andorra 1993-12-23
+3039767 Pleta Mosquera Pleta Mosquera 42.63333 1.51667 L GRAZ AD 00 0 1894 Europe/Andorra 1993-12-23
+3039768 Mosquera Mosquera 42.55 1.58333 P PPL AD 03 0 1499 Europe/Andorra 1993-12-23
+3039769 Tosa de Moscatosa Tosa de Moscatosa 42.55 1.71667 T UPLD AD 00 0 2192 Europe/Andorra 1993-12-23
+3039770 Clots de Moscatosa Clots de Moscatosa 42.55 1.7 H RVN AD 00 0 2358 Europe/Andorra 1993-12-23
+3039771 Pont de Mos Pont de Mos 42.58333 1.63333 S BDG AD 00 0 1722 Europe/Andorra 1993-12-23
+3039772 Plana Mortalla Plana Mortalla 42.58333 1.55 T UPLD AD 00 0 2357 Europe/Andorra 1993-12-23
+3039773 Estany Mort Estany Mort 42.63333 1.61667 H LK AD 00 0 2541 Europe/Andorra 1993-12-23
+3039774 Estany Mort Estany Mort 42.58333 1.71667 H LK AD 00 0 2553 Europe/Andorra 1993-12-23
+3039775 Canya dels Moros Canya dels Moros 42.55 1.51667 S CAVE AD 00 0 1397 Europe/Andorra 1993-12-23
+3039776 Pleta de Moretó Pleta de Moreto 42.55 1.68333 L GRAZ AD 00 0 2254 Europe/Andorra 1993-12-23
+3039777 Planell de Moretó Planell de Moreto 42.55 1.68333 T UPLD AD 00 0 2254 Europe/Andorra 1993-12-23
+3039778 Bosc de Moretó Bosc de Moreto 42.55 1.7 V FRST AD 00 0 2358 Europe/Andorra 1993-12-23
+3039779 Estany Moreno Estany Moreno 42.51667 1.63333 H LK AD 00 0 2379 Europe/Andorra 1993-12-23
+3039780 Canal de Mora Canal de Mora 42.56667 1.58333 H STM AD 00 0 1919 Europe/Andorra 1993-12-23
+3039781 Tosal de la Truita Tosal de la Truita Pic de Monturull,Pic de Perafita,Tosal de la Truita 42.46667 1.58333 T PK AD 00 0 2367 Europe/Andorra 2011-11-05
+3039782 Riu de Montuell Riu de Montuell 42.51667 1.58333 H STM AD 00 0 1994 Europe/Andorra 1993-12-23
+3039783 Cap de Montuell Cap de Montuell 42.51667 1.61667 T PK AD 00 0 2254 Europe/Andorra 1993-12-23
+3039784 Roc de Montmantell Roc de Montmantell 42.59695 1.47085 T RDGE AD 00 0 2421 Europe/Andorra 2011-04-19
+3039785 Riu de Montmantell Riu de Montmantell 42.6 1.46667 H STM AD 00 0 2421 Europe/Andorra 1993-12-23
+3039786 Pleta de Montmantell Pleta de Montmantell 42.6 1.46667 L GRAZ AD 00 0 2421 Europe/Andorra 1993-12-23
+3039787 Obaga de Montmantell Obaga de Montmantell 42.6 1.46667 T SLP AD 00 0 2421 Europe/Andorra 1993-12-23
+3039788 Font de Montmantell Font de Montmantell 42.6 1.46667 H SPNG AD 00 0 2421 Europe/Andorra 1993-12-23
+3039789 Estanys de Montmantell Estanys de Montmantell 42.6 1.46667 H LKS AD 00 0 2421 Europe/Andorra 1993-12-23
+3039790 Collada de Montmantell Collada de Montmantell 42.6 1.46667 T PASS AD 00 0 2421 Europe/Andorra 1993-12-23
+3039791 Camà de Montmantell Cami de Montmantell 42.6 1.46667 R TRL AD 00 0 2421 Europe/Andorra 1993-12-23
+3039792 Montmantell Montmantell 42.6 1.46667 A ADMD AD 00 0 2421 Europe/Andorra 1993-12-23
+3039793 Pic de Montmalús Pic de Montmalus 42.50902 1.6861 T PK AD 00 0 2445 Europe/Andorra 2011-04-19
+3039794 Estany de Montmalús Estany de Montmalus 42.5 1.68333 H LK AD 00 0 2425 Europe/Andorra 1993-12-23
+3039795 Collada de Montmalús Collada de Montmalus 42.51667 1.7 T PASS AD 00 0 2435 Europe/Andorra 1993-12-23
+3039796 Montmalús Montmalus 42.5 1.7 A ADMD AD 00 0 2323 Europe/Andorra 1993-12-23
+3039797 Torrent de Montllobar Torrent de Montllobar 42.45 1.51667 H STM AD 00 0 1790 Europe/Andorra 1993-12-23
+3039798 Font de Montllobar Font de Montllobar 42.45 1.51667 H SPNG AD 00 0 1790 Europe/Andorra 1993-12-23
+3039799 Montllobar Montllobar 42.45 1.51667 L LCTY AD 00 0 1790 Europe/Andorra 1993-12-23
+3039800 Riu de Montaup Riu de Montaup 42.56667 1.6 H STM AD 00 0 1655 Europe/Andorra 1993-12-23
+3039801 Prats de Montaup Prats de Montaup 42.56667 1.58333 L GRAZ AD 00 0 1919 Europe/Andorra 1993-12-23
+3039802 Collet de Montaup Collet de Montaup 42.56667 1.58333 T SPUR AD 00 0 1919 Europe/Andorra 1993-12-23
+3039803 Carrera de Montaup Carrera de Montaup 42.56667 1.6 R RD AD 00 0 1655 Europe/Andorra 1993-12-23
+3039804 Barranc de Montaup Barranc de Montaup 42.56667 1.6 H STM AD 00 0 1655 Europe/Andorra 1993-12-23
+3039805 Montaup Montaup 42.58333 1.58333 A ADMD AD 00 0 1993 Europe/Andorra 1993-12-23
+3039806 Riu Montaner Riu Montaner 42.52965 1.52042 H STM AD 00 0 1361 Europe/Andorra 2011-04-19
+3039807 Collada de Montaner Collada de Montaner Col de Montaner,Col de Montanér,Coll de Montane,Coll de Montané,Collada de Montaner 42.51798 1.46716 T PASS AD 00 0 1840 Europe/Andorra 2011-11-05
+3039808 Riu de Montalarà Riu de Montalari 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3039809 Bordes de Montalarà Bordes de Montalari 42.55 1.58333 S HUTS AD 00 0 1499 Europe/Andorra 1993-12-23
+3039810 Mónjol de Cabana Sorda Monjol de Cabana Sorda 42.61667 1.68333 T SLP AD 00 0 2406 Europe/Andorra 1993-12-23
+3039811 Canal del Monjo Canal del Monjo 42.48333 1.5 H STM AD 00 0 1631 Europe/Andorra 1993-12-23
+3039812 Tossal Momó Tossal Momo 42.53333 1.46667 T PK AD 00 0 1846 Europe/Andorra 1993-12-23
+3039813 Pleta dels Moltons Pleta dels Moltons 42.55 1.73333 L GRAZ AD 00 0 2100 Europe/Andorra 1993-12-23
+3039814 Obaga de la Mollerra Obaga de la Mollerra 42.6 1.51667 T SLP AD 00 0 1445 Europe/Andorra 1993-12-23
+3039815 Pont de Molleres Pont de Molleres 42.55 1.58333 S BDG AD 00 0 1499 Europe/Andorra 1993-12-23
+3039816 Canal de les Molleres Canal de les Molleres 42.51667 1.56667 H STM AD 00 0 1759 Europe/Andorra 1993-12-23
+3039817 Canal de les Molleres Canal de les Molleres 42.5 1.55 H STM AD 00 0 1566 Europe/Andorra 1993-12-23
+3039818 Bosc de les Molleres Bosc de les Molleres 42.51667 1.56667 V FRST AD 00 0 1759 Europe/Andorra 1993-12-23
+3039819 Molleres Molleres 42.55103 1.58958 P PPL AD 02 0 1640 Europe/Andorra 2011-04-19
+3039820 Molleres Molleres 42.55 1.58333 L LCTY AD 00 0 1499 Europe/Andorra 1993-12-23
+3039821 Bosc de la Mollera Bosc de la Mollera 42.6 1.51667 V FRST AD 00 0 1445 Europe/Andorra 1993-12-23
+3039822 Bordes de la Mollera Bordes de la Mollera 42.6 1.51667 S HUTS AD 00 0 1445 Europe/Andorra 1993-12-23
+3039823 Torrent de la Molina Torrent de la Molina 42.45 1.51667 H STM AD 00 0 1790 Europe/Andorra 1993-12-23
+3039824 Riu de la Molina Riu de la Molina 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3039825 Obaga de la Molina Obaga de la Molina 42.56667 1.48333 T SLP AD 00 0 1508 Europe/Andorra 1993-12-23
+3039826 Obaga de la Molina Obaga de la Molina 42.45 1.51667 T SLP AD 00 0 1790 Europe/Andorra 1993-12-23
+3039827 Grau de la Molina Grau de la Molina 42.53333 1.6 H RVN AD 00 0 1888 Europe/Andorra 1993-12-23
+3039828 Canal de la Molina Canal de la Molina 42.48333 1.56667 H STM AD 00 0 2231 Europe/Andorra 1993-12-23
+3039829 Canal de la Molina Canal de la Molina 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3039830 Camà de la Molina Cami de la Molina 42.53333 1.6 R TRL AD 00 0 1888 Europe/Andorra 1993-12-23
+3039831 Bordes de la Molina Bordes de la Molina 42.51667 1.58333 S HUT AD 00 0 1994 Europe/Andorra 1993-12-23
+3039832 Plana Moleta Plana Moleta 42.56667 1.53333 T UPLD AD 00 0 1669 Europe/Andorra 1993-12-23
+3039833 Roc de les Moles Roc de les Moles 42.53333 1.51667 T RK AD 00 0 1361 Europe/Andorra 1993-12-23
+3039834 Pont de les Moles Pont de les Moles 42.6 1.53333 S BDG AD 00 0 1695 Europe/Andorra 1993-12-23
+3039835 Canal de les Moles Canal de les Moles 42.56667 1.61667 H RVN AD 00 0 1920 Europe/Andorra 1993-12-23
+3039836 Pont de la Mola Pont de la Mola 42.56667 1.66667 S BDG AD 00 0 1938 Europe/Andorra 1993-12-23
+3039837 Roc del Moixó Roc del Moixo 42.56667 1.5 T RK AD 00 0 1636 Europe/Andorra 1993-12-23
+3039838 Borda del Moixellaire Borda del Moixellaire 42.45 1.45 S HUTS AD 00 0 1482 Europe/Andorra 1993-12-23
+3039839 Torrent de la Moixella Torrent de la Moixella 42.43333 1.48333 H STM AD 00 0 1228 Europe/Andorra 1993-12-23
+3039840 Riu de la Moixella Riu de la Moixella 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3039841 Bosc de la Moixella Bosc de la Moixella 42.45 1.46667 V FRST AD 00 0 935 Europe/Andorra 1993-12-23
+3039842 Serra Mitjana Serra Mitjana 42.46667 1.61667 T RDGE AD 00 0 2448 Europe/Andorra 1993-12-23
+3039843 Serra Mitjana Serra Mitjana 42.46667 1.58333 T MT AD 00 0 2367 Europe/Andorra 1993-12-23
+3039844 Bony de Mitgeu Bony de Mitgeu 42.55 1.53333 T SPUR AD 00 0 1593 Europe/Andorra 1993-12-23
+3039845 Collet de la Mira Collet de la Mira 42.56667 1.53333 T SPUR AD 00 0 1669 Europe/Andorra 1993-12-23
+3039846 Serrat dels Miquelets Serrat dels Miquelets 42.55 1.6 T RDGE AD 00 0 2210 Europe/Andorra 1993-12-23
+3039847 Font dels Miquelets Font dels Miquelets 42.58333 1.43333 H SPNG AD 00 0 2412 Europe/Andorra 1993-12-23
+3039848 Pont de les Mines Pont de les Mines 42.6 1.53333 S BDG AD 00 0 1695 Europe/Andorra 1993-12-23
+3039849 Tosa del Mig Tosa del Mig 42.58333 1.7 T UPLD AD 00 0 2584 Europe/Andorra 1993-12-23
+3039850 Serra del Mig Serra del Mig 42.58333 1.71667 T RDGE AD 00 0 2553 Europe/Andorra 1993-12-23
+3039851 Roc del Mig Roc del Mig 42.56667 1.71667 T RK AD 00 0 2219 Europe/Andorra 1993-12-23
+3039852 Estany del Mig Estany del Mig 42.63999 1.48612 H LK AD 07 0 2254 Europe/Andorra 2007-03-04
+3039853 Coma del Mig Coma del Mig 42.65 1.53333 T CRQ AD 00 0 2564 Europe/Andorra 1993-12-23
+3039854 Collada del Mig Collada del Mig 42.61667 1.55 T SPUR AD 00 0 2007 Europe/Andorra 1993-12-23
+3039855 Clots del Mig Clots del Mig 42.56667 1.55 H RVN AD 00 0 1996 Europe/Andorra 1993-12-23
+3039856 Carrera del Mig Carrera del Mig 42.56667 1.61667 R TRL AD 00 0 1920 Europe/Andorra 1993-12-23
+3039857 Basers del Mig Basers del Mig 42.58333 1.7 T CLF AD 00 0 2584 Europe/Andorra 1993-12-23
+3039858 Clots de Més Amunt de la Pleta Clots de Mes Amunt de la Pleta 42.6 1.48333 T TAL AD 00 0 2441 Europe/Andorra 1993-12-23
+3039859 Pleta de Més Amunt Pleta de Mes Amunt 42.51667 1.63333 L GRAZ AD 00 0 2379 Europe/Andorra 1993-12-23
+3039860 Estany de Més Amunt Estany de Mes Amunt 42.6457 1.48659 H LK AD 07 0 2530 Europe/Andorra 2007-03-04
+3039861 Carretera Meritxell Carretera Meritxell 42.55 1.58333 R RD AD 00 0 1499 Europe/Andorra 1993-12-23
+3039862 Meritxell Meritxell Sanctuaire de Meritxeli,Sanctuaire de Meritxell,Santuari de Meritxell 42.55403 1.59087 P PPL AD AD 02 0 1640 Europe/Andorra 2007-04-16
+3039863 Rec de Mereig Rec de Mereig 42.56667 1.58333 H CNL AD 00 0 1919 Europe/Andorra 1993-12-23
+3039864 Pont de Mereig Pont de Mereig 42.55 1.6 S BDG AD 00 0 2210 Europe/Andorra 1993-12-23
+3039865 Planells de Mereig Planells de Mereig 42.56667 1.58333 T UPLD AD 00 0 1919 Europe/Andorra 1993-12-23
+3039866 Bosc de Mereig Bosc de Mereig 42.56667 1.58333 V FRST AD 00 0 1919 Europe/Andorra 1993-12-23
+3039867 Bordes de Mereig Bordes de Mereig 42.56302 1.58293 S FRMS AD 00 0 1637 Europe/Andorra 2011-04-19
+3039868 Mereig Mereig 42.56667 1.58333 A ADMD AD 00 0 1919 Europe/Andorra 1993-12-23
+3039869 Font de la Mentirosa Font de la Mentirosa 42.43333 1.51667 H SPNG AD 00 0 2031 Europe/Andorra 1993-12-23
+3039870 Estany dels Meners de la Coma Estany dels Meners de la Coma 42.61667 1.61667 H LK AD 00 0 2352 Europe/Andorra 1993-12-23
+3039871 Riu dels Meners Riu dels Meners 42.61667 1.63333 H STM AD 00 0 2331 Europe/Andorra 1993-12-23
+3039872 Font dels Meners Font dels Meners 42.61667 1.6 H SPNG AD 00 0 2528 Europe/Andorra 1993-12-23
+3039873 Font dels Meners Font dels Meners 42.46667 1.48333 H SPNG AD 00 0 1134 Europe/Andorra 1993-12-23
+3039874 Collada dels Meners Collada dels Meners 42.61667 1.6 T PASS AD 00 0 2528 Europe/Andorra 1993-12-23
+3039875 Pic de la Menera Pic de la Menera 42.51667 1.71667 T PK AD 00 0 2591 Europe/Andorra 1993-12-23
+3039876 Clots de la Menera Clots de la Menera 42.51667 1.71667 H RVN AD 00 0 2591 Europe/Andorra 1993-12-23
+3039877 Bosc del MenadÃs Bosc del Menadis 42.45 1.46667 V FRST AD 00 0 935 Europe/Andorra 1993-12-23
+3039878 Meligar d’Emportona Meligar d'Emportona 42.53333 1.66667 L LCTY AD 00 0 2489 Europe/Andorra 1993-12-23
+3039879 Serrat de Meligar Serrat de Meligar 42.55 1.45 T RDGE AD 00 0 1788 Europe/Andorra 1993-12-23
+3039880 Estany del Meligar Estany del Meligar 42.51667 1.66667 H LK AD 00 0 2410 Europe/Andorra 1993-12-23
+3039881 Roc del Melic Roc del Melic 42.58333 1.58333 T RK AD 00 0 1993 Europe/Andorra 1993-12-23
+3039882 Pic de Medécourbe Pic de Medecourbe Pic de Madecourbe,Pic de Medecorba,Pic de Medecourbe,Pic de Medécourbe 42.6037 1.44264 T PK AD 00 0 2914 2658 Europe/Andorra 2011-02-09
+3039883 Camà dels Matxos Cami dels Matxos 42.5 1.56667 R TRL AD 00 0 1776 Europe/Andorra 1993-12-23
+3039884 Basera Mateu Basera Mateu 42.5 1.5 T CLF AD 00 0 1135 Europe/Andorra 1993-12-23
+3039885 Serrat dels Matets Serrat dels Matets 42.56667 1.5 T SPUR AD 00 0 1636 Europe/Andorra 1993-12-23
+3039886 Bosc del Matet Bosc del Matet 42.61667 1.53333 V FRST AD 00 0 1609 Europe/Andorra 1993-12-23
+3039887 Bosc de les Matelles Bosc de les Matelles 42.51667 1.48333 V FRST AD 00 0 1839 Europe/Andorra 1993-12-23
+3039888 Clot de la Mata Clot de la Mata 42.56667 1.68333 T SLP AD 00 0 2340 Europe/Andorra 1993-12-23
+3039889 Canals de la Mata Canals de la Mata 42.61667 1.58333 H RVN AD 00 0 2374 Europe/Andorra 1993-12-23
+3039890 Canal de la Mata Canal de la Mata 42.51667 1.51667 H STM AD 00 0 1265 Europe/Andorra 1993-12-23
+3039891 Bosc de la Mata Bosc de la Mata 42.6 1.68333 V FRST AD 00 0 2089 Europe/Andorra 1993-12-23
+3039892 Bosc de la Mata Bosc de la Mata 42.6 1.61667 V FRST AD 00 0 2271 Europe/Andorra 1993-12-23
+3039893 Massolina Massolina 42.5 1.61667 H RVN AD 00 0 2560 Europe/Andorra 1993-12-23
+3039894 Riu de Massat Riu de Massat 42.556 1.68641 H STM AD 00 0 2083 Europe/Andorra 2011-04-19
+3039895 Cortal del Masover Cortal del Masover 42.53333 1.53333 S CRRL AD 00 0 1521 Europe/Andorra 1993-12-23
+3039896 Mas de Ribafeta Mas de Ribafeta 42.56936 1.48837 P PPL AD 04 0 1655 Europe/Andorra 2011-04-19
+3039897 Pont del Mas d’En Soler Pont del Mas d'En Soler 42.56667 1.51667 S BDG AD 00 0 1500 Europe/Andorra 1993-12-23
+3039898 Devesa del Mas d’Alins Devesa del Mas d'Alins 42.45 1.45 L GRAZ AD 00 0 1482 Europe/Andorra 1993-12-23
+3039899 Carretera del Mas d’Alins Carretera del Mas d'Alins 42.45 1.46667 R RD AD 00 0 935 Europe/Andorra 1993-12-23
+3039900 Borda del Mas d’Alins Borda del Mas d'Alins 42.43333 1.45 S HUTS AD 00 0 877 Europe/Andorra 1993-12-23
+3039901 Mas d’Alins Mas d'Alins 42.44126 1.44944 P PPL AD 06 0 1197 Europe/Andorra 2011-04-19
+3039902 Mas d’Alins Mas d'Alins 42.45 1.45 A ADMD AD 00 0 1482 Europe/Andorra 1993-12-23
+3039903 Solà del Mas Sola del Mas 42.45 1.5 T SLP AD 00 0 1614 Europe/Andorra 1993-12-23
+3039904 Obac del Mas Obac del Mas 42.56667 1.5 T SLP AD 00 0 1636 Europe/Andorra 1993-12-23
+3039905 Camà del Mas Cami del Mas 42.45 1.5 R TRL AD 00 0 1614 Europe/Andorra 1993-12-23
+3039906 Bordes del Mas Bordes del Mas 42.45 1.5 S HUTS AD 00 0 1614 Europe/Andorra 1993-12-23
+3039907 Allau del Mas Allau del Mas 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3039908 Borda del Marticella Borda del Marticella 42.58333 1.65 S HUT AD 00 0 1767 Europe/Andorra 1993-12-23
+3039909 Cortal del Martà Cortal del Marti 42.53333 1.53333 S CRRL AD 00 0 1521 Europe/Andorra 1993-12-23
+3039910 Collet Martà Collet Marti 42.5 1.48333 T SPUR AD 00 0 1316 Europe/Andorra 1993-12-23
+3039911 Collet Martà Collet Marti 42.46667 1.46667 T PK AD 00 0 1340 Europe/Andorra 1993-12-23
+3039912 Borda del Martà Borda del Marti 42.56667 1.6 S HUT AD 00 0 1655 Europe/Andorra 1993-12-23
+3039913 Marrades Negres Marrades Negres 42.56667 1.7 T TAL AD 00 0 2375 Europe/Andorra 1993-12-23
+3039914 Marrades Negres Marrades Negres 42.56667 1.55 H STM AD 00 0 1996 Europe/Andorra 1993-12-23
+3039915 Camà de les Marrades Cami de les Marrades 42.55 1.5 R TRL AD 00 0 1292 Europe/Andorra 1993-12-23
+3039916 Bony de les Marrades Bony de les Marrades 42.53333 1.5 T PK AD 00 0 1357 Europe/Andorra 1993-12-23
+3039917 Roc de Maria Roc de Maria 42.5 1.48333 T RK AD 00 0 1316 Europe/Andorra 1993-12-23
+3039918 Canal de Maria Canal de Maria 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3039919 Pont de la Margineda Pont de la Margineda 42.4838 1.49042 S BDG AD 00 0 991 Europe/Andorra 2011-04-19
+3039920 Coll de la Manyiga Coll de la Manyiga 42.46667 1.53333 T SPUR AD 00 0 2332 Europe/Andorra 1993-12-23
+3039921 Coll de la Manyiga Coll de la Manyiga 42.46667 1.48333 T PASS AD 00 0 1134 Europe/Andorra 1993-12-23
+3039922 Cortals de Manyat Cortals de Manyat 42.48333 1.51667 S HUTS AD 00 0 2061 Europe/Andorra 1993-12-23
+3039923 Manyat Manyat 42.47591 1.51661 L LCTY AD 00 0 1892 Europe/Andorra 2011-04-19
+3039924 Riu del Manegor Riu del Manegor 42.6 1.68333 H STM AD 00 0 2089 Europe/Andorra 1993-12-23
+3039925 Pleta del Manegor Pleta del Manegor 42.61667 1.7 L GRAZ AD 00 0 2285 Europe/Andorra 1993-12-23
+3039926 Bosc de la Mandurana Bosc de la Mandurana 42.56667 1.61667 V FRST AD 00 0 1920 Europe/Andorra 1993-12-23
+3039927 Borda del Mandicó Borda del Mandico 42.51667 1.55 S HUT AD 00 0 1322 Europe/Andorra 1993-12-23
+3039928 Pont del Mamó Pont del Mamo 42.55 1.48333 S BDG AD 00 0 1548 Europe/Andorra 1993-12-23
+3039929 Font de Mallol Font de Mallol 42.55 1.55 H SPNG AD 00 0 2097 Europe/Andorra 1993-12-23
+3039930 Canals Males Canals Males 42.46667 1.48333 H STM AD 00 0 1134 Europe/Andorra 1993-12-23
+3039931 Canals Males Canals Males 42.58333 1.46667 H RVN AD 00 0 1643 Europe/Andorra 1993-12-23
+3039932 Canal Mala Canal Mala 42.45 1.5 H RVN AD 00 0 1614 Europe/Andorra 1993-12-23
+3039933 Roca Major Roca Major 42.43333 1.5 T PK AD 00 0 1804 Europe/Andorra 1993-12-23
+3039934 Clot dels Mais Clot dels Mais 42.56667 1.61667 H RVN AD 00 0 1920 Europe/Andorra 1993-12-23
+3039935 Planada dels Maians Planada dels Maians 42.55 1.61667 T UPLD AD 00 0 2206 Europe/Andorra 1993-12-23
+3039936 Pic dels Maians Pic dels Maians 42.55 1.61667 T PK AD 00 0 2206 Europe/Andorra 1993-12-23
+3039937 Costa dels Maians Costa dels Maians 42.55 1.61667 T SLP AD 00 0 2206 Europe/Andorra 1993-12-23
+3039938 Canal dels Maians Canal dels Maians 42.5 1.51667 H STM AD 00 0 1410 Europe/Andorra 1993-12-23
+3039939 Bosc dels Maians Bosc dels Maians 42.48333 1.51667 V FRST AD 00 0 2061 Europe/Andorra 1993-12-23
+3039940 Bony dels Maians Bony dels Maians 42.53333 1.61667 T SPUR AD 00 0 2237 Europe/Andorra 1993-12-23
+3039941 Pic de la Maiana Pic de la Maiana 42.4821 1.60014 T PK AD 00 0 2250 Europe/Andorra 2011-04-19
+3039942 Collada de la Maiana Collada de la Maiana 42.48333 1.6 T PASS AD 00 0 2250 Europe/Andorra 1993-12-23
+3039943 Tosa del Maià Tosa del Maia 42.55 1.71667 T UPLD AD 00 0 2192 Europe/Andorra 1993-12-23
+3039944 Pleta del Maià Pleta del Maia 42.56667 1.73333 L GRAZ AD 00 0 2096 Europe/Andorra 1993-12-23
+3039945 Pic del Maià Pic del Maia Pic Mata,Pic del Maia,Pic del Maià 42.55 1.71667 T PK AD AD 00 0 2192 Europe/Andorra 2011-11-05
+3039946 Obagot del Maià Obagot del Maia 42.56667 1.73333 T SLP AD 00 0 2096 Europe/Andorra 1993-12-23
+3039947 Riu Madriu Riu Madriu Madriu,Riu Madriu 42.49697 1.57954 H STM AD 00 0 1888 Europe/Andorra 2011-11-05
+3039948 Basers de Madona Basers de Madona 42.61667 1.63333 T CLF AD 00 0 2331 Europe/Andorra 1993-12-23
+3039949 Canal de Luixent Passader Canal de Luixent Passader 42.5 1.5 H STM AD 00 0 1135 Europe/Andorra 1993-12-23
+3039950 L’Ovella L'Ovella 42.56667 1.6 L LCTY AD 00 0 1655 Europe/Andorra 1993-12-23
+3039951 Los Travessers Los Travessers 42.56667 1.43333 L LCTY AD 00 0 2402 Europe/Andorra 1993-12-23
+3039952 L’Orri Amagat L'Orri Amagat 42.6 1.6 L LCTY AD 00 0 2143 Europe/Andorra 1993-12-23
+3039953 L’Obagueta L'Obagueta 42.53333 1.55 L LCTY AD 00 0 1344 Europe/Andorra 1993-12-23
+3039954 Roc dels Llumeners Roc dels Llumeners 42.58333 1.46667 T RK AD 00 0 1643 Europe/Andorra 1993-12-23
+3039955 Canal dels Llumeners Canal dels Llumeners 42.58333 1.46667 H RVN AD 00 0 1643 Europe/Andorra 1993-12-23
+3039956 Riu de Llumeneres Riu de Llumeneres 42.46591 1.49579 H STM AD 00 0 1232 Europe/Andorra 2011-04-19
+3039957 Plana de Llumeneres Plana de Llumeneres 42.46667 1.51667 T UPLD AD 00 0 1985 Europe/Andorra 1993-12-23
+3039958 Cortal de Llumeneres Cortal de Llumeneres 42.46667 1.5 S CRRL AD 00 0 1383 Europe/Andorra 1993-12-23
+3039959 Llumeneres Llumeneres Llumeneres 42.46667 1.51667 P PPL AD 06 0 1985 Europe/Andorra 2011-11-05
+3039960 Canals del Lloset Canals del Lloset 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3039961 Canal del Lloset Canal del Lloset 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3039962 Bordes del Lloset Bordes del Lloset 42.55 1.6 S HUT AD 00 0 2210 Europe/Andorra 1993-12-23
+3039963 Lloset Lloset 42.53333 1.6 A ADMD AD 00 0 1888 Europe/Andorra 1993-12-23
+3039964 Costa del Lloser de Naudà Costa del Lloser de Naudi 42.56667 1.56667 T SLP AD 00 0 2089 Europe/Andorra 1993-12-23
+3039965 Solana del Lloser Solana del Lloser 42.46667 1.45 T SLP AD 00 0 1562 Europe/Andorra 1993-12-23
+3039966 Roc del Lloser Roc del Lloser 42.58333 1.51667 T RK AD 00 0 1722 Europe/Andorra 1993-12-23
+3039967 Canal del Lloser Canal del Lloser 42.51667 1.51667 H STM AD 00 0 1265 Europe/Andorra 1993-12-23
+3039968 Tossal de la Llosada Tossal de la Llosada Pic Llosada,Tossal de la Llosada 42.54862 1.64567 T PK AD 00 0 2422 Europe/Andorra 2011-11-05
+3039969 Tosa de la Llosada Tosa de la Llosada 42.55 1.63333 T UPLD AD 00 0 2336 Europe/Andorra 1993-12-23
+3039970 Riu de la Llosada Riu de la Llosada 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3039971 Obaga de la Llosada Obaga de la Llosada 42.53333 1.61667 T SLP AD 00 0 2237 Europe/Andorra 1993-12-23
+3039972 Emprius de la Llosada Emprius de la Llosada 42.53333 1.61667 L CMN AD 00 0 2237 Europe/Andorra 1993-12-23
+3039973 Basers de la Llosada Basers de la Llosada 42.55 1.63333 T CLF AD 00 0 2336 Europe/Andorra 1993-12-23
+3039974 Riu Llosà Riu Llosa 42.45 1.46667 H STM AD 00 0 935 Europe/Andorra 1993-12-23
+3039975 Grau de la Llosa Grau de la Llosa 42.61667 1.55 T SLP AD 00 0 2007 Europe/Andorra 1993-12-23
+3039976 Collet de la Llosa Collet de la Llosa 42.56667 1.5 T PASS AD 00 0 1636 Europe/Andorra 1993-12-23
+3039977 Clots de la Llosa Clots de la Llosa 42.61667 1.61667 H RVN AD 00 0 2352 Europe/Andorra 1993-12-23
+3039978 Canal de la Llosa Canal de la Llosa 42.56667 1.5 H STM AD 00 0 1636 Europe/Andorra 1993-12-23
+3039979 Llorts Llorts Llors,Llorta,Lors 42.59625 1.52658 P PPL AD AD 05 0 1695 Europe/Andorra 2007-04-16
+3039980 Canal de les Llongues Canal de les Llongues 42.55 1.51667 H STM AD 00 0 1397 Europe/Andorra 1993-12-23
+3039981 Bosc de les Llongues Bosc de les Llongues 42.55 1.51667 V FRST AD 00 0 1397 Europe/Andorra 1993-12-23
+3039982 Costa dels Llomassos Costa dels Llomassos 42.58333 1.45 T SLP AD 00 0 2156 Europe/Andorra 1993-12-23
+3039983 Canal dels Llomassos Canal dels Llomassos 42.55 1.46667 H STM AD 00 0 1585 Europe/Andorra 1993-12-23
+3039984 Pleta del Llomar Pleta del Llomar 42.61667 1.56667 L GRAZ AD 00 0 2228 Europe/Andorra 1993-12-23
+3039985 Clot del Llomar Clot del Llomar 42.53333 1.48333 H RVN AD 00 0 1677 Europe/Andorra 1993-12-23
+3039986 Camà de la Llobatera Cami de la Llobatera 42.55 1.48333 R TRL AD 00 0 1548 Europe/Andorra 1993-12-23
+3039987 Canal Llisa Canal Llisa 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3039988 Canal Llisa Canal Llisa 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3039989 Torrent Llimois Torrent Llimois 42.48333 1.43333 H STM AD 00 0 1938 Europe/Andorra 1993-12-23
+3039990 Solana dels Llimois Solana dels Llimois 42.5 1.43333 T SLP AD 00 0 1654 Europe/Andorra 1993-12-23
+3039991 Bosc de Llevai Bosc de Llevai 42.53333 1.55 V FRST AD 00 0 1344 Europe/Andorra 1993-12-23
+3039992 Basers de la Llessa Basers de la Llessa 42.48333 1.5 T CLF AD 00 0 1631 Europe/Andorra 1993-12-23
+3039993 Planell de la Llentiga Planell de la Llentiga 42.6 1.51667 T UPLD AD 00 0 1445 Europe/Andorra 1993-12-23
+3039994 Barranc del Llempo Barranc del Llempo 42.55 1.51667 H STM AD 00 0 1397 Europe/Andorra 1993-12-23
+3039995 Casa Llècsia Casa Llecsia 42.56667 1.6 S HSE AD 00 0 1655 Europe/Andorra 1993-12-23
+3039996 Borda del Llècsia Borda del Llecsia 42.58333 1.6 S HUT AD 00 0 1828 Europe/Andorra 1993-12-23
+3039997 Solana dels Llaurers Solana dels Llaurers 42.48333 1.43333 T SLP AD 00 0 1938 Europe/Andorra 1993-12-23
+3039998 Obaga dels Llaurers Obaga dels Llaurers 42.48333 1.43333 T SLP AD 00 0 1938 Europe/Andorra 1993-12-23
+3039999 Bony dels Llaurers Bony dels Llaurers 42.48333 1.43333 T PK AD 00 0 1938 Europe/Andorra 1993-12-23
+3040000 Roc Llarg Roc Llarg 42.55 1.45 T SPUR AD 00 0 1788 Europe/Andorra 1993-12-23
+3040001 Pont del Llarg Pont del Llarg 42.6 1.68333 S BDG AD 00 0 2089 Europe/Andorra 1993-12-23
+3040002 Fontanal Llarg Fontanal Llarg 42.53333 1.46667 H SPNG AD 00 0 1846 Europe/Andorra 1993-12-23
+3040003 Casa Llarg Casa Llarg 42.55 1.58333 S HSE AD 00 0 1499 Europe/Andorra 1993-12-23
+3040004 Canal del Llarg Canal del Llarg 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3040005 Solana dels Llampells Solana dels Llampells 42.51667 1.46667 T SLP AD 00 0 1840 Europe/Andorra 1993-12-23
+3040006 Torrent de la Llampa Torrent de la Llampa 42.56667 1.6 H STM AD 00 0 1655 Europe/Andorra 1993-12-23
+3040007 Riu dels Llacs Riu dels Llacs 42.55 1.43333 H STM AD 00 0 1949 Europe/Andorra 1993-12-23
+3040008 Pleta dels Llacs Pleta dels Llacs 42.6 1.63333 L GRAZ AD 00 0 1893 Europe/Andorra 1993-12-23
+3040009 Pic dels Llacs Pic dels Llacs Pic dels Llacs 42.53333 1.41667 T PK AD 00 0 1948 Europe/Andorra 2011-11-05
+3040010 Pales dels Llacs Pales dels Llacs 42.6 1.61667 T CLF AD 00 0 2271 Europe/Andorra 1993-12-23
+3040011 Font dels Llacs Font dels Llacs 42.55 1.43333 H SPNG AD 00 0 1949 Europe/Andorra 1993-12-23
+3040012 Bosc dels Llacs Bosc dels Llacs 42.55 1.41667 V FRST AD 00 0 2105 Europe/Andorra 1993-12-23
+3040013 L’Hospital L'Hospital 42.48333 1.55 A ADMD AD 00 0 2233 Europe/Andorra 1993-12-23
+3040014 L’Hortó L'Horto 42.58333 1.63333 L LCTY AD 00 0 1722 Europe/Andorra 1993-12-23
+3040015 L’Hortó L'Horto 42.56667 1.51667 L LCTY AD 00 0 1500 Europe/Andorra 1993-12-23
+3040016 L’Hortell L'Hortell 42.61667 1.51667 L LCTY AD 00 0 1716 Europe/Andorra 1993-12-23
+3040017 Les Tres Oles Les Tres Oles 42.51667 1.55 H RVN AD 00 0 1322 Europe/Andorra 1993-12-23
+3040018 Les Tres Fonts Les Tres Fonts 42.5 1.46667 H SPNG AD 00 0 1678 Europe/Andorra 1993-12-23
+3040019 Les Tarteres Les Tarteres 42.45 1.48333 L LCTY AD 00 0 1111 Europe/Andorra 1993-12-23
+3040020 L’Estanyassa L'Estanyassa 42.58333 1.65 L LCTY AD 00 0 1767 Europe/Andorra 1993-12-23
+3040021 Les Tallades Les Tallades 42.61667 1.55 A ADMD AD 00 0 2007 Europe/Andorra 1993-12-23
+3040022 L’Estall L'Estall 42.5 1.58333 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040023 Les Sorobilles Les Sorobilles 42.56667 1.53333 T CLF AD 00 0 1669 Europe/Andorra 1993-12-23
+3040024 Les Solanelles Les Solanelles 42.55 1.6 L LCTY AD 00 0 2210 Europe/Andorra 1993-12-23
+3040025 Les Sobiranes Les Sobiranes 42.61667 1.55 L LCTY AD 00 0 2007 Europe/Andorra 1993-12-23
+3040026 Les Salses Les Salses 42.63333 1.5 T SLP AD 00 0 1979 Europe/Andorra 1993-12-23
+3040027 Les Salines Les Salines 42.60952 1.53697 P PPL AD 05 0 1793 Europe/Andorra 2007-04-16
+3040028 Les Ribaltes Les Ribaltes 42.53333 1.53333 L LCTY AD 00 0 1521 Europe/Andorra 1993-12-23
+3040029 Les Rebes Les Rebes 42.61667 1.61667 T UPLD AD 00 0 2352 Europe/Andorra 1993-12-23
+3040030 Les Queroles Les Queroles 42.6 1.53333 L LCTY AD 00 0 1695 Europe/Andorra 1993-12-23
+3040031 Les Portes Les Portes 42.48333 1.48333 L LCTY AD 00 0 981 Europe/Andorra 1993-12-23
+3040032 Les Planes de Sornàs Les Planes de Sornas 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3040033 Les Planes Les Planes 42.63333 1.5 L LCTY AD 00 0 1979 Europe/Andorra 1993-12-23
+3040034 Les Planes Les Planes 42.53333 1.6 A ADMD AD 00 0 1888 Europe/Andorra 1993-12-23
+3040035 Les Planades Les Planades 42.6 1.68333 L LCTY AD 00 0 2089 Europe/Andorra 1993-12-23
+3040036 Les Pedrusques Les Pedrusques 42.51667 1.63333 T SLP AD 00 0 2379 Europe/Andorra 1993-12-23
+3040037 Les Pedrasses Les Pedrasses 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3040038 Les Pardines Les Pardines 42.58333 1.48333 T SLP AD 00 0 1809 Europe/Andorra 1993-12-23
+3040039 Les Pardines Les Pardines 42.43333 1.46667 S HUTS AD 00 0 1113 Europe/Andorra 1993-12-23
+3040040 Les Obagues Les Obagues 42.58333 1.48333 L LCTY AD 00 0 1809 Europe/Andorra 1993-12-23
+3040041 Les Neres Les Neres 42.55 1.56667 A ADMD AD 00 0 1828 Europe/Andorra 1993-12-23
+3040042 Les Molleres Les Molleres 42.55 1.53333 L LCTY AD 00 0 1593 Europe/Andorra 1993-12-23
+3040043 Les Majobarnes Les Majobarnes 42.55 1.48333 L LCTY AD 00 0 1548 Europe/Andorra 1993-12-23
+3040044 Les Llanasques Les Llanasques 42.58333 1.58333 L LCTY AD 00 0 1993 Europe/Andorra 1993-12-23
+3040045 Les Forques Les Forques 42.55 1.53333 L LCTY AD 00 0 1593 Europe/Andorra 1993-12-23
+3040046 Les Fonts Les Fonts 42.6 1.6 T UPLD AD 00 0 2143 Europe/Andorra 1993-12-23
+3040047 Les Fonts Les Fonts 42.6 1.48333 L LCTY AD 00 0 2441 Europe/Andorra 1993-12-23
+3040048 Les Feixes Les Feixes 42.55 1.43333 L LCTY AD 00 0 1949 Europe/Andorra 1993-12-23
+3040049 Les Feixes Les Feixes 42.53333 1.46667 L LCTY AD 00 0 1846 Europe/Andorra 1993-12-23
+3040050 Les Fargues Les Fargues 42.48333 1.6 S ANS AD 00 0 2250 Europe/Andorra 1993-12-23
+3040051 les Escaldes les Escaldes Ehskal'des-Ehndzhordani,Escaldes,Escaldes-Engordany,Les Escaldes,esukarudesu=engorudani jiao qu,lai sai si ka er de-en ge er da,ÐÑкальдеÑ-Ðнджордани,エスカルデスï¼ã‚¨ãƒ³ã‚´ãƒ«ãƒ€ãƒ‹æ•™åŒº,èŠå¡žæ–¯å¡çˆ¾å¾·-æ©æˆˆçˆ¾é”,èŠå¡žæ–¯å¡çˆ¾å¾·ï¼æ©æˆˆçˆ¾é” 42.50729 1.53414 P PPLA AD 08 15853 1350 Europe/Andorra 2008-10-15
+3040052 Les Deveses Les Deveses 42.53333 1.65 A ADMD AD 00 0 2508 Europe/Andorra 1993-12-23
+3040053 Les Costes Les Costes 42.51667 1.55 A ADMD AD 00 0 1322 Europe/Andorra 1993-12-23
+3040054 Les Costeres Les Costeres 42.53333 1.53333 L LCTY AD 00 0 1521 Europe/Andorra 1993-12-23
+3040055 Les Comelletes Les Comelletes 42.48333 1.46667 L LCTY AD 00 0 1148 Europe/Andorra 1993-12-23
+3040056 Les Comarques Les Comarques 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3040057 Les Colleroles Les Colleroles 42.58333 1.6 T SLP AD 00 0 1828 Europe/Andorra 1993-12-23
+3040058 Les Codolles Les Codolles 42.53333 1.51667 L LCTY AD 00 0 1361 Europe/Andorra 1993-12-23
+3040059 L’Escobar L'Escobar 42.61667 1.68333 L LCTY AD 00 0 2406 Europe/Andorra 1993-12-23
+3040060 Les Claperes Les Claperes 42.55 1.5 L LCTY AD 00 0 1292 Europe/Andorra 1993-12-23
+3040061 Les Carboneres Les Carboneres 42.48333 1.46667 L LCTY AD 00 0 1148 Europe/Andorra 1993-12-23
+3040062 Les Canyorques Les Canyorques 42.58333 1.43333 T SLP AD 00 0 2412 Europe/Andorra 1993-12-23
+3040063 Les Canals Les Canals 42.58333 1.58333 L LCTY AD 00 0 1993 Europe/Andorra 1993-12-23
+3040064 Les Canaletes Les Canaletes 42.6 1.45 T RKS AD 00 0 2174 Europe/Andorra 1993-12-23
+3040065 Les Canadilles Les Canadilles 42.48333 1.48333 H STM AD 00 0 981 Europe/Andorra 1993-12-23
+3040066 Les Bordes Les Bordes 42.58333 1.63333 S HUTS AD 00 0 1722 Europe/Andorra 1993-12-23
+3040067 Les Bons Les Bons Els Bons 42.53873 1.58649 P PPL AD AD 03 0 1490 Europe/Andorra 2007-04-16
+3040068 Les Bartigues Les Bartigues 42.6 1.53333 L LCTY AD 00 0 1695 Europe/Andorra 1993-12-23
+3040069 Les Barreres Les Barreres 42.55 1.6 L LCTY AD 00 0 2210 Europe/Andorra 1993-12-23
+3040070 Les Barreres Les Barreres 42.53333 1.53333 L LCTY AD 00 0 1521 Europe/Andorra 1993-12-23
+3040071 Les Bagelles Les Bagelles 42.46667 1.5 L LCTY AD 00 0 1383 Europe/Andorra 1993-12-23
+3040072 Les Aubes Les Aubes 42.51667 1.55 L LCTY AD 00 0 1322 Europe/Andorra 1993-12-23
+3040073 Les Aspades Les Aspades 42.56667 1.55 L LCTY AD 00 0 1996 Europe/Andorra 1993-12-23
+3040074 Les Angleves Les Angleves 42.53333 1.53333 L LCTY AD 00 0 1521 Europe/Andorra 1993-12-23
+3040075 Les Allaus Les Allaus 42.63333 1.53333 L LCTY AD 00 0 2072 Europe/Andorra 1993-12-23
+3040076 Les Agols Les Agols 42.51667 1.6 A ADMD AD 00 0 2085 Europe/Andorra 1993-12-23
+3040077 Les Abelletes Les Abelletes 42.53333 1.73333 L LCTY AD 00 0 2300 Europe/Andorra 1993-12-23
+3040078 L’Ensegur L'Ensegur 42.58333 1.53333 A ADMD AD 00 0 1924 Europe/Andorra 1993-12-23
+3040079 L’Avetar L'Avetar 42.6 1.68333 L LCTY AD 00 0 2089 Europe/Andorra 1993-12-23
+3040080 Planell de Laverdú Planell de Laverdu 42.63333 1.53333 T UPLD AD 00 0 2072 Europe/Andorra 1993-12-23
+3040081 Laverdú Laverdu 42.63333 1.53333 L LCTY AD 00 0 2072 Europe/Andorra 1993-12-23
+3040082 La Vaquerissa La Vaquerissa 42.55 1.45 L LCTY AD 00 0 1788 Europe/Andorra 1993-12-23
+3040083 L’Ausany L'Ausany 42.61667 1.53333 L LCTY AD 00 0 1609 Europe/Andorra 1993-12-23
+3040084 La Uïna La Uina 42.55 1.51667 A ADMD AD 00 0 1397 Europe/Andorra 1993-12-23
+3040085 La Trava La Trava 42.58333 1.61667 L LCTY AD 00 0 1707 Europe/Andorra 1993-12-23
+3040086 La Trava La Trava 42.56667 1.53333 L GRAZ AD 00 0 1669 Europe/Andorra 1993-12-23
+3040087 La Tosa La Tosa 42.56667 1.46667 L LCTY AD 00 0 1673 Europe/Andorra 1993-12-23
+3040088 L’Assalador L'Assalador 42.5 1.43333 L GRAZ AD 00 0 1654 Europe/Andorra 1993-12-23
+3040089 L’Asparró L'Asparro 42.48333 1.5 T PK AD 00 0 1631 Europe/Andorra 1993-12-23
+3040090 La Solaneta La Solaneta 42.53333 1.53333 L LCTY AD 00 0 1521 Europe/Andorra 1993-12-23
+3040091 La Solanella La Solanella 42.46667 1.5 L LCTY AD 00 0 1383 Europe/Andorra 1993-12-23
+3040092 La Solana La Solana 42.58333 1.76667 A ADMD AD 00 0 1945 Europe/Andorra 1993-12-23
+3040093 La Serreta La Serreta 42.58333 1.61667 T SPUR AD 00 0 1707 Europe/Andorra 1993-12-23
+3040094 La Serradora La Serradora 42.46667 1.46667 L LCTY AD 00 0 1340 Europe/Andorra 1993-12-23
+3040095 La Serra La Serra 42.45204 1.49609 S HUTS AD 00 0 1270 Europe/Andorra 2011-04-19
+3040096 La Senyoreta La Senyoreta 42.45 1.48333 A ADMD AD 00 0 1111 Europe/Andorra 1993-12-23
+3040097 L’Artiga L'Artiga 42.56946 1.60243 L LCTY AD 00 0 1655 Europe/Andorra 2011-04-19
+3040098 L’Artic L'Artic 42.5 1.46667 L LCTY AD 00 0 1678 Europe/Andorra 1993-12-23
+3040099 La Rovira La Rovira 42.45 1.46667 L LCTY AD 00 0 935 Europe/Andorra 1993-12-23
+3040100 La Roca La Roca 42.55 1.58333 L LCTY AD 00 0 1499 Europe/Andorra 1993-12-23
+3040101 L’Armiana L'Armiana 42.58333 1.6 A ADMD AD 00 0 1828 Europe/Andorra 1993-12-23
+3040102 La Riberola La Riberola 42.45 1.48333 L LCTY AD 00 0 1111 Europe/Andorra 1993-12-23
+3040103 La Rabassa La Rabassa 42.63333 1.55 A ADMD AD 00 0 2053 Europe/Andorra 1993-12-23
+3040104 La Rabassa La Rabassa Bois de la Rabassa,Bois de la Rabossa,La Rabassa 42.43333 1.51667 A ADMD AD AD 00 0 2031 Europe/Andorra 2011-11-05
+3040105 La Quera La Quera 42.51667 1.51667 L LCTY AD 00 0 1265 Europe/Andorra 1993-12-23
+3040106 La Presó La Preso 42.61667 1.56667 L LCTY AD 00 0 2228 Europe/Andorra 1993-12-23
+3040107 La Posa La Posa 42.53333 1.6 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040108 La Portelleta La Portelleta 42.48333 1.65 L LCTY AD 00 0 2658 Europe/Andorra 1993-12-23
+3040109 La Portella La Portella 42.55218 1.62761 T PK AD 00 0 2364 Europe/Andorra 2011-04-19
+3040110 La Portella La Portella 42.45 1.5 T PK AD 00 0 1614 Europe/Andorra 1993-12-23
+3040111 La Portella La Portella 42.56667 1.71667 A ADMD AD 00 0 2219 Europe/Andorra 1993-12-23
+3040112 La Polguera La Polguera 42.56667 1.58333 L LCTY AD 00 0 1919 Europe/Andorra 1993-12-23
+3040113 La Pleta La Pleta 42.55 1.43333 S HUTS AD 00 0 1949 Europe/Andorra 1993-12-23
+3040114 La Planada La Planada 42.6 1.6 T UPLD AD 00 0 2143 Europe/Andorra 1993-12-23
+3040115 La Plana La Plana 42.51667 1.51667 T UPLD AD 00 0 1265 Europe/Andorra 1993-12-23
+3040116 La Plana La Plana 42.5 1.55 L LCTY AD 00 0 1566 Europe/Andorra 1993-12-23
+3040117 La Pinatella La Pinatella 42.53333 1.6 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040118 La Peracaus La Peracaus 42.56667 1.6 L LCTY AD 00 0 1655 Europe/Andorra 1993-12-23
+3040119 La Pera La Pera 42.56667 1.46667 L LCTY AD 00 0 1673 Europe/Andorra 1993-12-23
+3040120 La Peguera La Peguera 42.45 1.53333 A ADMD AD 00 0 1859 Europe/Andorra 1993-12-23
+3040121 La Pedregosa La Pedregosa 42.58333 1.61667 L LCTY AD 00 0 1707 Europe/Andorra 1993-12-23
+3040122 La Passera La Passera 42.6 1.53333 H STM AD 00 0 1695 Europe/Andorra 1993-12-23
+3040123 La Palomera La Palomera 42.58333 1.78333 L LCTY AD 00 0 1694 Europe/Andorra 1993-12-23
+3040124 L’Angonella L'Angonella 42.6 1.5 A ADMD AD 00 0 1923 Europe/Andorra 1993-12-23
+3040125 L’Andorrana L'Andorrana 42.55 1.43333 L LCTY AD 00 0 1949 Europe/Andorra 1993-12-23
+3040126 La Muga La Muga 42.48333 1.65 L LCTY AD 00 0 2658 Europe/Andorra 1993-12-23
+3040127 La Molina La Molina 42.45 1.51667 L LCTY AD 00 0 1790 Europe/Andorra 1993-12-23
+3040128 La Molina La Molina 42.51667 1.6 A ADMD AD 00 0 2085 Europe/Andorra 1993-12-23
+3040129 La Moixella La Moixella 42.43333 1.46667 S HUTS AD 00 0 1113 Europe/Andorra 1993-12-23
+3040130 La Mentirosa La Mentirosa 42.43333 1.51667 L LCTY AD 00 0 2031 Europe/Andorra 1993-12-23
+3040131 Parròquia de la Massana Parroquia de la Massana La Massana,Parroquia de la Massana,Parròquia de la Massana,la Massana 42.56667 1.48333 A ADM1 AD 04 8953 1508 Europe/Andorra 2008-03-17
+3040132 la Massana la Massana La Macana,La Massana,La Maçana,La-Massana,la Massana,ma sa na,Ла-МаÑÑана,ラ・マサナ教区,马è¨çº³ 42.54499 1.51483 P PPLA AD 04 7211 1257 Europe/Andorra 2008-10-15
+3040133 La Margineda La Margineda 42.48333 1.5 A ADMD AD 00 0 1631 Europe/Andorra 1993-12-23
+3040134 La Mandurana La Mandurana 42.56667 1.61667 A ADMD AD 00 0 1920 Europe/Andorra 1993-12-23
+3040135 L’Alzinar L'Alzinar 42.48333 1.5 L LCTY AD 00 0 1631 Europe/Andorra 1993-12-23
+3040136 La Lomera La Lomera 42.46667 1.48333 L LCTY AD 00 0 1134 Europe/Andorra 1993-12-23
+3040137 La Llosada La Llosada 42.55 1.63333 A ADMD AD 00 0 2336 Europe/Andorra 1993-12-23
+3040138 Serrat de La Llonga Serrat de La Llonga 42.56667 1.51667 T RDGE AD 00 0 1500 Europe/Andorra 1993-12-23
+3040139 La Llonga La Llonga 42.56667 1.51667 A ADMD AD 00 0 1500 Europe/Andorra 1993-12-23
+3040140 l'Aldosa l'Aldosa 42.58333 1.63333 P PPL AD 02 195 1722 Europe/Andorra 2007-04-29
+3040141 l'Aldosa l'Aldosa 42.54391 1.52289 P PPL AD 04 594 1397 Europe/Andorra 2007-04-29
+3040142 L’Airola L'Airola 42.46667 1.46667 L LCTY AD 00 0 1340 Europe/Andorra 1993-12-23
+3040143 La Gonarda La Gonarda 42.55 1.53333 L LCTY AD 00 0 1593 Europe/Andorra 1993-12-23
+3040144 La Gonarda La Gonarda 42.55 1.53333 A ADMD AD 00 0 1593 Europe/Andorra 1993-12-23
+3040145 La Ginebrosa La Ginebrosa 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3040146 La Garganta La Garganta 42.53333 1.6 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040147 La Gargallera La Gargallera 42.48333 1.45 L LCTY AD 00 0 1195 Europe/Andorra 1993-12-23
+3040148 La Fonteta La Fonteta 42.58333 1.46667 L LCTY AD 00 0 1643 Europe/Andorra 1993-12-23
+3040149 La Fita La Fita 42.55 1.45 L LCTY AD 00 0 1788 Europe/Andorra 1993-12-23
+3040150 La Cuina La Cuina 42.56667 1.48333 T SPUR AD 00 0 1508 Europe/Andorra 1993-12-23
+3040151 La Creu de les Portes La Creu de les Portes 42.48333 1.48333 L LCTY AD 00 0 981 Europe/Andorra 1993-12-23
+3040152 La Costa La Costa 42.57834 1.64324 P PPL AD 02 0 1737 Europe/Andorra 2011-04-19
+3040153 La Coruvilla La Coruvilla 42.58333 1.46667 A ADMD AD 00 0 1643 Europe/Andorra 1993-12-23
+3040154 La Cortinada La Cortinada La Cortinada 42.57601 1.51896 P PPL AD 05 0 1722 Europe/Andorra 2011-11-05
+3040155 La Cortada La Cortada 42.45 1.5 L LCTY AD 00 0 1614 Europe/Andorra 1993-12-23
+3040156 La Comella La Comella 42.51667 1.68333 L LCTY AD 00 0 2352 Europe/Andorra 1993-12-23
+3040157 La Comella La Comella 42.5 1.53333 S HUTS AD 00 0 1574 Europe/Andorra 1993-12-23
+3040158 La Comarqueta La Comarqueta 42.58333 1.71667 L LCTY AD 00 0 2553 Europe/Andorra 1993-12-23
+3040159 La Comarqueta La Comarqueta 42.48333 1.63333 L LCTY AD 00 0 2296 Europe/Andorra 1993-12-23
+3040160 La Colilla La Colilla 42.53333 1.6 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040161 La Colilla La Colilla 42.5 1.58333 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040162 La Clota La Clota 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3040163 La Caubella La Caubella 42.58333 1.48333 L CLG AD 00 0 1809 Europe/Andorra 1993-12-23
+3040164 La Castelleta La Castelleta 42.53333 1.51667 T SPUR AD 00 0 1361 Europe/Andorra 1993-12-23
+3040165 La Carbonera La Carbonera 42.46667 1.63333 L LCTY AD 00 0 2619 Europe/Andorra 1993-12-23
+3040166 La Caolla La Caolla 42.5 1.61667 T SPUR AD 00 0 2560 Europe/Andorra 1993-12-23
+3040167 La Callisa La Callisa 42.56667 1.48333 S BDG AD 00 0 1508 Europe/Andorra 1993-12-23
+3040168 La Cabeça La Cabeca 42.48333 1.45 L LCTY AD 00 0 1195 Europe/Andorra 1994-04-16
+3040169 La Cabanella La Cabanella 42.51667 1.46667 A ADMD AD 00 0 1840 Europe/Andorra 1993-12-23
+3040170 La Burna La Burna 42.6 1.48333 T CRQ AD 00 0 2441 Europe/Andorra 1993-12-23
+3040171 La Borda Nova La Borda Nova 42.61667 1.53333 S HUT AD 00 0 1609 Europe/Andorra 1993-12-23
+3040172 La Bor La Bor 42.55 1.58333 A ADMD AD 00 0 1499 Europe/Andorra 1993-12-23
+3040173 La Boixosa La Boixosa 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3040174 La Beçosa La Becosa 42.61667 1.55 L LCTY AD 00 0 2007 Europe/Andorra 1994-04-16
+3040175 La Bauma La Bauma 42.48333 1.61667 S CAVE AD 00 0 2217 Europe/Andorra 1993-12-23
+3040176 La Basseta La Basseta 42.48333 1.65 H LK AD 00 0 2658 Europe/Andorra 1993-12-23
+3040177 La Basera La Basera 42.58333 1.65 L LCTY AD 00 0 1767 Europe/Andorra 1993-12-23
+3040178 La Bartra del Ganxo La Bartra del Ganxo 42.48333 1.46667 L LCTY AD 00 0 1148 Europe/Andorra 1993-12-23
+3040179 La Bartra La Bartra 42.51667 1.55 L LCTY AD 00 0 1322 Europe/Andorra 1993-12-23
+3040180 La Bartra La Bartra 42.46667 1.46667 L LCTY AD 00 0 1340 Europe/Andorra 1993-12-23
+3040181 La Bartra La Bartra La Barta,La Bartra 42.51667 1.56667 A ADMD AD AD 00 0 1759 Europe/Andorra 2011-11-05
+3040182 L’Abarsetar L'Abarsetar 42.61667 1.51667 L LCTY AD 00 0 1716 Europe/Andorra 1993-12-23
+3040183 L’Abarsetar L'Abarsetar 42.53333 1.61667 L LCTY AD 00 0 2237 Europe/Andorra 1993-12-23
+3040184 La Baladosa La Baladosa 42.6 1.68333 L LCTY AD 00 0 2089 Europe/Andorra 1993-12-23
+3040185 Tosa de Juclar Tosa de Juclar 42.6 1.71667 T UPLD AD 00 0 2516 Europe/Andorra 1993-12-23
+3040186 Solana de Juclar Solana de Juclar 42.6 1.7 T SLP AD 00 0 2354 Europe/Andorra 1993-12-23
+3040187 Riu de Juclar Riu de Juclar 42.60124 1.69807 H STM AD 00 0 2147 Europe/Andorra 2011-04-19
+3040188 Pleta de Juclar Pleta de Juclar 42.6 1.71667 L GRAZ AD 00 0 2516 Europe/Andorra 1993-12-23
+3040189 Obaga de Juclar Obaga de Juclar 42.61667 1.7 T SLP AD 00 0 2285 Europe/Andorra 1993-12-23
+3040190 Collada de Juclar Collada de Juclar Col de Joucla,Col de l' Albe,Col de l’ Albe,Collada de Juclar,Port de Jogela,Port de Joucla 42.61667 1.73333 T PASS AD 00 0 2508 Europe/Andorra 2011-11-05
+3040191 Canals de Juclar Canals de Juclar 42.6 1.71667 H RVN AD 00 0 2516 Europe/Andorra 1993-12-23
+3040192 Camà de Juclar Cami de Juclar 42.6 1.7 R TRL AD 00 0 2354 Europe/Andorra 1993-12-23
+3040193 Alt de Juclar Alt de Juclar 42.61667 1.7 T RDGE AD 00 0 2285 Europe/Andorra 1993-12-23
+3040194 Juclar Juclar 42.6 1.71667 A ADMD AD 00 0 2516 Europe/Andorra 1993-12-23
+3040195 Carretera de la Juberrussa Carretera de la Juberrussa 42.45 1.48333 R RD AD 00 0 1111 Europe/Andorra 1993-12-23
+3040196 Canal de la Juberrussa Canal de la Juberrussa 42.43333 1.48333 H STM AD 00 0 1228 Europe/Andorra 1993-12-23
+3040197 Bosc de la Juberrussa Bosc de la Juberrussa 42.43333 1.48333 V FRST AD 00 0 1228 Europe/Andorra 1993-12-23
+3040198 Bordes de la Juberrussa Bordes de la Juberrussa 42.43333 1.48333 S FRM AD 00 0 1228 Europe/Andorra 1993-12-23
+3040199 Juberrussa Juberrussa 42.43333 1.48333 A ADMD AD 00 0 1228 Europe/Andorra 1993-12-23
+3040200 Juberri Juberri Juberri,Juverri 42.44069 1.48972 P PPL AD 06 0 1460 Europe/Andorra 2011-11-05
+3040201 Font de la Jubanya Font de la Jubanya 42.58333 1.45 H SPNG AD 00 0 2156 Europe/Andorra 1993-12-23
+3040202 Coll Jovell Coll Jovell 42.45 1.53333 T SPUR AD 00 0 1859 Europe/Andorra 1993-12-23
+3040203 Coll Jovell Coll Jovell 42.5 1.56667 T PK AD 00 0 1776 Europe/Andorra 1993-12-23
+3040204 Coll Jovell Coll Jovell 42.48333 1.48333 T PK AD 00 0 981 Europe/Andorra 1993-12-23
+3040205 Jovell Jovell 42.58333 1.63333 L LCTY AD 00 0 1722 Europe/Andorra 1993-12-23
+3040206 Coll de Jou Coll de Jou 42.51667 1.55 T SPUR AD 00 0 1322 Europe/Andorra 1993-12-23
+3040207 Coll de Jou Coll de Jou 42.45 1.46667 T PASS AD 00 0 935 Europe/Andorra 1993-12-23
+3040208 Canal del Jou Canal del Jou 42.56667 1.5 H STM AD 00 0 1636 Europe/Andorra 1993-12-23
+3040209 Camà del Jou Cami del Jou 42.56667 1.5 R TRL AD 00 0 1636 Europe/Andorra 1993-12-23
+3040210 Bosc del Jou Bosc del Jou 42.56667 1.51667 V FRST AD 00 0 1500 Europe/Andorra 1993-12-23
+3040211 Solana del Jordà Solana del Jorda 42.51667 1.6 T SLP AD 00 0 2085 Europe/Andorra 1993-12-23
+3040212 Borda del Joansaus Borda del Joansaus 42.58333 1.65 S HUT AD 00 0 1767 Europe/Andorra 1993-12-23
+3040213 Portella de Joan Antoni Portella de Joan Antoni 42.51195 1.70887 T PASS AD 00 0 2550 Europe/Andorra 2011-04-19
+3040214 Pleta de Jes Agols Pleta de Jes Agols 42.51667 1.6 L GRAZ AD 00 0 2085 Europe/Andorra 1993-12-23
+3040215 Borda del Jep Borda del Jep Borda del Gep,Borda del Jep 42.56667 1.58333 S HUT AD AD 00 0 1919 Europe/Andorra 2011-11-05
+3040216 Borda del Jarca Borda del Jarca 42.56667 1.58333 S HUT AD 00 0 1919 Europe/Andorra 1993-12-23
+3040217 Borda del Jarca Borda del Jarca 42.55 1.6 S HUT AD 00 0 2210 Europe/Andorra 1993-12-23
+3040218 Borda del Janramon Borda del Janramon 42.56667 1.58333 S HUT AD 00 0 1919 Europe/Andorra 1993-12-23
+3040219 Turó de Jan Turo de Jan 42.61667 1.63333 T SPUR AD 00 0 2331 Europe/Andorra 1993-12-23
+3040220 Riu de Jan Riu de Jan 42.61667 1.63333 H STM AD 00 0 2331 Europe/Andorra 1993-12-23
+3040221 Pala de Jan Pala de Jan 42.61667 1.63333 T CLF AD 00 0 2331 Europe/Andorra 1993-12-23
+3040222 Cóms de Jan Coms de Jan 42.61667 1.63333 H LK AD 00 0 2331 Europe/Andorra 1993-12-23
+3040223 Collada de Jan Collada de Jan Collada de Jan 42.61667 1.63333 T PASS AD 00 0 2331 Europe/Andorra 2011-11-05
+3040224 Estany de l’ Isla Estany de l' Isla 42.61667 1.68333 H LK AD 00 0 2406 Europe/Andorra 1993-12-23
+3040225 Font dels Isards Font dels Isards 42.58333 1.7 H SPNG AD 00 0 2584 Europe/Andorra 1993-12-23
+3040226 Costa dels Isards Costa dels Isards 42.58333 1.6 T SLP AD 00 0 1828 Europe/Andorra 1993-12-23
+3040227 Coll dels Isards Coll dels Isards 42.51831 1.73762 T PASS AD 00 0 2659 Europe/Andorra 2011-04-19
+3040228 Canal dels Isards Canal dels Isards 42.5 1.66667 H RVN AD 00 0 2441 Europe/Andorra 1993-12-23
+3040229 Canal de l’ Isard Canal de l' Isard 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3040230 Pla de l’ Ingla Pla de l' Ingla 42.48333 1.61667 T UPLD AD 00 0 2217 Europe/Andorra 1993-12-23
+3040231 Collet de l’ Infern Collet de l' Infern 42.48333 1.6 T PASS AD 00 0 2250 Europe/Andorra 1993-12-23
+3040232 Canal de l’ Infern Canal de l' Infern 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3040233 Torrent dels Indrets Torrent dels Indrets 42.48333 1.45 H STM AD 00 0 1195 Europe/Andorra 1993-12-23
+3040234 Riu d’ Incles Riu d' Incles 42.60159 1.68721 H STM AD 00 0 2014 Europe/Andorra 2011-04-19
+3040235 Prats d’ Incles Prats d' Incles 42.6 1.66667 L GRAZ AD 00 0 1858 Europe/Andorra 1993-12-23
+3040236 Port de Fontargente Port de Fontargente Port d' Incles,Port de Fontargent,Port de Fontargente,Port d’ Incles 42.61667 1.71667 T PASS AD 00 0 2352 Europe/Andorra 2011-11-05
+3040237 Pont d’ Incles Pont d' Incles 42.58333 1.66667 S BDG AD 00 0 2159 Europe/Andorra 1993-12-23
+3040238 Camà d’ Incles Cami d' Incles 42.6 1.66667 R TRL AD 00 0 1858 Europe/Andorra 1993-12-23
+3040239 Bosc d’ Incles Bosc d' Incles 42.58333 1.66667 V FRST AD 00 0 2159 Europe/Andorra 1993-12-23
+3040240 Estany de l’ Illa Estany de l' Illa 42.49785 1.65852 H RSV AD 00 0 2553 Europe/Andorra 2011-04-19
+3040241 Obagues de la Iesca Obagues de la Iesca 42.48333 1.43333 T SLP AD 00 0 1938 Europe/Andorra 1993-12-23
+3040242 Bosc de l’ Hoste Bosc de l' Hoste 42.55 1.68333 V FRST AD 00 0 2254 Europe/Andorra 1993-12-23
+3040243 Bosc de l’ Hostal del Poll Bosc de l' Hostal del Poll 42.61667 1.63333 V FRST AD 00 0 2331 Europe/Andorra 1993-12-23
+3040244 Riu dels Hortons Riu dels Hortons 42.51667 1.46667 H STM AD 00 0 1840 Europe/Andorra 1993-12-23
+3040245 Bosc dels Hortons Bosc dels Hortons 42.53333 1.55 V FRST AD 00 0 1344 Europe/Andorra 1993-12-23
+3040246 Torrent dels Hortells Torrent dels Hortells 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3040247 Serra de l’ Hortell Serra de l' Hortell 42.61667 1.51667 T MT AD 00 0 1716 Europe/Andorra 1993-12-23
+3040248 Pic de l’ Hortell Pic de l' Hortell 42.61914 1.50815 T PK AD 00 0 2390 Europe/Andorra 2011-04-19
+3040249 Torrent dels Hortals Torrent dels Hortals 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3040250 Serra de l’ Honor Serra de l' Honor 42.53333 1.51667 T SPUR AD 00 0 1361 Europe/Andorra 1993-12-23
+3040251 Roc del’ Home Dret Roc del' Home Dret 42.58333 1.66667 T SPUR AD 00 0 2159 Europe/Andorra 1993-12-23
+3040252 Roca Herbosa Roca Herbosa 42.48333 1.56667 T RK AD 00 0 2231 Europe/Andorra 1993-12-23
+3040253 Pleta de Guitard Pleta de Guitard Pleta de Guitard,Pleta de Guitart 42.53333 1.6 L GRAZ AD AD 00 0 1888 Europe/Andorra 2011-11-05
+3040254 Cortal del Guitard Cortal del Guitard Cortal del Guitard,Cortal del Guitart 42.45 1.5 S CRRL AD AD 00 0 1614 Europe/Andorra 2011-11-05
+3040255 Tartera de les Guineus Tartera de les Guineus 42.56667 1.68333 T TAL AD 00 0 2340 Europe/Andorra 1993-12-23
+3040256 Roc de la Guilla Roc de la Guilla 42.51667 1.56667 T RK AD 00 0 1759 Europe/Andorra 1993-12-23
+3040257 Bony de les Guardioles Bony de les Guardioles 42.55 1.51667 T SPUR AD 00 0 1397 Europe/Andorra 1993-12-23
+3040258 Serra de la Guardiola Serra de la Guardiola 42.58081 1.71111 T RDGE AD 00 0 2544 Europe/Andorra 2011-04-19
+3040259 Obaga de la Guardiola Obaga de la Guardiola 42.56667 1.68333 T SLP AD 00 0 2340 Europe/Andorra 1993-12-23
+3040260 Planell de la Guà rdia Planell de la Guardia 42.61667 1.51667 T UPLD AD 00 0 1716 Europe/Andorra 1993-12-23
+3040261 Roques Grosses Roques Grosses 42.58333 1.66667 T RKS AD 00 0 2159 Europe/Andorra 1993-12-23
+3040262 Riba Grossa Riba Grossa 42.55 1.6 T TAL AD 00 0 2210 Europe/Andorra 1993-12-23
+3040263 Roc Gros Roc Gros 42.58333 1.48333 T RK AD 00 0 1809 Europe/Andorra 1993-12-23
+3040264 Riu Gros Riu Gros 42.56667 1.66667 H STM AD 00 0 1938 Europe/Andorra 1993-12-23
+3040265 Alt del Griu Alt del Griu 42.52534 1.65114 T MT AD 00 0 2508 Europe/Andorra 2011-04-19
+3040266 Riu del Grill Riu del Grill 42.51667 1.6 H STM AD 00 0 2085 Europe/Andorra 1993-12-23
+3040267 Font de les Greixes Font de les Greixes 42.46667 1.45 H SPNG AD 00 0 1562 Europe/Andorra 1993-12-23
+3040268 Clot de les Greixes Clot de les Greixes 42.46667 1.45 H RVN AD 00 0 1562 Europe/Andorra 1993-12-23
+3040269 Camà de les Gravades Cami de les Gravades 42.53333 1.51667 R TRL AD 00 0 1361 Europe/Andorra 1993-12-23
+3040270 Clot dels Gravaders Clot dels Gravaders 42.46667 1.55 H RVN AD 00 0 2341 Europe/Andorra 1993-12-23
+3040271 Pleta dels Graus Pleta dels Graus 42.48333 1.56667 L GRAZ AD 00 0 2231 Europe/Andorra 1993-12-23
+3040272 Grau Roig Grau Roig 42.53333 1.7 S RSRT AD 03 0 2357 Europe/Andorra 2010-01-11
+3040273 Obaga de Graupont Obaga de Graupont 42.48333 1.46667 T SLP AD 00 0 1148 Europe/Andorra 1993-12-23
+3040274 Clot de Graupont Clot de Graupont 42.48333 1.46667 H RVN AD 00 0 1148 Europe/Andorra 1993-12-23
+3040275 Grau d’Incles Grau d'Incles 42.58333 1.65 L LCTY AD 00 0 1767 Europe/Andorra 1993-12-23
+3040276 Grau del Cabrer Grau del Cabrer 42.56667 1.71667 L LCTY AD 00 0 2219 Europe/Andorra 1993-12-23
+3040277 Plana del Grau Plana del Grau 42.58333 1.51667 T UPLD AD 00 0 1722 Europe/Andorra 1993-12-23
+3040278 Costa del Grau Costa del Grau 42.56667 1.6 T SLP AD 00 0 1655 Europe/Andorra 1993-12-23
+3040279 Canal del Grau Canal del Grau 42.56667 1.6 H STM AD 00 0 1655 Europe/Andorra 1993-12-23
+3040280 Canal Gran de la Serrera Canal Gran de la Serrera 42.61667 1.58333 H RVN AD 00 0 2374 Europe/Andorra 1993-12-23
+3040281 Canal Gran de la Grella Canal Gran de la Grella 42.51667 1.51667 H STM AD 00 0 1265 Europe/Andorra 1993-12-23
+3040282 Planell Gran de la Cebollera Planell Gran de la Cebollera 42.63333 1.58333 T UPLD AD 00 0 2470 Europe/Andorra 1993-12-23
+3040283 Costa de les Grandalles Costa de les Grandalles 42.53333 1.7 T SLP AD 00 0 2357 Europe/Andorra 1993-12-23
+3040284 Planell Gran Planell Gran 42.63333 1.55 T UPLD AD 00 0 2053 Europe/Andorra 1993-12-23
+3040285 Planell Gran Planell Gran 42.58333 1.66667 T UPLD AD 00 0 2159 Europe/Andorra 1993-12-23
+3040286 Planell Gran Planell Gran 42.56667 1.46667 T UPLD AD 00 0 1673 Europe/Andorra 1993-12-23
+3040287 Planell Gran Planell Gran 42.55 1.68333 T UPLD AD 00 0 2254 Europe/Andorra 1993-12-23
+3040288 Planell Gran Planell Gran 42.55 1.65 T UPLD AD 00 0 2432 Europe/Andorra 1993-12-23
+3040289 Planell Gran Planell Gran 42.46667 1.56667 T UPLD AD 00 0 2365 Europe/Andorra 1993-12-23
+3040290 Costa Gran Costa Gran 42.61667 1.61667 T SLP AD 00 0 2352 Europe/Andorra 1993-12-23
+3040291 Costa Gran Costa Gran 42.6 1.61667 T SLP AD 00 0 2271 Europe/Andorra 1993-12-23
+3040292 Costa Gran Costa Gran 42.51667 1.48333 T SLP AD 00 0 1839 Europe/Andorra 1993-12-23
+3040293 Canal Gran Canal Gran 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3040294 Canal Gran Canal Gran 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3040295 Canal Gran Canal Gran 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3040296 Canal Gran Canal Gran 42.55 1.58333 H STM AD 00 0 1499 Europe/Andorra 1993-12-23
+3040297 Canal Gran Canal Gran 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3040298 Canal Gran Canal Gran 42.5 1.63333 H STM AD 00 0 2545 Europe/Andorra 1993-12-23
+3040299 Canal Gran Canal Gran 42.48333 1.48333 H STM AD 00 0 981 Europe/Andorra 1993-12-23
+3040300 Canal Gran Canal Gran 42.46667 1.48333 H STM AD 00 0 1134 Europe/Andorra 1993-12-23
+3040301 Basera Gran Basera Gran 42.5 1.48333 T CLF AD 00 0 1316 Europe/Andorra 1993-12-23
+3040302 Plana de Gral Plana de Gral 42.58333 1.48333 T UPLD AD 00 0 1809 Europe/Andorra 1993-12-23
+3040303 Canya de les Grailes Canya de les Grailes 42.48333 1.51667 S CAVE AD 00 0 2061 Europe/Andorra 1993-12-23
+3040304 Canya de les Grailes Canya de les Grailes 42.46667 1.46667 S CAVE AD 00 0 1340 Europe/Andorra 1993-12-23
+3040305 Canal de les Grailes Canal de les Grailes 42.5 1.61667 H STM AD 00 0 2560 Europe/Andorra 1993-12-23
+3040306 Roc del Grailer Roc del Grailer 42.56667 1.51667 T RK AD 00 0 1500 Europe/Andorra 1993-12-23
+3040307 Tartera del Goter Tartera del Goter 42.56667 1.73333 T TAL AD 00 0 2096 Europe/Andorra 1993-12-23
+3040308 Roc del Goter Roc del Goter 42.56667 1.73333 T CLF AD 00 0 2096 Europe/Andorra 1993-12-23
+3040309 Pala del Goter Pala del Goter 42.56667 1.73333 T SLP AD 00 0 2096 Europe/Andorra 1993-12-23
+3040310 Bosc de la Gonarda Bosc de la Gonarda 42.55 1.53333 V FRST AD 00 0 1593 Europe/Andorra 1993-12-23
+3040311 Coll de Gomà Coll de Goma 42.55 1.55 T PASS AD 00 0 2097 Europe/Andorra 1993-12-23
+3040312 Planell de Ginestar Planell de Ginestar 42.6 1.55 T UPLD AD 00 0 2298 Europe/Andorra 1993-12-23
+3040313 Bony de la Ginebrera Bony de la Ginebrera 42.45 1.46667 T SPUR AD 00 0 935 Europe/Andorra 1993-12-23
+3040314 Font del Ginebre Font del Ginebre 42.43333 1.45 H SPNG AD 00 0 877 Europe/Andorra 1993-12-23
+3040315 Prat del Gilet Prat del Gilet 42.51667 1.6 L GRAZ AD 00 0 2085 Europe/Andorra 1993-12-23
+3040316 Pla del Géspit Pla del Gespit 42.55 1.61667 T UPLD AD 00 0 2206 Europe/Andorra 1993-12-23
+3040317 Pala del Géspit Pala del Gespit 42.61667 1.55 T SLP AD 00 0 2007 Europe/Andorra 1993-12-23
+3040318 Pala del Géspit Pala del Gespit 42.56667 1.55 T SLP AD 00 0 1996 Europe/Andorra 1993-12-23
+3040319 Borda del Germà Borda del Germa 42.45 1.48333 S FRM AD 00 0 1111 Europe/Andorra 1993-12-23
+3040320 Costa de les Gerderes Costa de les Gerderes 42.55 1.6 T SLP AD 00 0 2210 Europe/Andorra 1993-12-23
+3040321 Cortal del Genret Cortal del Genret 42.45 1.5 S CRRL AD 00 0 1614 Europe/Andorra 1993-12-23
+3040322 Clot del Gel Clot del Gel 42.51667 1.48333 H RVN AD 00 0 1839 Europe/Andorra 1993-12-23
+3040323 Canal del Gel Canal del Gel 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3040324 Solana de la Gaverna Solana de la Gaverna 42.51667 1.6 T SLP AD 00 0 2085 Europe/Andorra 1993-12-23
+3040325 Font de la Gavatxa Font de la Gavatxa 42.53333 1.73333 H SPNG AD 00 0 2300 Europe/Andorra 1993-12-23
+3040326 Borda del Gastó Borda del Gasto 42.45 1.46667 S HUT AD 00 0 935 Europe/Andorra 1993-12-23
+3040327 Serrat de la Garriga Serrat de la Garriga 42.53333 1.61667 T SPUR AD 00 0 2237 Europe/Andorra 1993-12-23
+3040328 Crestes de Gargantillar Crestes de Gargantillar 42.50208 1.63061 T RDGE AD 00 0 2666 Europe/Andorra 2011-04-19
+3040329 Collades de Gargantillar Collades de Gargantillar 42.5 1.65 T PASS AD 00 0 2542 Europe/Andorra 1993-12-23
+3040330 Clots de Gargantillar Clots de Gargantillar 42.5 1.65 H RVN AD 00 0 2542 Europe/Andorra 1993-12-23
+3040331 Gargantillar Gargantillar 42.5 1.65 A ADMD AD 00 0 2542 Europe/Andorra 1993-12-23
+3040332 Roc de la Garganta Roc de la Garganta 42.55 1.6 T RK AD 00 0 2210 Europe/Andorra 1993-12-23
+3040333 Pas dels Gargalls Pas dels Gargalls 42.6 1.68333 T PASS AD 00 0 2089 Europe/Andorra 1993-12-23
+3040334 Bosc de la Gargallosa Bosc de la Gargallosa 42.56667 1.51667 V FRST AD 00 0 1500 Europe/Andorra 1993-12-23
+3040335 Torrent del Gargallet Torrent del Gargallet 42.45 1.53333 H STM AD 00 0 1859 Europe/Andorra 1993-12-23
+3040336 Font del Gargallet Font del Gargallet 42.45 1.53333 H SPNG AD 00 0 1859 Europe/Andorra 1993-12-23
+3040337 Clotada del Gargallet Clotada del Gargallet 42.45 1.55 H RVN AD 00 0 2457 Europe/Andorra 1993-12-23
+3040338 Solana de Galliner Solana de Galliner 42.56667 1.46667 T SLP AD 00 0 1673 Europe/Andorra 1993-12-23
+3040339 Riu de Galliner Riu de Galliner 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3040340 Galliner Galliner 42.56667 1.46667 A ADMD AD 00 0 1673 Europe/Andorra 1993-12-23
+3040341 Planell de la Gallina Planell de la Gallina 42.43333 1.5 T UPLD AD 00 0 1804 Europe/Andorra 1993-12-23
+3040342 Coll de la Gallina Coll de la Gallina 42.46667 1.45 T PASS AD 00 0 1562 Europe/Andorra 1993-12-23
+3040343 Canal dels Gais Canal dels Gais 42.48333 1.55 H STM AD 00 0 2233 Europe/Andorra 1993-12-23
+3040344 Borda del Gabriel Borda del Gabriel 42.56667 1.58333 S HUT AD 00 0 1919 Europe/Andorra 1993-12-23
+3040345 Canal de la Fusta Canal de la Fusta 42.55 1.55 H RVN AD 00 0 2097 Europe/Andorra 1993-12-23
+3040346 Font Freda Font Freda 42.63333 1.56667 H SPNG AD 00 0 2394 Europe/Andorra 1993-12-23
+3040347 Font Freda Font Freda 42.56667 1.46667 H SPNG AD 00 0 1673 Europe/Andorra 1993-12-23
+3040348 Font Freda Font Freda 42.45 1.5 H SPNG AD 00 0 1614 Europe/Andorra 1993-12-23
+3040349 Font Fred Font Fred 42.48333 1.6 H SPNG AD 00 0 2250 Europe/Andorra 1993-12-23
+3040350 Francolà Francoli 42.48333 1.43333 A ADMD AD 00 0 1938 Europe/Andorra 1993-12-23
+3040351 Obaga Fosca Obaga Fosca 42.48333 1.43333 T SLP AD 00 0 1938 Europe/Andorra 1993-12-23
+3040352 Collada Fosca Collada Fosca 42.43333 1.5 T SPUR AD 00 0 1804 Europe/Andorra 1993-12-23
+3040353 Canal Fosca Canal Fosca 42.6 1.53333 H STM AD 00 0 1695 Europe/Andorra 1993-12-23
+3040354 Canal Fosca Canal Fosca 42.5 1.56667 H STM AD 00 0 1776 Europe/Andorra 1993-12-23
+3040355 Bosc Fosc Bosc Fosc 42.56667 1.66667 V FRST AD 00 0 1938 Europe/Andorra 1993-12-23
+3040356 Pleta del Forquilló Pleta del Forquillo 42.61667 1.7 L GRAZ AD 00 0 2285 Europe/Andorra 1993-12-23
+3040357 Collet de Forns Collet de Forns 42.48333 1.48333 T SPUR AD 00 0 981 Europe/Andorra 1993-12-23
+3040358 Canals de la Forniga Canals de la Forniga 42.5 1.45 H STM AD 00 0 1840 Europe/Andorra 1993-12-23
+3040359 Prat del Fornet Prat del Fornet 42.55 1.61667 L GRAZ AD 00 0 2206 Europe/Andorra 1993-12-23
+3040360 Font del Fornell Font del Fornell 42.45 1.5 H SPNG AD 00 0 1614 Europe/Andorra 1993-12-23
+3040361 Canal del Forn de la Calç Canal del Forn de la Calc 42.55 1.55 H STM AD 00 0 2097 Europe/Andorra 1993-12-23
+3040362 Forn de Cals Forn de Cals 42.56667 1.6 L LCTY AD 00 0 1655 Europe/Andorra 1993-12-23
+3040363 Turó del Forn Turo del Forn 42.63333 1.58333 T PK AD 00 0 2470 Europe/Andorra 1993-12-23
+3040364 Torrent del Forn Torrent del Forn 42.5 1.51667 H STM AD 00 0 1410 Europe/Andorra 1993-12-23
+3040365 Serrat del Forn Serrat del Forn 42.55 1.51667 T SPUR AD 00 0 1397 Europe/Andorra 1993-12-23
+3040366 Roca del Forn Roca del Forn 42.55 1.61667 T RK AD 00 0 2206 Europe/Andorra 1993-12-23
+3040367 Riu del Forn Riu del Forn 42.55 1.6 H STM AD 00 0 2210 Europe/Andorra 1993-12-23
+3040368 Portella del Forn Portella del Forn 42.63333 1.58333 T PASS AD 00 0 2470 Europe/Andorra 1993-12-23
+3040369 Planells del Forn Planells del Forn 42.55 1.6 T UPLD AD 00 0 2210 Europe/Andorra 1993-12-23
+3040370 Obaga del Forn Obaga del Forn 42.48333 1.43333 T SLP AD 00 0 1938 Europe/Andorra 1993-12-23
+3040371 Clots del Forn Clots del Forn 42.63333 1.58333 H RVN AD 00 0 2470 Europe/Andorra 1993-12-23
+3040372 Carretera del Forn Carretera del Forn 42.55 1.58333 R RD AD 00 0 1499 Europe/Andorra 1993-12-23
+3040373 Carrerada del Forn Carrerada del Forn 42.56667 1.63333 R TRL AD 00 0 2016 Europe/Andorra 1993-12-23
+3040374 Canal del Forn Canal del Forn 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3040375 Camà del Forn Cami del Forn 42.55 1.58333 R TRL AD 00 0 1499 Europe/Andorra 1993-12-23
+3040376 Estanys Forcats Estanys Forcats 42.6 1.45 H LKS AD 00 0 2174 Europe/Andorra 1993-12-23
+3040377 Torrent Forcat Torrent Forcat 42.45 1.51667 H STM AD 00 0 1790 Europe/Andorra 1993-12-23
+3040378 Estany Forcat Estany Forcat 42.49439 1.63825 H LK AD 00 0 2538 Europe/Andorra 2011-04-19
+3040379 Estany Forcat Estany Forcat 42.5 1.63333 H LK AD 00 0 2545 Europe/Andorra 1993-12-23
+3040380 Forats de l’Óssa Forats de l'Ossa 42.61667 1.53333 L LCTY AD 00 0 1609 Europe/Andorra 1993-12-23
+3040381 Forat Fosc Forat Fosc 42.55 1.56667 L LCTY AD 00 0 1828 Europe/Andorra 1993-12-23
+3040382 Riu de Forat de Rius Riu de Forat de Rius 42.58333 1.63333 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3040383 Collada del Forat de Malhiverns Collada del Forat de Malhiverns 42.6 1.43333 T PASS AD 00 0 2667 Europe/Andorra 1993-12-23
+3040384 Cresta del Forat dels Malhiverns Cresta del Forat dels Malhiverns Cresta del Forat dels Malhiverns 42.6 1.43333 T RDGE AD 00 0 2667 Europe/Andorra 2011-11-05
+3040385 Forat dels Malhiverns Forat dels Malhiverns 42.6 1.45 L LCTY AD 00 0 2174 Europe/Andorra 1993-12-23
+3040386 Forat dels Clots de Massat Forat dels Clots de Massat 42.55 1.7 L LCTY AD 00 0 2358 Europe/Andorra 1993-12-23
+3040387 Forat d’Arau Forat d'Arau 42.56667 1.48333 L LCTY AD 00 0 1508 Europe/Andorra 1993-12-23
+3040388 Coma del Forat Coma del Forat 42.61667 1.48333 H STMH AD 00 0 2470 Europe/Andorra 1993-12-23
+3040389 Font de Fontverd Font de Fontverd 42.5 1.6 H SPNG AD 00 0 2416 Europe/Andorra 1993-12-23
+3040390 Fontverd Fontverd 42.48333 1.6 S RUIN AD 00 0 2250 Europe/Andorra 1993-12-23
+3040391 Fontverd Fontverd 42.5 1.6 A ADMD AD 00 0 2416 Europe/Andorra 1993-12-23
+3040392 Riu de les Fonts de la Tosa Riu de les Fonts de la Tosa 42.6 1.68333 H STM AD 00 0 2089 Europe/Andorra 1993-12-23
+3040393 Tosa de les Fonts Tosa de les Fonts 42.55 1.65 T UPLD AD 00 0 2432 Europe/Andorra 1993-12-23
+3040394 Pic de les Fonts Pic de les Fonts 42.6 1.6 T PK AD 00 0 2143 Europe/Andorra 1993-12-23
+3040395 Pic de les Fonts Pic de les Fonts 42.6 1.48333 T PK AD 00 0 2441 Europe/Andorra 1993-12-23
+3040396 Estany de les Fonts Estany de les Fonts 42.51667 1.66667 H LK AD 00 0 2410 Europe/Andorra 1993-12-23
+3040397 Comella de les Fonts Comella de les Fonts 42.45 1.46667 H STM AD 00 0 935 Europe/Andorra 1993-12-23
+3040398 Clots de les Fonts Clots de les Fonts 42.58333 1.68333 H RVN AD 00 0 2294 Europe/Andorra 1993-12-23
+3040399 Clot de les Fonts Clot de les Fonts 42.55 1.65 H RVN AD 00 0 2432 Europe/Andorra 1993-12-23
+3040400 Clot de les Fonts Clot de les Fonts 42.46667 1.45 H RVN AD 00 0 1562 Europe/Andorra 1993-12-23
+3040401 Clotada de les Fonts Clotada de les Fonts 42.6 1.48333 T SLP AD 00 0 2441 Europe/Andorra 1993-12-23
+3040402 Canal de les Fonts Canal de les Fonts 42.61667 1.51667 H STM AD 00 0 1716 Europe/Andorra 1993-12-23
+3040403 Bosc de les Fonts Bosc de les Fonts 42.56667 1.6 V FRST AD 00 0 1655 Europe/Andorra 1993-12-23
+3040404 Aspre de les Fonts Aspre de les Fonts 42.6 1.48333 V VINS AD 00 0 2441 Europe/Andorra 1993-12-23
+3040405 Bosc de la Font Roja Bosc de la Font Roja 42.55 1.45 V FRST AD 00 0 1788 Europe/Andorra 1993-12-23
+3040406 Collet de Font Podrida Collet de Font Podrida 42.58333 1.46667 T PK AD 00 0 1643 Europe/Andorra 1993-12-23
+3040407 Canal de la Font Llarga Canal de la Font Llarga 42.6 1.65 H STM AD 00 0 2131 Europe/Andorra 1993-12-23
+3040408 Costa de Font Freda Costa de Font Freda 42.63333 1.56667 T SLP AD 00 0 2394 Europe/Andorra 1993-12-23
+3040409 Font de Fontduà Font de Fontdui 42.56667 1.68333 H SPNG AD 00 0 2340 Europe/Andorra 1993-12-23
+3040410 Obaga de la Font dels Pets Obaga de la Font dels Pets 42.6 1.48333 T SLP AD 00 0 2441 Europe/Andorra 1993-12-23
+3040411 Torrent de la Font dels Pals Torrent de la Font dels Pals 42.45 1.51667 H STM AD 00 0 1790 Europe/Andorra 1993-12-23
+3040412 Font del Solà Font del Sola 42.58333 1.66667 H STM AD 00 0 2159 Europe/Andorra 1993-12-23
+3040413 Costa de la Font dels Miquelets Costa de la Font dels Miquelets 42.58333 1.43333 T SLP AD 00 0 2412 Europe/Andorra 1993-12-23
+3040414 Font dels Comellassos Font dels Comellassos 42.6 1.68333 H STM AD 00 0 2089 Europe/Andorra 1993-12-23
+3040415 Bosc de la Font del Pi Bosc de la Font del Pi 42.58333 1.53333 V FRST AD 00 0 1924 Europe/Andorra 1993-12-23
+3040416 Bosc de la Font del Pascol Bosc de la Font del Pascol 42.53333 1.55 V FRST AD 00 0 1344 Europe/Andorra 1993-12-23
+3040417 Serrat de la Font del Mallol Serrat de la Font del Mallol 42.55 1.55 T SPUR AD 00 0 2097 Europe/Andorra 1993-12-23
+3040418 Bosc de la Font del Mallol Bosc de la Font del Mallol 42.55 1.55 V FRST AD 00 0 2097 Europe/Andorra 1993-12-23
+3040419 Canal de la Font del Llop Canal de la Font del Llop 42.55 1.46667 H STM AD 00 0 1585 Europe/Andorra 1993-12-23
+3040420 Bosc de la Font del Gripal Bosc de la Font del Gripal 42.51667 1.5 V FRST AD 00 0 1688 Europe/Andorra 1993-12-23
+3040421 Canal de la Font del Cuc Canal de la Font del Cuc 42.48333 1.51667 H STM AD 00 0 2061 Europe/Andorra 1993-12-23
+3040422 Canal de la Font del Boix Canal de la Font del Boix 42.55 1.48333 H STM AD 00 0 1548 Europe/Andorra 1993-12-23
+3040423 Bosc de la Font del Bisbe Bosc de la Font del Bisbe 42.55 1.46667 V FRST AD 00 0 1585 Europe/Andorra 1993-12-23
+3040424 Barranc de la Font de la Pauca Barranc de la Font de la Pauca 42.56667 1.58333 H STM AD 00 0 1919 Europe/Andorra 1993-12-23
+3040425 Canal de la Font de l’Angleveta Canal de la Font de l'Angleveta 42.53333 1.5 H STM AD 00 0 1357 Europe/Andorra 1993-12-23
+3040426 Planell de la Font de l’Altar Planell de la Font de l'Altar 42.55 1.41667 T UPLD AD 00 0 2105 Europe/Andorra 1993-12-23
+3040427 Canal de la Font de la Gallina Canal de la Font de la Gallina 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3040428 Oratori de la Font de Joans Oratori de la Font de Joans 42.48333 1.48333 S AMTH AD 00 0 981 Europe/Andorra 1993-12-23
+3040429 Basers de la Font de Joans Basers de la Font de Joans 42.48333 1.48333 T CLF AD 00 0 981 Europe/Andorra 1993-12-23
+3040430 Canal de la Font de Gambada Canal de la Font de Gambada 42.56667 1.5 H STM AD 00 0 1636 Europe/Andorra 1993-12-23
+3040431 Fontauzina Fontauzina 42.58333 1.63333 T SLP AD 00 0 1722 Europe/Andorra 1993-12-23
+3040432 Camà de Fontargent Cami de Fontargent 42.61667 1.71667 R TRL AD 00 0 2352 Europe/Andorra 1993-12-23
+3040433 Barranc de la Font Antiga Barranc de la Font Antiga 42.55 1.48333 H STM AD 00 0 1548 Europe/Andorra 1993-12-23
+3040434 Canal de les Fontanelles Canal de les Fontanelles 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3040435 Pont de Fontaneda Pont de Fontaneda 42.46667 1.48333 S BDG AD 00 0 1134 Europe/Andorra 1993-12-23
+3040436 Boscarró de Fontaneda Boscarro de Fontaneda 42.45 1.48333 V FRST AD 00 0 1111 Europe/Andorra 1993-12-23
+3040437 Fontaneda Fontaneda 42.45432 1.46402 P PPL AD 06 0 1253 Europe/Andorra 2011-04-19
+3040438 Fontanals del Pui Fontanals del Pui 42.46667 1.48333 H STM AD 00 0 1134 Europe/Andorra 1993-12-23
+3040439 Bosc dels Fontanals Bosc dels Fontanals 42.55 1.58333 V FRST AD 00 0 1499 Europe/Andorra 1993-12-23
+3040440 Font del Fontanal Font del Fontanal 42.46667 1.5 H SPNG AD 00 0 1383 Europe/Andorra 1993-12-23
+3040441 Canal del Fontanal Canal del Fontanal 42.58333 1.65 H RVN AD 00 0 1767 Europe/Andorra 1993-12-23
+3040442 Riu de Font Amagada Riu de Font Amagada 42.53333 1.53333 H STM AD 00 0 1521 Europe/Andorra 1993-12-23
+3040443 Planell de la Font Planell de la Font 42.56667 1.66667 T UPLD AD 00 0 1938 Europe/Andorra 1993-12-23
+3040444 Costa de la Font Costa de la Font 42.53333 1.51667 T SLP AD 00 0 1361 Europe/Andorra 1993-12-23
+3040445 Clot de la Font Clot de la Font 42.6 1.66667 H RVN AD 00 0 1858 Europe/Andorra 1993-12-23
+3040446 Canal de la Font Canal de la Font 42.58333 1.48333 H STM AD 00 0 1809 Europe/Andorra 1993-12-23
+3040447 Clots Fondos Clots Fondos 42.55 1.61667 H RVN AD 00 0 2206 Europe/Andorra 1993-12-23
+3040448 Collada Fonda Collada Fonda Collada Fonda 42.46667 1.63333 T PASS AD 00 0 2619 Europe/Andorra 2011-11-05
+3040449 Canal Fonda Canal Fonda 42.53333 1.61667 H STM AD 00 0 2237 Europe/Andorra 1993-12-23
+3040450 Cortal del Folc Cortal del Folc 42.45 1.5 S CRRL AD 00 0 1614 Europe/Andorra 1993-12-23
+3040451 Planell del Fogal Planell del Fogal 42.63333 1.56667 T UPLD AD 00 0 2394 Europe/Andorra 1993-12-23
+3040452 Planell de les Flores Planell de les Flores 42.61667 1.5 T UPLD AD 00 0 2390 Europe/Andorra 1993-12-23
+3040453 Canal de les Flamies Canal de les Flamies 42.58333 1.61667 H RVN AD 00 0 1707 Europe/Andorra 1993-12-23
+3040454 Roc del Fiter Roc del Fiter 42.45 1.51667 T CLF AD 00 0 1790 Europe/Andorra 1993-12-23
+3040455 Borda del Fiter Borda del Fiter 42.58333 1.61667 S HUT AD 00 0 1707 Europe/Andorra 1993-12-23
+3040456 Canal de la Fita Canal de la Fita 42.53333 1.6 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3040457 Bosc de la Fita Bosc de la Fita 42.56667 1.53333 V FRST AD 00 0 1669 Europe/Andorra 1993-12-23
+3040458 Coll de Finestres Coll de Finestres 42.43333 1.55 T SPUR AD 00 0 2178 Europe/Andorra 1993-12-23
+3040459 Coll de la Finestra Coll de la Finestra 42.5 1.53333 T SPUR AD 00 0 1574 Europe/Andorra 1993-12-23
+3040460 Bosc de la Finestra Bosc de la Finestra 42.55 1.46667 V FRST AD 00 0 1585 Europe/Andorra 1993-12-23
+3040461 Canal de les Fijoles Canal de les Fijoles 42.58333 1.48333 H RVN AD 00 0 1809 Europe/Andorra 1993-12-23
+3040462 Canal de Fhasa Canal de Fhasa 42.51667 1.56667 H CNL AD 00 0 1759 Europe/Andorra 1993-12-23
+3040463 Font de Ferrús Font de Ferrus 42.51667 1.51667 H SPNG AD 00 0 1265 Europe/Andorra 1993-12-23
+3040464 Font de Ferro del Planell Gran Font de Ferro del Planell Gran 42.53333 1.6 H SPNG AD 00 0 1888 Europe/Andorra 1993-12-23
+3040465 Font de Ferro Font de Ferro 42.58333 1.58333 H SPNG AD 00 0 1993 Europe/Andorra 1993-12-23
+3040466 Font de Ferro Font de Ferro 42.46667 1.48333 H SPNG AD 00 0 1134 Europe/Andorra 1993-12-23
+3040467 Solana de Ferreroles Solana de Ferreroles 42.6 1.56667 T SLP AD 00 0 2513 Europe/Andorra 1993-12-23
+3040468 Riu de Ferreroles Riu de Ferreroles 42.6 1.53333 H STM AD 00 0 1695 Europe/Andorra 1993-12-23
+3040469 Planells de Ferreroles Planells de Ferreroles 42.6 1.55 T UPLD AD 00 0 2298 Europe/Andorra 1993-12-23
+3040470 Collada de Ferreroles Collada de Ferreroles 42.61667 1.56667 T PASS AD 00 0 2228 Europe/Andorra 1993-12-23
+3040471 Clots de Ferreroles Clots de Ferreroles 42.6 1.56667 H RVN AD 00 0 2513 Europe/Andorra 1993-12-23
+3040472 Pont de Ferreres Pont de Ferreres 42.6 1.53333 S BDG AD 00 0 1695 Europe/Andorra 1993-12-23
+3040473 Font del Feritxet Font del Feritxet 42.51667 1.6 H SPNG AD 00 0 2085 Europe/Andorra 1993-12-23
+3040474 Feritxet Feritxet 42.51667 1.6 A ADMD AD 00 0 2085 Europe/Andorra 1993-12-23
+3040475 Font del Fenoll Font del Fenoll 42.58333 1.46667 H SPNG AD 00 0 1643 Europe/Andorra 1993-12-23
+3040476 Barranc del Fenetau Barranc del Fenetau 42.6 1.66667 H STM AD 00 0 1858 Europe/Andorra 1993-12-23
+3040477 Bosc dels Feners Bosc dels Feners 42.43333 1.5 V FRST AD 00 0 1804 Europe/Andorra 1993-12-23
+3040478 Bordes dels Fenerols Bordes dels Fenerols 42.53333 1.5 S HUTS AD 00 0 1357 Europe/Andorra 1993-12-23
+3040479 Fener Llong Fener Llong 42.55 1.55 L LCTY AD 00 0 2097 Europe/Andorra 1993-12-23
+3040480 Canal de Fener de Cussols Canal de Fener de Cussols 42.53333 1.51667 H STM AD 00 0 1361 Europe/Andorra 1993-12-23
+3040481 Fener de Comadejó Fener de Comadejo 42.5 1.48333 L LCTY AD 00 0 1316 Europe/Andorra 1993-12-23
+3040482 Canal del Fener Blanc Canal del Fener Blanc 42.48333 1.48333 H STM AD 00 0 981 Europe/Andorra 1993-12-23
+3040483 Torrent dels Fenerals Torrent dels Fenerals 42.5 1.45 H STM AD 00 0 1840 Europe/Andorra 1993-12-23
+3040484 Obagues dels Fenerals Obagues dels Fenerals 42.5 1.45 T SLP AD 00 0 1840 Europe/Andorra 1993-12-23
+3040485 Bosc de les Fenemores Bosc de les Fenemores 42.53333 1.48333 V FRST AD 00 0 1677 Europe/Andorra 1993-12-23
+3040486 Borda del Fenemars Borda del Fenemars 42.58333 1.63333 S HUT AD 00 0 1722 Europe/Andorra 1993-12-23
+3040487 Barraca de Fembra Morta Barraca de Fembra Morta 42.56667 1.71667 S HUT AD 00 0 2219 Europe/Andorra 1993-12-23
+3040488 Bosc del Felegrill Bosc del Felegrill 42.53333 1.53333 V FRST AD 00 0 1521 Europe/Andorra 1993-12-23
+3040489 Bosc de les Feixes Bosc de les Feixes 42.55 1.43333 V FRST AD 00 0 1949 Europe/Andorra 1993-12-23
+3040490 Feixar de Setut Feixar de Setut 42.46667 1.63333 L LCTY AD 00 0 2619 Europe/Andorra 1993-12-23
+3040491 Feixar del Baladre Feixar del Baladre 42.65 1.55 L LCTY AD 00 0 2181 Europe/Andorra 1993-12-23
+3040492 Pic del Feixar Pic del Feixar 42.46667 1.63333 T PK AD 00 0 2619 Europe/Andorra 1993-12-23
+3040493 Font del Feixar Font del Feixar 42.46667 1.63333 H SPNG AD 00 0 2619 Europe/Andorra 1993-12-23
+3040494 Feixants de Xixerella Feixants de Xixerella 42.55 1.48333 L LCTY AD 00 0 1548 Europe/Andorra 1993-12-23
+3040495 Feixa de l’Escobar Feixa de l'Escobar 42.43333 1.5 L LCTY AD 00 0 1804 Europe/Andorra 1993-12-23
+3040496 Clots de la Febrerrussa Clots de la Febrerrussa 42.46667 1.55 H RVN AD 00 0 2341 Europe/Andorra 1993-12-23
+3040497 Canal del Favar Canal del Favar 42.51667 1.53333 H STM AD 00 0 1460 Europe/Andorra 1993-12-23
+3040498 Solà de Faucellers Sola de Faucellers 42.45 1.5 T SLP AD 00 0 1614 Europe/Andorra 1993-12-23
+3040499 Pont de Faucellers Pont de Faucellers 42.45 1.5 S BDG AD 00 0 1614 Europe/Andorra 1993-12-23
+3040500 Faucellers Faucellers 42.45 1.5 L LCTY AD 00 0 1614 Europe/Andorra 1993-12-23
+3040501 Serrat de la Farga Serrat de la Farga 42.45 1.53333 T RDGE AD 00 0 1859 Europe/Andorra 1993-12-23
+3040502 Pont de la Farga Pont de la Farga 42.61667 1.53333 S BDG AD 00 0 1609 Europe/Andorra 1993-12-23
+3040503 Barraca de la Farga Barraca de la Farga 42.5 1.6 S HUT AD 00 0 2416 Europe/Andorra 1993-12-23
+3040504 Roc del Far Roc del Far 42.43333 1.5 T RK AD 00 0 1804 Europe/Andorra 1993-12-23
+3040505 Plana del Far Plana del Far 42.48333 1.41667 T SLP AD 00 0 1920 Europe/Andorra 1993-12-23
+3040506 Fangots de Moretó Fangots de Moreto 42.55 1.68333 L LCTY AD 00 0 2254 Europe/Andorra 1993-12-23
+3040507 Fangots dels Maians Fangots dels Maians 42.55 1.61667 L LCTY AD 00 0 2206 Europe/Andorra 1993-12-23
+3040508 Fangot Gran Fangot Gran 42.53333 1.63333 L LCTY AD 00 0 2360 Europe/Andorra 1993-12-23
+3040509 Canya de les Falgueres Canya de les Falgueres 42.45 1.46667 S CAVE AD 00 0 935 Europe/Andorra 1993-12-23
+3040510 Falconeres Falconeres 42.55 1.7 L LCTY AD 00 0 2358 Europe/Andorra 1993-12-23
+3040511 Serra de Falcobà Serra de Falcobi 42.63333 1.55 T MT AD 00 0 2053 Europe/Andorra 1993-12-23
+3040512 Canya dels Eucassers Canya dels Eucassers 42.55 1.61667 S CAVE AD 00 0 2206 Europe/Andorra 1993-12-23
+3040513 Cabana de l' Eucasser Cabana de l' Eucasser 42.63086 1.48341 S HUT AD 07 0 2111 Europe/Andorra 2007-03-04
+3040514 Cabana de l’ Eucasser Cabana de l' Eucasser 42.61667 1.56667 S HUT AD 00 0 2228 Europe/Andorra 1993-12-23
+3040515 Cabana de l’ Eucasser Cabana de l' Eucasser 42.58333 1.61667 S HUT AD 00 0 1707 Europe/Andorra 1993-12-23
+3040516 Pala Estreta Pala Estreta 42.55 1.7 T SLP AD 00 0 2358 Europe/Andorra 1993-12-23
+3040517 Torrent Estret Torrent Estret 42.53333 1.56667 H STM AD 00 0 1418 Europe/Andorra 1993-12-23
+3040518 Cortal de Estevet Cortal de Estevet 42.5 1.53333 S CRRL AD 00 0 1574 Europe/Andorra 1993-12-23
+3040519 Collada dels Estanys Forcats Collada dels Estanys Forcats Collada dels Estanys Forcats 42.6 1.45 T PK AD 00 0 2174 Europe/Andorra 2011-11-05
+3040520 Clots de l’ Estany Segon Clots de l' Estany Segon 42.61667 1.73333 H RVN AD 00 0 2508 Europe/Andorra 1993-12-23
+3040521 Estanys de Tristaina Estanys de Tristaina 42.64217 1.48615 H LKS AD 07 0 2530 Europe/Andorra 2007-04-29
+3040522 Estanys del Pessons Estanys del Pessons 42.51667 1.66667 L LCTY AD 00 0 2410 Europe/Andorra 1993-12-23
+3040523 Estanys de l’Obac Estanys de l'Obac 42.51667 1.66667 L LCTY AD 00 0 2410 Europe/Andorra 1993-12-23
+3040524 Estanys de la Solana Estanys de la Solana 42.53333 1.66667 L LCTY AD 00 0 2489 Europe/Andorra 1993-12-23
+3040525 Estanys de Juclar Estanys de Juclar 42.61667 1.71667 L LCTY AD 00 0 2352 Europe/Andorra 1993-12-23
+3040526 Serra dels Estanys Serra dels Estanys 42.58333 1.58333 T RDGE AD 00 0 1993 Europe/Andorra 1993-12-23
+3040527 Riu dels Estanys Riu dels Estanys 42.6 1.61667 H STM AD 00 0 2271 Europe/Andorra 1993-12-23
+3040528 Collada dels Estanys Collada dels Estanys 42.58333 1.58333 T PASS AD 00 0 1993 Europe/Andorra 1993-12-23
+3040529 Camà dels Estanys Cami dels Estanys 42.48333 1.65 R TRL AD 00 0 2658 Europe/Andorra 1993-12-23
+3040530 Cabana dels Estanys Cabana dels Estanys 42.48333 1.65 S HUT AD 00 0 2658 Europe/Andorra 1993-12-23
+3040531 Estanyons de Banyell Estanyons de Banyell 42.65 1.56667 L LCTY AD 00 0 2471 Europe/Andorra 1993-12-23
+3040532 Pic dels Estanyons Pic dels Estanyons Pic dels Estanyons,Toseta de la Colilla,Tosseta de la Caulla,Tosseta de la Caülla 42.46667 1.61667 T PK AD 00 0 2448 Europe/Andorra 2011-11-05
+3040533 Estanys dels Estanyons Estanys dels Estanyons 42.46667 1.61667 H LKS AD 00 0 2448 Europe/Andorra 1993-12-23
+3040534 Canals Tancades dels Estanyons Canals Tancades dels Estanyons 42.46667 1.63333 H RVN AD 00 0 2619 Europe/Andorra 1993-12-23
+3040535 Bosc dels Estanyons Bosc dels Estanyons 42.48333 1.63333 V FRST AD 00 0 2296 Europe/Andorra 1993-12-23
+3040536 Serra de l’ Estanyó Serra de l' Estanyo 42.6 1.58333 T MT AD 00 0 2461 Europe/Andorra 1993-12-23
+3040537 Saleres de l’ Estanyó Saleres de l' Estanyo 42.61667 1.56667 L SALT AD 00 0 2228 Europe/Andorra 1993-12-23
+3040538 Riu de l’ Estanyó Riu de l' Estanyo 42.61667 1.56667 H STM AD 00 0 2228 Europe/Andorra 1993-12-23
+3040539 Pic de l’ Estanyó Pic de l' Estanyo Montagne de l' Estanyo,Montagne de l’ Estanyó,Pic de l' Estanyo,Pic de l’ Estanyó 42.6088 1.59235 T PK AD 00 0 2709 Europe/Andorra 2011-11-05
+3040540 Estret de l’ Estanyó Estret de l' Estanyo 42.61667 1.56667 T PASS AD 00 0 2228 Europe/Andorra 1993-12-23
+3040541 Estany de l’ Estanyó Estany de l' Estanyo 42.61667 1.58333 H LK AD 00 0 2374 Europe/Andorra 1993-12-23
+3040542 Clots de l’ Estanyó Clots de l' Estanyo 42.61667 1.58333 H STMH AD 00 0 2374 Europe/Andorra 1993-12-23
+3040543 Canals de l’ Estanyó Canals de l' Estanyo 42.6 1.58333 H RVN AD 00 0 2461 Europe/Andorra 1993-12-23
+3040544 Serrat de l’ Estany Negre Serrat de l' Estany Negre 42.58333 1.43333 T SPUR AD 00 0 2412 Europe/Andorra 1993-12-23
+3040545 Rocs de l’ Estany Negre Rocs de l' Estany Negre 42.58333 1.43333 T RKS AD 00 0 2412 Europe/Andorra 1993-12-23
+3040546 Basses de l’ Estany Negre Basses de l' Estany Negre 42.58333 1.43333 H LK AD 00 0 2412 Europe/Andorra 1993-12-23
+3040547 Bony de l’ Estany Mort Bony de l' Estany Mort 42.61667 1.61667 T SPUR AD 00 0 2352 Europe/Andorra 1993-12-23
+3040548 Roc de l’ Estany Moreno Roc de l' Estany Moreno 42.51667 1.63333 T CLF AD 00 0 2379 Europe/Andorra 1993-12-23
+3040549 Pala de l’ Estany Gran Pala de l' Estany Gran 42.6 1.58333 T CLF AD 00 0 2461 Europe/Andorra 1993-12-23
+3040550 Riu de l' Estany Esbalçat Riu de l' Estany Esbalcat 42.63612 1.51736 H STM AD 07 0 2334 Europe/Andorra 2007-03-04
+3040551 Port de l’ Estany Esbalçat Port de l' Estany Esbalcat 42.65 1.51667 T PASS AD 00 0 2546 Europe/Andorra 1993-12-23
+3040552 Costa de l’ Estany de Més Avall Costa de l' Estany de Mes Avall 42.6 1.48333 T SPUR AD 00 0 2441 Europe/Andorra 1993-12-23
+3040553 Clots de l’ Estany de Més Avall Clots de l' Estany de Mes Avall 42.6 1.48333 T TAL AD 00 0 2441 Europe/Andorra 1993-12-23
+3040554 Costa de l’ Estany de Més Amunt Costa de l' Estany de Mes Amunt 42.61667 1.48333 T SLP AD 00 0 2470 Europe/Andorra 1993-12-23
+3040555 Costa de l’ Estany del Mig Costa de l' Estany del Mig 42.65 1.48333 T SLP AD 00 0 2341 Europe/Andorra 1993-12-23
+3040556 Canals d’Amunt de l’ Estany del Mig Canals d'Amunt de l' Estany del Mig 42.6 1.48333 H RVN AD 00 0 2441 Europe/Andorra 1993-12-23
+3040557 Riu de l’ Estany de l’Isla Riu de l' Estany de l'Isla 42.61667 1.7 H STM AD 00 0 2285 Europe/Andorra 1993-12-23
+3040558 Canal de l’ Estany de l’Isla Canal de l' Estany de l'Isla 42.61667 1.68333 H RVN AD 00 0 2406 Europe/Andorra 1993-12-23
+3040559 Solana de l’ Estany de l’Illa Solana de l' Estany de l'Illa 42.5 1.65 T SLP AD 00 0 2542 Europe/Andorra 1993-12-23
+3040560 Baser de l’ Estany de les Truites Baser de l' Estany de les Truites 42.56667 1.43333 T CLF AD 00 0 2402 Europe/Andorra 1993-12-23
+3040561 Turó de l’ Estany de la Nou Turo de l' Estany de la Nou 42.46667 1.58333 T SPUR AD 00 0 2367 Europe/Andorra 1993-12-23
+3040562 Camà de l’ Estany de la Nou Cami de l' Estany de la Nou 42.48333 1.58333 R TRL AD 00 0 2349 Europe/Andorra 1993-12-23
+3040563 Riu de l' Estany de Creussans Riu de l' Estany de Creussans 42.63292 1.47976 H STM AD 07 0 2283 Europe/Andorra 2007-03-04
+3040564 Vial de l’ Estany Blau Vial de l' Estany Blau 42.5 1.61667 R RD AD 00 0 2560 Europe/Andorra 1993-12-23
+3040565 Turo de l’ Estany Blau Turo de l' Estany Blau 42.5 1.61667 T PK AD 00 0 2560 Europe/Andorra 1993-12-23
+3040566 Riu de l’ Estany Blau Riu de l' Estany Blau 42.5 1.61667 H STM AD 00 0 2560 Europe/Andorra 1993-12-23
+3040567 Canals de l’ Estany Blau Canals de l' Estany Blau 42.5 1.6 H RVN AD 00 0 2416 Europe/Andorra 1993-12-23
+3040568 Serrat de l’ Estany Serrat de l' Estany 42.51667 1.56667 T MT AD 00 0 1759 Europe/Andorra 1993-12-23
+3040569 Riu de l’ Estany Riu de l' Estany 42.58333 1.43333 H STM AD 00 0 2412 Europe/Andorra 1993-12-23
+3040570 Pla de l’ Estany Pla de l' Estany 42.6 1.46667 T UPLD AD 00 0 2421 Europe/Andorra 1993-12-23
+3040571 Pleta de l’ Estall Serrer Pleta de l' Estall Serrer 42.48333 1.61667 L GRAZ AD 00 0 2217 Europe/Andorra 1993-12-23
+3040572 Planells de l’ Estall Serrer Planells de l' Estall Serrer 42.48333 1.61667 T UPLD AD 00 0 2217 Europe/Andorra 1993-12-23
+3040573 Canals de l’ Estall Serrer Canals de l' Estall Serrer 42.48333 1.61667 H RVN AD 00 0 2217 Europe/Andorra 1993-12-23
+3040574 Bosc de l’ Estall Serrer Bosc de l' Estall Serrer 42.48333 1.61667 V FRST AD 00 0 2217 Europe/Andorra 1993-12-23
+3040575 Estall Serrer Estall Serrer 42.48333 1.61667 L LCTY AD 00 0 2217 Europe/Andorra 1993-12-23
+3040576 Estall Serrer Estall Serrer 42.46667 1.61667 A ADMD AD 00 0 2448 Europe/Andorra 1993-12-23
+3040577 Roc de l’ Estall Roc de l' Estall 42.5 1.58333 T RK AD 00 0 1888 Europe/Andorra 1993-12-23
+3040578 Planell de l’ Estall Planell de l' Estall 42.55 1.55 T UPLD AD 00 0 2097 Europe/Andorra 1993-12-23
+3040579 Collada de l’ Estall Collada de l' Estall 42.55 1.55 T PASS AD 00 0 2097 Europe/Andorra 1993-12-23
+3040580 Carretera de l’ Estall Carretera de l' Estall 42.53333 1.53333 R RD AD 00 0 1521 Europe/Andorra 1993-12-23
+3040581 Borda de l’ Estall Borda de l' Estall 42.55 1.55 S HUT AD 00 0 2097 Europe/Andorra 1993-12-23
+3040582 Pleta de l’ Estaleritx Pleta de l' Estaleritx 42.61667 1.55 L GRAZ AD 00 0 2007 Europe/Andorra 1993-12-23
+3040583 Collada de l’ Estaleritx Collada de l' Estaleritx 42.61667 1.55 T SPUR AD 00 0 2007 Europe/Andorra 1993-12-23
+3040584 Pont dels Esquirols Pont dels Esquirols 42.56667 1.48333 S BDG AD 00 0 1508 Europe/Andorra 1993-12-23
+3040585 Roc de les Esquiroles Roc de les Esquiroles 42.46667 1.5 T RK AD 00 0 1383 Europe/Andorra 1993-12-23
+3040586 Roc de les Esquiroles Roc de les Esquiroles 42.45 1.48333 T RK AD 00 0 1111 Europe/Andorra 1993-12-23
+3040587 Canal de les Esquiroles Canal de les Esquiroles 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3040588 Roc d’ Esquers Roc d' Esquers 42.5 1.55 T RK AD 00 0 1566 Europe/Andorra 1993-12-23
+3040589 Roc de l’ Espluga Roc de l' Espluga 42.46667 1.46667 T RK AD 00 0 1340 Europe/Andorra 1993-12-23
+3040590 Planell de l’ Espluga Planell de l' Espluga 42.46667 1.46667 T UPLD AD 00 0 1340 Europe/Andorra 1993-12-23
+3040591 Tosa dels Espiolets Tosa dels Espiolets 42.55 1.65 T UPLD AD 00 0 2432 Europe/Andorra 1993-12-23
+3040592 Espeluga Espeluga 42.56667 1.43333 L LCTY AD 00 0 2402 Europe/Andorra 1993-12-23
+3040593 Espeluga Espeluga 42.53333 1.55 A ADMD AD 00 0 1344 Europe/Andorra 1993-12-23
+3040594 Pont de l’ Espalmera Pont de l' Espalmera 42.55 1.46667 S BDG AD 00 0 1585 Europe/Andorra 1993-12-23
+3040595 Riu de l’ Escobet Riu de l' Escobet 42.46667 1.51667 H STM AD 00 0 1985 Europe/Andorra 1993-12-23
+3040596 Escobet Escobet 42.46667 1.51667 L LCTY AD 00 0 1985 Europe/Andorra 1993-12-23
+3040597 Cylindre d’ Ascobes Cylindre d' Ascobes Cylindre d' Ascobes,Cylindre d’ Ascobes,Pic d' Escobes,Pic d’ Escobes 42.6 1.73333 T PK AD 00 0 2383 Europe/Andorra 2011-11-05
+3040598 Comella dels Esclops Comella dels Esclops 42.43333 1.48333 H STM AD 00 0 1228 Europe/Andorra 1993-12-23
+3040599 Solà d’ Escàs Sola d' Escas 42.55 1.5 T SLP AD 00 0 1292 Europe/Andorra 1993-12-23
+3040600 Camà d’ Escàs Cami d' Escas 42.55 1.5 R TRL AD 00 0 1292 Europe/Andorra 1993-12-23
+3040601 Escàs Escas 42.54643 1.50895 P PPL AD 04 0 1257 Europe/Andorra 2011-04-19
+3040602 Roc d’ Escalluquer Roc d' Escalluquer 42.55 1.5 T CLF AD 00 0 1292 Europe/Andorra 1993-12-23
+3040603 Bosc d’ Escalluquer Bosc d' Escalluquer 42.55 1.5 V FRST AD 00 0 1292 Europe/Andorra 1993-12-23
+3040604 Escalluquer Escalluquer 42.55 1.5 A ADMD AD 00 0 1292 Europe/Andorra 1993-12-23
+3040605 Canal de l’ Escalella Canal de l' Escalella 42.5 1.45 H STM AD 00 0 1840 Europe/Andorra 1993-12-23
+3040606 Vial de l’ Escala Vial de l' Escala 42.48333 1.5 R RD AD 00 0 1631 Europe/Andorra 1993-12-23
+3040607 Estany Esbalçat Estany Esbalcat 42.64002 1.51371 H LK AD 07 0 2130 Europe/Andorra 2007-03-04
+3040608 Obac d’ Erts Obac d' Erts 42.56667 1.5 T SLP AD 00 0 1636 Europe/Andorra 1993-12-23
+3040609 Erts Erts Ercs,Ercz,Erez 42.56218 1.4968 P PPL AD AD 04 0 1430 Europe/Andorra 2007-04-16
+3040610 Costa de les Eroles Costa de les Eroles 42.56667 1.45 T SLP AD 00 0 2137 Europe/Andorra 1993-12-23
+3040611 Solana de l’ Era de Mitges Solana de l' Era de Mitges 42.46667 1.45 T SLP AD 00 0 1562 Europe/Andorra 1993-12-23
+3040612 Refugi d’ Envalira Refugi d' Envalira 42.53333 1.68333 S RSRT AD 00 0 2322 Europe/Andorra 1993-12-23
+3040613 Port d’ Envalira Port d' Envalira Port d' Envalira,Port d’ Envalira,Puerto d' Envalira,Puerto d’ Envalira 42.54041 1.71897 T PASS AD 00 0 2230 Europe/Andorra 2011-11-05
+3040615 Bordes d’ Envalira Bordes d' Envalira Bordas d' Envalira,Bordas d’ Envalira,Bordes d' Envalira,Bordes d’ Envalira 42.56667 1.68333 S HUTS AD 00 0 2340 Europe/Andorra 2011-11-05
+3040616 Envalira Envalira 42.53333 1.7 A ADMD AD 00 0 2357 Europe/Andorra 1993-12-23
+3040617 Solà d’ Entremesaiqües Sola d' Entremesaiques 42.50094 1.55771 T SLP AD 00 0 1621 Europe/Andorra 2011-04-19
+3040618 Pont d’ Entremesaigües Pont d' Entremesaigues 42.5 1.56667 S BDG AD 00 0 1776 Europe/Andorra 1993-12-23
+3040619 Entremesaigües Entremesaigues 42.49692 1.55577 L LCTY AD 00 0 1566 Europe/Andorra 2011-04-19
+3040620 Roca Entravessada Roca Entravessada 42.5986 1.44241 T SPUR AD 00 0 2406 Europe/Andorra 2011-04-19
+3040621 Font de les Entrades Font de les Entrades 42.56667 1.48333 H SPNG AD 00 0 1508 Europe/Andorra 1993-12-23
+3040622 Bosc de l’ Entrada Bosc de l' Entrada 42.55 1.46667 V FRST AD 00 0 1585 Europe/Andorra 1993-12-23
+3040623 Camà d’ Entor Cami d' Entor 42.58333 1.63333 R TRL AD 00 0 1722 Europe/Andorra 1993-12-23
+3040624 Bosc d’ Entor Bosc d' Entor 42.58333 1.65 V FRST AD 00 0 1767 Europe/Andorra 1993-12-23
+3040625 Entor Entor 42.6 1.65 A ADMD AD 00 0 2131 Europe/Andorra 1993-12-23
+3040626 Collada d’ Entinyola Collada d' Entinyola 42.51667 1.65 T PASS AD 00 0 2633 Europe/Andorra 1993-12-23
+3040627 Clots d’ Entinyac Clots d' Entinyac 42.58333 1.68333 H STMH AD 00 0 2294 Europe/Andorra 1993-12-23
+3040628 Tarteres d’ Entalàs Tarteres d' Entalas 42.53333 1.63333 T TAL AD 00 0 2360 Europe/Andorra 1993-12-23
+3040629 Canals d’ Entalàs Canals d' Entalas 42.53333 1.63333 H RVN AD 00 0 2360 Europe/Andorra 1993-12-23
+3040630 Bosc d’ En Som Bosc d' En Som 42.56667 1.58333 V FRST AD 00 0 1919 Europe/Andorra 1993-12-23
+3040631 Serra de l’ Ensegur Serra de l' Ensegur 42.58333 1.55 T RDGE AD 00 0 2357 Europe/Andorra 1993-12-23
+3040632 Riu de l’ Ensegur Riu de l' Ensegur 42.6 1.53333 H STM AD 00 0 1695 Europe/Andorra 1993-12-23
+3040633 Obaga de l’ Ensegur Obaga de l' Ensegur 42.58333 1.55 T SLP AD 00 0 2357 Europe/Andorra 1993-12-23
+3040634 Collada de l’ Ensegur Collada de l' Ensegur 42.58333 1.53333 T PASS AD 00 0 1924 Europe/Andorra 1993-12-23
+3040635 Clot de l’ Ensegur Clot de l' Ensegur 42.58333 1.55 H RVN AD 00 0 2357 Europe/Andorra 1993-12-23
+3040636 Camà de l’ Ensegur Cami de l' Ensegur 42.58333 1.53333 R TRL AD 00 0 1924 Europe/Andorra 1993-12-23
+3040637 Bosc de l’ Ensegur Bosc de l' Ensegur 42.6 1.55 V FRST AD 00 0 2298 Europe/Andorra 1993-12-23
+3040638 Bordes de l’ Ensegur Bordes de l' Ensegur 42.58333 1.55 S HUTS AD 00 0 2357 Europe/Andorra 1993-12-23
+3040639 Aspres de l’ Ensegur Aspres de l' Ensegur 42.58333 1.55 V VINS AD 00 0 2357 Europe/Andorra 1993-12-23
+3040640 Solana d’ Ensagents Solana d' Ensagents 42.51667 1.63333 T SLP AD 00 0 2379 Europe/Andorra 1993-12-23
+3040641 Riu d’ Ensagents Riu d' Ensagents 42.52752 1.6099 H STM AD 00 0 2101 Europe/Andorra 2011-04-19
+3040642 Obaga d’ Ensagents Obaga d' Ensagents 42.51667 1.63333 T SLP AD 00 0 2379 Europe/Andorra 1993-12-23
+3040643 Estanys d’ Ensagents Estanys d' Ensagents 42.52041 1.64793 H LKS AD 00 0 2627 Europe/Andorra 2011-04-19
+3040644 Ensagents Ensagents 42.51667 1.65 A ADMD AD 00 0 2633 Europe/Andorra 1993-12-23
+3040645 Costa d’ Enradort Costa d' Enradort 42.53333 1.66667 T SLP AD 00 0 2489 Europe/Andorra 1993-12-23
+3040646 Collada d’ Enradort Collada d' Enradort 42.53333 1.66667 T PASS AD 00 0 2489 Europe/Andorra 1993-12-23
+3040647 Rec d’ Engordany Rec d' Engordany 42.51667 1.53333 H CNL AD 00 0 1460 Europe/Andorra 1993-12-23
+3040648 Engordany Engordany Engordany 42.51115 1.54118 P PPL AD 08 0 1139 Europe/Andorra 2007-04-05
+3040649 Pla d’ Engolasters Pla d' Engolasters 42.5 1.56667 T UPLD AD 00 0 1776 Europe/Andorra 1993-12-23
+3040650 Estany d'Engolasters Estany d'Engolasters 42.51966 1.56772 H LK AD 07 0 1615 1759 Europe/Andorra 2007-04-05
+3040651 Carretera d’ Engolasters Carretera d' Engolasters 42.51667 1.56667 R RD AD 00 0 1759 Europe/Andorra 1993-12-23
+3040652 Canal d’ Engolasters Canal d' Engolasters 42.53333 1.6 H CNL AD 00 0 1888 Europe/Andorra 1993-12-23
+3040653 Engolasters Engolasters 42.5 1.56667 A ADMD AD 00 0 1776 Europe/Andorra 1993-12-23
+3040654 Estany d’ Engaït Estany d' Engait 42.51667 1.71667 H LK AD 00 0 2591 Europe/Andorra 1993-12-23
+3040655 Engaït Engait 42.51667 1.71667 A ADMD AD 00 0 2591 Europe/Andorra 1993-12-23
+3040656 Serrat de l’ Enfreu Serrat de l' Enfreu 42.56667 1.56667 T RDGE AD 00 0 2089 Europe/Andorra 1993-12-23
+3040657 Riu de l’ Enfreu Riu de l' Enfreu 42.56667 1.55 H STM AD 00 0 1996 Europe/Andorra 1993-12-23
+3040658 Bosc de l’ Enfreu Bosc de l' Enfreu 42.56667 1.55 V FRST AD 00 0 1996 Europe/Andorra 1993-12-23
+3040659 Borda d’ Endrieta Borda d' Endrieta 42.55 1.58333 S HUT AD 00 0 1499 Europe/Andorra 1993-12-23
+3040660 Obaga d’ Encortesa Obaga d' Encortesa 42.45 1.51667 T SLP AD 00 0 1790 Europe/Andorra 1993-12-23
+3040661 Carretera Encortesa Carretera Encortesa 42.45 1.5 R RD AD 00 0 1614 Europe/Andorra 1993-12-23
+3040662 Encortesa Encortesa 42.45 1.51667 L LCTY AD 00 0 1790 Europe/Andorra 1993-12-23
+3040663 Camà d’ Encodina Cami d' Encodina 42.63333 1.53333 R TRL AD 00 0 2072 Europe/Andorra 1993-12-23
+3040664 Encodina Encodina 42.63333 1.53333 L LCTY AD 00 0 2072 Europe/Andorra 1993-12-23
+3040665 Serra d’ Enclar Serra d' Enclar 42.51408 1.48329 T RDGE AD 00 0 2069 Europe/Andorra 2011-04-19
+3040666 Riu d’ Enclar Riu d' Enclar 42.49067 1.49562 H STM AD 00 0 1353 Europe/Andorra 2011-04-19
+3040667 Prat d’ Enclar Prat d' Enclar 42.5 1.48333 L GRAZ AD 00 0 1316 Europe/Andorra 1993-12-23
+3040668 Bony de Garci Bony de Garci Bony de Garci,Pic d' Enclar,Pic d’ Enclar,Puig de Ancla,Puig de Anclá 42.51667 1.46667 T PK AD 00 0 1840 Europe/Andorra 2011-11-05
+3040669 Camà d’ Enclar Cami d' Enclar 42.5 1.48333 R TRL AD 00 0 1316 Europe/Andorra 1993-12-23
+3040670 Bosc d’ Enclar Bosc d' Enclar 42.48333 1.48333 V FRST AD 00 0 981 Europe/Andorra 1993-12-23
+3040671 Font de l’ Enciam Font de l' Enciam 42.61667 1.51667 H SPNG AD 00 0 1716 Europe/Andorra 1993-12-23
+3040672 Carrerons d’ Encenrera Carrerons d' Encenrera 42.53333 1.68333 R TRL AD 00 0 2322 Europe/Andorra 1993-12-23
+3040673 Encenrera Encenrera 42.53333 1.68333 L LCTY AD 00 0 2322 Europe/Andorra 1993-12-23
+3040674 Clots d’ Encarners Clots d' Encarners 42.6 1.58333 H RVN AD 00 0 2461 Europe/Andorra 1993-12-23
+3040675 Basera d’ Encarners Basera d' Encarners 42.5 1.45 T CLF AD 00 0 1840 Europe/Andorra 1993-12-23
+3040676 Basera d’ Encarners Basera d' Encarners 42.46667 1.55 T CLF AD 00 0 2341 Europe/Andorra 1993-12-23
+3040677 Solà d’ Encampadana Sola d' Encampadana 42.55 1.61667 T SLP AD 00 0 2206 Europe/Andorra 1993-12-23
+3040678 Pic d’ Encampadana Pic d' Encampadana Pic d' Encampadana,Pic d’ Encampadana,Tossa d' Encampdana,Tossa d’ Encampdana,Tossal d' Encampdana,Tossal d’ Encampdana 42.5552 1.63153 T PK AD 00 0 2364 Europe/Andorra 2011-11-05
+3040679 Obaga d’ Encampadana Obaga d' Encampadana 42.55 1.61667 T SLP AD 00 0 2206 Europe/Andorra 1993-12-23
+3040680 Font d’ Encampadana Font d' Encampadana 42.55 1.61667 H SPNG AD 00 0 2206 Europe/Andorra 1993-12-23
+3040681 Clot d’ Encampadana Clot d' Encampadana 42.55 1.61667 H RVN AD 00 0 2206 Europe/Andorra 1993-12-23
+3040682 Encampadana Encampadana 42.56667 1.61667 A ADMD AD 00 0 1920 Europe/Andorra 1993-12-23
+3040683 Serra d’ Encamp Serra d' Encamp 42.53333 1.55 T RDGE AD 00 0 1344 Europe/Andorra 1993-12-23
+3040684 Parròquia d'Encamp Parroquia d'Encamp Encamp,Parroquia d'Encamp,Parròquia d'Encamp 42.53333 1.63333 A ADM1 AD 03 13685 2360 Europe/Andorra 2008-03-17
+3040685 Estany d’ Encamp Estany d' Encamp 42.5 1.65 H LK AD 00 0 2542 Europe/Andorra 1993-12-23
+3040686 Encamp Encamp Ehnkam,Encamp,en kan pu,enkanpu jiao qu,Ðнкам,エンカンプ教区,æ©åŽæ™® 42.53451 1.5767 P PPLA AD 03 11223 1309 Europe/Andorra 2011-11-05
+3040687 Borda d’ En Cadena Borda d' En Cadena 42.53333 1.56667 S HUT AD 00 0 1418 Europe/Andorra 1993-12-23
+3040688 Pleta d’ Emportona Pleta d' Emportona 42.53333 1.65 L GRAZ AD 00 0 2508 Europe/Andorra 1993-12-23
+3040689 Emportona Emportona 42.53333 1.65 A ADMD AD 00 0 2508 Europe/Andorra 1993-12-23
+3040690 Riu de l’ Empallador Riu de l' Empallador 42.53333 1.7 H STM AD 00 0 2357 Europe/Andorra 1993-12-23
+3040691 Canal de l’ Embut Canal de l' Embut 42.58333 1.48333 H RVN AD 00 0 1809 Europe/Andorra 1993-12-23
+3040692 Clots d’ Embolcar Clots d' Embolcar 42.61667 1.61667 T CRQS AD 00 0 2352 Europe/Andorra 1993-12-23
+3040693 Basers d’ Embolcar Basers d' Embolcar 42.61667 1.61667 T CLF AD 00 0 2352 Europe/Andorra 1993-12-23
+3040694 El Vilar El Vilar El Vilar 42.57226 1.60781 P PPL AD 02 0 1655 Europe/Andorra 2011-11-05
+3040695 El Vedat El Vedat 42.46667 1.48333 A ADMD AD 00 0 1134 Europe/Andorra 1993-12-23
+3040696 El Turer El Turer 42.56667 1.53333 T SPUR AD 00 0 1669 Europe/Andorra 1993-12-23
+3040697 El Trillar El Trillar 42.46667 1.5 L LCTY AD 00 0 1383 Europe/Andorra 1993-12-23
+3040698 El Tremat El Tremat 42.53348 1.58056 P PPL AD 03 0 1309 Europe/Andorra 2011-04-19
+3040699 El Torrentill El Torrentill 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3040700 El Tarter El Tarter 42.58026 1.64902 P PPL AD 02 0 1737 Europe/Andorra 2007-04-16
+3040701 El Tamany El Tamany 42.63333 1.51667 T SLP AD 00 0 1894 Europe/Andorra 1993-12-23
+3040702 Els Tarters Els Tarters 42.56667 1.68333 L LCTY AD 00 0 2340 Europe/Andorra 1993-12-23
+3040703 Els Tarterals Els Tarterals 42.48333 1.51667 L LCTY AD 00 0 2061 Europe/Andorra 1993-12-23
+3040704 Els Rebolians Els Rebolians 42.53333 1.51667 L LCTY AD 00 0 1361 Europe/Andorra 1993-12-23
+3040705 Els Quatre Rocs Els Quatre Rocs 42.46667 1.53333 T RKS AD 00 0 2332 Europe/Andorra 1993-12-23
+3040706 Els Pujols Els Pujols 42.48333 1.48333 L LCTY AD 00 0 981 Europe/Andorra 1993-12-23
+3040707 Els Puiols Els Puiols 42.56667 1.63333 L LCTY AD 00 0 2016 Europe/Andorra 1993-12-23
+3040708 Els Pletius Els Pletius 42.61667 1.68333 L LCTY AD 00 0 2406 Europe/Andorra 1993-12-23
+3040709 Els Plans Els Plans 42.58142 1.63273 P PPL AD 02 0 1722 Europe/Andorra 2011-04-19
+3040710 Els Plans Els Plans 42.45084 1.50641 L LCTY AD 00 0 1394 Europe/Andorra 2011-04-19
+3040711 Els Plans Els Plans 42.45 1.5 S FRM AD 00 0 1614 Europe/Andorra 1993-12-23
+3040712 Els Plans Els Plans 42.53333 1.51667 A ADMD AD 00 0 1361 Europe/Andorra 1993-12-23
+3040713 Els Pessons Els Pessons 42.51667 1.66667 A ADMD AD 00 0 2410 Europe/Andorra 1993-12-23
+3040714 Els Pallers Els Pallers 42.56667 1.75 L LCTY AD 00 0 1923 Europe/Andorra 1993-12-23
+3040715 Els Pallerils Els Pallerils 42.58333 1.53333 L LCTY AD 00 0 1924 Europe/Andorra 1993-12-23
+3040716 Els Padals Els Padals 42.58333 1.66667 L LCTY AD 00 0 2159 Europe/Andorra 1993-12-23
+3040717 Els Orris Els Orris 42.55 1.46667 L LCTY AD 00 0 1585 Europe/Andorra 1993-12-23
+3040718 Els Oriosos Els Oriosos 42.53333 1.53333 L LCTY AD 00 0 1521 Europe/Andorra 1993-12-23
+3040719 El Solà El Sola 42.55 1.53333 L LCTY AD 00 0 1593 Europe/Andorra 1993-12-23
+3040720 Els Obacs Els Obacs 42.6 1.5 T SLP AD 00 0 1923 Europe/Andorra 1993-12-23
+3040721 Els Obacs Els Obacs 42.55 1.48333 L LCTY AD 00 0 1548 Europe/Andorra 1993-12-23
+3040722 Els Maians Els Maians 42.55 1.61667 L LCTY AD 00 0 2206 Europe/Andorra 1993-12-23
+3040723 Els Llacs Els Llacs 42.53333 1.41667 T UPLD AD 00 0 1948 Europe/Andorra 1993-12-23
+3040724 Els Indrets Els Indrets 42.48333 1.45 L LCTY AD 00 0 1195 Europe/Andorra 1993-12-23
+3040725 Els Hortells Els Hortells 42.45 1.5 L LCTY AD 00 0 1614 Europe/Andorra 1993-12-23
+3040726 Els Graus Els Graus 42.56667 1.73333 L LCTY AD 00 0 2096 Europe/Andorra 1993-12-23
+3040727 Els Graus Els Graus 42.48333 1.56667 A ADMD AD 00 0 2231 Europe/Andorra 1993-12-23
+3040728 Els Fontanals Els Fontanals 42.56667 1.68333 L LCTY AD 00 0 2340 Europe/Andorra 1993-12-23
+3040729 Els Feners Els Feners 42.58333 1.66667 T SLP AD 00 0 2159 Europe/Andorra 1993-12-23
+3040730 Els Feners Els Feners 42.48333 1.48333 L LCTY AD 00 0 981 Europe/Andorra 1993-12-23
+3040731 Els Fenerols Els Fenerols 42.53333 1.56667 L LCTY AD 00 0 1418 Europe/Andorra 1993-12-23
+3040732 Els Fenerols Els Fenerols 42.53333 1.5 L LCTY AD 00 0 1357 Europe/Andorra 1993-12-23
+3040733 Els Fenerals Els Fenerals 42.5 1.46667 A ADMD AD 00 0 1678 Europe/Andorra 1993-12-23
+3040734 Els Estanys Els Estanys 42.48333 1.63333 A ADMD AD 00 0 2296 Europe/Andorra 1993-12-23
+3040735 Els Estanyons Els Estanyons 42.46667 1.61667 L LCTY AD 00 0 2448 Europe/Andorra 1993-12-23
+3040736 Els Espiolets Els Espiolets 42.56667 1.66667 L LCTY AD 00 0 1938 Europe/Andorra 1993-12-23
+3040737 El Serrat El Serrat Lo Serrat 42.6183 1.53912 P PPL AD AD 05 0 1704 Europe/Andorra 2007-04-16
+3040738 El Seig El Seig 42.53333 1.58333 A ADMD AD 00 0 1571 Europe/Andorra 1993-12-23
+3040739 Els Cuiners Els Cuiners 42.63333 1.55 L CLG AD 00 0 2053 Europe/Andorra 1993-12-23
+3040740 Els Cortals Els Cortals 42.53333 1.61667 A ADMD AD 00 0 2237 Europe/Andorra 1993-12-23
+3040741 Els Corralets Els Corralets 42.56667 1.55 L LCTY AD 00 0 1996 Europe/Andorra 1993-12-23
+3040742 Els Colls Els Colls 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3040743 Els Collets Els Collets 42.53333 1.51667 L LCTY AD 00 0 1361 Europe/Andorra 1993-12-23
+3040744 Els Collells Els Collells 42.56667 1.48333 T PK AD 00 0 1508 Europe/Andorra 1993-12-23
+3040745 Els Colells Els Colells 42.6 1.7 L LCTY AD 00 0 2354 Europe/Andorra 1993-12-23
+3040746 Els Colells Els Colells 42.51667 1.7 A ADMD AD 00 0 2435 Europe/Andorra 1993-12-23
+3040747 Els Clots Els Clots 42.56667 1.6 T SLP AD 00 0 1655 Europe/Andorra 1993-12-23
+3040748 Els Clots Els Clots 42.55 1.43333 L LCTY AD 00 0 1949 Europe/Andorra 1993-12-23
+3040749 Els Caubets Els Caubets 42.55 1.48333 T VAL AD 00 0 1548 Europe/Andorra 1993-12-23
+3040750 Els Carabedius Els Carabedius 42.46667 1.45 L LCTY AD 00 0 1562 Europe/Andorra 1993-12-23
+3040751 Els Canalons Els Canalons 42.48333 1.48333 H RVN AD 00 0 981 Europe/Andorra 1993-12-23
+3040752 Els Botaders Els Botaders 42.48333 1.56667 L LCTY AD 00 0 2231 Europe/Andorra 1993-12-23
+3040753 Els Bedres Els Bedres 42.55 1.48333 L LCTY AD 00 0 1548 Europe/Andorra 1993-12-23
+3040754 Els Beçolans Els Becolans 42.63333 1.55 L LCTY AD 00 0 2053 Europe/Andorra 1993-12-23
+3040755 Els Bassots Els Bassots 42.55 1.65 H LKS AD 00 0 2432 Europe/Andorra 1993-12-23
+3040756 Els Aubells Els Aubells 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3040757 Els Astrells Els Astrells 42.5 1.56667 L LCTY AD 00 0 1776 Europe/Andorra 1993-12-23
+3040758 Els Assaladors Els Assaladors 42.55 1.66667 L LCTY AD 00 0 2224 Europe/Andorra 1993-12-23
+3040759 Els Aspres Els Aspres 42.56667 1.45 T RKS AD 00 0 2137 Europe/Andorra 1993-12-23
+3040760 Els Aspedius Els Aspedius 42.48333 1.53333 L LCTY AD 00 0 2255 Europe/Andorra 1993-12-23
+3040761 El Saquet El Saquet 42.6 1.53333 A ADMD AD 00 0 1695 Europe/Andorra 1993-12-23
+3040762 Els Amorriadors Els Amorriadors 42.51667 1.6 L LCTY AD 00 0 2085 Europe/Andorra 1993-12-23
+3040763 Els Alabars Els Alabars 42.5 1.48333 L LCTY AD 00 0 1316 Europe/Andorra 1993-12-23
+3040764 El Riguer El Riguer 42.48333 1.55 A ADMD AD 00 0 2233 Europe/Andorra 1993-12-23
+3040765 El Remugar El Remugar 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3040766 El Ramer El Ramer 42.56667 1.48333 L LCTY AD 00 0 1508 Europe/Andorra 1993-12-23
+3040767 El Pui El Pui 42.54731 1.51439 P PPL AD 04 0 1257 Europe/Andorra 2011-04-19
+3040768 El Pouader El Pouader El Ponader,El Pouader 42.53333 1.6 T RK AD AD 00 0 1888 Europe/Andorra 2011-11-05
+3040769 El Pletiu El Pletiu 42.5 1.6 L GRAZ AD 00 0 2416 Europe/Andorra 1993-12-23
+3040770 El Planellet El Planellet 42.61667 1.53333 L LCTY AD 00 0 1609 Europe/Andorra 1993-12-23
+3040771 El Pla El Pla 42.53333 1.58333 L LCTY AD 00 0 1571 Europe/Andorra 1993-12-23
+3040772 El Pas Mal El Pas Mal 42.53333 1.65 T PASS AD 00 0 2508 Europe/Andorra 1993-12-23
+3040773 El Pardal El Pardal 42.55 1.5 A ADMD AD 00 0 1292 Europe/Andorra 1993-12-23
+3040774 El Palinqueró El Palinquero 42.58333 1.66667 L LCTY AD 00 0 2159 Europe/Andorra 1993-12-23
+3040775 El Noguer El Noguer 42.51667 1.55 A ADMD AD 00 0 1322 Europe/Andorra 1993-12-23
+3040776 El Mollar El Mollar 42.53333 1.6 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040777 El Moixeret El Moixeret 42.58333 1.51667 L LCTY AD 00 0 1722 Europe/Andorra 1993-12-23
+3040778 El Meligar El Meligar 42.5 1.61667 T RDGE AD 00 0 2560 Europe/Andorra 1993-12-23
+3040779 El Mas El Mas 42.56667 1.5 A ADMD AD 00 0 1636 Europe/Andorra 1993-12-23
+3040780 El Maià El Maia 42.56667 1.73333 A ADMD AD 00 0 2096 Europe/Andorra 1993-12-23
+3040781 El Madriu El Madriu 42.48333 1.58333 A ADMD AD 00 0 2349 Europe/Andorra 1993-12-23
+3040782 El Llempo El Llempo 42.58333 1.6 L LCTY AD 00 0 1828 Europe/Andorra 1993-12-23
+3040783 El Jou El Jou 42.56667 1.5 A ADMD AD 00 0 1636 Europe/Andorra 1993-12-23
+3040784 El Griu El Griu 42.53333 1.63333 A ADMD AD 00 0 2360 Europe/Andorra 1993-12-23
+3040785 El Fornet El Fornet 42.55 1.6 L LCTY AD 00 0 2210 Europe/Andorra 1993-12-23
+3040786 El Fornell El Fornell 42.51667 1.51667 L LCTY AD 00 0 1265 Europe/Andorra 1993-12-23
+3040787 El Fornell El Fornell 42.45 1.5 L LCTY AD 00 0 1614 Europe/Andorra 1993-12-23
+3040788 El Forn El Forn 42.55 1.6 A ADMD AD 00 0 2210 Europe/Andorra 1993-12-23
+3040789 El Fontanal El Fontanal 42.46667 1.5 L LCTY AD 00 0 1383 Europe/Andorra 1993-12-23
+3040790 El Cubil El Cubil 42.43333 1.55 L LCTY AD 00 0 2178 Europe/Andorra 1993-12-23
+3040791 El Cubil El Cubil 42.53333 1.68333 A ADMD AD 00 0 2322 Europe/Andorra 1993-12-23
+3040792 El Cresper El Cresper 42.51667 1.56667 A ADMD AD 00 0 1759 Europe/Andorra 1993-12-23
+3040793 El Cortalet El Cortalet 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3040794 El Cortalet El Cortalet 42.51667 1.56667 L LCTY AD 00 0 1759 Europe/Andorra 1993-12-23
+3040795 El Corbater El Corbater 42.55 1.71667 L LCTY AD 00 0 2192 Europe/Andorra 1993-12-23
+3040796 El Confòs El Confos 42.51667 1.58333 A ADMD AD 00 0 1994 Europe/Andorra 1993-12-23
+3040797 El Collell El Collell 42.45 1.5 T PASS AD 00 0 1614 Europe/Andorra 1993-12-23
+3040798 El Colitx El Colitx 42.58333 1.51667 L LCTY AD 00 0 1722 Europe/Andorra 1993-12-23
+3040799 El Clos El Clos 42.53333 1.6 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040800 El Castellar El Castellar 42.63333 1.51667 A ADMD AD 00 0 1894 Europe/Andorra 1993-12-23
+3040801 El Carregador El Carregador 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3040802 El Cardemeller El Cardemeller 42.55 1.46667 L LCTY AD 00 0 1585 Europe/Andorra 1993-12-23
+3040803 El Campús El Campus 42.43333 1.48333 T UPLD AD 00 0 1228 Europe/Andorra 1993-12-23
+3040804 El Bullidor El Bullidor 42.55 1.71667 L LCTY AD 00 0 2192 Europe/Andorra 1993-12-23
+3040805 El Brossós El Brossos 42.61667 1.53333 A ADMD AD 00 0 1609 Europe/Andorra 1993-12-23
+3040806 El Braibal El Braibal 42.5 1.58333 L LCTY AD 00 0 1888 Europe/Andorra 1993-12-23
+3040807 El Bosquet El Bosquet 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3040808 El Boscarró El Boscarro 42.46667 1.48333 L LCTY AD 00 0 1134 Europe/Andorra 1993-12-23
+3040809 El Barrerol El Barrerol 42.43333 1.45 L LCTY AD 00 0 877 Europe/Andorra 1993-12-23
+3040810 Camà d’ Easagents Cami d' Easagents 42.53333 1.61667 R TRL AD 00 0 2237 Europe/Andorra 1993-12-23
+3040811 Pleta de Duedra Pleta de Duedra 42.63333 1.5 L GRAZ AD 00 0 1979 Europe/Andorra 1993-12-23
+3040812 Pleta de Duedra Pleta de Duedra 42.61667 1.56667 L GRAZ AD 00 0 2228 Europe/Andorra 1993-12-23
+3040813 Plana Duedra Plana Duedra 42.58333 1.51667 T UPLD AD 00 0 1722 Europe/Andorra 1993-12-23
+3040814 Planella del Duc Planella del Duc 42.45 1.5 T SLP AD 00 0 1614 Europe/Andorra 1993-12-23
+3040815 Canal del Duc Canal del Duc 42.58333 1.61667 H RVN AD 00 0 1707 Europe/Andorra 1993-12-23
+3040816 Canal Dreta Canal Dreta 42.55 1.53333 H STM AD 00 0 1593 Europe/Andorra 1993-12-23
+3040817 Canal Dreta Canal Dreta 42.51667 1.48333 H STM AD 00 0 1839 Europe/Andorra 1993-12-23
+3040818 Port Dret Port Dret 42.57454 1.70316 T PASS AD 00 0 2375 Europe/Andorra 2011-04-19
+3040819 Mas del Diumenge Mas del Diumenge 42.51667 1.53333 S FRM AD 00 0 1460 Europe/Andorra 1993-12-23
+3040820 Canal del Diumenge Canal del Diumenge 42.51667 1.53333 H STM AD 00 0 1460 Europe/Andorra 1993-12-23
+3040821 Clot del Diable Clot del Diable 42.56667 1.7 T CRQ AD 00 0 2375 Europe/Andorra 1993-12-23
+3040822 Devesassa Devesassa 42.56667 1.6 L LCTY AD 00 0 1655 Europe/Andorra 1993-12-23
+3040823 Costa de la Devesa Costa de la Devesa 42.55 1.45 T SLP AD 00 0 1788 Europe/Andorra 1993-12-23
+3040824 Bosc de la Devesa Bosc de la Devesa 42.5 1.55 V FRST AD 00 0 1566 Europe/Andorra 1993-12-23
+3040825 Bony de les Deu Hores Bony de les Deu Hores 42.53333 1.65 T SPUR AD 00 0 2508 Europe/Andorra 1993-12-23
+3040826 Pleta de Dalt Pleta de Dalt 42.48333 1.43333 L GRAZ AD 00 0 1938 Europe/Andorra 1993-12-23
+3040827 Carrera de Dalt Carrera de Dalt 42.55 1.61667 R TRL AD 00 0 2206 Europe/Andorra 1993-12-23
+3040828 Serra del Cussol Serra del Cussol 42.45 1.45 T RDGE AD 00 0 1482 Europe/Andorra 1993-12-23
+3040829 Bosc del Cussol Bosc del Cussol 42.45 1.45 V FRST AD 00 0 1482 Europe/Andorra 1993-12-23
+3040830 Borda de la Cultiassa Borda de la Cultiassa 42.53333 1.58333 S HUT AD 00 0 1571 Europe/Andorra 1993-12-23
+3040831 Obaga del Cultiar Obaga del Cultiar 42.55 1.6 T SLP AD 00 0 2210 Europe/Andorra 1993-12-23
+3040832 Cultiar Cultiar 42.55 1.58333 L LCTY AD 00 0 1499 Europe/Andorra 1993-12-23
+3040833 Canal del Cul Canal del Cul 42.48333 1.43333 H STM AD 00 0 1938 Europe/Andorra 1993-12-23
+3040834 Serrat dels Cuiners Serrat dels Cuiners 42.63333 1.55 T SPUR AD 00 0 2053 Europe/Andorra 1993-12-23
+3040835 Roc dels Cuiners Roc dels Cuiners 42.63333 1.55 T RK AD 00 0 2053 Europe/Andorra 1993-12-23
+3040836 Canal del Cuinal Canal del Cuinal 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3040837 Bosc del Cúbol Bosc del Cubol 42.6 1.7 V FRST AD 00 0 2354 Europe/Andorra 1993-12-23
+3040838 Costa del Cubil d’Erts Costa del Cubil d'Erts 42.58333 1.48333 T SLP AD 00 0 1809 Europe/Andorra 1993-12-23
+3040839 Bony del Cubil d’Erts Bony del Cubil d'Erts 42.58333 1.48333 T PK AD 00 0 1809 Europe/Andorra 1993-12-23
+3040840 Solana del Cubil Solana del Cubil 42.56667 1.46667 T SLP AD 00 0 1673 Europe/Andorra 1993-12-23
+3040841 Serrat del Cubil Serrat del Cubil 42.55 1.66667 T RDGE AD 00 0 2224 Europe/Andorra 1993-12-23
+3040842 Riu del Cubil Riu del Cubil 42.56667 1.46667 H STM AD 00 0 1673 Europe/Andorra 1993-12-23
+3040843 Riu del Cubil Riu del Cubil 42.55568 1.68579 H STM AD 00 0 2083 Europe/Andorra 2011-04-19
+3040844 Pla del Cubil Pla del Cubil 42.53333 1.66667 T UPLD AD 00 0 2489 Europe/Andorra 1993-12-23
+3040845 Pic del Cubil Pic del Cubil Pic del Cubil 42.53333 1.45 T PK AD 00 0 2130 Europe/Andorra 2011-11-05
+3040846 Pic Baix del Cubil Pic Baix del Cubil 42.53333 1.68333 T PK AD 00 0 2322 Europe/Andorra 1993-12-23
+3040847 Pic Alt del Cubil Pic Alt del Cubil Pic Alt del Cubil,Pic de Cuvil,Pic de Suvil 42.52808 1.66868 T PK AD 00 0 2489 Europe/Andorra 2011-11-05
+3040848 Obaga del Cubil Obaga del Cubil 42.56667 1.46667 T SLP AD 00 0 1673 Europe/Andorra 1993-12-23
+3040849 Llac del Cubil Llac del Cubil 42.53647 1.66891 H LK AD 00 0 2335 Europe/Andorra 2011-04-19
+3040850 Port de Caraussans Port de Caraussans Port de Caraussans,Port de Creussans 42.63333 1.46667 T PASS AD 00 0 2324 Europe/Andorra 2011-11-05
+3040851 Estany de Creussans Estany de Creussans 42.63484 1.47697 H LK AD 07 0 2291 Europe/Andorra 2007-03-04
+3040852 Creussans Creussans 42.63355 1.47785 L LCTY AD 07 0 2291 Europe/Andorra 2007-03-04
+3040853 Planell de la Creueta Planell de la Creueta 42.55 1.55 T UPLD AD 00 0 2097 Europe/Andorra 1993-12-23
+3040854 Creu de Noral Creu de Noral 42.56667 1.55 L LCTY AD 00 0 1996 Europe/Andorra 1993-12-23
+3040855 Serra de la Creu Serra de la Creu 42.48333 1.51667 T MT AD 00 0 2061 Europe/Andorra 1993-12-23
+3040856 Roc de la Creu Roc de la Creu 42.56667 1.48333 T RK AD 00 0 1508 Europe/Andorra 1993-12-23
+3040857 Borda del Cresper Borda del Cresper 42.53333 1.56667 S HUT AD 00 0 1418 Europe/Andorra 1993-12-23
+3040858 Rocs del Cresp Rocs del Cresp 42.58333 1.51667 T RKS AD 00 0 1722 Europe/Andorra 1993-12-23
+3040859 Roc del Cresp Roc del Cresp 42.56667 1.48333 T SPUR AD 00 0 1508 Europe/Andorra 1993-12-23
+3040860 Canal del Cresp Canal del Cresp 42.58333 1.51667 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3040861 Font del Crau Font del Crau 42.6 1.51667 H SPNG AD 00 0 1445 Europe/Andorra 1993-12-23
+3040862 Pla de la Cot Pla de la Cot 42.53333 1.46667 T UPLD AD 00 0 1846 Europe/Andorra 1993-12-23
+3040863 Canal de les Costes Canal de les Costes 42.51667 1.53333 H STM AD 00 0 1460 Europe/Andorra 1993-12-23
+3040864 Canal de Costa Verda Canal de Costa Verda 42.5 1.56667 H STM AD 00 0 1776 Europe/Andorra 1993-12-23
+3040865 Collet de Costasseda Collet de Costasseda 42.48333 1.5 T PASS AD 00 0 1631 Europe/Andorra 1993-12-23
+3040866 Bosc de la Costassa Bosc de la Costassa 42.55 1.51667 V FRST AD 00 0 1397 Europe/Andorra 1993-12-23
+3040867 Barranc de la Costa Rodona Barranc de la Costa Rodona 42.56667 1.73333 H STM AD 00 0 2096 Europe/Andorra 1993-12-23
+3040868 Font de la Costa Gran Font de la Costa Gran 42.51667 1.48333 H SPNG AD 00 0 1839 Europe/Andorra 1993-12-23
+3040869 Bony de la Costa del Sodorn Bony de la Costa del Sodorn 42.5 1.45 T PK AD 00 0 1840 Europe/Andorra 1993-12-23
+3040870 Canal de la Costa de les Salineres Canal de la Costa de les Salineres 42.5 1.46667 H STM AD 00 0 1678 Europe/Andorra 1993-12-23
+3040871 Costa de les Neres Costa de les Neres 42.55 1.55 A ADMD AD 00 0 2097 Europe/Andorra 1993-12-23
+3040872 Canal de la Costa de les Gerderes Canal de la Costa de les Gerderes 42.55 1.6 H RVN AD 00 0 2210 Europe/Andorra 1993-12-23
+3040873 Collet de la Costa del Bony Roig Collet de la Costa del Bony Roig 42.6 1.61667 T PASS AD 00 0 2271 Europe/Andorra 1993-12-23
+3040874 Ras de la Costa de l’Avier Ras de la Costa de l'Avier 42.58333 1.5 T SLP AD 00 0 1595 Europe/Andorra 1993-12-23
+3040875 Font de la Costa de l’Avier Font de la Costa de l'Avier 42.56667 1.53333 H SPNG AD 00 0 1669 Europe/Andorra 1993-12-23
+3040876 Canal de Costa de l’Avier Canal de Costa de l'Avier 42.58333 1.48333 H STM AD 00 0 1809 Europe/Andorra 1993-12-23
+3040877 Roc de la Costa Roc de la Costa 42.56667 1.48333 T RK AD 00 0 1508 Europe/Andorra 1993-12-23
+3040878 Solana del Cosp Solana del Cosp 42.56667 1.61667 T SLP AD 00 0 1920 Europe/Andorra 1993-12-23
+3040879 Roca Cosconera Roca Cosconera 42.48333 1.56667 T RK AD 00 0 2231 Europe/Andorra 1993-12-23
+3040880 Font de la Coruvilla Font de la Coruvilla 42.58333 1.46667 H SPNG AD 00 0 1643 Europe/Andorra 1993-12-23
+3040881 Canals de la Coruvilla Canals de la Coruvilla 42.58333 1.46667 H RVN AD 00 0 1643 Europe/Andorra 1993-12-23
+3040882 Borda de la Coruvilla Borda de la Coruvilla 42.58333 1.46667 S FRM AD 00 0 1643 Europe/Andorra 1993-12-23
+3040883 Corts d’Ern Corts d'Ern Corts d'Aern,Corts d'Ern,Corts d’Aern,Corts d’Ern 42.5 1.48333 L LCTY AD 00 0 1316 Europe/Andorra 2011-11-05
+3040884 Bosc de les Corts Bosc de les Corts 42.56667 1.53333 V FRST AD 00 0 1669 Europe/Andorra 1993-12-23
+3040885 Cort d’Esteve Cort d'Esteve 42.6 1.51667 L LCTY AD 00 0 1445 Europe/Andorra 1993-12-23
+3040886 Cort de Rossell Cort de Rossell 42.46667 1.48333 L LCTY AD 00 0 1134 Europe/Andorra 1993-12-23
+3040887 Cort Cremada Cort Cremada 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3040888 Bosc de les Cortanbelles Bosc de les Cortanbelles 42.46667 1.45 V FRST AD 00 0 1562 Europe/Andorra 1993-12-23
+3040889 Canal del Cortal Vell Canal del Cortal Vell 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3040890 Cortal Vell Cortal Vell 42.58333 1.46667 L LCTY AD 00 0 1643 Europe/Andorra 1993-12-23
+3040891 Cortals de Sispony Cortals de Sispony Corfots de Sispony,Cortals de Sispony 42.53333 1.5 L LCTY AD AD 00 0 1357 Europe/Andorra 2011-11-05
+3040892 Cortals de Fontaneda Cortals de Fontaneda 42.45 1.46667 L LCTY AD 00 0 935 Europe/Andorra 1993-12-23
+3040893 Riu dels Cortals Riu dels Cortals 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3040894 Riu dels Cortals Riu dels Cortals 42.53333 1.51667 H STM AD 00 0 1361 Europe/Andorra 1993-12-23
+3040895 Carretera dels Cortals Carretera dels Cortals 42.53333 1.58333 R RD AD 00 0 1571 Europe/Andorra 1993-12-23
+3040896 Camà dels Cortals Cami dels Cortals 42.53333 1.53333 R TRL AD 00 0 1521 Europe/Andorra 1993-12-23
+3040897 Canal del Cortalet Canal del Cortalet 42.48333 1.48333 H STM AD 00 0 981 Europe/Andorra 1993-12-23
+3040898 Camà del Cortal de la Serra Cami del Cortal de la Serra 42.53333 1.5 R TRL AD 00 0 1357 Europe/Andorra 1993-12-23
+3040899 Mas del Cortal Mas del Cortal 42.56667 1.6 S FRM AD 00 0 1655 Europe/Andorra 1993-12-23
+3040900 Canal del Cortà Canal del Corta 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3040901 Canal de la Corruga Canal de la Corruga 42.58333 1.63333 H RVN AD 00 0 1722 Europe/Andorra 1993-12-23
+3040902 Bosc de la Corruga Bosc de la Corruga 42.56667 1.65 V FRST AD 00 0 1988 Europe/Andorra 1993-12-23
+3040903 Font del Correus Font del Correus 42.56667 1.71667 H SPNG AD 00 0 2219 Europe/Andorra 1993-12-23
+3040904 Corrals de la Mentirosa Corrals de la Mentirosa 42.43333 1.51667 S RUIN AD 00 0 2031 Europe/Andorra 1993-12-23
+3040905 Serra dels Corrals Serra dels Corrals 42.55 1.45 T MT AD 00 0 1788 Europe/Andorra 1993-12-23
+3040906 Serrat de Corpalanca Serrat de Corpalanca 42.53333 1.46667 T RDGE AD 00 0 1846 Europe/Andorra 1993-12-23
+3040907 Canal de Cordabalba Canal de Cordabalba 42.48333 1.55 H STM AD 00 0 2233 Europe/Andorra 1993-12-23
+3040908 Roc dels Corbs Roc dels Corbs 42.5 1.51667 T RK AD 00 0 1410 Europe/Andorra 1993-12-23
+3040909 Torrent de les Corbelles Torrent de les Corbelles 42.55 1.6 H STM AD 00 0 2210 Europe/Andorra 1993-12-23
+3040910 Roca Corba Roca Corba 42.51667 1.53333 T RK AD 00 0 1460 Europe/Andorra 1993-12-23
+3040911 Font del Corb Font del Corb 42.55 1.46667 H SPNG AD 00 0 1585 Europe/Andorra 1993-12-23
+3040912 Canal del Corb Canal del Corb 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3040913 Canal del Corb Canal del Corb 42.53333 1.56667 H STM AD 00 0 1418 Europe/Andorra 1993-12-23
+3040914 Camà del Corb Cami del Corb 42.63333 1.51667 R TRL AD 00 0 1894 Europe/Andorra 1993-12-23
+3040915 Basers del Corb Basers del Corb 42.63333 1.51667 T CLF AD 00 0 1894 Europe/Andorra 1993-12-23
+3040916 Serrat del Corantell Serrat del Corantell 42.53333 1.5 T RDGE AD 00 0 1357 Europe/Andorra 1993-12-23
+3040917 Roc de la Copa Roc de la Copa 42.5 1.46667 T RK AD 00 0 1678 Europe/Andorra 1993-12-23
+3040918 Font de Conxa Font de Conxa 42.6 1.65 H SPNG AD 00 0 2131 Europe/Andorra 1993-12-23
+3040919 Bosc de Conxa Bosc de Conxa 42.6 1.65 V FRST AD 00 0 2131 Europe/Andorra 1993-12-23
+3040920 Solana del Contador Solana del Contador 42.48333 1.43333 T SLP AD 00 0 1938 Europe/Andorra 1993-12-23
+3040921 Font del Coniol Font del Coniol 42.5 1.58333 H SPNG AD 00 0 1888 Europe/Andorra 1993-12-23
+3040922 Costa del Congost Costa del Congost 42.6 1.46667 T SLP AD 00 0 2421 Europe/Andorra 1993-12-23
+3040923 Conangle Conangle 42.43333 1.51667 L LCTY AD 00 0 2031 Europe/Andorra 1993-12-23
+3040924 Cóms de Jan Coms de Jan 42.63333 1.61667 L LCTY AD 00 0 2541 Europe/Andorra 1993-12-23
+3040925 Roc dels Cóms Roc dels Coms 42.63333 1.63333 T CLF AD 00 0 2603 Europe/Andorra 1993-12-23
+3040926 Planells dels Cóms Planells dels Coms 42.55 1.6 T UPLD AD 00 0 2210 Europe/Andorra 1993-12-23
+3040927 Obaga dels Cóms Obaga dels Coms 42.48333 1.41667 T SLP AD 00 0 1920 Europe/Andorra 1993-12-23
+3040928 Font dels Cóms Font dels Coms 42.45 1.45 H SPNG AD 00 0 1482 Europe/Andorra 1993-12-23
+3040929 Canal dels Cóms Canal dels Coms 42.48333 1.41667 H STM AD 00 0 1920 Europe/Andorra 1993-12-23
+3040930 Bosc dels Cóms Bosc dels Coms 42.53333 1.46667 V FRST AD 00 0 1846 Europe/Andorra 1993-12-23
+3040931 Riu del ComÃs Vell Riu del Comis Vell 42.63667 1.52189 H STM AD 07 0 2334 Europe/Andorra 2007-03-04
+3040932 ComÃs Vell Comis Vell 42.63748 1.52098 L LCTY AD 07 0 2334 Europe/Andorra 2007-03-04
+3040933 Borda del Comet Borda del Comet 42.56667 1.58333 S HUT AD 00 0 1919 Europe/Andorra 1993-12-23
+3040934 Comes de Banyàs Comes de Banyas 42.55 1.51667 L LCTY AD 00 0 1397 Europe/Andorra 1993-12-23
+3040935 Bosc de Comes Beçoses Bosc de Comes Becoses 42.51667 1.58333 V FRST AD 00 0 1994 Europe/Andorra 1993-12-23
+3040936 Solà de les Comes Sola de les Comes 42.58333 1.5 T SLP AD 00 0 1595 Europe/Andorra 1993-12-23
+3040937 Portell de les Comes Portell de les Comes 42.5 1.46667 T PASS AD 00 0 1678 Europe/Andorra 1993-12-23
+3040938 Canals de les Comes Canals de les Comes 42.56667 1.51667 H RVN AD 00 0 1500 Europe/Andorra 1993-12-23
+3040939 Borda de les Comes Borda de les Comes 42.53333 1.58333 S FRM AD 00 0 1571 Europe/Andorra 1993-12-23
+3040940 Bosc dels Comellassos Bosc dels Comellassos 42.6 1.66667 V FRST AD 00 0 1858 Europe/Andorra 1993-12-23
+3040941 Costa del Comellar Llarg Costa del Comellar Llarg 42.56667 1.61667 T SLP AD 00 0 1920 Europe/Andorra 1993-12-23
+3040942 Camà del Comellar Llarg Cami del Comellar Llarg 42.56667 1.61667 R TRL AD 00 0 1920 Europe/Andorra 1993-12-23
+3040943 Clot de la Comellada Clot de la Comellada 42.53333 1.45 H RVN AD 00 0 2130 Europe/Andorra 1993-12-23
+3040944 Riu de la Comella Riu de la Comella 42.5 1.53333 H STM AD 00 0 1574 Europe/Andorra 1993-12-23
+3040945 Carretera de la Comella Carretera de la Comella 42.51667 1.55 R RD AD 00 0 1322 Europe/Andorra 1993-12-23
+3040946 Bony de Comascura Bony de Comascura 42.5 1.55 T SPUR AD 00 0 1566 Europe/Andorra 1993-12-23
+3040947 Canal de Coma Sansa Canal de Coma Sansa 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3040948 Riu de la Comarqueta d’Incles Riu de la Comarqueta d'Incles 42.6 1.61667 H STM AD 00 0 2271 Europe/Andorra 1993-12-23
+3040949 Collet de la Comarqueta d’Incles Collet de la Comarqueta d'Incles 42.6 1.61667 T PASS AD 00 0 2271 Europe/Andorra 1993-12-23
+3040950 Comarqueta d’Incles Comarqueta d'Incles 42.6 1.61667 L LCTY AD 00 0 2271 Europe/Andorra 1993-12-23
+3040951 Tosa de la Comarqueta Tosa de la Comarqueta 42.56667 1.73333 T UPLD AD 00 0 2096 Europe/Andorra 1993-12-23
+3040952 Font de la Comarqueta Font de la Comarqueta 42.56667 1.71667 H SPNG AD 00 0 2219 Europe/Andorra 1993-12-23
+3040953 Basers de la Comarqueta Basers de la Comarqueta 42.58333 1.71667 T CLF AD 00 0 2553 Europe/Andorra 1993-12-23
+3040954 Riu de la Comarca de les Fonts Riu de la Comarca de les Fonts 42.6 1.61667 H STM AD 00 0 2271 Europe/Andorra 1993-12-23
+3040955 Obaga de la Comarca Obaga de la Comarca 42.56667 1.46667 T SLP AD 00 0 1673 Europe/Andorra 1993-12-23
+3040956 Font de la Comarca Font de la Comarca 42.56667 1.45 H SPNG AD 00 0 2137 Europe/Andorra 1993-12-23
+3040957 Riu de Coma Pedrosa Riu de Coma Pedrosa 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3040958 Pleta de Coma Pedrosa Pleta de Coma Pedrosa 42.58333 1.45 L GRAZ AD 00 0 2156 Europe/Andorra 1993-12-23
+3040959 Pic de Coma Pedrosa Pic de Coma Pedrosa Pic Alt de la Coma Pedrosa,Pic Alt de la Pedrosa,Pic de Coma Pedrosa 42.5917 1.44428 T PK AD 00 0 2406 Europe/Andorra 2011-11-05
+3040960 Obaga de Coma Pedrosa Obaga de Coma Pedrosa 42.58387 1.44182 T SLP AD 00 0 2350 Europe/Andorra 2011-04-19
+3040961 Grau de Coma Pedrosa Grau de Coma Pedrosa 42.58333 1.46667 T SLP AD 00 0 1643 Europe/Andorra 1993-12-23
+3040962 Collet de Coma Pedrosa Collet de Coma Pedrosa 42.58333 1.45 T PK AD 00 0 2156 Europe/Andorra 1993-12-23
+3040963 Camà de Coma Pedrosa Cami de Coma Pedrosa 42.58333 1.48333 R TRL AD 00 0 1809 Europe/Andorra 1993-12-23
+3040964 Coma Pedrosa Coma Pedrosa Coma Pedrosa 42.58333 1.43333 A ADMD AD 00 0 2412 Europe/Andorra 2011-11-05
+3040965 Serra de Coma Obaga i Ferreroles Serra de Coma Obaga i Ferreroles 42.61667 1.56667 T RDGE AD 00 0 2228 Europe/Andorra 1993-12-23
+3040966 Solana de Coma Obaga Solana de Coma Obaga 42.61667 1.55 T SLP AD 00 0 2007 Europe/Andorra 1993-12-23
+3040967 Serra de Coma Obaga Serra de Coma Obaga 42.61667 1.56667 T RDGE AD 00 0 2228 Europe/Andorra 1993-12-23
+3040968 Pala de Coma Obaga Pala de Coma Obaga 42.61667 1.56667 T SLP AD 00 0 2228 Europe/Andorra 1993-12-23
+3040969 Coma Obaga Coma Obaga 42.6 1.55 A ADMD AD 00 0 2298 Europe/Andorra 1993-12-23
+3040970 Comangerra Comangerra 42.56667 1.5 T SPUR AD 00 0 1636 Europe/Andorra 1993-12-23
+3040971 Canal de Coma Llonga Canal de Coma Llonga 42.56667 1.6 H STM AD 00 0 1655 Europe/Andorra 1993-12-23
+3040972 Riu de Comallempla Riu de Comallempla 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3040973 Portella de Comallempla Portella de Comallempla 42.56667 1.45 T PASS AD 00 0 2137 Europe/Andorra 1993-12-23
+3040974 Camà de Comallempla Cami de Comallempla 42.56667 1.48333 R TRL AD 00 0 1508 Europe/Andorra 1993-12-23
+3040975 Bordes de Comallempla Bordes de Comallempla 42.56667 1.46667 S HUTS AD 00 0 1673 Europe/Andorra 1993-12-23
+3040976 Comallempla Comallempla 42.56667 1.46667 A ADMD AD 00 0 1673 Europe/Andorra 1993-12-23
+3040977 Canal de Coma Fregona Canal de Coma Fregona 42.58333 1.51667 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3040978 Canal de Coma Fregona Canal de Coma Fregona 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3040979 Coma Estreta Coma Estreta 42.58333 1.55 L LCTY AD 00 0 2357 Europe/Andorra 1993-12-23
+3040980 Coma Estremera Coma Estremera 42.51667 1.68333 A ADMD AD 00 0 2352 Europe/Andorra 1993-12-23
+3040981 Col de la Portaneille Col de la Portaneille Col de la Portaneille,Portella de la Coma de Varilles 42.61667 1.65 T PASS AD 00 0 2567 Europe/Andorra 2011-11-05
+3040982 Pics de la Portaneille Pics de la Portaneille Passada,Pic de la Coma de Varilles,Pic de la Passada,Picos de la Passade,Pics de la Passade,Pics de la Portaneille 42.61667 1.66667 T PKS AD 00 0 2536 Europe/Andorra 2011-11-05
+3040983 Rocs de Coma de Teix Rocs de Coma de Teix 42.46667 1.46667 T RKS AD 00 0 1340 Europe/Andorra 1993-12-23
+3040984 Coma de Ransol Coma de Ransol 42.6 1.63333 A ADMD AD 00 0 1893 Europe/Andorra 1993-12-23
+3040985 Solana de la Coma dels Llops Solana de la Coma dels Llops 42.51667 1.63333 T SLP AD 00 0 2379 Europe/Andorra 1993-12-23
+3040986 Riu de la Coma dels Llops Riu de la Coma dels Llops 42.52722 1.60965 H STM AD 00 0 2101 Europe/Andorra 2011-04-19
+3040987 Pleta de la Coma dels Llops Pleta de la Coma dels Llops 42.51667 1.61667 L GRAZ AD 00 0 2254 Europe/Andorra 1993-12-23
+3040988 Cap de la Coma dels Llops Cap de la Coma dels Llops 42.50769 1.62525 T RDGE AD 00 0 2666 Europe/Andorra 2011-04-19
+3040989 Bosc de la Coma dels Llops Bosc de la Coma dels Llops 42.51667 1.61667 V FRST AD 00 0 2254 Europe/Andorra 1993-12-23
+3040990 Coma dels Llops Coma dels Llops 42.51667 1.61667 A ADMD AD 00 0 2254 Europe/Andorra 1993-12-23
+3040991 Clot de la Coma del Prat Clot de la Coma del Prat 42.51667 1.46667 H RVN AD 00 0 1840 Europe/Andorra 1993-12-23
+3040992 Clot de la Coma del Pou Clot de la Coma del Pou 42.51667 1.48333 H RVN AD 00 0 1839 Europe/Andorra 1993-12-23
+3040993 Riu de la Coma del Mig Riu de la Coma del Mig 42.63333 1.51667 H STM AD 00 0 1894 Europe/Andorra 1993-12-23
+3040994 Riu de la Coma del Forat Riu de la Coma del Forat 42.63333 1.5 H STM AD 00 0 1979 Europe/Andorra 1993-12-23
+3040995 Costa de la Coma del Forat Costa de la Coma del Forat 42.63333 1.48333 T SLP AD 00 0 2283 Europe/Andorra 1993-12-23
+3040996 Coma del Favar Coma del Favar 42.51667 1.58333 L LCTY AD 00 0 1994 Europe/Andorra 1993-12-23
+3040997 Clot de la Coma de la Sella Clot de la Coma de la Sella 42.51667 1.48333 H RVN AD 00 0 1839 Europe/Andorra 1993-12-23
+3040998 Solà de la Coma de Cardes Sola de la Coma de Cardes 42.58333 1.61667 T SLP AD 00 0 1707 Europe/Andorra 1993-12-23
+3040999 Tosa de Coma Bella Tosa de Coma Bella 42.58333 1.68333 T UPLD AD 00 0 2294 Europe/Andorra 1993-12-23
+3041000 Font de Coma Bella Font de Coma Bella 42.45 1.5 H SPNG AD 00 0 1614 Europe/Andorra 1993-12-23
+3041001 Canal de Coma Bella Canal de Coma Bella 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3041002 Coma Bella Coma Bella 42.45 1.5 L LCTY AD 00 0 1614 Europe/Andorra 1993-12-23
+3041003 Planell de Coma Aubosa Planell de Coma Aubosa 42.58333 1.5 T UPLD AD 00 0 1595 Europe/Andorra 1993-12-23
+3041004 Clot de Coma Aubosa Clot de Coma Aubosa 42.58333 1.48333 H RVN AD 00 0 1809 Europe/Andorra 1993-12-23
+3041005 Roc de la Coma Roc de la Coma 42.51667 1.55 T RK AD 00 0 1322 Europe/Andorra 1993-12-23
+3041006 Riu de la Coma Riu de la Coma 42.58333 1.63333 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3041007 Riu de la Coma Riu de la Coma 42.45 1.46667 H STM AD 00 0 935 Europe/Andorra 1993-12-23
+3041008 Prats de Coma Prats de Coma 42.56667 1.46667 L GRAZ AD 00 0 1673 Europe/Andorra 1993-12-23
+3041009 Pont de la Coma Pont de la Coma 42.55 1.46667 S BDG AD 00 0 1585 Europe/Andorra 1993-12-23
+3041010 Collet de la Coma Collet de la Coma 42.65 1.51667 T PASS AD 00 0 2546 Europe/Andorra 1993-12-23
+3041011 Collada de la Coma Collada de la Coma 42.6 1.61667 T PASS AD 00 0 2271 Europe/Andorra 1993-12-23
+3041012 Canal de la Coma Canal de la Coma 42.61667 1.55 H STM AD 00 0 2007 Europe/Andorra 1993-12-23
+3041013 Canal de la Coma Canal de la Coma 42.55 1.55 H STM AD 00 0 2097 Europe/Andorra 1993-12-23
+3041014 Canal de la Coma Canal de la Coma 42.6 1.51667 H RVN AD 00 0 1445 Europe/Andorra 1993-12-23
+3041015 Bosc de Coma Bosc de Coma 42.53333 1.55 V FRST AD 00 0 1344 Europe/Andorra 1993-12-23
+3041016 Bony de la Coma Bony de la Coma 42.55 1.53333 T SPUR AD 00 0 1593 Europe/Andorra 1993-12-23
+3041017 Rocs del Colomer Rocs del Colomer 42.53333 1.51667 T SPUR AD 00 0 1361 Europe/Andorra 1993-12-23
+3041018 Collet dels Colls Collet dels Colls 42.55 1.51667 T PASS AD 00 0 1397 Europe/Andorra 1993-12-23
+3041019 Solana de Coll Pa Solana de Coll Pa 42.55 1.43333 T SLP AD 00 0 1949 Europe/Andorra 1993-12-23
+3041020 Serrat de Coll Pa Serrat de Coll Pa 42.55 1.43333 T RDGE AD 00 0 1949 Europe/Andorra 1993-12-23
+3041021 Planell de Coll Pa Planell de Coll Pa 42.48333 1.56667 T UPLD AD 00 0 2231 Europe/Andorra 1993-12-23
+3041022 Pic de Coll Pa Pic de Coll Pa 42.51667 1.48333 T PK AD 00 0 1839 Europe/Andorra 1993-12-23
+3041023 Bosc de Coll Pa Bosc de Coll Pa 42.48333 1.55 V FRST AD 00 0 2233 Europe/Andorra 1993-12-23
+3041024 Basers de Coll Pa Basers de Coll Pa 42.48333 1.56667 T CLF AD 00 0 2231 Europe/Andorra 1993-12-23
+3041025 Canal de Coll Jovell Canal de Coll Jovell 42.5 1.56667 H STM AD 00 0 1776 Europe/Andorra 1993-12-23
+3041026 Canal de Collet Purgat Canal de Collet Purgat 42.46667 1.48333 H STM AD 00 0 1134 Europe/Andorra 1993-12-23
+3041027 Clot del Collet de Font Podrida Clot del Collet de Font Podrida 42.58333 1.46667 T SLP AD 00 0 1643 Europe/Andorra 1993-12-23
+3041028 Camà dels Collells Cami dels Collells 42.56667 1.46667 R TRL AD 00 0 1673 Europe/Andorra 1993-12-23
+3041029 Carretera del Coll d’Ordino Carretera del Coll d'Ordino 42.55 1.53333 R RD AD 00 0 1593 Europe/Andorra 1993-12-23
+3041030 Camà del Coll d’Ordino Cami del Coll d'Ordino 42.55 1.56667 R TRL AD 00 0 1828 Europe/Andorra 1993-12-23
+3041031 Font del Coll de Vista Font del Coll de Vista 42.48333 1.45 H SPNG AD 00 0 1195 Europe/Andorra 1993-12-23
+3041032 Canal del Coll de Vista Canal del Coll de Vista 42.48333 1.45 H STM AD 00 0 1195 Europe/Andorra 1993-12-23
+3041033 Canal del Coll de Turer Canal del Coll de Turer 42.56667 1.46667 H STM AD 00 0 1673 Europe/Andorra 1993-12-23
+3041034 Camà de Coll de Turer Cami de Coll de Turer 42.56667 1.46667 R TRL AD 00 0 1673 Europe/Andorra 1993-12-23
+3041035 Canal de Coll d’Eres Canal de Coll d'Eres 42.5 1.51667 H STM AD 00 0 1410 Europe/Andorra 1993-12-23
+3041036 Camà del Coll dels Isards Cami del Coll dels Isards 42.51667 1.73333 R TRL AD 00 0 2484 Europe/Andorra 1993-12-23
+3041037 Canal del Coll de l’Obac Canal del Coll de l'Obac 42.48333 1.48333 H STM AD 00 0 981 Europe/Andorra 1993-12-23
+3041038 Canal del Coll de les Cases Canal del Coll de les Cases 42.56667 1.5 H STM AD 00 0 1636 Europe/Andorra 1993-12-23
+3041039 Bony del Coll de l’Era Bony del Coll de l'Era 42.48333 1.45 T SPUR AD 00 0 1195 Europe/Andorra 1993-12-23
+3041040 Riu del Coll de l’Aquell Riu del Coll de l'Aquell 42.48333 1.43333 H STM AD 00 0 1938 Europe/Andorra 1993-12-23
+3041041 Canal del Coll de l’Acaumader Canal del Coll de l'Acaumader 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3041042 Canal del Coll de la Cauba Canal del Coll de la Cauba 42.58333 1.61667 H STM AD 00 0 1707 Europe/Andorra 1993-12-23
+3041043 Canal del Coll de la Basera Canal del Coll de la Basera 42.58333 1.65 H RVN AD 00 0 1767 Europe/Andorra 1993-12-23
+3041044 Camà del Coll d’Arenes Cami del Coll d'Arenes 42.6 1.58333 R TRL AD 00 0 2461 Europe/Andorra 1993-12-23
+3041045 Pala de Coll Carnisser Pala de Coll Carnisser 42.6 1.46667 T SLP AD 00 0 2421 Europe/Andorra 1993-12-23
+3041046 Collada de Coll Carnisser Collada de Coll Carnisser 42.6 1.48333 T PASS AD 00 0 2441 Europe/Andorra 1993-12-23
+3041047 Prats de Collart Prats de Collart 42.58333 1.65 L GRAZ AD 00 0 1767 Europe/Andorra 1993-12-23
+3041048 Canal de Collart Canal de Collart 42.58333 1.65 H STM AD 00 0 1767 Europe/Andorra 1993-12-23
+3041049 Bosc de Collart Bosc de Collart 42.56667 1.66667 V FRST AD 00 0 1938 Europe/Andorra 1993-12-23
+3041050 La Colladeta La Colladeta 42.55 1.55 T PASS AD 00 0 2097 Europe/Andorra 1993-12-23
+3041051 Bony de les Collades Bony de les Collades 42.55 1.51667 T MT AD 00 0 1397 Europe/Andorra 1993-12-23
+3041052 Canal de la Collada Gran Canal de la Collada Gran 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3041053 Camà de la Collada de Sanfons Cami de la Collada de Sanfons 42.58333 1.45 R TRL AD 00 0 2156 Europe/Andorra 1993-12-23
+3041054 Camà de la Collada d’Enradort Cami de la Collada d'Enradort 42.53333 1.61667 R TRL AD 00 0 2237 Europe/Andorra 1993-12-23
+3041055 Camà de la Collada de la Maiana Cami de la Collada de la Maiana 42.48333 1.61667 R TRL AD 00 0 2217 Europe/Andorra 1993-12-23
+3041056 Camà de la Collada de Ferreroles Cami de la Collada de Ferreroles 42.6 1.56667 R TRL AD 00 0 2513 Europe/Andorra 1993-12-23
+3041057 Clots de la Collada Clots de la Collada 42.61667 1.61667 H RVN AD 00 0 2352 Europe/Andorra 1993-12-23
+3041058 Bosc de la Collada Bosc de la Collada 42.51667 1.46667 V FRST AD 00 0 1840 Europe/Andorra 1993-12-23
+3041059 Planell del Colitx Planell del Colitx 42.61667 1.51667 T UPLD AD 00 0 1716 Europe/Andorra 1993-12-23
+3041060 Font de la Colilla Font de la Colilla 42.51667 1.6 H SPNG AD 00 0 2085 Europe/Andorra 1993-12-23
+3041061 Canal de la Colilla Canal de la Colilla 42.48333 1.58333 H STM AD 00 0 2349 Europe/Andorra 1993-12-23
+3041062 Canal de la Colija Canal de la Colija 42.58333 1.65 H STM AD 00 0 1767 Europe/Andorra 1993-12-23
+3041063 Bosc de la Colija Bosc de la Colija 42.58333 1.65 V FRST AD 00 0 1767 Europe/Andorra 1993-12-23
+3041064 Riu dels Colells Riu dels Colells 42.53333 1.7 H STM AD 00 0 2357 Europe/Andorra 1993-12-23
+3041065 Circ dels Colells Circ dels Colells 42.51667 1.7 T CRQ AD 00 0 2435 Europe/Andorra 1993-12-23
+3041066 Bosc dels Colells Bosc dels Colells 42.53333 1.7 V FRST AD 00 0 2357 Europe/Andorra 1993-12-23
+3041067 Cortal del Coix Cortal del Coix 42.55 1.55 S HUTS AD 00 0 2097 Europe/Andorra 1993-12-23
+3041068 Tartera del Coferony Tartera del Coferony 42.5 1.45 T SLP AD 00 0 1840 Europe/Andorra 1993-12-23
+3041069 Coferony Coferony 42.48333 1.45 L LCTY AD 00 0 1195 Europe/Andorra 1993-12-23
+3041070 Canal de les Codolles Canal de les Codolles 42.53333 1.51667 H STM AD 00 0 1361 Europe/Andorra 1993-12-23
+3041071 Serrat de Codinet Serrat de Codinet 42.51667 1.6 T SPUR AD 00 0 2085 Europe/Andorra 1993-12-23
+3041072 Collada del Clot Sord Collada del Clot Sord 42.6 1.65 T PASS AD 00 0 2131 Europe/Andorra 1993-12-23
+3041073 Pala dels Clots d’Entinyac Pala dels Clots d'Entinyac 42.58333 1.68333 T SLP AD 00 0 2294 Europe/Andorra 1993-12-23
+3041074 Tosa dels Clots de Massat Tosa dels Clots de Massat 42.56667 1.7 T UPLD AD 00 0 2375 Europe/Andorra 1993-12-23
+3041075 Pala dels Clots de Massat Pala dels Clots de Massat 42.56667 1.7 T SLP AD 00 0 2375 Europe/Andorra 1993-12-23
+3041076 Bassot dels Clots de Massat Bassot dels Clots de Massat 42.56667 1.7 H LK AD 00 0 2375 Europe/Andorra 1993-12-23
+3041077 Clots de Massat Clots de Massat 42.56667 1.68333 A ADMD AD 00 0 2340 Europe/Andorra 1993-12-23
+3041078 Clots de l’Ós Clots de l'Os 42.58333 1.68333 A ADMD AD 00 0 2294 Europe/Andorra 1993-12-23
+3041079 Font dels Clots de la Llosa Font dels Clots de la Llosa 42.61667 1.61667 H SPNG AD 00 0 2352 Europe/Andorra 1993-12-23
+3041080 Font dels Clots Font dels Clots 42.55 1.71667 H SPNG AD 00 0 2192 Europe/Andorra 1993-12-23
+3041081 Canal del Clot del Mener Canal del Clot del Mener 42.5 1.53333 H STM AD 00 0 1574 Europe/Andorra 1993-12-23
+3041082 Barranc del Clot de les Deveses Barranc del Clot de les Deveses 42.53333 1.51667 H STM AD 00 0 1361 Europe/Andorra 1993-12-23
+3041083 Pic del Clot del Cavall Pic del Clot del Cavall 42.6 1.5 T PK AD 00 0 1923 Europe/Andorra 1993-12-23
+3041084 Barranc del Clot d’Aixades Barranc del Clot d'Aixades 42.56667 1.58333 H STM AD 00 0 1919 Europe/Andorra 1993-12-23
+3041085 Font de la Closa Font de la Closa 42.5 1.56667 H SPNG AD 00 0 1776 Europe/Andorra 1993-12-23
+3041086 Pas de la Clau Pas de la Clau 42.51667 1.61667 T PASS AD 00 0 2254 Europe/Andorra 1993-12-23
+3041087 Riu de Claror i Perafita Riu de Claror i Perafita 42.5 1.55 H STM AD 00 0 1566 Europe/Andorra 1993-12-23
+3041088 Riu de Claror Riu de Claror 42.47897 1.56898 H STM AD 00 0 2231 Europe/Andorra 2011-04-19
+3041089 Pleta de Claror Pleta de Claror 42.48333 1.56667 L GRAZ AD 00 0 2231 Europe/Andorra 1993-12-23
+3041090 Camà de Claror Cami de Claror 42.48333 1.56667 R TRL AD 00 0 2231 Europe/Andorra 1993-12-23
+3041091 Cabana de Claror Cabana de Claror 42.46667 1.56667 S HUT AD 00 0 2365 Europe/Andorra 1993-12-23
+3041092 Claror Claror 42.46667 1.56667 A ADMD AD 00 0 2365 Europe/Andorra 1993-12-23
+3041093 Riu de les Claperes Riu de les Claperes 42.55 1.51667 H STM AD 00 0 1397 Europe/Andorra 1993-12-23
+3041094 Canal de les Claperes Canal de les Claperes 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3041095 Civòs Civos 42.45 1.48333 L LCTY AD 00 0 1111 Europe/Andorra 1993-12-23
+3041096 Canal de la Cirera Canal de la Cirera 42.5 1.5 H STM AD 00 0 1135 Europe/Andorra 1993-12-23
+3041097 Conreu de Certers Conreu de Certers 42.48333 1.5 V CULT AD 00 0 1631 Europe/Andorra 1993-12-23
+3041098 Certers Certers Certers,Certes,Certés,Sertes 42.47468 1.50575 P PPL AD 06 0 1383 Europe/Andorra 2011-11-05
+3041099 Coll del Cerc Coll del Cerc 42.46667 1.5 T PASS AD 00 0 1383 Europe/Andorra 1993-12-23
+3041100 Cementiri Cementiri 42.56667 1.73333 L LCTY AD 00 0 2096 Europe/Andorra 1993-12-23
+3041101 Costa dell Cell Costa dell Cell 42.48333 1.48333 T SLP AD 00 0 981 Europe/Andorra 1993-12-23
+3041102 Tarteres de la Cebollera Tarteres de la Cebollera 42.63333 1.6 T TAL AD 00 0 2635 Europe/Andorra 1993-12-23
+3041103 Riu de la Cebollera Riu de la Cebollera 42.61667 1.56667 H STM AD 00 0 2228 Europe/Andorra 1993-12-23
+3041104 Portella de la Cebollera Portella de la Cebollera Portella de la Cebollera 42.63333 1.6 T PASS AD 00 0 2635 Europe/Andorra 2011-11-05
+3041105 Pleta de la Cebollera Pleta de la Cebollera 42.63333 1.58333 L GRAZ AD 00 0 2470 Europe/Andorra 1993-12-23
+3041106 Basses de la Cebollera Basses de la Cebollera 42.63333 1.58333 H LKS AD 00 0 2470 Europe/Andorra 1993-12-23
+3041107 Aspres de la Cebollera Aspres de la Cebollera 42.63333 1.58333 V VINS AD 00 0 2470 Europe/Andorra 1993-12-23
+3041108 Riu de les Cebes Riu de les Cebes 42.61667 1.58333 H STM AD 00 0 2374 Europe/Andorra 1993-12-23
+3041109 Clot del Cavall Clot del Cavall 42.6 1.5 T SLP AD 00 0 1923 Europe/Andorra 1993-12-23
+3041110 Costa del Caup Costa del Caup 42.6 1.66667 T SLP AD 00 0 1858 Europe/Andorra 1993-12-23
+3041111 Solana de la Caülla Solana de la Caulla 42.48333 1.53333 T SLP AD 00 0 2255 Europe/Andorra 1993-12-23
+3041112 Collada de la Caülla Collada de la Caulla 42.48333 1.53333 T PASS AD 00 0 2255 Europe/Andorra 1993-12-23
+3041113 Clots de la Caülla Clots de la Caulla 42.48333 1.53333 H RVN AD 00 0 2255 Europe/Andorra 1993-12-23
+3041114 Bosc de la Caülla Bosc de la Caulla 42.48333 1.53333 V FRST AD 00 0 2255 Europe/Andorra 1993-12-23
+3041115 Planell de la Caubella Planell de la Caubella 42.53333 1.48333 T UPLD AD 00 0 1677 Europe/Andorra 1993-12-23
+3041116 Torrent de la Cauba Torrent de la Cauba 42.55 1.51667 H STM AD 00 0 1397 Europe/Andorra 1993-12-23
+3041117 Roc de la Cauba Roc de la Cauba 42.56114 1.51506 T RK AD 00 0 1551 Europe/Andorra 2011-04-19
+3041118 Coll de la Cauba Coll de la Cauba 42.58333 1.61667 T PASS AD 00 0 1707 Europe/Andorra 1993-12-23
+3041119 Bosc de la Cauba Bosc de la Cauba 42.56667 1.51667 V FRST AD 00 0 1500 Europe/Andorra 1993-12-23
+3041120 Catolla la Guineu Catolla la Guineu 42.46667 1.46667 L LCTY AD 00 0 1340 Europe/Andorra 1993-12-23
+3041121 Pic de Cataverdis Pic de Cataverdis Pic de Cataperdis,Pic de CataperdÃs,Pic de Cataverdis 42.61667 1.46667 T PK AD 00 0 2442 Europe/Andorra 2011-11-05
+3041122 Roc dels Castells Roc dels Castells 42.51667 1.55 T RK AD 00 0 1322 Europe/Andorra 1993-12-23
+3041123 Riu de les Castelletes Riu de les Castelletes 42.45 1.53333 H STM AD 00 0 1859 Europe/Andorra 1993-12-23
+3041124 Roc de la Castelleta Roc de la Castelleta 42.56667 1.5 T RK AD 00 0 1636 Europe/Andorra 1993-12-23
+3041125 Canal de la Castelleta Canal de la Castelleta 42.53333 1.5 H STM AD 00 0 1357 Europe/Andorra 1993-12-23
+3041126 Bosc de la Castelleta Bosc de la Castelleta 42.53333 1.5 V FRST AD 00 0 1357 Europe/Andorra 1993-12-23
+3041127 Bony de la Castelleta Bony de la Castelleta 42.53333 1.53333 T SPUR AD 00 0 1521 Europe/Andorra 1993-12-23
+3041128 Font del Casteller Font del Casteller 42.53333 1.55 H SPNG AD 00 0 1344 Europe/Andorra 1993-12-23
+3041129 Castell de Sant Vicenç Castell de Sant Vicenc Castell de Sant Vicenc,Castell de Sant Vicens,Castell de Sant Vicenç 42.49658 1.48686 S RUIN AD 00 0 1027 Europe/Andorra 2011-11-05
+3041130 Castell dels Moros Castell dels Moros Castel dels Moros La Meca,Castell dels Moros,La Meca 42.55 1.53333 T PROM AD AD 00 0 1593 Europe/Andorra 2011-11-05
+3041131 Pleta del Castellar Pleta del Castellar 42.63333 1.51667 L GRAZ AD 00 0 1894 Europe/Andorra 1993-12-23
+3041132 Canal del Castellar Canal del Castellar Canal del Castella,Canal del Castellar,Canal del Castellà 42.6 1.63333 H STM AD AD 00 0 1893 Europe/Andorra 2011-11-05
+3041133 Bosc del Castellar Bosc del Castellar 42.63333 1.51667 V FRST AD 00 0 1894 Europe/Andorra 1993-12-23
+3041134 Bordes del Castellar Bordes del Castellar 42.53031 1.60873 S HUTS AD 00 0 2101 Europe/Andorra 2011-04-19
+3041135 Rocs de Castell Rocs de Castell 42.46667 1.45 T RKS AD 00 0 1562 Europe/Andorra 1993-12-23
+3041136 Roc del Castell Roc del Castell 42.46667 1.46667 T RK AD 00 0 1340 Europe/Andorra 1993-12-23
+3041137 Canal del Castell Canal del Castell 42.58333 1.51667 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3041138 Bosc del Castell Bosc del Castell 42.58333 1.51667 V FRST AD 00 0 1722 Europe/Andorra 1993-12-23
+3041139 Coll de les Cases Coll de les Cases 42.58333 1.5 T PK AD 00 0 1595 Europe/Andorra 1993-12-23
+3041140 Canal de les Casasses Canal de les Casasses 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3041141 Serra de Casamanya Serra de Casamanya 42.58877 1.57163 T RDGE AD 00 0 2423 Europe/Andorra 2011-04-19
+3041142 Riu de Casamanya Riu de Casamanya 42.55 1.55 H STM AD 00 0 2097 Europe/Andorra 1993-12-23
+3041143 Pic de Casamanya Pic de Casamanya Pic de Camanya,Pic de Casamanya 42.58619 1.56971 T PK AD 00 0 2423 Europe/Andorra 2011-11-05
+3041144 Camà de Casamanya Cami de Casamanya 42.56667 1.55 R TRL AD 00 0 1996 Europe/Andorra 1993-12-23
+3041145 Casamanya Casamanya 42.56667 1.55 A ADMD AD 00 0 1996 Europe/Andorra 1993-12-23
+3041146 Borda del Casadet Borda del Casadet 42.56667 1.6 S HUT AD 00 0 1655 Europe/Andorra 1993-12-23
+3041147 Bordes de la Casa Bordes de la Casa Bordes,Bordes de la Casa 42.53333 1.61667 S HUTS AD AD 00 0 2237 Europe/Andorra 2011-11-05
+3041148 Bordes de la Casa Bordes de la Casa 42.53333 1.6 S HUTS AD 00 0 1888 Europe/Andorra 1993-12-23
+3041149 Pic de Carroi Pic de Carroi 42.51667 1.5 T PK AD 00 0 1688 Europe/Andorra 1993-12-23
+3041150 Roc del Carret Roc del Carret 42.56667 1.48333 T RK AD 00 0 1508 Europe/Andorra 1993-12-23
+3041151 Roc del Carrador Roc del Carrador 42.56667 1.48333 T RK AD 00 0 1508 Europe/Andorra 1993-12-23
+3041152 Bosc del Carpider Bosc del Carpider 42.51667 1.5 V FRST AD 00 0 1688 Europe/Andorra 1993-12-23
+3041153 Canal Carnissera Canal Carnissera 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3041154 Canal Carnissera Canal Carnissera 42.5 1.6 H STM AD 00 0 2416 Europe/Andorra 1993-12-23
+3041155 Canal Carnissera Canal Carnissera 42.65 1.55 H RVN AD 00 0 2181 Europe/Andorra 1993-12-23
+3041156 Roca de Carmenús Roca de Carmenus 42.55 1.61667 T RK AD 00 0 2206 Europe/Andorra 1993-12-23
+3041157 Clots de Carmenús Clots de Carmenus 42.55 1.61667 H RVN AD 00 0 2206 Europe/Andorra 1993-12-23
+3041158 Riu del Cardemeller Riu del Cardemeller 42.55 1.46667 H STM AD 00 0 1585 Europe/Andorra 1993-12-23
+3041159 Borda del Cardago Borda del Cardago 42.53333 1.58333 S HUT AD 00 0 1571 Europe/Andorra 1993-12-23
+3041160 Roca de Carcamanyà Roca de Carcamanya 42.53333 1.56667 T RK AD 00 0 1418 Europe/Andorra 1993-12-23
+3041161 Barranc del Carcabanyat Barranc del Carcabanyat 42.55 1.48333 H STM AD 00 0 1548 Europe/Andorra 1993-12-23
+3041162 Carboneres de Ferrer Carboneres de Ferrer 42.48333 1.6 L LCTY AD 00 0 2250 Europe/Andorra 1993-12-23
+3041163 Bosc de les Carboneres Bosc de les Carboneres 42.51667 1.51667 V FRST AD 00 0 1265 Europe/Andorra 1993-12-23
+3041164 Bony de les Carboneres Bony de les Carboneres 42.56667 1.65 T SPUR AD 00 0 1988 Europe/Andorra 1993-12-23
+3041165 Pleta Carbona Pleta Carbona 42.63333 1.5 L GRAZ AD 00 0 1979 Europe/Andorra 1993-12-23
+3041166 Planell del Carbó Planell del Carbo 42.53333 1.48333 T UPLD AD 00 0 1677 Europe/Andorra 1993-12-23
+3041167 Canal de la Carbassa Canal de la Carbassa 42.51667 1.48333 H STM AD 00 0 1839 Europe/Andorra 1993-12-23
+3041168 Tosa de Caraup Tosa de Caraup 42.6 1.65 T UPLD AD 00 0 2131 Europe/Andorra 1993-12-23
+3041169 Planells de Caraup Planells de Caraup 42.61667 1.65 T UPLD AD 00 0 2567 Europe/Andorra 1993-12-23
+3041170 Clots de Caraup Clots de Caraup 42.61667 1.65 H RVN AD 00 0 2567 Europe/Andorra 1993-12-23
+3041171 Riu de Cap Torrent Riu de Cap Torrent 42.53333 1.56667 H STM AD 00 0 1418 Europe/Andorra 1993-12-23
+3041172 Pont de Capigol Pont de Capigol 42.56667 1.66667 S BDG AD 00 0 1938 Europe/Andorra 1993-12-23
+3041173 Font dels Capellans Font dels Capellans 42.6 1.63333 H SPNG AD 00 0 1893 Europe/Andorra 1993-12-23
+3041174 Cortal del Capdevila Cortal del Capdevila 42.53333 1.53333 S CRRL AD 00 0 1521 Europe/Andorra 1993-12-23
+3041175 Estany del Cap dels Pessons Estany del Cap dels Pessons 42.51996 1.67873 H LK AD 00 0 2352 Europe/Andorra 2011-04-19
+3041176 Tosa del Cap del Siscaró Tosa del Cap del Siscaro 42.58333 1.71667 T UPLD AD 00 0 2553 Europe/Andorra 1993-12-23
+3041177 Cap dels Clots de Massat Cap dels Clots de Massat 42.56667 1.7 L LCTY AD 00 0 2375 Europe/Andorra 1993-12-23
+3041178 Collada del Cap dels Clots Collada del Cap dels Clots 42.55 1.63333 T PASS AD 00 0 2336 Europe/Andorra 1993-12-23
+3041179 Cap dels Clots Cap dels Clots 42.55 1.63333 L LCTY AD 00 0 2336 Europe/Andorra 1993-12-23
+3041180 Canal del Cap dels Camp Canal del Cap dels Camp 42.55 1.53333 H STM AD 00 0 1593 Europe/Andorra 1993-12-23
+3041181 Clot del Cap del Maià Clot del Cap del Maia 42.55 1.71667 H RVN AD 00 0 2192 Europe/Andorra 1993-12-23
+3041182 Pala del Cap de les Tallades Pala del Cap de les Tallades 42.61667 1.55 T SLP AD 00 0 2007 Europe/Andorra 1993-12-23
+3041183 Planada del Cap de les Canals dels Obacs Planada del Cap de les Canals dels Obacs 42.6 1.5 T UPLD AD 00 0 1923 Europe/Andorra 1993-12-23
+3041184 Cap de les Agols Cap de les Agols 42.5 1.61667 L LCTY AD 00 0 2560 Europe/Andorra 1993-12-23
+3041185 Cap del Bosc de Moretó Cap del Bosc de Moreto 42.53333 1.68333 L LCTY AD 00 0 2322 Europe/Andorra 1993-12-23
+3041186 Cap del Bosc dels Plans Cap del Bosc dels Plans 42.58333 1.61667 L LCTY AD 00 0 1707 Europe/Andorra 1993-12-23
+3041187 Cap de la Solana del Forn Cap de la Solana del Forn 42.53333 1.65 L LCTY AD 00 0 2508 Europe/Andorra 1993-12-23
+3041188 Cap de la Montada Cap de la Montada 42.56667 1.58333 L LCTY AD 00 0 1919 Europe/Andorra 1993-12-23
+3041189 Tarteres del Cap de la Coma Tarteres del Cap de la Coma 42.61667 1.48333 T TAL AD 00 0 2470 Europe/Andorra 1993-12-23
+3041190 Serra del Cap de la Coma Serra del Cap de la Coma 42.61667 1.48333 T RDGE AD 00 0 2470 Europe/Andorra 1993-12-23
+3041191 Alt de la Capa Alt de la Capa 42.56293 1.45424 T PK AD 00 0 2173 Europe/Andorra 2011-04-19
+3041192 Font de les Canyorques Font de les Canyorques 42.58333 1.43333 H SPNG AD 00 0 2412 Europe/Andorra 1993-12-23
+3041193 Coves de la Canya Gran Coves de la Canya Gran 42.48333 1.46667 S CAVE AD 00 0 1148 Europe/Andorra 1993-12-23
+3041194 Planell de la Canya Planell de la Canya 42.58333 1.6 T UPLD AD 00 0 1828 Europe/Andorra 1993-12-23
+3041195 Bosc de la Canya Bosc de la Canya 42.58333 1.6 V FRST AD 00 0 1828 Europe/Andorra 1993-12-23
+3041196 Font de Cantallops Font de Cantallops 42.56667 1.5 H SPNG AD 00 0 1636 Europe/Andorra 1993-12-23
+3041197 Canal de Cantallops Canal de Cantallops 42.56667 1.5 H STM AD 00 0 1636 Europe/Andorra 1993-12-23
+3041198 Roc de Canomala Roc de Canomala 42.56667 1.68333 T RK AD 00 0 2340 Europe/Andorra 1993-12-23
+3041199 Santuari de Canòlic Santuari de Canolic 42.46667 1.45 S CH AD 00 0 1562 Europe/Andorra 1993-12-23
+3041200 Conreu de Canòlic Conreu de Canolic 42.48333 1.45 V CULT AD 00 0 1195 Europe/Andorra 1993-12-23
+3041201 Carretera de Canòlic Carretera de Canolic 42.48333 1.46667 R RD AD 00 0 1148 Europe/Andorra 1993-12-23
+3041202 Canòlic Canolic 42.46667 1.45 A ADMD AD 00 0 1562 Europe/Andorra 1993-12-23
+3041203 Parròquia de Canillo Parroquia de Canillo Canillo,Parroquia de Canillo,Parròquia de Canillo 42.58333 1.66667 A ADM1 AD AD 02 5067 2159 Europe/Andorra 2011-11-05
+3041204 Canillo Canillo Canillo,Kanil'o,ka ni e,kaniryo jiao qu,Канильо,カニーリョ教区,å¡å°¼ç•¥ 42.5669 1.59556 P PPLA AD 02 3292 1640 Europe/Andorra 2011-11-05
+3041205 Estany de les Canals Roges Estany de les Canals Roges 42.58333 1.71667 H LK AD 00 0 2553 Europe/Andorra 1993-12-23
+3041206 Cap de les Canals de Ribanelles Cap de les Canals de Ribanelles 42.58333 1.46667 T PK AD 00 0 1643 Europe/Andorra 1993-12-23
+3041207 Pic de les Canals de Montmantell Pic de les Canals de Montmantell 42.6 1.48333 T PK AD 00 0 2441 Europe/Andorra 1993-12-23
+3041208 Canals dels Planells de Baell Canals dels Planells de Baell 42.5 1.6 L LCTY AD 00 0 2416 Europe/Andorra 1993-12-23
+3041209 Canals dels Obacs Canals dels Obacs 42.6 1.5 H RVN AD 00 0 1923 Europe/Andorra 1993-12-23
+3041210 Canals del Pla de l’Ingla Canals del Pla de l'Ingla 42.48333 1.61667 L LCTY AD 00 0 2217 Europe/Andorra 1993-12-23
+3041211 Canals del Maià Canals del Maia 42.56667 1.73333 L LCTY AD 00 0 2096 Europe/Andorra 1993-12-23
+3041212 Canals de la Rabassa Canals de la Rabassa 42.63333 1.56667 L LCTY AD 00 0 2394 Europe/Andorra 1993-12-23
+3041213 Canals de la Comarqueta Canals de la Comarqueta 42.48333 1.61667 L LCTY AD 00 0 2217 Europe/Andorra 1993-12-23
+3041214 Canals de la Burna Canals de la Burna 42.58333 1.5 L LCTY AD 00 0 1595 Europe/Andorra 1993-12-23
+3041215 Serra de les Canals de Falcobà Serra de les Canals de Falcobi 42.63333 1.55 T RDGE AD 00 0 2053 Europe/Andorra 1993-12-23
+3041216 Canals de Falcobà Canals de Falcobi 42.63333 1.55 L LCTY AD 00 0 2053 Europe/Andorra 1993-12-23
+3041217 Canals de Comascura Canals de Comascura 42.5 1.55 L LCTY AD 00 0 1566 Europe/Andorra 1993-12-23
+3041218 Riu de les Canals Riu de les Canals 42.58333 1.63333 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3041219 Camà de les Canals Cami de les Canals 42.58333 1.63333 R TRL AD 00 0 1722 Europe/Andorra 1993-12-23
+3041220 Bosc de les Canals Bosc de les Canals 42.58333 1.63333 V FRST AD 00 0 1722 Europe/Andorra 1993-12-23
+3041221 Bosc de la Canal Llisa Bosc de la Canal Llisa 42.56667 1.5 V FRST AD 00 0 1636 Europe/Andorra 1993-12-23
+3041222 Serrat de la Canal de Nicolau Serrat de la Canal de Nicolau 42.61667 1.55 T RDGE AD 00 0 2007 Europe/Andorra 1993-12-23
+3041223 Solana de la Canal Solana de la Canal 42.48333 1.43333 T SLP AD 00 0 1938 Europe/Andorra 1993-12-23
+3041224 Torrent de la Canadilla Torrent de la Canadilla 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3041225 Camps de Sispony Camps de Sispony 42.53333 1.51667 L LCTY AD 00 0 1361 Europe/Andorra 1993-12-23
+3041226 Torrent dels Camps de Pardellà Torrent dels Camps de Pardella 42.46667 1.5 H STM AD 00 0 1383 Europe/Andorra 1993-12-23
+3041227 Camps de Pardellà Camps de Pardella 42.46667 1.51667 L LCTY AD 00 0 1985 Europe/Andorra 1993-12-23
+3041228 Camp Ramonet Camp Ramonet 42.47397 1.53854 L LCTY AD 00 0 2541 Europe/Andorra 2011-04-19
+3041229 Planell de Campillar Planell de Campillar 42.58333 1.68333 T UPLD AD 00 0 2294 Europe/Andorra 1993-12-23
+3041230 Fonts del Campeà Fonts del Campea 42.51667 1.61667 H SPNG AD 00 0 2254 Europe/Andorra 1993-12-23
+3041231 Bosc del Campeà Bosc del Campea 42.53333 1.63333 V FRST AD 00 0 2360 Europe/Andorra 1993-12-23
+3041232 Camp de Vassalló Camp de Vassallo 42.58333 1.53333 L LCTY AD 00 0 1924 Europe/Andorra 1993-12-23
+3041233 Camp del Sastre Camp del Sastre 42.45 1.53333 L LCTY AD 00 0 1859 Europe/Andorra 1993-12-23
+3041234 Camp del Remugar Camp del Remugar 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3041235 Camp del Cortal Camp del Cortal 42.56667 1.5 L LCTY AD 00 0 1636 Europe/Andorra 1993-12-23
+3041236 Camp de la Trava Camp de la Trava 42.56667 1.53333 L LCTY AD 00 0 1669 Europe/Andorra 1993-12-23
+3041237 Camp de la Llosa Camp de la Llosa 42.56667 1.51667 L LCTY AD 00 0 1500 Europe/Andorra 1993-12-23
+3041238 Bosc del Camp de la Finestra Bosc del Camp de la Finestra 42.5 1.53333 V FRST AD 00 0 1574 Europe/Andorra 1993-12-23
+3041239 Pedrusques del Camp de Claror Pedrusques del Camp de Claror 42.48333 1.55 T TAL AD 00 0 2233 Europe/Andorra 1993-12-23
+3041240 Camp de Claror Camp de Claror 42.46667 1.55 L LCTY AD 00 0 2341 Europe/Andorra 1993-12-23
+3041241 Camp Borrut Camp Borrut 42.45 1.56667 L LCTY AD 00 0 2558 Europe/Andorra 1993-12-23
+3041242 Pont del Camp Pont del Camp 42.55 1.48333 S BDG AD 00 0 1548 Europe/Andorra 1993-12-23
+3041243 Camà del Canal Cami del Canal 42.51667 1.58333 H CNL AD 00 0 1994 Europe/Andorra 1993-12-23
+3041244 Cal Toni Cal Toni 42.55 1.6 S HSE AD 00 0 2210 Europe/Andorra 1993-12-23
+3041245 Cal Serra Cal Serra 42.46667 1.5 S FRM AD 00 0 1383 Europe/Andorra 1993-12-23
+3041246 Cal Ponet Cal Ponet 42.56667 1.6 S HSE AD 00 0 1655 Europe/Andorra 1993-12-23
+3041247 Cal Patxeta Cal Patxeta 42.56667 1.6 S HSE AD 00 0 1655 Europe/Andorra 1993-12-23
+3041248 Cal Jaumina Cal Jaumina 42.55 1.58333 S HSE AD 00 0 1499 Europe/Andorra 1993-12-23
+3041249 Canal de la Calcinera Canal de la Calcinera Canal de la Calcinera,Canal de la Calzinera 42.53333 1.58333 H STM AD AD 00 0 1571 Europe/Andorra 2011-11-05
+3041250 Cal Call Cal Call 42.55 1.6 S HSE AD 00 0 2210 Europe/Andorra 1993-12-23
+3041251 Cal Borronet Cal Borronet 42.56667 1.6 S HSE AD 00 0 1655 Europe/Andorra 1993-12-23
+3041252 Cal Borró Cal Borro 42.55 1.6 S HSE AD 00 0 2210 Europe/Andorra 1993-12-23
+3041253 Cal Becaina Cal Becaina 42.55 1.58333 S HSE AD 00 0 1499 Europe/Andorra 1993-12-23
+3041254 Cal Bartreta Cal Bartreta 42.55 1.6 S HSE AD 00 0 2210 Europe/Andorra 1993-12-23
+3041255 Solà de Calaup Sola de Calaup 42.58333 1.65 T SLP AD 00 0 1767 Europe/Andorra 1993-12-23
+3041256 Font de la Caitanta Font de la Caitanta 42.48333 1.63333 H SPNG AD 00 0 2296 Europe/Andorra 1993-12-23
+3041257 Serrat del Caire Forc Serrat del Caire Forc 42.53333 1.61667 T SPUR AD 00 0 2237 Europe/Andorra 1993-12-23
+3041258 Riu del Caire Forc Riu del Caire Forc 42.53333 1.61667 H STM AD 00 0 2237 Europe/Andorra 1993-12-23
+3041259 Caire Forc Caire Forc 42.53333 1.63333 L LCTY AD 00 0 2360 Europe/Andorra 1993-12-23
+3041260 Torrent dels CÃ cols Torrent dels Cacols 42.56667 1.48333 H STM AD 00 0 1508 Europe/Andorra 1993-12-23
+3041261 Roc de la Cacarulla Roc de la Cacarulla 42.55 1.48333 T RK AD 00 0 1548 Europe/Andorra 1993-12-23
+3041262 Collado de Cabris Collado de Cabris Collado de Cabris,Port de Cabus,Port de Cabús 42.55 1.41667 T PASS AD 00 0 2105 Europe/Andorra 2011-11-05
+3041263 Solana de Caborreu Solana de Caborreu 42.43333 1.55 T SLP AD 00 0 2178 Europe/Andorra 1993-12-23
+3041264 Riu de Caborreu Riu de Caborreu 42.45 1.53333 H STM AD 00 0 1859 Europe/Andorra 1993-12-23
+3041265 Bosc de la Cabeça Bosc de la Cabeca 42.5 1.45 V FRST AD 00 0 1840 Europe/Andorra 1993-12-23
+3041266 Pleta de la Cabaneta Pleta de la Cabaneta 42.6 1.61667 L GRAZ AD 00 0 2271 Europe/Andorra 1993-12-23
+3041267 Pic de la Cabaneta Pic de la Cabaneta 42.61667 1.6 T PK AD 00 0 2528 Europe/Andorra 1993-12-23
+3041268 Pic de la Cabanette Pic de la Cabanette Cabaneta,Pic de la Cabaneta,Pic de la Cabanette 42.58333 1.73333 T PK AD 00 0 2378 Europe/Andorra 2011-11-05
+3041269 Bosc de la Cabanella Bosc de la Cabanella 42.51667 1.46667 V FRST AD 00 0 1840 Europe/Andorra 1993-12-23
+3041270 Serra de Cabana Sorda Serra de Cabana Sorda 42.61667 1.66667 T RDGE AD 00 0 2536 Europe/Andorra 1993-12-23
+3041271 Riu de Cabana Sorda Riu de Cabana Sorda 42.6 1.68333 H STM AD 00 0 2089 Europe/Andorra 1993-12-23
+3041272 Pleta de Cabana Sorda Pleta de Cabana Sorda 42.61667 1.68333 L GRAZ AD 00 0 2406 Europe/Andorra 1993-12-23
+3041273 Pales de Cabana Sorda Pales de Cabana Sorda 42.61667 1.66667 T CLF AD 00 0 2536 Europe/Andorra 1993-12-23
+3041274 Estany de Cabana Sorda Estany de Cabana Sorda 42.61667 1.66667 H LK AD 00 0 2536 Europe/Andorra 1993-12-23
+3041275 Cabana Sorda Cabana Sorda 42.61667 1.66667 A ADMD AD 00 0 2536 Europe/Andorra 1993-12-23
+3041276 Font de la Cabana de l’Eucasser Font de la Cabana de l'Eucasser 42.58333 1.61667 H SPNG AD 00 0 1707 Europe/Andorra 1993-12-23
+3041277 Pic de Cabayrou Pic de Cabayrou Cabairu,Cabairú,Pic de Cabagnau,Pic de Cabairu,Pic de Cabairú,Pic de Cabayrou 42.63333 1.46667 T PK AD 00 0 2324 Europe/Andorra 2011-11-05
+3041278 Font de la Ca Font de la Ca 42.45 1.5 H SPNG AD 00 0 1614 Europe/Andorra 1993-12-23
+3041279 Pla de Buscalls Pla de Buscalls Pla de Buscalls,Pla de Busoalls 42.56667 1.65 T UPLD AD AD 00 0 1988 Europe/Andorra 2011-11-05
+3041280 Serrat de la Burna Serrat de la Burna 42.6 1.48333 T SPUR AD 00 0 2441 Europe/Andorra 1993-12-23
+3041281 Pic de la Burna Pic de la Burna 42.6 1.48333 T PK AD 00 0 2441 Europe/Andorra 1993-12-23
+3041282 Clot de la Burna Clot de la Burna 42.6 1.48333 T SLP AD 00 0 2441 Europe/Andorra 1993-12-23
+3041283 Font dels Bullidors Font dels Bullidors 42.46667 1.45 H SPNG AD 00 0 1562 Europe/Andorra 1993-12-23
+3041284 Toll Bullidor Toll Bullidor 42.58333 1.61667 L GRAZ AD 00 0 1707 Europe/Andorra 1993-12-23
+3041285 Barranc del Bullidor Barranc del Bullidor 42.53333 1.71667 H STM AD 00 0 2400 Europe/Andorra 1993-12-23
+3041286 Roc de Bruna Roja Roc de Bruna Roja 42.63333 1.53333 T RK AD 00 0 2072 Europe/Andorra 1993-12-23
+3041287 Pleta del Bruig Pleta del Bruig 42.63333 1.5 L GRAZ AD 00 0 1979 Europe/Andorra 1993-12-23
+3041288 Marrades del Bruig Marrades del Bruig 42.63333 1.5 R TRL AD 00 0 1979 Europe/Andorra 1993-12-23
+3041289 Basers del Bruig Basers del Bruig 42.65 1.5 T CLF AD 00 0 2455 Europe/Andorra 1993-12-23
+3041290 Bruig Bruig 42.65 1.5 T SLP AD 00 0 2455 Europe/Andorra 1993-12-23
+3041291 Pic del Brossós Pic del Brossos 42.61667 1.51667 T PK AD 00 0 1716 Europe/Andorra 1993-12-23
+3041292 Canals del Brossós Canals del Brossos 42.61667 1.53333 H RVN AD 00 0 1609 Europe/Andorra 1993-12-23
+3041293 Camà del Brossós Cami del Brossos 42.61667 1.53333 R TRL AD 00 0 1609 Europe/Andorra 1993-12-23
+3041294 Cortal del Bringuer Cortal del Bringuer 42.45 1.46667 S CRRL AD 00 0 935 Europe/Andorra 1993-12-23
+3041295 Cortal de Bringuer Cortal de Bringuer 42.53333 1.53333 S CRRL AD 00 0 1521 Europe/Andorra 1993-12-23
+3041296 Borda del Bringuer Borda del Bringuer 42.45 1.48333 S HUTS AD 00 0 1111 Europe/Andorra 1993-12-23
+3041297 Clot dels Brillons Clot dels Brillons 42.46667 1.46667 H RVN AD 00 0 1340 Europe/Andorra 1993-12-23
+3041298 Brancs de la Farga Brancs de la Farga 42.48333 1.61667 L LCTY AD 00 0 2217 Europe/Andorra 1993-12-23
+3041299 Tosa del Braibal Tosa del Braibal Tosa del Braibal,Tossal Braibal 42.50302 1.59836 T PK AD 00 0 2436 Europe/Andorra 2011-11-05
+3041300 Planells del Braibal Planells del Braibal 42.51667 1.58333 T UPLD AD 00 0 1994 Europe/Andorra 1993-12-23
+3041301 Font del Braibal Font del Braibal 42.51667 1.58333 H SPNG AD 00 0 1994 Europe/Andorra 1993-12-23
+3041302 Estany de la Bova Estany de la Bova 42.48333 1.65 H LK AD 00 0 2658 Europe/Andorra 1993-12-23
+3041303 Canal de la Bova Canal de la Bova 42.5 1.58333 H STM AD 00 0 1888 Europe/Andorra 1993-12-23
+3041304 Costa de Bou Mort Costa de Bou Mort 42.46667 1.56667 T SLP AD 00 0 2365 Europe/Andorra 1993-12-23
+3041305 Plana del Bou Plana del Bou 42.45 1.46667 T UPLD AD 00 0 935 Europe/Andorra 1993-12-23
+3041306 Cortal del Bou Cortal del Bou 42.45 1.46667 S HUT AD 00 0 935 Europe/Andorra 1993-12-23
+3041307 Roc de la Botiffarra Roc de la Botiffarra 42.48333 1.48333 T RK AD 00 0 981 Europe/Andorra 1993-12-23
+3041308 Col de la Botella Col de la Botella 42.55 1.46667 T PASS AD 00 0 1585 Europe/Andorra 1993-12-23
+3041309 Canal del Botàs Canal del Botas 42.55 1.58333 H STM AD 00 0 1499 Europe/Andorra 1993-12-23
+3041310 Canal dels Botaders Canal dels Botaders 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3041311 Canal del Bosc Nou Canal del Bosc Nou 42.58333 1.65 H STM AD 00 0 1767 Europe/Andorra 1993-12-23
+3041312 Canal del Bosc Negre Canal del Bosc Negre 42.55 1.46667 H STM AD 00 0 1585 Europe/Andorra 1993-12-23
+3041313 Bosc del Coll d’Ordino Bosc del Coll d'Ordino 42.55 1.55 A ADMD AD 00 0 2097 Europe/Andorra 1993-12-23
+3041314 Canal del Bosc de Coma Canal del Bosc de Coma 42.53333 1.53333 H STM AD 00 0 1521 Europe/Andorra 1993-12-23
+3041315 Boscarró Boscarro 42.55 1.46667 L LCTY AD 00 0 1585 Europe/Andorra 1993-12-23
+3041316 Torrent del Bosc Torrent del Bosc 42.48333 1.45 H STM AD 00 0 1195 Europe/Andorra 1993-12-23
+3041317 Pla del Bosc Pla del Bosc 42.56667 1.66667 T UPLD AD 00 0 1938 Europe/Andorra 1993-12-23
+3041318 Pla del Bosc Pla del Bosc 42.55 1.6 T UPLD AD 00 0 2210 Europe/Andorra 1993-12-23
+3041319 Clot del Bosc Clot del Bosc 42.43333 1.51667 T SLP AD 00 0 2031 Europe/Andorra 1993-12-23
+3041320 Barranc del Bosc Barranc del Bosc 42.56667 1.58333 H STM AD 00 0 1919 Europe/Andorra 1993-12-23
+3041321 Boïgues de Borró Boigues de Borro 42.55 1.6 V CULT AD 00 0 2210 Europe/Andorra 1993-12-23
+3041322 Pleta dels Borrecs Pleta dels Borrecs 42.58333 1.68333 L GRAZ AD 00 0 2294 Europe/Andorra 1993-12-23
+3041323 Borrassica Borrassica 42.5 1.48333 L LCTY AD 00 0 1316 Europe/Andorra 1993-12-23
+3041324 Pla de Borràs Pla de Borras 42.53333 1.48333 T UPLD AD 00 0 1677 Europe/Andorra 1993-12-23
+3041325 Bosc del Bornal Bosc del Bornal 42.53333 1.46667 V FRST AD 00 0 1846 Europe/Andorra 1993-12-23
+3041326 Pla de Bordetes Pla de Bordetes 42.6 1.66667 T UPLD AD 00 0 1858 Europe/Andorra 1993-12-23
+3041327 Bordes de Ramonet Bordes de Ramonet 42.51667 1.55 A ADMD AD 00 0 1322 Europe/Andorra 1993-12-23
+3041328 Canal de les Bordes Canal de les Bordes 42.56667 1.61667 H RVN AD 00 0 1920 Europe/Andorra 1993-12-23
+3041329 Palanca de la Borda del Sabater Palanca de la Borda del Sabater 42.45 1.48333 S BDG AD 00 0 1111 Europe/Andorra 1993-12-23
+3041330 Bosc de la Borda del Rauquet Bosc de la Borda del Rauquet 42.6 1.63333 V FRST AD 00 0 1893 Europe/Andorra 1993-12-23
+3041331 Borda del Molines Borda del Molines 42.5 1.43333 S RUIN AD 00 0 1654 Europe/Andorra 1993-12-23
+3041332 Borda del Ferrer Nou Borda del Ferrer Nou 42.45 1.48333 S RUIN AD 00 0 1111 Europe/Andorra 1993-12-23
+3041333 Camà de la Borda del Cosp Cami de la Borda del Cosp 42.45 1.48333 R TRL AD 00 0 1111 Europe/Andorra 1993-12-23
+3041334 Basera de la Borda de l’Arena Basera de la Borda de l'Arena 42.48333 1.46667 T CLF AD 00 0 1148 Europe/Andorra 1993-12-23
+3041335 Borda de l’Alma Borda de l'Alma 42.58333 1.65 S RUIN AD 00 0 1767 Europe/Andorra 1993-12-23
+3041336 Riu de la Bor Riu de la Bor 42.58333 1.63333 H STM AD 00 0 1722 Europe/Andorra 1993-12-23
+3041337 Gorges de la Bor Gorges de la Bor 42.55 1.58333 T GRGE AD 00 0 1499 Europe/Andorra 1993-12-23
+3041338 Costa del Bony Roig Costa del Bony Roig 42.6 1.61667 T SLP AD 00 0 2271 Europe/Andorra 1993-12-23
+3041339 Planades del Bony Negre Planades del Bony Negre 42.56667 1.45 T UPLD AD 00 0 2137 Europe/Andorra 1993-12-23
+3041340 Canal Gran del Bony de la Pica Canal Gran del Bony de la Pica 42.5 1.46667 H STM AD 00 0 1678 Europe/Andorra 1993-12-23
+3041341 Obaga del Bony de la Costa Obaga del Bony de la Costa 42.56667 1.53333 T SLP AD 00 0 1669 Europe/Andorra 1993-12-23
+3041342 Obaga del Bony Obaga del Bony 42.5 1.58333 T SLP AD 00 0 1888 Europe/Andorra 1993-12-23
+3041343 Boïga del Bony Boiga del Bony 42.56667 1.48333 V CULT AD 00 0 1508 Europe/Andorra 1993-12-23
+3041344 Boïga del Bony Boiga del Bony 42.45 1.53333 V CULT AD 00 0 1859 Europe/Andorra 1993-12-23
+3041345 Canal de les Bons Canal de les Bons 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3041346 Canal de la Boneta Canal de la Boneta 42.5 1.5 H STM AD 00 0 1135 Europe/Andorra 1993-12-23
+3041347 Pont de Bonavida Pont de Bonavida 42.6 1.68333 S BDG AD 00 0 2089 Europe/Andorra 1993-12-23
+3041348 Bombal Bombal 42.6 1.53333 L LCTY AD 00 0 1695 Europe/Andorra 1993-12-23
+3041349 Bosc de la Boixera Bosc de la Boixera 42.53333 1.51667 V FRST AD 00 0 1361 Europe/Andorra 1993-12-23
+3041350 Canal de la Boïgueta Canal de la Boigueta 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3041351 Costa de les Boïgues Costa de les Boigues 42.48333 1.46667 T SLP AD 00 0 1148 Europe/Andorra 1993-12-23
+3041352 Canal de les Boïgues Canal de les Boigues 42.55 1.46667 H STM AD 00 0 1585 Europe/Andorra 1993-12-23
+3041353 Boïgots Boigots 42.55 1.55 L LCTY AD 00 0 2097 Europe/Andorra 1993-12-23
+3041354 Font del Boïgot Font del Boigot 42.5 1.56667 H SPNG AD 00 0 1776 Europe/Andorra 1993-12-23
+3041355 Canal del Boïgot Canal del Boigot 42.5 1.55 H STM AD 00 0 1566 Europe/Andorra 1993-12-23
+3041356 Bosc de Boïga Plana Bosc de Boiga Plana 42.45 1.45 V FRST AD 00 0 1482 Europe/Andorra 1993-12-23
+3041357 Font de la Boïga Mitgera Font de la Boiga Mitgera 42.5 1.46667 H SPNG AD 00 0 1678 Europe/Andorra 1993-12-23
+3041358 Canal dels Boïgals Canal dels Boigals 42.51667 1.5 H STM AD 00 0 1688 Europe/Andorra 1993-12-23
+3041359 Serrat de Boïga Gran Serrat de Boiga Gran 42.48333 1.48333 T SPUR AD 00 0 981 Europe/Andorra 1993-12-23
+3041360 Font de la Boïga del Roi Font de la Boiga del Roi 42.5 1.48333 H SPNG AD 00 0 1316 Europe/Andorra 1993-12-23
+3041361 Roc de Boïga Curta Roc de Boiga Curta 42.51667 1.55 T RK AD 00 0 1322 Europe/Andorra 1993-12-23
+3041362 Estany Blau Estany Blau 42.49666 1.62069 H LK AD 00 0 2560 Europe/Andorra 2011-04-19
+3041363 Roques Blanques Roques Blanques 42.48333 1.48333 T CLF AD 00 0 981 Europe/Andorra 1993-12-23
+3041364 Rocs Blancs Rocs Blancs 42.45 1.45 T SLP AD 00 0 1482 Europe/Andorra 1993-12-23
+3041365 Roca Blanca Roca Blanca 42.56667 1.48333 T RK AD 00 0 1508 Europe/Andorra 1993-12-23
+3041366 La Porteille Blanche La Porteille Blanche La Porteille Blanche,Porteille Blanche d'Andorra,Porteille Blanche d'Andorre,Porteille Blanche d’Andorra,Porteille Blanche d’Andorre,Portella Blanca 42.5 1.73333 T PASS AD 00 0 2686 Europe/Andorra 2011-11-05
+3041367 Font Blanca Font Blanca 42.65 1.53333 H SPNG AD 00 0 2564 Europe/Andorra 1993-12-23
+3041368 Font Blanca Font Blanca 42.58333 1.58333 H SPNG AD 00 0 1993 Europe/Andorra 1993-12-23
+3041369 Canal Blanca Canal Blanca 42.55 1.61667 H RVN AD 00 0 2206 Europe/Andorra 1993-12-23
+3041370 Vial Blanc Vial Blanc 42.48333 1.5 R RD AD 00 0 1631 Europe/Andorra 1993-12-23
+3041371 Roc Blanc Roc Blanc 42.48333 1.53333 T RK AD 00 0 2255 Europe/Andorra 1993-12-23
+3041372 Riu Blanc Riu Blanc 42.53333 1.58333 H STM AD 00 0 1571 Europe/Andorra 1993-12-23
+3041373 Coll Blanc Coll Blanc 42.52961 1.72014 T PK AD 00 0 2400 Europe/Andorra 2011-04-19
+3041374 Carretera de Bixessarri Carretera de Bixessarri 42.48333 1.46667 R RD AD 00 0 1148 Europe/Andorra 1993-12-23
+3041375 Bixessarri Bixessarri Bicisarri,Bixessarri,Bixisarri,Biçisarri,Vixesarri 42.48238 1.45949 P PPL AD 06 0 1178 Europe/Andorra 2011-11-05
+3041376 Bissets Bissets 42.53333 1.53333 L LCTY AD 00 0 1521 Europe/Andorra 1993-12-23
+3041377 Bisset Bisset 42.55 1.53333 L LCTY AD 00 0 1593 Europe/Andorra 1993-12-23
+3041378 Planell del Bisbe Planell del Bisbe 42.48333 1.58333 T UPLD AD 00 0 2349 Europe/Andorra 1993-12-23
+3041379 Font del Bisbe Font del Bisbe 42.55 1.45 H SPNG AD 00 0 1788 Europe/Andorra 1993-12-23
+3041380 Font de la Birena Font de la Birena 42.51667 1.51667 H SPNG AD 00 0 1265 Europe/Andorra 1993-12-23
+3041381 Fontanal del Besurt Fontanal del Besurt 42.53333 1.48333 H SPNG AD 00 0 1677 Europe/Andorra 1993-12-23
+3041382 Roc del Bessó Roc del Besso 42.45 1.46667 T RK AD 00 0 935 Europe/Andorra 1993-12-23
+3041383 Font del Bessó Font del Besso 42.45 1.48333 H SPNG AD 00 0 1111 Europe/Andorra 1993-12-23
+3041384 Costa de Bescaran Costa de Bescaran 42.43333 1.5 T SLP AD 00 0 1804 Europe/Andorra 1993-12-23
+3041385 Portella de Besalà Portella de Besali 42.63333 1.55 T PASS AD 00 0 2053 Europe/Andorra 1993-12-23
+3041386 Pla de Besalà Pla de Besali 42.63333 1.55 T UPLD AD 00 0 2053 Europe/Andorra 1993-12-23
+3041387 Pic de Besalà Pic de Besali 42.63333 1.53333 T PK AD 00 0 2072 Europe/Andorra 1993-12-23
+3041388 Basers de Besalà Basers de Besali 42.63333 1.55 T CLF AD 00 0 2053 Europe/Andorra 1993-12-23
+3041389 Besalà Besali 42.63333 1.53333 A ADMD AD 00 0 2072 Europe/Andorra 1993-12-23
+3041390 Clot de les Berques Clot de les Berques 42.55 1.48333 H RVN AD 00 0 1548 Europe/Andorra 1993-12-23
+3041391 Coma Bella Coma Bella 42.58333 1.68333 T VAL AD 00 0 2294 Europe/Andorra 1993-12-23
+3041392 Riu de la Beixellosa Riu de la Beixellosa 42.53333 1.53333 H STM AD 00 0 1521 Europe/Andorra 1993-12-23
+3041393 Bosc de la Beixellosa Bosc de la Beixellosa 42.53333 1.53333 V FRST AD 00 0 1521 Europe/Andorra 1993-12-23
+3041394 Collada de BeixalÃs Collada de Beixalis 42.53333 1.55 T PASS AD 00 0 1344 Europe/Andorra 1993-12-23
+3041395 Carretera BeixalÃs Carretera Beixalis 42.53333 1.56667 R RD AD 00 0 1418 Europe/Andorra 1993-12-23
+3041396 Bosc de BeixalÃs Bosc de Beixalis 42.53333 1.55 V FRST AD 00 0 1344 Europe/Andorra 1993-12-23
+3041397 Bordes de BeixalÃs Bordes de Beixalis 42.53333 1.55 S FRMS AD 00 0 1344 Europe/Andorra 1993-12-23
+3041398 BeixalÃs Beixalis 42.53333 1.55 A ADMD AD 00 0 1344 Europe/Andorra 1993-12-23
+3041399 Comes Beçoses Comes Becoses 42.51667 1.6 H RVN AD 00 0 2085 Europe/Andorra 1993-12-23
+3041400 Planell dels Beços Planell dels Becos 42.61667 1.56667 T UPLD AD 00 0 2228 Europe/Andorra 1993-12-23
+3041401 Bosc del Becet Bosc del Becet 42.56667 1.53333 V FRST AD 00 0 1669 Europe/Andorra 1993-12-23
+3041402 Vial del Beç Vial del Bec 42.53333 1.46667 R TRL AD 00 0 1846 Europe/Andorra 1993-12-23
+3041403 Solana dels Batallats Solana dels Batallats 42.56667 1.51667 T SLP AD 00 0 1500 Europe/Andorra 1993-12-23
+3041404 Roc dels Batallassos Roc dels Batallassos 42.56667 1.6 T RK AD 00 0 1655 Europe/Andorra 1993-12-23
+3041405 Coll del Bast Coll del Bast 42.48333 1.48333 T PK AD 00 0 981 Europe/Andorra 1993-12-23
+3041406 Basses del Siscaró Basses del Siscaro 42.58333 1.7 L LCTY AD 00 0 2584 Europe/Andorra 1993-12-23
+3041407 Basses dels Basers Basses dels Basers 42.58333 1.7 L LCTY AD 00 0 2584 Europe/Andorra 1993-12-23
+3041408 Pales de les Basses de les Salamandres Pales de les Basses de les Salamandres 42.61667 1.66667 T SLP AD 00 0 2536 Europe/Andorra 1993-12-23
+3041409 Basses de la Burna Basses de la Burna 42.6 1.48333 T RKS AD 00 0 2441 Europe/Andorra 1993-12-23
+3041410 Planell de les Basses Planell de les Basses 42.55 1.58333 T UPLD AD 00 0 1499 Europe/Andorra 1993-12-23
+3041411 Pla de la Bassa de les Granotes Pla de la Bassa de les Granotes 42.58333 1.43333 T UPLD AD 00 0 2412 Europe/Andorra 1993-12-23
+3041412 Basers de l’Estany de Més Amunt Basers de l'Estany de Mes Amunt 42.6 1.46667 L LCTY AD 00 0 2421 Europe/Andorra 1993-12-23
+3041413 Basers de Font Blanca Basers de Font Blanca 42.65 1.55 L LCTY AD 00 0 2181 Europe/Andorra 1993-12-23
+3041414 Coll de Basers Coll de Basers 42.5 1.48333 T RDGE AD 00 0 1316 Europe/Andorra 1993-12-23
+3041415 Baser Negre Baser Negre 42.63333 1.46667 L LCTY AD 00 0 2324 Europe/Andorra 1993-12-23
+3041416 Cap del Baser de la Llonga Cap del Baser de la Llonga 42.56667 1.51667 T SPUR AD 00 0 1500 Europe/Andorra 1993-12-23
+3041417 Cap del Baser Cap del Baser 42.56667 1.51667 T SPUR AD 00 0 1500 Europe/Andorra 1993-12-23
+3041418 Canal de Bartreta Canal de Bartreta 42.55 1.6 H RVN AD 00 0 2210 Europe/Andorra 1993-12-23
+3041419 Bosc de les Bartres Bosc de les Bartres 42.51667 1.5 V FRST AD 00 0 1688 Europe/Andorra 1993-12-23
+3041420 Bosc de la Bartra Bosc de la Bartra 42.5 1.51667 V FRST AD 00 0 1410 Europe/Andorra 1993-12-23
+3041421 Pont dels Barrons Pont dels Barrons 42.61667 1.55 S BDG AD 00 0 2007 Europe/Andorra 1993-12-23
+3041422 Torrent del Barreró Torrent del Barrero 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3041423 Bosc del Barrer d’Areny Bosc del Barrer d'Areny 42.58333 1.46667 V FRST AD 00 0 1643 Europe/Andorra 1993-12-23
+3041424 Barrer d’Areny Barrer d'Areny 42.58333 1.46667 L GRAZ AD 00 0 1643 Europe/Andorra 1993-12-23
+3041425 Roc del Barrer Roc del Barrer 42.55 1.53333 T SPUR AD 00 0 1593 Europe/Andorra 1993-12-23
+3041426 Canal del Barrer Canal del Barrer 42.56667 1.51667 H STM AD 00 0 1500 Europe/Andorra 1993-12-23
+3041427 Riu de la Barraca Cremada Riu de la Barraca Cremada 42.53333 1.7 H STM AD 00 0 2357 Europe/Andorra 1993-12-23
+3041428 Solà de Barra Sola de Barra 42.58333 1.65 T SLP AD 00 0 1767 Europe/Andorra 1993-12-23
+3041429 Prats de la Baronia Prats de la Baronia 42.53333 1.61667 L GRAZ AD 00 0 2237 Europe/Andorra 1993-12-23
+3041430 Planell de la Baronia Planell de la Baronia 42.53333 1.61667 T UPLD AD 00 0 2237 Europe/Andorra 1993-12-23
+3041431 Borda de la Baronia Borda de la Baronia 42.53333 1.61667 S HUT AD 00 0 2237 Europe/Andorra 1993-12-23
+3041432 Canal dels Banys Canal dels Banys 42.53333 1.5 H STM AD 00 0 1357 Europe/Andorra 1993-12-23
+3041433 Bosc dels Banys Bosc dels Banys 42.53333 1.5 V FRST AD 00 0 1357 Europe/Andorra 1993-12-23
+3041434 Port de Banyell Port de Banyell 42.64218 1.5777 T PASS AD 00 0 2365 Europe/Andorra 2011-04-19
+3041435 Font de Banyell Font de Banyell 42.63333 1.56667 H SPNG AD 00 0 2394 Europe/Andorra 1993-12-23
+3041436 Banyell Banyell 42.63333 1.56667 L LCTY AD 00 0 2394 Europe/Andorra 1993-12-23
+3041437 Bosc dels Bancs Bosc dels Bancs 42.48333 1.46667 V FRST AD 00 0 1148 Europe/Andorra 1993-12-23
+3041438 Riu del Bancal Vedeller Riu del Bancal Vedeller 42.6 1.46667 H STM AD 00 0 2421 Europe/Andorra 1993-12-23
+3041439 Basers del Bancal Vedeller Basers del Bancal Vedeller 42.6 1.45 T CLF AD 00 0 2174 Europe/Andorra 1993-12-23
+3041440 Plana del Banc Plana del Banc 42.43333 1.5 T UPLD AD 00 0 1804 Europe/Andorra 1993-12-23
+3041441 Font del Baladre Font del Baladre 42.58333 1.48333 H SPNG AD 00 0 1809 Europe/Andorra 1993-12-23
+3041442 Pont de la Baladosa Pont de la Baladosa 42.6 1.68333 S BDG AD 00 0 2089 Europe/Andorra 1993-12-23
+3041443 Baladosa Baladosa 42.58333 1.68333 L LCTY AD 00 0 2294 Europe/Andorra 1993-12-23
+3041444 Collades Baixes d’Emportona Collades Baixes d'Emportona 42.51667 1.65 T PASS AD 00 0 2633 Europe/Andorra 1993-12-23
+3041445 Collades Baixes Collades Baixes 42.5 1.63333 T PASS AD 00 0 2545 Europe/Andorra 1993-12-23
+3041446 Baixant Baixant 42.55 1.48333 L LCTY AD 00 0 1548 Europe/Andorra 1993-12-23
+3041447 Pleta de Baix Pleta de Baix 42.51667 1.61667 L GRAZ AD 00 0 2254 Europe/Andorra 1993-12-23
+3041448 Pleta de Baix Pleta de Baix 42.48333 1.43333 L GRAZ AD 00 0 1938 Europe/Andorra 1993-12-23
+3041449 Estany de Baix Estany de Baix 42.58333 1.7 H LK AD 00 0 2584 Europe/Andorra 1993-12-23
+3041450 Font de Baiter Font de Baiter 42.55 1.51667 H SPNG AD 00 0 1397 Europe/Andorra 1993-12-23
+3041451 Port de Baiau Port de Baiau Port de Baiau 42.6 1.43333 T PASS AD 00 0 2667 Europe/Andorra 2011-11-05
+3041452 Pic de Baiau Pic de Baiau Pic de Baiau 42.58333 1.43333 T PK AD 00 0 2412 Europe/Andorra 2011-11-05
+3041453 Agulla de Baiau Agulla de Baiau Agulla de Baiau 42.58333 1.43333 T PK AD 00 0 2412 Europe/Andorra 2011-11-05
+3041454 Planells de Baell Planells de Baell 42.48333 1.6 T UPLD AD 00 0 2250 Europe/Andorra 1993-12-23
+3041455 Font de Baell Font de Baell 42.5 1.61667 H SPNG AD 00 0 2560 Europe/Andorra 1993-12-23
+3041456 Pleta de les Bacives Pleta de les Bacives 42.5 1.65 L GRAZ AD 00 0 2542 Europe/Andorra 1993-12-23
+3041457 Serra de l’ Avier Serra de l' Avier 42.6 1.5 T RDGE AD 00 0 1923 Europe/Andorra 1993-12-23
+3041458 Plana de l’ Avier Plana de l' Avier 42.6 1.5 T UPLD AD 00 0 1923 Europe/Andorra 1993-12-23
+3041459 Obaga de l’ Avier Obaga de l' Avier 42.48333 1.53333 T SLP AD 00 0 2255 Europe/Andorra 1993-12-23
+3041460 Costa de l’ Avier Costa de l' Avier 42.56667 1.55 T SLP AD 00 0 1996 Europe/Andorra 1993-12-23
+3041461 Camà de l’ Avier Cami de l' Avier 42.48333 1.55 R TRL AD 00 0 2233 Europe/Andorra 1993-12-23
+3041462 Bosc de l’ Avier Bosc de l' Avier 42.48333 1.53333 V FRST AD 00 0 2255 Europe/Andorra 1993-12-23
+3041463 Torrent de l’ Aviar Torrent de l' Aviar 42.53333 1.56667 H STM AD 00 0 1418 Europe/Andorra 1993-12-23
+3041464 Canal de l’ Avetar Canal de l' Avetar 42.58333 1.65 H STM AD 00 0 1767 Europe/Andorra 1993-12-23
+3041465 Canal de l’ Avetar Canal de l' Avetar 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3041466 Bosc de l’ Avetar Bosc de l' Avetar 42.55 1.55 V FRST AD 00 0 2097 Europe/Andorra 1993-12-23
+3041467 Costa de Avet Costa de Avet 42.5 1.56667 T SLP AD 00 0 1776 Europe/Andorra 1993-12-23
+3041468 Canal de l’ Avet Canal de l' Avet 42.55 1.5 H STM AD 00 0 1292 Europe/Andorra 1993-12-23
+3041469 Costa de l’ Ave Maria Costa de l' Ave Maria 42.61667 1.53333 T SLP AD 00 0 1609 Europe/Andorra 1993-12-23
+3041470 Solà de l’ Avellanet Sola de l' Avellanet 42.58333 1.48333 T SLP AD 00 0 1809 Europe/Andorra 1993-12-23
+3041471 Costa dels Avellaners Costa dels Avellaners 42.56667 1.61667 T SLP AD 00 0 1920 Europe/Andorra 1993-12-23
+3041472 Canal dels Avellaners Canal dels Avellaners 42.58333 1.46667 H RVN AD 00 0 1643 Europe/Andorra 1993-12-23
+3041473 Coma Aubosa Coma Aubosa 42.58333 1.5 T SLP AD 00 0 1595 Europe/Andorra 1993-12-23
+3041474 Riu d’ Aubinyà Riu d' Aubinya 42.45 1.48333 H STM AD 00 0 1111 Europe/Andorra 1993-12-23
+3041475 Aubinyà Aubinya Aubinya,Aubinyà ,Auvinya 42.45447 1.48761 P PPL AD 06 0 1159 Europe/Andorra 2011-11-05
+3041476 Aubinyà Aubinya 42.45 1.5 A ADMD AD 00 0 1614 Europe/Andorra 1993-12-23
+3041477 Roc de les Aubes Roc de les Aubes 42.51667 1.55 T SPUR AD 00 0 1322 Europe/Andorra 1993-12-23
+3041478 Riu de les Aubes Riu de les Aubes 42.55 1.53333 H STM AD 00 0 1593 Europe/Andorra 1993-12-23
+3041479 Costa de l’ Aubell Costa de l' Aubell 42.56667 1.53333 T SLP AD 00 0 1669 Europe/Andorra 1993-12-23
+3041480 Aubaderes Aubaderes 42.5 1.48333 L LCTY AD 00 0 1316 Europe/Andorra 1993-12-23
+3041481 Roc de l’ Auba Roc de l' Auba 42.56667 1.5 T RK AD 00 0 1636 Europe/Andorra 1993-12-23
+3041482 Canal dels Astrells Canal dels Astrells 42.48333 1.56667 H STM AD 00 0 2231 Europe/Andorra 1993-12-23
+3041483 Riu de l’ Astrell Riu de l' Astrell 42.55 1.55 H STM AD 00 0 2097 Europe/Andorra 1993-12-23
+3041484 Cap de l’ Astrell Cap de l' Astrell 42.56667 1.56667 T SPUR AD 00 0 2089 Europe/Andorra 1993-12-23
+3041485 Bosc de l’ Astrell Bosc de l' Astrell 42.56667 1.56667 V FRST AD 00 0 2089 Europe/Andorra 1993-12-23
+3041486 Canals dels Assaladors dels Pletius Canals dels Assaladors dels Pletius 42.5 1.46667 H STM AD 00 0 1678 Europe/Andorra 1993-12-23
+3041487 Assaladors del Planell Gran Assaladors del Planell Gran 42.56667 1.46667 L LCTY AD 00 0 1673 Europe/Andorra 1993-12-23
+3041488 Assaladors de Cabana Sorda Assaladors de Cabana Sorda 42.6 1.66667 T PKS AD 00 0 1858 Europe/Andorra 1993-12-23
+3041489 Canal de l’ Assalador de Rei Canal de l' Assalador de Rei 42.55 1.46667 H STM AD 00 0 1585 Europe/Andorra 1993-12-23
+3041490 Bosc de l’ Assalador Bosc de l' Assalador 42.61667 1.65 V FRST AD 00 0 2567 Europe/Andorra 1993-12-23
+3041491 Bosc de l’ Assalador Bosc de l' Assalador 42.56667 1.6 V FRST AD 00 0 1655 Europe/Andorra 1993-12-23
+3041492 Bosc de l’ Assalador Bosc de l' Assalador 42.56667 1.58333 V FRST AD 00 0 1919 Europe/Andorra 1993-12-23
+3041493 Serra del Cap dels Aspres de Banyell Serra del Cap dels Aspres de Banyell 42.63333 1.56667 T MT AD 00 0 2394 Europe/Andorra 1993-12-23
+3041494 Aspres de Banyell Aspres de Banyell 42.63333 1.56667 L LCTY AD 00 0 2394 Europe/Andorra 1993-12-23
+3041495 Pic dels Aspres Pic dels Aspres Pic dels Aspres 42.56667 1.45 T PK AD 00 0 2562 2137 Europe/Andorra 2011-11-05
+3041496 Rocs de l’ Aspra Rocs de l' Aspra 42.53333 1.53333 T RKS AD 00 0 1521 Europe/Andorra 1993-12-23
+3041497 Riu de l’ Aspra Riu de l' Aspra 42.55 1.55 H STM AD 00 0 2097 Europe/Andorra 1993-12-23
+3041498 Clots de l’ Aspra Clots de l' Aspra 42.51667 1.63333 H RVN AD 00 0 2379 Europe/Andorra 1993-12-23
+3041499 Alt de l’ Aspra Alt de l' Aspra 42.51667 1.65 T SLP AD 00 0 2633 Europe/Andorra 1993-12-23
+3041500 Canals d’ Aspones Canals d' Aspones 42.61667 1.7 H RVN AD 00 0 2285 Europe/Andorra 1993-12-23
+3041501 Torrent dels Aspedius Torrent dels Aspedius 42.48333 1.53333 H STM AD 00 0 2255 Europe/Andorra 1993-12-23
+3041502 Comella Ascura Comella Ascura 42.45 1.45 H RVN AD 00 0 1482 Europe/Andorra 1993-12-23
+3041503 Rocs de l’ Ascobet Rocs de l' Ascobet 42.61667 1.51667 T RKS AD 00 0 1716 Europe/Andorra 1993-12-23
+3041504 Ascobar de Puntal Ascobar de Puntal 42.63333 1.55 L LCTY AD 00 0 2053 Europe/Andorra 1993-12-23
+3041505 Canal de les Asclades Canal de les Asclades 42.5 1.53333 H STM AD 00 0 1574 Europe/Andorra 1993-12-23
+3041506 Serrat de l’ Ascladella Serrat de l' Ascladella 42.53333 1.46667 T RDGE AD 00 0 1846 Europe/Andorra 1993-12-23
+3041507 Fontanal de l’ Ascladella Fontanal de l' Ascladella 42.53333 1.46667 H SPNG AD 00 0 1846 Europe/Andorra 1993-12-23
+3041508 Coma de l’ Ascladella Coma de l' Ascladella 42.56667 1.48333 T SLP AD 00 0 1508 Europe/Andorra 1993-12-23
+3041509 Torrent dels Artics Torrent dels Artics 42.55 1.58333 H STM AD 00 0 1499 Europe/Andorra 1993-12-23
+3041510 Artic de Capell Artic de Capell 42.58333 1.6 L LCTY AD 00 0 1828 Europe/Andorra 1993-12-23
+3041511 Font de l’ Artic Font de l' Artic 42.48333 1.5 H SPNG AD 00 0 1631 Europe/Andorra 1993-12-23
+3041512 Bony de l’ Artic Bony de l' Artic 42.55 1.55 T SPUR AD 00 0 2097 Europe/Andorra 1993-12-23
+3041513 Serrat de l’ Arna Tova Serrat de l' Arna Tova 42.58333 1.46667 T SPUR AD 00 0 1643 Europe/Andorra 1993-12-23
+3041514 Arna Tova Arna Tova 42.58333 1.46667 L LCTY AD 00 0 1643 Europe/Andorra 1993-12-23
+3041515 Bordes de l’ Armiana Bordes de l' Armiana 42.58333 1.6 S HUTS AD 00 0 1828 Europe/Andorra 1993-12-23
+3041516 Font de l’ Arinsola Font de l' Arinsola 42.48333 1.41667 H SPNG AD 00 0 1920 Europe/Andorra 1993-12-23
+3041517 Riu d’ Arinsal Riu d' Arinsal Riu d' Arinsal,Riu d' Arinsul,Riu d’ Arinsal,Riu d’ Arinsul 42.5456 1.51523 H STM AD 00 0 1257 Europe/Andorra 2011-11-05
+3041518 Port d’ Arinsal Port d' Arinsal Port d' Arinsal,Port d’ Arinsal 42.6 1.46667 T PASS AD 00 0 2421 Europe/Andorra 2011-11-05
+3041519 Arinsal Arinsal Arinsal,ÐринÑал 42.57205 1.48453 P PPL AD 04 1419 1655 Europe/Andorra 2010-01-29
+3041520 Canal de Argelagosa Canal de Argelagosa 42.51667 1.53333 H STM AD 00 0 1460 Europe/Andorra 1993-12-23
+3041521 Roc de l’ Areny Roc de l' Areny 42.56667 1.6 T PROM AD 00 0 1655 Europe/Andorra 1993-12-23
+3041522 Rec d’ Areny Rec d' Areny 42.58333 1.46667 H STM AD 00 0 1643 Europe/Andorra 1993-12-23
+3041523 Coll d’ Arenes Coll d' Arenes 42.6 1.58333 T PASS AD 00 0 2461 Europe/Andorra 1993-12-23
+3041524 Bassa de l’ Arena Bassa de l' Arena 42.51667 1.63333 H STMH AD 00 0 2379 Europe/Andorra 1993-12-23
+3041525 Font d’ Arduix Font d' Arduix Font d' Arduix,Font d’ Arduix 42.45 1.45 H SPNG AD 00 0 1482 Europe/Andorra 2011-11-05
+3041526 Solana d’ Arcavell Solana d' Arcavell Solana d' Arcabell,Solana d' Arcavell,Solana d’ Arcabell,Solana d’ Arcavell 42.43333 1.51667 T SLP AD 00 0 2031 Europe/Andorra 2011-11-05
+3041527 Serra d’ ArcalÃs Serra d' Arcalis 42.61667 1.5 T RDGE AD 00 0 2390 Europe/Andorra 1993-12-23
+3041528 Roc d’ ArcalÃs Roc d' Arcalis 42.63333 1.5 T RK AD 00 0 1979 Europe/Andorra 1993-12-23
+3041529 Riu d’ ArcalÃs Riu d' Arcalis 42.63333 1.5 H STM AD 00 0 1979 Europe/Andorra 1993-12-23
+3041530 Portella d’ ArcalÃs Portella d' Arcalis 42.61667 1.48333 T PASS AD 00 0 2470 Europe/Andorra 1993-12-23
+3041531 Pont d’ ArcalÃs Pont d' Arcalis 42.63333 1.5 S BDG AD 00 0 1979 Europe/Andorra 1993-12-23
+3041532 Pic d’ ArcalÃs Pic d' Arcalis 42.61575 1.4904 T PK AD 00 0 2431 Europe/Andorra 2011-04-19
+3041533 Feixans d’ ArcalÃs Feixans d' Arcalis 42.63333 1.5 V CULT AD 00 0 1979 Europe/Andorra 1993-12-23
+3041534 Collades d’ ArcalÃs Collades d' Arcalis 42.63333 1.51667 T PASS AD 00 0 1894 Europe/Andorra 1993-12-23
+3041535 Cabana d’ ArcalÃs Cabana d' Arcalis 42.63333 1.5 S HUT AD 00 0 1979 Europe/Andorra 1993-12-23
+3041536 Basers d’ ArcalÃs Basers d' Arcalis 42.61667 1.5 T CLF AD 00 0 2390 Europe/Andorra 1993-12-23
+3041537 Port de l’ Albeille Port de l' Albeille Port de l' Albeille,Port de l' Albelle,Port de l' Arbeille,Port de l' Arbella,Port de l’ Albeille,Port de l’ Albelle,Port de l’ Arbeille,Port de l’ Arbella,Porteille de l' Albelle,Porteille de l’ Albelle 42.65 1.5 T PASS AD 00 0 2455 Europe/Andorra 2011-11-05
+3041538 Canal de l’ Arbeguer Canal de l' Arbeguer 42.48333 1.43333 H STM AD 00 0 1938 Europe/Andorra 1993-12-23
+3041539 Pont d’ Arans Pont d' Arans 42.58333 1.51667 S BDG AD 00 0 1722 Europe/Andorra 1993-12-23
+3041540 Canals d’ Arans Canals d' Arans 42.58333 1.5 H RVN AD 00 0 1595 Europe/Andorra 1993-12-23
+3041541 Arans Arans Arans 42.58226 1.51844 P PPL AD 05 0 1722 Europe/Andorra 2011-11-05
+3041542 Coll de la Quell Coll de la Quell Coll de l' Aquell,Coll de la Quell,Coll de l’ Aquell 42.48333 1.41667 T PASS AD 00 0 1920 Europe/Andorra 2011-11-05
+3041543 Anyós Anyos 42.53458 1.52672 P PPL AD 04 0 1491 Europe/Andorra 2011-04-19
+3041544 Bosc Gran de l’ Any de la Part Bosc Gran de l' Any de la Part 42.55 1.51667 V FRST AD 00 0 1397 Europe/Andorra 1993-12-23
+3041545 Planell d’ Antònia Planell d' Antonia 42.51667 1.55 T UPLD AD 00 0 1322 Europe/Andorra 1993-12-23
+3041546 Ansalonga Ansalonga Ansalonga 42.56919 1.52285 P PPL AD 05 0 1500 Europe/Andorra 2011-11-05
+3041547 Serra d’ Anrodat Serra d' Anrodat 42.61667 1.68333 T RDGE AD 00 0 2406 Europe/Andorra 1993-12-23
+3041548 Pic de la Coume d’Enfer Pic de la Coume d'Enfer Pic d' Anrodat,Pic de la Coume d'Enfer,Pic de la Coume d’Enfer,Pic d’ Anrodat 42.61667 1.68333 T RDGE AD 00 0 2406 Europe/Andorra 2011-11-05
+3041549 Estany d’ Anrodat Estany d' Anrodat 42.61667 1.68333 H LK AD 00 0 2406 Europe/Andorra 1993-12-23
+3041550 Coll d’ Anrodat Coll d' Anrodat Coll d' Anrodat,Coll d’ Anrodat 42.61667 1.7 T PASS AD 00 0 2285 Europe/Andorra 2011-11-05
+3041551 Anrodat Anrodat 42.61667 1.68333 A ADMD AD 00 0 2406 Europe/Andorra 1993-12-23
+3041552 Riu de l’ Angonella Riu de l' Angonella 42.6 1.53333 H STM AD 00 0 1695 Europe/Andorra 1993-12-23
+3041553 Port de l’ Angonella Port de l' Angonella Port de l' Angonella,Port de l’ Angonella 42.61667 1.46667 T PASS AD 00 0 2442 Europe/Andorra 2011-11-05
+3041554 Pleta de l’ Angonella Pleta de l' Angonella 42.6 1.5 L GRAZ AD 00 0 1923 Europe/Andorra 1993-12-23
+3041555 Pas de l’ Angonella Pas de l' Angonella 42.6 1.5 T PASS AD 00 0 1923 Europe/Andorra 1993-12-23
+3041556 Estret del l’ Angonella Estret del l' Angonella 42.6 1.5 T GRGE AD 00 0 1923 Europe/Andorra 1993-12-23
+3041557 Estanys de l’ Angonella Estanys de l' Angonella 42.6 1.48333 H LKS AD 00 0 2441 Europe/Andorra 1993-12-23
+3041558 Basers de l’ Angonella Basers de l' Angonella 42.61667 1.5 T CLF AD 00 0 2390 Europe/Andorra 1993-12-23
+3041559 Pic de les Angleves Pic de les Angleves 42.55 1.53333 T PK AD 00 0 1593 Europe/Andorra 1993-12-23
+3041560 Solà de Angleva Sola de Angleva 42.56667 1.48333 T SLP AD 00 0 1508 Europe/Andorra 1993-12-23
+3041561 Planell de Andreuet Planell de Andreuet 42.51667 1.6 T UPLD AD 00 0 2085 Europe/Andorra 1993-12-23
+3041562 Barranc de l’ Andorrana Barranc de l' Andorrana 42.55 1.43333 H STM AD 00 0 1949 Europe/Andorra 1993-12-23
+3041563 Andorra la Vella Andorra la Vella Ando-la-Vyey,Andora,Andora la Vela,Andora la Velja,Andora lja Vehl'ja,Andoro Malnova,Andorra,Andorra Tuan,Andorra a Vella,Andorra la Biella,Andorra la Vella,Andorra la Vielha,Andorra-a-Velha,Andorra-la-Vel'ja,Andorra-la-Vielye,Andorre-la-Vieille,Andò-la-Vyèy,Andòrra la Vièlha,an dao er cheng,andolalabeya,andwra la fyla,ΑνδόÏÏα,Ðндора ла ВелÑ,Ðндора ла Веља,Ðндора Ð»Ñ Ð’ÑльÑ,Ðндорра-ла-ВельÑ,×× ×“×•×¨×” לה וולה,أندورا لا Ùيلا,አንዶራ ላ ቬላ,アンドラ・ラ・ヴェリャ,安é“爾城,안ë„ë¼ë¼ë² 야 42.50779 1.52109 P PPLC AD 07 20430 1073 Europe/Andorra 2010-05-30
+3041564 Rec d’ Andorra Rec d' Andorra 42.51667 1.51667 H CNL AD 00 0 1265 Europe/Andorra 1993-12-23
+3041565 Principality of Andorra Principality of Andorra Andora,Andoro,Andorra,Andorre,Andorrë,Andurra,Andòrra,Andóra,L'Andorre,Landoraen,Landorän,Les Vallees d' Andorre,Les Vallées d’ Andorre,L’Andorre,Principado de Andorra,Principality of Andorra,Principat d' Andorra,Principat d’ Andorra,Principaute d' Andorre,Principauté d’ Andorre,Vallees et Suzerainetes d' Andorre,Valls d' Andorra,Valls d’ Andorra,Vallées et Suzerainetés d’ Andorre,an dao er,andola,andora,andwra,antora,endora,prathes xandxrra,xandxrra,yandwrra,ΑνδόÏα,ΑνδόÏÏα,Ðндора,Ðндорра,Ô±Õ¶Õ¤Õ¸Ö€Õ¡,×× ×“×•×¨×”,آندورا,أندورا,ئاندوررا,اندورا,انډورا,ÜܢܕܘܪÜ,अंडोरा,अनà¥à¤¡à¥‹à¤°à¤¾,à¤à¤£à¥à¤¡à¥‹à¤°à¤¾,অà§à¦¯à¦¾à¦¨à§à¦¡à§‹à¦°à¦¾,আনà§à¦¡à§‹à¦°à¦¾,à¦à¦¨à§à¦¡à§‹à¦°à¦¾,அனà¯à®Ÿà¯‹à®°à®¾,à´…à´¨àµâ€à´Ÿàµ‹à´±,ประเทศà¸à¸±à¸™à¸”à¸à¸£à¹Œà¸£à¸²,à¸à¸±à¸™à¸”à¸à¸£à¹Œà¸£à¸²,àºàº±àº™àº”à»àº¥àº²,ཨེན་ཌོ་རà¼,áƒáƒœáƒ“áƒáƒ áƒ,አንዶራ,អង់ដូររា,អានដូរ៉ា,アンドラ,安é“å°”,安é“爾,안ë„ë¼ 42.5 1.5 A PCLI AD 00 84000 1135 Europe/Andorra 2011-11-05
+3041566 Parròquia d'Andorra la Vella Parroquia d'Andorra la Vella Andorra la Vella,Andorre-la-Vieille,Parroquia d'Andorra la Vella,Parròquia d'Andorra la Vella 42.5045 1.49414 A ADM1 AD 07 24211 1236 Europe/Andorra 2008-03-17
+3041567 Canal Ampla Canal Ampla 42.48333 1.63333 H STM AD 00 0 2296 Europe/Andorra 1993-12-23
+3041568 Canal Ampla Canal Ampla 42.48333 1.6 H STM AD 00 0 2250 Europe/Andorra 1993-12-23
+3041569 Bosc dels Amorriadors Bosc dels Amorriadors 42.53333 1.48333 V FRST AD 00 0 1677 Europe/Andorra 1993-12-23
+3041570 Solana de l’ Alzinar Solana de l' Alzinar 42.45 1.48333 T SLP AD 00 0 1111 Europe/Andorra 1993-12-23
+3041571 Serrat de l’ Alzinar Serrat de l' Alzinar 42.48333 1.46667 T MT AD 00 0 1148 Europe/Andorra 1993-12-23
+3041572 Canal de l’ Alzina Canal de l' Alzina 42.5 1.5 H STM AD 00 0 1135 Europe/Andorra 1993-12-23
+3041573 Rocs Alts Rocs Alts 42.56667 1.43333 T RKS AD 00 0 2402 Europe/Andorra 1993-12-23
+3041574 Costa de l’ Alt de la Capa Costa de l' Alt de la Capa 42.56667 1.46667 T SLP AD 00 0 1673 Europe/Andorra 1993-12-23
+3041575 Pala Alta Pala Alta 42.6 1.63333 T CLF AD 00 0 1893 Europe/Andorra 1993-12-23
+3041576 Terregalls de l’ Alt Terregalls de l' Alt 42.56667 1.45 T TAL AD 00 0 2137 Europe/Andorra 1993-12-23
+3041577 Terregalls de l’ Alt Terregalls de l' Alt 42.58333 1.43333 T SPUR AD 00 0 2412 Europe/Andorra 1993-12-23
+3041578 Font de l’ Alt Font de l' Alt 42.56667 1.46667 H SPNG AD 00 0 1673 Europe/Andorra 1993-12-23
+3041579 Cresta de l’ Alt Cresta de l' Alt 42.58333 1.45 T SPUR AD 00 0 2156 Europe/Andorra 1993-12-23
+3041580 Costes de l’ Alt Costes de l' Alt 42.56667 1.45 T SLP AD 00 0 2137 Europe/Andorra 1993-12-23
+3041581 Canals de l’ Alt Canals de l' Alt 42.58333 1.43333 H RVN AD 00 0 2412 Europe/Andorra 1993-12-23
+3041582 Canal de l’ Alt Canal de l' Alt 42.58333 1.45 H STM AD 00 0 2156 Europe/Andorra 1993-12-23
+3041583 Planell dels Alls Planell dels Alls 42.5 1.6 T UPLD AD 00 0 2416 Europe/Andorra 1993-12-23
+3041584 Roca de les Allaus Roca de les Allaus 42.63333 1.53333 T RKS AD 00 0 2072 Europe/Andorra 1993-12-23
+3041585 Bosc de les Allaus Bosc de les Allaus 42.53333 1.6 V FRST AD 00 0 1888 Europe/Andorra 1993-12-23
+3041586 Solà de l’ Allau Sola de l' Allau 42.6 1.53333 T SLP AD 00 0 1695 Europe/Andorra 1993-12-23
+3041587 Planell de l’ Allau Planell de l' Allau 42.56667 1.68333 T UPLD AD 00 0 2340 Europe/Andorra 1993-12-23
+3041588 Planell de l’ Allau Planell de l' Allau 42.51667 1.58333 T UPLD AD 00 0 1994 Europe/Andorra 1993-12-23
+3041589 Camà de l’ Allau Cami de l' Allau 42.58333 1.61667 R TRL AD 00 0 1707 Europe/Andorra 1993-12-23
+3041590 Roc de l’ Àliga Roc de l' Aliga 42.55 1.5 T RK AD 00 0 1292 Europe/Andorra 1993-12-23
+3041591 Roc de l’ Àliga Roc de l' Aliga 42.55 1.46667 T RK AD 00 0 1585 Europe/Andorra 1993-12-23
+3041592 Roc de l’ Àliga Roc de l' Aliga 42.46667 1.5 T RK AD 00 0 1383 Europe/Andorra 1993-12-23
+3041593 Pic de l’ Àliga Pic de l' Aliga 42.5 1.66667 T PK AD 00 0 2441 Europe/Andorra 1993-12-23
+3041594 Canal dels Alegrets Canal dels Alegrets 42.5 1.48333 H STM AD 00 0 1316 Europe/Andorra 1993-12-23
+3041595 Pont de l’ Aldosa Pont de l' Aldosa 42.55 1.51667 S BDG AD 00 0 1397 Europe/Andorra 1993-12-23
+3041596 Carretera de l’ Aldosa Carretera de l' Aldosa 42.58333 1.63333 R RD AD 00 0 1722 Europe/Andorra 1993-12-23
+3041597 Pont d’ Aixovall Pont d' Aixovall 42.48333 1.48333 S BDG AD 00 0 981 Europe/Andorra 1993-12-23
+3041598 Aixovall Aixovall Aixovall 42.46667 1.48333 P PPL AD 06 0 1134 Europe/Andorra 2011-11-05
+3041599 Solà d’ Aixirivall Sola d' Aixirivall 42.46667 1.5 T SLP AD 00 0 1383 Europe/Andorra 1993-12-23
+3041600 Riu d’ Aixirivall Riu d' Aixirivall 42.46667 1.5 H STM AD 00 0 1383 Europe/Andorra 1993-12-23
+3041601 Conreu d’ Aixirivall Conreu d' Aixirivall 42.46667 1.5 V CULT AD 00 0 1383 Europe/Andorra 1993-12-23
+3041602 Carretera d’ Aixirivall Carretera d' Aixirivall 42.45 1.48333 R RD AD 00 0 1111 Europe/Andorra 1993-12-23
+3041603 Bordes d’ Aixirivall Bordes d' Aixirivall 42.45 1.51667 S FRMS AD 00 0 1790 Europe/Andorra 1993-12-23
+3041604 Aixirivall Aixirivall Aixirivali,Aixirivall,Aixirvall,Eixirivall 42.46321 1.5029 P PPL AD 06 0 1357 Europe/Andorra 2011-11-05
+3041605 Riu Aixec Riu Aixec 42.58333 1.66667 H STM AD 00 0 2159 Europe/Andorra 1993-12-23
+3041606 Riu Aixec Riu Aixec 42.55 1.58333 H STM AD 00 0 1499 Europe/Andorra 1993-12-23
+3041607 Riu d’ Aixàs Riu d' Aixas 42.48333 1.46667 H STM AD 00 0 1148 Europe/Andorra 1993-12-23
+3041608 Bosc d’ Aixàs Bosc d' Aixas 42.48333 1.46667 V FRST AD 00 0 1148 Europe/Andorra 1993-12-23
+3041609 Aixàs Aixas 42.48333 1.46667 P PPL AD 06 0 1148 Europe/Andorra 1993-12-23
+3041610 Aixàs Aixas 42.48333 1.46667 A ADMD AD 00 0 1148 Europe/Andorra 1993-12-23
+3041611 Clots d’ Aixades Clots d' Aixades 42.58333 1.58333 H RVN AD 00 0 1993 Europe/Andorra 1993-12-23
+3041612 Coll de l’ Airola Coll de l' Airola 42.48333 1.46667 T PK AD 00 0 1148 Europe/Andorra 1993-12-23
+3041613 Bosc de l’ Airola Bosc de l' Airola 42.56667 1.56667 V FRST AD 00 0 2089 Europe/Andorra 1993-12-23
+3041614 Solà d’ Airet Sola d' Airet 42.45 1.45 T SLP AD 00 0 1482 Europe/Andorra 1993-12-23
+3041615 Bosc d’ Aigües Juntes Bosc d' Aigues Juntes 42.58333 1.46667 V FRST AD 00 0 1643 Europe/Andorra 1993-12-23
+3041616 Aigües Juntes Aigues Juntes 42.58333 1.46667 H CNFL AD 00 0 1643 Europe/Andorra 1993-12-23
+3041617 Pont de l’ Aiguerola Pont de l' Aiguerola 42.53333 1.61667 S BDG AD 00 0 2237 Europe/Andorra 1993-12-23
+3041618 Clots d’ Aigua Vella Clots d' Aigua Vella 42.6 1.63333 H RVN AD 00 0 1893 Europe/Andorra 1993-12-23
+3041619 Canal d’ Aigua Vella Canal d' Aigua Vella 42.6 1.63333 H STM AD 00 0 1893 Europe/Andorra 1993-12-23
+3041620 Basers d’ Aigua Vella Basers d' Aigua Vella 42.6 1.63333 T CLF AD 00 0 1893 Europe/Andorra 1993-12-23
+3041621 Riu d’ Aiguarebre Riu d' Aiguarebre 42.6 1.51667 H STM AD 00 0 1445 Europe/Andorra 1993-12-23
+3041622 Planell d’ Aiguarebre Planell d' Aiguarebre 42.61667 1.51667 T UPLD AD 00 0 1716 Europe/Andorra 1993-12-23
+3041623 Costa d’ Aiguarebre Costa d' Aiguarebre 42.61667 1.51667 T SLP AD 00 0 1716 Europe/Andorra 1993-12-23
+3041624 Costa dels Aiguaders Costa dels Aiguaders 42.5 1.43333 T SLP AD 00 0 1654 Europe/Andorra 1993-12-23
+3041625 Presa d’ Aigua Presa d' Aigua 42.5 1.58333 S DAM AD 00 0 1888 Europe/Andorra 1993-12-23
+3041626 Font de Les Agunes Font de Les Agunes 42.58333 1.46667 H SPNG AD 00 0 1643 Europe/Andorra 1993-12-23
+3041627 Borda de les Agunes Borda de les Agunes 42.58333 1.46667 S FRM AD 00 0 1643 Europe/Andorra 1993-12-23
+3041628 Planell de les Agudelles Planell de les Agudelles 42.56667 1.55 T UPLD AD 00 0 1996 Europe/Andorra 1993-12-23
+3041629 Pont dels Agrels Pont dels Agrels 42.55 1.48333 S BDG AD 00 0 1548 Europe/Andorra 1993-12-23
+3041630 Canal dels Agrels Canal dels Agrels 42.55 1.48333 H STM AD 00 0 1548 Europe/Andorra 1993-12-23
+3041631 Serrat de l’ Agraullet Serrat de l' Agraullet 42.53333 1.55 T RDGE AD 00 0 1344 Europe/Andorra 1993-12-23
+3041632 Riu de les Agols Riu de les Agols 42.53125 1.59204 H STM AD 00 0 1756 Europe/Andorra 2011-04-19
+3041633 Coll dels Abòs Coll dels Abos 42.61667 1.53333 T PK AD 00 0 1609 Europe/Andorra 1993-12-23
+3041634 Riu de l’ Abeurador Riu de l' Abeurador 42.58333 1.65 H STM AD 00 0 1767 Europe/Andorra 1993-12-23
+3041635 Clot de les Abelletes Clot de les Abelletes 42.53333 1.73333 T CRQ AD 00 0 2300 Europe/Andorra 1993-12-23
+3041636 Serra del Cap de l’ Abarsetar de Rialb Serra del Cap de l' Abarsetar de Rialb 42.63333 1.55 T RDGE AD 00 0 2053 Europe/Andorra 1993-12-23
+3041637 Abarsetar de Rialb Abarsetar de Rialb 42.65 1.55 L LCTY AD 00 0 2181 Europe/Andorra 1993-12-23
+3041638 Abarsetar de la Coma Abarsetar de la Coma 42.62793 1.48624 L LCTY AD 07 0 2111 Europe/Andorra 2007-03-04
+3041639 Abarsetar de Ferreroles Abarsetar de Ferreroles 42.6 1.55 L LCTY AD 00 0 2298 Europe/Andorra 1993-12-23
+3041640 Abarsetar d'ArcalÃs Abarsetar d'Arcalis 42.62657 1.50148 L LCTY AD 07 0 1979 Europe/Andorra 2007-03-04
+3041641 Clots de l’ Abarsetar Clots de l' Abarsetar 42.63333 1.55 H RVN AD 00 0 2053 Europe/Andorra 1993-12-23
+3041642 Abarsetar Abarsetar 42.55 1.65 L LCTY AD 00 0 2432 Europe/Andorra 1993-12-23
+3041643 Pla de l’ Abarsa Pla de l' Abarsa 42.43333 1.51667 T UPLD AD 00 0 2031 Europe/Andorra 1993-12-23
+3041644 Costa de l’ Abalançc Costa de l' Abalancc 42.53333 1.53333 T SLP AD 00 0 1521 Europe/Andorra 1993-12-23
+3109332 Riu d’ Ós Riu d' Os Rio Saturia,Rio Seturia,Rio de Os,Riu d' Os,Riu d’ Ós,RÃo Saturia,RÃo Seturia,RÃo de Os 42.46667 1.5 H STM AD 00 0 1383 Europe/Andorra 2011-11-06
+3338529 Parròquia d'Escaldes-Engordany Parroquia d'Escaldes-Engordany Escaldes-Engordany,Parroquia d'Escaldes-Engordany,Parròquia d'Escaldes-Engordany 42.5 1.56667 A ADM1 AD 08 16391 1776 Europe/Andorra 2008-03-17
+6463133 Eurotel Eurotel 42.51286 1.53552 S HTL AD 07 0 1139 Europe/Andorra 2011-04-02
+6463689 Panorama Hotel Panorama Hotel 42.5069 1.5361 S HTL AD 07 0 1350 Europe/Andorra 2007-04-13
+6463694 Roc Blanc Hotel Roc Blanc Hotel 42.50927 1.53862 S HTL AD 07 0 1139 Europe/Andorra 2011-04-02
+6464858 Ahotels Prisma Ahotels Prisma 42.5087 1.5364 S HTL AD 07 0 1139 Europe/Andorra 2007-04-13
+6465286 Andorra Park Hotel Andorra Park Hotel 42.5092 1.5244 S HTL AD 07 0 1097 Europe/Andorra 2007-04-13
+6473225 Coma Coma 42.559 1.533 S HTL AD 07 0 1514 Europe/Andorra 2007-04-17
+6473433 Magic La Massana Magic La Massana 42.546 1.518 S HTL AD 07 0 1397 Europe/Andorra 2007-04-17
+6473450 Art Art 42.5062 1.5216 S HTL AD 07 0 1073 Europe/Andorra 2007-04-17
+6473528 Andorra Center Andorra Center 42.5074 1.52 S HTL AD 07 0 1073 Europe/Andorra 2007-04-17
+6473529 Hotel Cervol Hotel Cervol 42.50262 1.51288 S HTL AD 07 0 1011 Europe/Andorra 2011-04-02
+6473530 Novotel Andorra Novotel Andorra 42.50762 1.52734 S HTL AD 07 0 1205 Europe/Andorra 2011-04-03
+6473617 Roc del Sola Roc del Sola 42.505 1.514 S HTL AD 07 0 1011 Europe/Andorra 2007-04-17
+6473823 L'Angel Blanc L'Angel Blanc 42.577 1.667 S HTL AD 07 0 2159 Europe/Andorra 2007-04-17
+6473970 Refugi dels Isards Refugi dels Isards 42.50616 1.5289 S HTL AD 07 0 1205 Europe/Andorra 2007-04-17
+6474116 Panorama Panorama 42.507 1.535 S HTL AD 07 0 1350 Europe/Andorra 2007-04-17
+6474117 Font d'Argent Font d'Argent 42.542 1.732 S HTL AD 0 2100 Europe/Paris 2009-09-07
+6474170 Reial Pirineus Reial Pirineus 42.5405 1.7313 S HTL AD 0 2187 Europe/Paris 2009-06-28
+6474171 Hotansa Himalaya Hotansa Himalaya 42.5405 1.7313 S HTL AD 0 2187 Europe/Paris 2009-09-07
+6474212 F and G La Cabana F and G La Cabana 42.551 1.533 S HTL AD 07 0 1340 Europe/Andorra 2007-04-17
+6474230 Pic Maia Pic Maia 42.5075 1.5218 S HTL AD 07 0 1073 Europe/Andorra 2007-04-17
+6474307 Marco Polo Marco Polo 42.5411 1.5191 S HTL AD 07 0 1343 Europe/Andorra 2007-04-17
+6474384 Confort Pas de la Casa Confort Pas de la Casa 42.5413 1.7326 S HTL AD 0 2187 Europe/Paris 2009-09-07
+6474447 Magic Pas Magic Pas 42.54303 1.73188 S HTL AD 0 2100 Europe/Paris 2011-04-02
+6474512 Font del Marge Font del Marge 42.5043 1.5146 S HTL AD 07 0 1011 Europe/Andorra 2007-04-17
+6474514 Sant Jordi Sant Jordi 42.5043 1.5146 S HTL AD 07 0 1011 Europe/Andorra 2007-04-17
+6474580 AnyósPark AnyosPark 42.53417 1.52528 S HTL AD 04 0 1491 Europe/Andorra 2011-04-02
+6474582 Alaska Alaska 42.61877 1.53922 S HTL AD 07 0 1704 Europe/Andorra 2007-04-17
+6474659 President President 42.502 1.512 S HTL AD 07 0 1011 Europe/Andorra 2007-04-17
+6474661 Himalaia Soldeu Himalaia Soldeu 42.57647 1.66873 S HTL AD 07 0 2159 Europe/Andorra 2011-04-02
+6474662 Piolets Piolets 42.5769 1.66776 S HTL AD 07 0 2159 Europe/Andorra 2007-04-17
+6475094 Servissim Servissim 42.566 1.596 S HTL AD 07 0 1677 Europe/Andorra 2007-04-17
+6475095 Artic Artic 42.5054 1.5183 S HTL AD 07 0 1073 Europe/Andorra 2007-04-17
+6475301 Magic Ski Magic Ski 42.54557 1.51746 S HTL AD 07 0 1397 Europe/Andorra 2007-04-17
+6475379 Sporting Sporting 42.5075 1.5218 S HTL AD 07 0 1073 Europe/Andorra 2007-04-17
+6483664 Hotel Apsis Florida Hotel Apsis Florida 42.50811 1.52255 S HTL AD 07 0 1073 Europe/Andorra 2007-04-15
+6483820 Hotel Apsis Art Hotel Hotel Apsis Art Hotel 42.5 1.51667 S HTL AD 07 0 1410 Europe/Andorra 2007-04-15
+6491562 Hotel Delfos Hotel Delfos 42.50682 1.5344 S HTL AD 07 0 1350 Europe/Andorra 2011-04-02
+6491674 Hotel Fénix Hotel Fenix 42.5086 1.5394 S HTL AD 07 0 1139 Europe/Andorra 2007-04-15
+6491682 Hotel Comtes d'Urgell Hotel Comtes d'Urgell 42.51028 1.54099 S HTL AD 07 0 1139 Europe/Andorra 2011-04-02
+6492312 Ahotels Princep Ahotels Princep 42.5073 1.5316 S HTL AD 07 0 1205 Europe/Andorra 2007-04-15
+6493558 Club Dorada el Tarter Club Dorada el Tarter 42.5793 1.6414 S HTL AD 07 0 1727 Europe/Andorra 2007-04-14
+6493930 Residence Deusol Residence Deusol 42.6195 1.5389 S HTL AD 07 0 1704 Europe/Andorra 2007-04-14
+6493995 HUSA Centric HUSA Centric 42.5087 1.5283 S HTL AD 07 0 1041 Europe/Andorra 2007-04-14
+6494290 HUSA Xalet Besoli HUSA Xalet Besoli 42.5718 1.4843 S HTL AD 07 0 1655 Europe/Andorra 2007-04-14
+6494345 HUSA Imperial HUSA Imperial 42.472 1.4923 S HTL AD 06 0 1164 Europe/Andorra 2010-01-12
+6494491 Hotel Marco Polo Hotel Marco Polo 42.5423 1.5174 S HTL AD 07 0 1397 Europe/Andorra 2007-04-14
+6494790 Husa Xalet Verdu Husa Xalet Verdu 42.571 1.4715 S HTL AD 07 0 1673 Europe/Andorra 2007-04-14
+6494958 Apsis Arthotel Apsis Arthotel 42.5072 1.5261 S HTL AD 07 0 1205 Europe/Andorra 2007-04-14
+6495028 Hotel Himalaia Pas Hotel Himalaia Pas 42.5403 1.7311 S HTL AD 0 2187 Europe/Paris 2009-07-01
+6495053 Hotel Font Hotel Font 42.5436 1.5117 S HTL AD 07 0 1257 Europe/Andorra 2007-04-14
+6495582 Hotel Austria Hotel Austria 42.5788 1.6686 S HTL AD 07 0 2159 Europe/Andorra 2007-04-14
+6495704 Hotel Magic Canillo Hotel Magic Canillo 42.5638 1.5965 S HTL AD 07 0 1677 Europe/Andorra 2007-04-14
+6495769 Hotel F&G La Cabana Hotel F&G La Cabana 42.5538 1.5319 S HTL AD 07 0 1340 Europe/Andorra 2007-04-14
+6495790 Hotel Princesa Parc Hotel Princesa Parc 42.574 1.4824 S HTL AD 07 0 1508 Europe/Andorra 2007-04-14
+6495826 Hotel Magic La Massana Hotel Magic La Massana 42.5448 1.5163 S HTL AD 07 0 1257 Europe/Andorra 2007-04-14
+6497139 AHotels Confort Patagonia AHotels Confort Patagonia 42.5627 1.4952 S HTL AD 07 0 1430 Europe/Andorra 2007-04-14
+6497391 Hotel Magic Andorra Hotel Magic Andorra 42.5091 1.5297 S HTL AD 07 0 1041 Europe/Andorra 2007-04-14
+6497888 Ahotels Piolets Park & Spa Ahotels Piolets Park & Spa 42.5772 1.6652 S HTL AD 07 0 1925 Europe/Andorra 2007-04-14
+6498453 Hotel Carlton Plaza Hotel Carlton Plaza 42.508 1.5254 S HTL AD 07 0 1205 Europe/Andorra 2007-04-14
+6498890 Hotel Plaza Hotel Plaza 42.5062 1.5296 S HTL AD 07 0 1205 Europe/Andorra 2007-04-14
+6499190 Hotel Ski Plaza Hotel Ski Plaza 42.5657 1.5973 S HTL AD 07 0 1677 Europe/Andorra 2007-04-14
+6500087 Hesperia Andorra la Vella Hesperia Andorra la Vella 42.5089 1.5289 S HTL AD 07 0 1041 Europe/Andorra 2007-04-14
+6501396 Ahotels El Serrat Ahotels El Serrat 42.61652 1.53833 S HTL AD 07 0 1793 Europe/Andorra 2009-06-25
+6502212 Hotel Euro Esqui Hotel Euro Esqui 42.5794 1.6585 S HTL AD 07 0 1925 Europe/Andorra 2007-04-14
+6504216 Abba Xalet Suites Abba Xalet Suites 42.5331 1.516 S HTL AD 07 0 1211 Europe/Andorra 2007-04-14
+6505101 Hotel Valira HOTEL VALIRA 42.5 1.5333 S HTL AD 07 0 1574 Europe/Andorra 2007-04-13
+6508304 Mercure Andorra 4 MERCURE ANDORRA 4 42.5 1.5166 S HTL AD 07 0 1230 Europe/Andorra 2007-04-13
+6520395 Ahotels Confort Patagonia AHOTELS CONFORT PATAGONIA 42.5124 1.5389 S HTL AD 07 0 1139 Europe/Andorra 2007-04-15
+6521569 Ahotels El Serrat AHOTELS EL SERRAT 42.5575 1.5325 S HTL AD 07 0 1340 Europe/Andorra 2007-04-15
+6522685 Salvia D Or Hotel SALVIA D OR HOTEL 42.5 1.5166 S HTL AD 07 0 1230 Europe/Andorra 2007-04-14
+6525900 Roc De Caldes ROC DE CALDES 42.51065 1.54706 S HTL AD 07 0 1227 Europe/Andorra 2011-04-03
+6529214 Metropolis METROPOLIS 42.5166 1.55 S HTL AD 07 0 1498 Europe/Andorra 2007-04-14
+6529355 Diplomatic DIPLOMATIC 42.50521 1.52715 S HTL AD 07 0 1205 Europe/Andorra 2011-04-02
+6529413 Eureka EUREKA 42.50873 1.53989 S HTL AD 07 0 1139 Europe/Andorra 2011-04-02
+6529930 Cassany CASSANY 42.50798 1.5248 S HTL AD 07 0 1073 Europe/Andorra 2011-04-02
+6942550 Andorra Palace ANDORRA PALACE 42.50837 1.52706 S HTL AD 07 0 1041 Europe/Andorra 2009-06-24
+6942551 Annapurna ANNAPURNA 42.55768 1.53255 S HTL AD 07 0 1340 Europe/Andorra 2009-06-24
+6942562 Bringue BRINGUE 42.55768 1.53255 S HTL AD 07 0 1340 Europe/Andorra 2009-06-25
+6942564 Co Princeps CO PRINCEPS 42.46815 1.49329 S HTL AD 06 0 1164 Europe/Andorra 2010-01-12
+6942568 Crowne Plaza Andorra CROWNE PLAZA ANDORRA 42.50574 1.52002 S HTL AD 07 0 1073 Europe/Andorra 2009-06-25
+6942569 Diana Parc Diana Parc 42.5444 1.5115 S HTL AD 07 0 1257 Europe/Andorra 2009-06-25
+6942578 Encamp Encamp 42.53321 1.57914 S HTL AD 0 1571 Europe/Andorra 2009-06-25
+6942579 Diana parc Diana parc 42.61886 1.53939 S HTL AD 07 0 1704 Europe/Andorra 2009-06-25
+6942582 Font d'argent Canillo Font d'argent Canillo 42.57086 1.60782 S HTL AD 0 1655 Europe/Andorra 2011-04-03
+6942589 GRAU ROIG GRAU ROIG 42.5327 1.70116 S HTL AD 0 2357 Europe/Andorra 2009-06-25
+6942604 Husa Patagonia Husa Patagonia 42.57139 1.48566 S HTL AD 07 0 1655 Europe/Andorra 2009-06-26
+6942646 Ibis Andorra Ibis Andorra 42.51252 1.55059 S HTL AD 07 0 1498 Europe/Andorra 2011-04-03
+6942647 Lake placid Lake placid 42.54119 1.73333 S HTL AD 0 2187 Europe/Paris 2009-06-28
+6942648 Les closes Les closes 42.5087 1.53993 S HTL AD 07 0 1139 Europe/Andorra 2009-06-28
+6942649 L Isard L Isard 42.50775 1.52288 S HTL AD 07 0 1073 Europe/Andorra 2009-06-28
+6942650 Paris Paris 42.528 1.56927 S HTL AD 0 1418 Europe/Andorra 2009-06-28
+6942651 Pere D Urg Pere D Urg 42.5347 1.58012 S HTL AD 0 1309 Europe/Andorra 2009-06-28
+6942667 Prat de les mines Prat de les mines 42.59406 1.52684 S HTL AD 07 0 1695 Europe/Andorra 2009-06-28
+6942674 Rialb Rialb 42.61886 1.53939 S HTL AD 07 0 1704 Europe/Andorra 2009-06-28
+6942675 Rutllan Rutllan 42.54738 1.51345 S HTL AD 04 0 1257 Europe/Andorra 2009-06-28
+6942676 Sant Eloi Sant Eloi 42.45793 1.48717 S HTL AD 0 1159 Europe/Madrid 2011-04-02
+6942677 Shusski Shusski 42.5338 1.58577 S HTL AD 0 1490 Europe/Andorra 2009-06-28
+6942678 Solana Solana 42.58099 1.5193 S HTL AD 0 1722 Europe/Andorra 2009-06-28
+6942680 Somriu Comapedrosa Somriu Comapedrosa 42.58099 1.5193 S HTL AD 0 1722 Europe/Andorra 2009-06-28
+6942681 Somriu Galanthus Somriu Galanthus 42.5769 1.66776 S HTL AD 07 0 2159 Europe/Andorra 2009-06-28
+6942682 Les Terres Les Terres 42.57916 1.63023 S HTL AD 0 1722 Europe/Andorra 2011-04-02
+6942683 Somriu Segle Xx SOMRIU SEGLE XX 42.58118 1.63827 S HTL AD 0 1727 Europe/Andorra 2009-09-08
+6942684 Somriu Solana Del Ransol SOMRIU SOLANA DEL RANSOL 42.58002 1.64432 S HTL AD 07 0 1737 Europe/Andorra 2009-06-28
+6942685 Somriu Tivoli SOMRIU TIVOLI 42.5076 1.53223 S HTL AD 07 0 1205 Europe/Andorra 2009-06-28
+6942686 Somriu Vall Ski Somriu Vall Ski 42.57723 1.66752 S HTL AD 07 0 2159 Europe/Andorra 2009-06-28
+6942696 Sport Sport 42.57688 1.66688 S HTL AD 07 0 2159 Europe/Andorra 2009-06-29
+6942697 Sport Hermitage SPORT HERMITAGE 42.57727 1.6672 S HTL AD 07 0 2159 Europe/Andorra 2009-06-29
+6942698 Sport Village Sport Village 42.57688 1.66688 S HTL AD 07 0 2159 Europe/Andorra 2009-06-29
+6942740 APARTAMENTS TURISTICS GLAÇ APARTAMENTS TURISTICS GLAC 42.58 1.64623 S HTL AD 07 0 1737 Europe/Andorra 2009-06-30
+6942759 Aparthotel Cosmos Aparthotel Cosmos 42.51 1.541 S HTL AD 07 0 1139 Europe/Andorra 2009-06-30
+6942761 Carlemany Carlemany 42.509 1.538 S HTL AD 07 0 1139 Europe/Andorra 2009-06-30
+6942762 Cims Andorra Cims Andorra 42.5085 1.5307 S HTL AD 07 0 1041 Europe/Andorra 2009-06-30
+6942764 Folch Folch 42.471 1.493 S HTL AD 06 0 1164 Europe/Andorra 2010-01-12
+6942766 Hotel La Solana Hotel La Solana 42.533 1.593 S HTL AD 07 0 1756 Europe/Andorra 2009-09-07
+6942767 Phoebus Phoebus 42.54103 1.72985 S HTL AD 0 2187 Europe/Paris 2009-06-30
+6942768 Pol Pol 42.465 1.491 S HTL AD 06 0 1045 Europe/Andorra 2009-06-30
+6942770 Subira Subira 42.632 1.5 S HTL AD 0 1979 Europe/Andorra 2009-06-30
+6942771 Vip Plus Vip Plus 42.535 1.588 S HTL AD 0 1490 Europe/Andorra 2009-06-30
+6942793 Acta Arthotel Acta Arthotel 42.50728 1.52609 S HTL AD 07 0 1205 Europe/Andorra 2009-07-01
+6942794 Ahotels Patagonia Austral Ahotels Patagonia Austral 42.57246 1.48436 S HTL AD 07 0 1655 Europe/Andorra 2009-07-01
+6942796 Del Tarter Del Tarter 42.58035 1.64893 S HTL AD 07 0 1737 Europe/Andorra 2009-07-01
+6942797 Hotel Llop Gris Hotel Llop Gris 42.57804 1.64845 S HTL AD 07 0 1737 Europe/Andorra 2009-07-01
+6948955 Apartamentos Xixerella APARTAMENTOS XIXERELLA 42.55309 1.48746 S HTL AD 04 0 1520 Europe/Andorra 2009-09-07
+6948956 Apartaments Gla APARTAMENTS GLA 42.56 1.67396 S HTL AD 0 1963 Europe/Andorra 2009-09-07
+6948957 Boston Boston 42.6083 1.5394 S HTL AD 07 0 1821 Europe/Andorra 2009-09-07
+6948958 Coma Bella COMA BELLA 42.46487 1.49115 S HTL AD 06 0 1045 Europe/Andorra 2009-09-07
+6948959 Confort Soldeu CONFORT SOLDEU 42.5769 1.66776 S HTL AD 07 0 2159 Europe/Andorra 2009-09-07
+6948960 Coray CORAY 42.535 1.584 S HTL AD 0 1490 Europe/Andorra 2009-09-07
+6948963 Del Clos Del Clos 42.6187 1.5392 S HTL AD 07 0 1704 Europe/Andorra 2009-09-07
+6948965 El Xalet EL XALET 42.58035 1.64892 S HTL AD 07 0 1737 Europe/Andorra 2009-09-07
+6948966 Els Llacs ELS LLACS 42.56012 1.68173 S HTL AD 0 2094 Europe/Andorra 2009-09-07
+6948967 Els Meners ELS MENERS 42.566 1.596 S HTL AD 07 0 1677 Europe/Andorra 2009-09-07
+6948968 Espel Espel 42.509 1.542 S HTL AD 07 0 1227 Europe/Andorra 2009-09-07
+6948969 Euro Ski Euro Ski 42.6187 1.5392 S HTL AD 07 0 1704 Europe/Andorra 2009-09-07
+6948970 Guillen Guillen 42.536 1.583 S HTL AD 0 1309 Europe/Andorra 2009-09-07
+6948971 Hotansa Austria HOTANSA AUSTRIA 42.56012 1.68173 S HTL AD 0 2094 Europe/Andorra 2009-09-07
+6948972 Iglu IGLU 42.53263 1.70035 S HTL AD 0 2357 Europe/Andorra 2009-09-07
+6948973 Kandahar KANDAHAR 42.5413 1.7325 S HTL AD 0 2187 Europe/Paris 2009-09-07
+6948974 L Obaga Blanca L OBAGA BLANCA 42.5617 1.60352 S HTL AD 0 1969 Europe/Andorra 2009-09-07
+6948975 Montecarlo MONTECARLO 42.528 1.569 S HTL AD 0 1418 Europe/Andorra 2009-09-07
+6948976 Naudi Naudi 42.6187 1.5392 S HTL AD 07 0 1704 Europe/Andorra 2009-09-07
+6948977 Nordic NORDIC 42.58035 1.64892 S HTL AD 07 0 1737 Europe/Andorra 2009-09-07
+6948991 Hotel Oros Hotel Oros 42.54564 1.73176 S HTL AD 0 2100 Europe/Paris 2011-04-03
+6948993 Palome Palome 42.56148 1.49741 S HTL AD 07 0 1430 Europe/Andorra 2009-09-08
+6948995 Paradis Blanc PARADIS BLANC 42.54152 1.73378 S HTL AD 0 2385 Europe/Paris 2009-09-08
+6948996 Rutllan Xalet De Muntanya RUTLLAN XALET DE MUNTANYA 42.5479 1.51319 S HTL AD 04 0 1257 Europe/Andorra 2009-09-08
+6948997 Sant Bernat Apartments SANT BERNAT APARTMENTS 42.56609 1.5967 S HTL AD 07 0 1677 Europe/Andorra 2009-09-08
+6948998 Solana de ransol Solana de ransol 42.57831 1.6352 S HTL AD 0 1727 Europe/Andorra 2009-09-08
+6948999 Soldeu Maistre Soldeu Maistre 42.5769 1.66776 S HTL AD 07 0 2159 Europe/Andorra 2009-09-08
+6949002 St Gothard ST GOTHARD 42.5723 1.484 S HTL AD 07 0 1655 Europe/Andorra 2009-09-08
+6949004 Univers Univers 42.53647 1.58195 S HTL AD 0 1309 Europe/Andorra 2009-09-08
+6949005 Velvet Velvet 42.7467 1.7081 S HTL AD 0 1563 Europe/Paris 2009-09-08
+6949010 Xalet Montana Xalet Montana 42.57701 1.66627 S HTL AD 07 0 1925 Europe/Andorra 2009-09-08
+7114070 andorra magica andorra magica 42.50727 1.52202 S HTL AD 07 0 1073 Europe/Andorra 2009-12-02
+7284857 Apartamentos L Angel Blanc Apartamentos L Angel Blanc 42.577 1.667 S HTL AD 07 0 2159 Europe/Andorra 2010-04-01
+7284858 Apartamentos Giberga Apartamentos Giberga 42.552 1.51081 S HTL AD 0 1400 Europe/Andorra 2010-04-01
+7284859 Aparthotel l'Alba Aparthotel l'Alba 42.5787 1.6532 S HTL AD 02 0 1767 Europe/Andorra 2010-04-01
+7284866 Magic Canillo Apartments Magic Canillo Apartments 42.5669 1.60041 S HTL AD 02 0 1655 Europe/Andorra 2010-04-01
+7284867 Apartaments Sant Romà Apartaments Sant Roma 42.5736 1.48311 S HTL AD 0 1508 Europe/Andorra 2010-04-01
+7284869 Saporo Saporo 42.5432 1.73327 S HTL AD 03 0 2100 Europe/Paris 2010-04-01
+7284872 Aparthotel Casa Vella Aparthotel Casa Vella 42.5537 1.53215 S HTL AD 0 1340 Europe/Andorra 2010-04-01
+7284874 Caldea Centre Termolùdic d' Andorra Caldea Centre Termoludic d' Andorra 42.5117 1.5367 S HTL AD 07 0 1139 Europe/Andorra 2010-04-01
+7284875 Comabella Hotel Comabella Hotel 42.5 1.5166 S HTL AD 07 0 1230 Europe/Andorra 2010-04-01
+7284915 Hotel Galanthus Hotel Galanthus 42.5829 1.66268 S HTL AD 02 0 1925 Europe/Andorra 2010-04-02
+7284920 Hotel Cims Pas Hotel Cims Pas 42.5428 1.73479 S HTL AD 03 0 2230 Europe/Paris 2010-04-02
+7284921 Hotel Cims Pas de La Casa Hotel Cims Pas de La Casa 42.5428 1.73479 S HTL AD 03 0 2230 Europe/Paris 2010-04-02
+7284925 Hotel del Clos Hotel del Clos 42.5786 1.65121 S HTL AD 02 0 1767 Europe/Andorra 2010-04-02
+7284926 Hotel Erts Hotel Erts 42.5454 1.51909 S HTL AD 07 0 1397 Europe/Andorra 2010-04-02
+7284927 Hotel Font De Ferro Hotel Font De Ferro 42.5902 1.52436 S HTL AD 0 1525 Europe/Andorra 2010-04-02
+7284952 Magic Ski La Massana Hotel Magic Ski La Massana Hotel 42.5457 1.51838 S HTL AD 07 0 1397 Europe/Andorra 2010-04-04
+7284953 Hotel Metropolis Hotel Metropolis 42.5102 1.54086 S HTL AD 07 0 1139 Europe/Andorra 2010-04-04
+7284954 Hotel Palarine Hotel Palarine 42.5763 1.5187 S HTL AD 0 1722 Europe/Andorra 2010-04-04
+7284955 Hotel Paris Londres Hotel Paris Londres 42.5087 1.54083 S HTL AD 07 0 1139 Europe/Andorra 2010-04-04
+7284956 Hotel Parma Hotel Parma 42.5432 1.73297 S HTL AD 03 0 2100 Europe/Paris 2010-04-04
+7284957 Residència Daina Residencia Daina 42.5615 1.49775 S HTL AD 0 1430 Europe/Andorra 2010-04-04
+7284958 Hotel Roc del Castell Hotel Roc del Castell 42.566 1.596 S HTL AD 07 0 1677 Europe/Andorra 2010-04-04
+7284959 Hotel Siracusa Hotel Siracusa 42.509 1.54305 S HTL AD 07 0 1227 Europe/Andorra 2010-04-04
+7284960 Somriu Hotel Refugi dels Isards Somriu Hotel Refugi dels Isards 42.5433 1.73494 S HTL AD 03 0 2230 Europe/Paris 2010-04-04
+7284961 Sport Hotel Hermitage & Spa Sport Hotel Hermitage & Spa 42.5763 1.66991 S HTL AD 07 0 2159 Europe/Andorra 2010-04-04
+7284962 Hotel Tristaina Hotel Tristaina 42.6189 1.53714 S HTL AD 0 1704 Europe/Andorra 2010-04-04
+7287697 Hotel Montane Hotel Montane 42.5454 1.51909 S HTL AD 07 0 1397 Europe/Andorra 2010-04-06
+7287698 Hotel Pitiusa Hotel Pitiusa 42.5089 1.53258 S HTL AD 07 0 1041 Europe/Andorra 2010-04-06
+7287699 Hotel PrÃncep Hotel Princep 42.5087 1.54083 S HTL AD 07 0 1139 Europe/Andorra 2010-04-06
+7287700 Hotel I Termes Carlemany Hotel I Termes Carlemany 42.5092 1.54409 S HTL AD 07 0 1227 Europe/Andorra 2010-04-06
+7287701 Hotel Viena Hotel Viena 42.5069 1.52004 S HTL AD 07 0 1073 Europe/Andorra 2010-04-06
+7302102 La Margineda La Margineda 42.484 1.49242 P PPL AD 07 0 1353 Europe/Andorra 2010-05-26
+7730819 Andorra la Vella Heliport Andorra la Vella Heliport ALV 42.5005 1.51712 S AIRH AD 0 1073 Europe/Andorra 2011-03-17
+7733010 Grau Roig Grau Roig 42.53251 1.69923 P PPL AD 0 2204 Europe/Andorra 2011-04-07
+251130 WÄdÄ« Siqattah Wadi Siqattah Wadi Siqatta,Wadi Siqattah,WÄdÄ« Siqatta,WÄdÄ« Siqattah 25.6225 56.2225 H WAD AE 00 0 99 Asia/Dubai 2011-11-06
+286280 Suhaylah Suhaylah Suhaylah,Suheila 24.80388 56.19449 P PPL AE 02 0 238 Asia/Dubai 2011-11-06
+288716 WÄdÄ« al Bīḩ Wadi al Bih Wadi al Bih,WÄdÄ« al Bīḩ 25.77361 56.04389 H WAD AE 05 0 49 Asia/Dubai 2011-11-06
+290399 Zuyūd Zuyud Zuyud,Zuyūd 25.2 56.21667 L TRB AE 04 0 223 Asia/Dubai 2011-11-06
+290400 Z̧uwayhir Zuwayhir Dhawaihir,Duwaihir,Zuwayhir,Z̧uwayhir 23.28333 53.2 P PPL AE AE 01 0 170 Asia/Dubai 2011-11-06
+290401 Z̧uwayhir Zuwayhir Dhuwaiher,Zuwayhir,Zuweihir,Z̧uwayhir 23.13916 53.6934 P PPL AE 01 0 104 Asia/Dubai 2011-11-06
+290402 Zuwayghir Zuwayghir Zuwaighar,Zuwayghir 24.08333 55.26667 H WLL AE AE 01 0 167 Asia/Dubai 2011-11-06
+290403 WÄdÄ« Zuqaybah Wadi Zuqaybah Wadi Zuqaybah,WÄdÄ« Zuqaybah 25.40753 56.12592 H WAD AE 04 0 383 Asia/Dubai 2011-11-06
+290404 Ţawī Z̧ulaymah Tawi Zulaymah Dhalaima,Dulaima,Tawi Dhalaima,Tawi Dhelaimah,Tawi Dulaymah,Tawi Zeleimah,Tawi Zulaymah,Ţawī Dhalaima,Ţawī Dulaymah,Ţawī Z̧ulaymah 24.67083 55.53083 H WLLQ AE AE 01 0 208 Asia/Dubai 2011-11-06
+290405 Z̧ulaymah Zulaymah Dulaymah,Zulaymah,Z̧ulaymah 24.65 55.53333 T TRGD AE AE 03 0 230 Asia/Dubai 2011-11-06
+290406 Ruqq az Zukum Ruqq az Zukum Rak al Lakum,Rak az Zakum,Rig az Zakum,Ruqq az Zaqqum,Ruqq az Zaqqūm,Ruqq az Zukum 24.8 53.7 H SHOL AE AE 01 0 -9999 Asia/Dubai 2011-11-06
+290407 Zubyah Zubyah Zabia,Zubyah 24.96667 55.06667 T SAND AE AE 03 0 20 Asia/Dubai 2011-11-06
+290408 Jabal al ‘Azab Jabal al `Azab Jabal Azab,Jabal Zubb al `Azab,Jabal Zubb al ‘Azab,Jabal al `Azab,Jabal al ‘Azab 25.16139 55.83861 T DUNE AE 06 0 275 Asia/Dubai 2011-11-06
+290409 ZubÄrah Zubarah Zubara,Zubarah,ZubÄra,ZubÄrah 25.40487 56.35971 P PPL AE 04 0 -9999 Asia/Dubai 2011-11-06
+290410 ZirkÅ«h Zirkuh Az Zarqa',Az ZarqÄ’,Jazirat Zarka,Jazirat Zirku,Jaztal Zarakkuh,Jaztal ZarakkÅ«h,JazÄ«rat ZarkÄ,JazÄ«rat ZÄ«rkÅ«,Jezirat Zirko,JezÄ«rat Zirko,Zarakkawh,Zarakkuh,Zarqa,Zirko Island,Zirkuh,ZirkÅ«h 24.88417 53.07222 T ISL AE AE 01 0 161 Asia/Dubai 2011-11-06
+290411 Zirđah Zira`ah Zira`ah,Zirđah 24.06667 55.53333 T SAND AE 01 0 182 Asia/Dubai 2011-11-06
+290412 Zirđ Zira` Zira`,Zirđ 23.76822 54.20993 H WLL AE 01 0 116 Asia/Dubai 2011-11-06
+290413 Sabkhat ZinÄd Sabkhat Zinad Sabkhat Zinad,Sabkhat ZinÄd 24.48293 55.22135 H SBKH AE 01 0 145 Asia/Dubai 2011-11-06
+290414 ZinÄd Zinad Zinad,ZinÄd 24.5 55.23333 T SAND AE 01 0 151 Asia/Dubai 2011-11-06
+290415 Zimmat Ḩalamah Zimmat Halamah Zimmat Halamah,Zimmat Ḩalamah 24.13333 55.48333 T DUNE AE 01 0 187 Asia/Dubai 2011-11-06
+290416 Zimmat ‘Ankah Zimmat `Ankah Zimmat `Ankah,Zimmat ‘Ankah 24.16667 55.33333 T DUNE AE 01 0 173 Asia/Dubai 2011-11-06
+290417 WÄdÄ« Zikt Wadi Zikt Wadi Zikt,WÄdÄ« Zikt 25.52536 56.31864 H WAD AE 04 0 140 Asia/Dubai 2011-11-06
+290418 Jabal Zikt Jabal Zikt Jabal Zikt 25.51435 56.31373 T HLL AE 04 0 285 Asia/Dubai 2011-11-06
+290419 Zikt Zikt Zikt 25.5118 56.32328 P PPL AE 04 0 41 Asia/Dubai 2011-11-06
+290420 Ziffah Ziffah Ziffah 25.02385 56.28954 V CULT AE 04 0 99 Asia/Dubai 2011-11-06
+290421 Zidm Zidm Zidm 25.48333 56.15 S RUIN AE 04 0 507 Asia/Dubai 2011-11-06
+290422 Zibara Zibara Zibara,Zibarah,ZibÄrah 24.61889 54.63417 T DUNE AE 01 0 11 Asia/Dubai 2011-11-06
+290423 Zi‘Äb Zi`ab Za`ab,Za‘Äb,Zi`ab,Zi‘Äb 25.03333 56.36667 L TRB AE 06 0 -9999 Asia/Dubai 2011-11-06
+290424 ZayqÄt Zayqat Zaiqat,Zayqat,ZayqÄt 23.36667 52.15 S OILW AE AE 01 0 52 Asia/Dubai 2011-11-06
+290425 ZayqÄt Zayqat Zayqat,ZayqÄt 23.35 52.15 S CMPQ AE 01 0 53 Asia/Dubai 2011-11-06
+290426 MÄ«nÄ’ ZÄyid Mina' Zayid Mina Zayed,Mina' Zayid,MÄ«nÄ’ ZÄyid,Port Zayed,mynaʾ zayd,ميناء زايد 24.52518 54.38651 L PRT AE 01 0 -9999 Asia/Dubai 2011-11-06
+290427 Ramlat Zayd Ramlat Zayd Ramlat Zaid,Ramlat Zayd 24.21667 55.2 T DUNE AE AE 01 0 150 Asia/Dubai 2011-11-06
+290428 Bi’r Zayd Bi'r Zayd Bada` Zaid,Bada‘ Zaid,Bi'r Zayd,Bir Zaid,Bi’r Zayd 24.2 55.35 H WLL AE 01 0 168 Asia/Dubai 2011-11-06
+290429 Bid‘ Zayd Bid` Zayd Bada' Zaid,Bada’ Zaid,Bid` Zayd,Bid‘ Zayd 24.21667 55.11667 H WLL AE 01 0 118 Asia/Dubai 2011-11-06
+290430 Khawr Zawrah Khawr Zawrah Khawr Zawrah,Khawr Zora,Khawr az Zawra,Khawr az ZawrÄ 25.44417 55.47944 H INLT AE AE 02 0 -9999 Asia/Dubai 2011-11-06
+290431 JazÄ«rat Zawrah Jazirat Zawrah Al-Zura,Al-ZÅ«rÄ,Jazirat Zawrah,JazÄ«rat Zawrah,Zora Island 25.43778 55.46472 T ISL AE AE 00 0 8 Asia/Dubai 2011-11-06
+290432 Zawr Zawr Zawr 25.5275 55.59667 L LCTY AE 07 0 19 Asia/Dubai 2011-11-06
+290433 Z̧awÄhir Zawahir Dhawahir,DhawÄhir,Dhuwahir,Zawahir,Z̧awÄhir 24.33333 55.58333 L TRB AE AE 01 0 208 Asia/Dubai 2011-11-06
+290434 ZÄrÅ«b Zarub Zarub,ZarÅ«b,ZÄrÅ«b 25.01806 56.21 V CULT AE AE 06 0 406 Asia/Dubai 2011-11-06
+290435 ZarqÄ’ Zarqa' Zarqa',ZarqÄ’ 25.34622 55.87143 L LCTY AE 07 0 136 Asia/Dubai 2011-11-06
+290436 ZarÄrah Zararah Zararah,Zarrara,Zarrarah,Zarta,ZarÄrah 22.66667 54.13333 L OILF AE AE 01 0 145 Asia/Dubai 2011-11-06
+290437 ZarÄrah Zararah Zararah,ZarÄrah 22.87043 53.83424 T DPR AE 01 0 51 Asia/Dubai 2011-11-06
+290438 ZarÄf Zaraf Zaraf,ZarÄf 23.79064 54.21261 H WLL AE 01 0 84 Asia/Dubai 2011-11-06
+290439 Qarn Zaqīq Qarn Zaqiq Qarn Bu Naidar,Qarn Zaqiq,Qarn Zaqīq,Qarn al Khabta 24.31463 52.59967 T HLL AE 01 0 129 Asia/Dubai 2011-11-06
+290440 Jabal az̧ Z̧annah Jabal az Zannah Az Zannah,Az̧ Z̧annah,Djebel Dhanna,Jabal Danna,Jabal Dhannah,Jabal Dhanni,Jabal az Zannah,Jabal az̧ Z̧annah 24.1709 52.59488 T HLL AE 01 0 111 Asia/Dubai 2011-11-06
+290441 Dawḩat az̧ Z̧annah Dawhat az Zannah Dawhat az Zannah,Dawḩat az̧ Z̧annah 24.15252 52.71794 H BGHT AE 01 0 -9999 Asia/Dubai 2011-11-06
+290442 Z̧anḩah Zanhah Ghob,Zanhah,Z̧anḩah 25.5659 56.20217 P PPL AE 04 0 93 Asia/Dubai 2011-11-06
+290443 Ţawī Za‘lah Tawi Za`lah Tawi Za`lah,Ţawī Za‘lah 24.34848 55.43925 H WLLQ AE 01 0 178 Asia/Dubai 2011-11-06
+290444 Sayḩ Za‘lah Sayh Za`lah Sayh Za`lah,Sayḩ Za‘lah 24.33804 55.4268 T TRGD AE 01 0 179 Asia/Dubai 2011-11-06
+290445 Sayḩ Za‘lah Sayh Za`lah Sayh Za`lah,Sayḩ Za‘lah 24.2894 55.41804 T TRGD AE 01 0 175 Asia/Dubai 2011-11-06
+290446 Sayḩ Za‘lah Sayh Za`lah Sayh Za`lah,Sayḩ Za‘lah 24.28333 55.41667 T TRGD AE 01 0 163 Asia/Dubai 2011-11-06
+290447 ZÄkhir Zakhir Zakhir,ZÄkhir 24.11667 55.68333 S HSE AE 01 0 246 Asia/Dubai 2011-11-06
+290448 Ţawī ZĒid Tawi Za'id Tawi Za'id,Ţawī ZĒid 25.59556 55.88444 H WLL AE 07 0 35 Asia/Dubai 2011-11-06
+290449 Z̧ahūriyīn Zahuriyin Zahuriyin,Z̧ahūriyīn 26.05 56.13333 L TRB AE 05 0 680 Asia/Dubai 2011-11-06
+290450 Zaḩūm Zahum Zahum,Zaḩūm,Zihum 25.25 56.06667 L TRB AE AE 00 0 285 Asia/Dubai 2011-11-06
+290451 ZahrÄnÄ« Zahrani Zahrani,ZahrÄnÄ« 23.21667 54.18333 H WLL AE 01 0 116 Asia/Dubai 2011-11-06
+290452 Bū Ţabr Bu Tabr Bu Tabr,Bū Ţabr,Dahar,Dhahar,Zahr,Z̧ahr 24.35944 52.60583 S FRM AE AE 01 0 -9999 Asia/Dubai 2011-11-06
+290453 WÄdÄ« Z̧aḩah Wadi Zahah Wadi Zahah,WÄdÄ« Z̧aḩah 25.57036 56.20654 H WAD AE 04 0 76 Asia/Dubai 2011-11-06
+290454 Z̧afÄ«r Zafir Dafir,Dhafir,DhafÄ«r,Zafir,Z̧afÄ«r,á¸Äfir 23.12732 53.75439 P PPL AE 01 0 198 Asia/Dubai 2011-11-06
+290455 Z̧afīr Zafir Zafir,Z̧afīr 23.12027 53.75554 T DPR AE 01 0 82 Asia/Dubai 2011-11-06
+290456 Qarn Zabut Qarn Zabut Qarn Zabut 23.99048 54.07328 T HLL AE 01 0 20 Asia/Dubai 2011-11-06
+290457 Za‘bīl Za`bil Za`bil,Za‘bīl 25.22402 55.30452 S PAL AE 03 0 33 Asia/Dubai 2011-11-06
+290458 Å¢awÄ« Za‘ÄbÄ«yah Tawi Za`abiyah Tawi Za`abiyah,Å¢awÄ« Za‘ÄbÄ«yah 25.24917 55.88472 H WLL AE 06 0 128 Asia/Dubai 2011-11-06
+290459 Za‘Äb Za`ab Za`ab,Za‘Äb 25.68751 55.84732 L TRB AE 07 0 16 Asia/Dubai 2011-11-06
+290460 Ţawī Yudayyah Tawi Yudayyah Bir Yidayah,Tawi Yudayyah,Yidaiya,Ţawī Yudayyah 24.88904 55.78333 H WLL AE 06 0 203 Asia/Dubai 2011-11-06
+290461 Jabal Yīs Jabal Yis Jabal Yis,Jabal Yīs 25.35781 56.1109 T MT AE 04 0 683 Asia/Dubai 2011-11-06
+290462 Yinas Yinas Yinas 25.73158 56.13405 P PPL AE 05 0 1079 Asia/Dubai 2011-11-06
+290463 Yilak Yilak Yilak 23.06607 53.67436 T DPR AE 01 0 75 Asia/Dubai 2011-11-06
+290464 Yilaiyis Yilaiyis Yilaiyis 23.86184 55.43317 T DUNE AE 01 0 152 Asia/Dubai 2011-11-06
+290465 WÄdÄ« Yifan Wadi Yifan Wadi Yifan,WÄdÄ« Yifan 25.06066 56.30444 H WAD AE 04 0 38 Asia/Dubai 2011-11-06
+290466 Jabal Yibir Jabal Yibir Jabal Yibir 25.66752 56.13572 T MT AE 05 0 1527 1485 Asia/Dubai 2011-02-09
+290467 YÄsÄt ÅžaghÄ«rah Yasat Saghirah Yasat Saghirah,YÄsÄt ÅžaghÄ«rah 24.15912 52.0007 T ISL AE 01 0 6 Asia/Dubai 2011-11-06
+290468 Al YÄsÄt as Suflá Al Yasat as Sufla Al Yasat as Sufla,Al YÄsÄt as Suflá,Yasat Safli,YÄsÄt SaflÄ« 24.18675 51.9948 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290469 Al YÄsÄt al ‘UlyÄ Al Yasat al `Ulya Al Yasat al `Ulya,Al YÄsÄt al ‘UlyÄ,Yasat `Ali,YÄsÄt ‘AlÄ« 24.23662 52.01234 T ISL AE 01 0 10 Asia/Dubai 2011-11-06
+290470 Yarīrah Yarirah Yarirah,Yarīrah 22.8707 54.22656 T DPR AE 01 0 94 Asia/Dubai 2011-11-06
+290471 WÄdÄ« YamÄn Wadi Yaman Wadi Yaman,WÄdÄ« YamÄn 25.38499 56.32647 H WAD AE 06 0 53 Asia/Dubai 2011-11-06
+290472 Å¢awÄ« YÄl Tawi Yal Tawi Yal,Å¢awÄ« YÄl 25.42336 56.10511 H WLL AE 04 0 302 Asia/Dubai 2011-11-06
+290473 YÄkhÅ«n Yakhun Yakhun,YÄkhÅ«n 24.88795 55.72161 T DUNE AE 06 0 210 Asia/Dubai 2011-11-06
+290474 Ya‘rid Ya`rid Ya`rid,Yairad,Ya‘rid,Yerad 24.80324 55.03569 T SAND AE 01 0 28 Asia/Dubai 2011-11-06
+290475 WÄdÄ« Yaif Wadi Yaif Wadi Yaif,WÄdÄ« Yaif 24.93722 56.1025 H WAD AE 05 0 295 Asia/Dubai 2011-11-06
+290476 Yaif Yaif Yaif 24.94 56.09722 V CULT AE 05 0 530 Asia/Dubai 2011-11-06
+290477 Yahli Yahli Yahli 23.9879 54.70761 H WLL AE 01 0 106 Asia/Dubai 2011-11-06
+290478 Ţawī Yaḩfar Tawi Yahfar Tawi Yahfar,Ţawī Yaḩfar 25.02556 55.83361 H WLL AE 06 0 172 Asia/Dubai 2011-11-06
+290479 Sayḩ YÄfÅ«kh Sayh Yafukh Sayh Yafukh,Sayḩ YÄfÅ«kh 23.6458 55.49207 T TRGD AE 01 0 167 Asia/Dubai 2011-11-06
+290480 Yafnah Yafnah Yafnah,Yifnah 25.73424 56.09919 V CULT AE 05 0 766 Asia/Dubai 2011-11-06
+290481 Jazīrat Yabr Jazirat Yabr Jazirat Yabr,Jazīrat Yabr 24.31856 52.71939 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290482 Ţawī Yabbah Tawi Yabbah Tawi Yabbah,Ţawī Yabbah 25.35306 56.05111 H WLL AE 04 0 266 Asia/Dubai 2011-11-06
+290483 Ya‘alla Ya`alla Ya`alla,Ya‘alla 23.90646 54.99539 T DUNE AE 01 0 135 Asia/Dubai 2011-11-06
+290484 WÄdÄ« Ya’a Wadi Ya'a Wadi Ya'a,WÄdÄ« Ya’a 25.09611 56.26028 H WAD AE 04 0 224 Asia/Dubai 2011-11-06
+290485 Jabal Wutayd Jabal Wutayd Jabal Witeid,Jabal Wutaid,Jabal Wutayd 23.93615 52.29663 T HLL AE 01 0 40 Asia/Dubai 2011-11-06
+290486 Å¢awÄ« WushÄḩ Tawi Wushah Tawi Wishah,Tawi Wushah,TÄwÄ« Wishah,Wusha,Å¢awÄ« WushÄḩ 25.22834 55.91456 H WLL AE 06 0 143 Asia/Dubai 2011-11-06
+290487 WÄdÄ« Wurayyah Wadi Wurayyah Wadi Waraiya,Wadi Wurayyah,WÄdÄ« Waraiya,WÄdÄ« Wurayyah 25.38343 56.26224 H WAD AE 04 0 415 Asia/Dubai 2011-11-06
+290488 Wuqnah Wuqnah Wuqnah 23.0326 53.67397 T DPR AE 01 0 86 Asia/Dubai 2011-11-06
+290489 Wuḩaydah Wuhaydah Wuhaydah,Wuḩaydah 23.13333 53.93333 L OAS AE 01 0 98 Asia/Dubai 2011-11-06
+290490 Wuḩaydah Wuhaydah Wuhaida,Wuhaydah,Wuḩaydah 23.11764 53.77261 L OAS AE 01 0 194 Asia/Dubai 2011-11-06
+290491 WudibsÄ Wudibsa Wudibsa,WudibsÄ 23.73726 52.25189 T SAND AE 01 0 32 Asia/Dubai 2011-11-06
+290492 WÄdÄ« WiqÄ’ Wadi Wiqa' Wadi Wiqa',Wadi al Amlah,WÄdÄ« WiqÄ’,WÄdÄ« al Amlaḩ 25.19143 56.04543 H WAD AE 05 0 210 Asia/Dubai 2011-11-06
+290493 Ţawī Widd Tawi Widd Tawi Wid,Tawi Widd,Ţawī Wid,Ţawī Widd 25.63296 56.01934 H WLL AE 05 0 108 Asia/Dubai 2011-11-06
+290494 Webb Rock Webb Rock Webb Rock 24.08437 52.2437 T RK AE 01 0 -9999 Asia/Dubai 2011-11-06
+290495 WÄdÄ« Wayqah Wadi Wayqah Wadi Wayqah,WÄdÄ« Wayqah 25.32211 56.12861 H WAD AE 04 0 403 Asia/Dubai 2011-11-06
+290496 Watigh Watigh Watigh 23.68728 55.07948 T DUNE AE 01 0 139 Asia/Dubai 2011-11-06
+290497 Watauq Watauq Watauq 24.30306 54.91357 H WLL AE 01 0 70 Asia/Dubai 2011-11-06
+290498 Raml WÄsiÅ£ Raml Wasit Raml Wasit,Raml WÄsiÅ£,Wasit,WÄsit 25.3425 55.47167 T SAND AE AE 06 0 21 Asia/Dubai 2011-11-06
+290499 WÄsiÅ£ Wasit Wasit,WÄsiÅ£ 25.72194 55.87806 H WLL AE 05 0 32 Asia/Dubai 2011-11-06
+290500 WÄsiÅ£ Wasit Wasit,WÄsiÅ£ 25.60698 56.2548 P PPL AE 04 0 9 Asia/Dubai 2011-11-06
+290501 WÄsiÅ£ Wasit Wasit,WÄsiÅ£ 23.0036 53.44942 T DPR AE 01 0 176 Asia/Dubai 2011-11-06
+290502 Wasaţ Wasat Wasat,Wasaţ 22.80611 53.31093 H WLL AE 01 0 70 Asia/Dubai 2011-11-06
+290503 WarÄ«sÄn Warisan Warisan,WarÄ«sÄn 25.16744 55.40708 P PPL AE 03 0 20 Asia/Dubai 2011-11-06
+290504 Ţawī Waraqah Tawi Waraqah Tawi Waraqah,Ţawī Waraqah 24.77782 56.14991 H WLL AE 03 0 341 Asia/Dubai 2011-11-06
+290505 Khaţm Waraq Khatm Waraq Khatm Waraq,Khaţm Waraq 24.36667 55.45 T DUNE AE 01 0 158 Asia/Dubai 2011-11-06
+290506 WarÄq Waraq Waraq,WarÄq 22.97996 54.18115 T DPR AE 01 0 98 Asia/Dubai 2011-11-06
+290507 WÄdÄ« Wamm Wadi Wamm Wadi Wamm,WÄdÄ« Wamm 25.60512 56.22705 H WAD AE 04 0 42 Asia/Dubai 2011-11-06
+290508 Jabal Wamm Jabal Wamm Jabal Wamm 25.6021 56.19906 T MT AE 04 0 490 Asia/Dubai 2011-11-06
+290509 Wamm Wamm Wamm 25.60031 56.22838 P PPL AE 04 0 42 Asia/Dubai 2011-11-06
+290510 Walters Shoal Walters Shoal Walters Shoal 24.39524 52.47387 H RF AE 01 0 -9999 Asia/Dubai 2011-11-06
+290511 Sayḩ al Wakrah Sayh al Wakrah Sayh al Wakrah,Sayḩ al Wakrah 24.59461 54.96476 T TRGD AE 01 0 57 Asia/Dubai 2011-11-06
+290512 Wahala Wahala Wahala,Wahlah,Waḩlah 24.90897 56.30316 P PPL AE 02 0 71 Asia/Muscat 2011-11-06
+290513 WÄdÄ« WahÄ«yah Wadi Wahiyah Wadi Wahiya,Wadi Wahiyah,WÄdÄ« Wahiya,WÄdÄ« WahÄ«yah 24.8183 56.15931 H WAD AE 02 0 282 Asia/Dubai 2011-11-06
+290514 Jabal Waḩīd Jabal Wahid Jabal Wahid,Jabal Waḩīd,The Hummock 24.31115 52.61141 T HLL AE 01 0 45 Asia/Dubai 2011-11-06
+290515 WÄḩid Wahid Wahid,WÄḩid 24.82444 56.08111 V CULT AE 02 0 339 Asia/Dubai 2011-11-06
+290516 WaharmÄ Waharma Waharma,WaharmÄ 22.75519 53.44766 T DPR AE 01 0 66 Asia/Dubai 2011-11-06
+290517 Wafd Wafd Qutuf,Quţūf,Waafit,Wafd,Wafid,Wefid 23.10262 53.71159 P PPL AE 01 0 84 Asia/Dubai 2011-11-06
+290518 Wad Wid Wad Wid Wad Wid 25.62515 56.01396 P PPL AE 05 0 17 Asia/Dubai 2011-11-06
+290519 WÄdÄ« ShÄ« Wadi Shi Wadi Shi,WÄdÄ« ShÄ« 25.35 56.31667 P PPL AE 06 0 77 Asia/Dubai 2011-11-06
+290520 Ḩabl WÄdÄ« ÅžafÄ Habl Wadi Safa Habl Wadi Safa,Wadi Safa,WÄdÄ« ÅžafÄ,Ḩabl WÄdÄ« ÅžafÄ 25.06092 55.27055 T DUNE AE 03 0 33 Asia/Dubai 2011-11-06
+290521 Wadhīl Wadhil Wadhil,Wadhīl,Wedheil,Wudhayl 23.0474 54.13332 P PPL AE 01 0 99 Asia/Dubai 2011-11-06
+290522 WÄdÄ« Wa‘bayn Wadi Wa`bayn Wadi Wa`bayn,WÄdÄ« Wa‘bayn 25.57261 56.12374 H WAD AE 04 0 400 Asia/Dubai 2011-11-06
+290523 Wa‘bayn Wa`bayn Wa'abain,Wa`bayn,Wa‘bayn,Wa’abain 25.57046 56.13536 P PPL AE 04 0 357 Asia/Dubai 2011-11-06
+290524 ‘Uzlah `Uzlah `Uzalah,`Uzlah,‘Uzalah,‘Uzlah 22.99545 54.16649 T DPR AE 01 0 91 Asia/Dubai 2011-11-06
+290525 WÄdÄ« al ‘Uyaynah Wadi al `Uyaynah Wadi Ayeina,Wadi al `Uyaynah,WÄdÄ« Ayeina,WÄdÄ« al ‘Uyaynah 25.4742 56.18514 H WAD AE 04 0 176 Asia/Dubai 2011-11-06
+290526 GhaffÄt al ‘Uwayyirah Ghaffat al `Uwayyirah Ghaffat al `Uwayyirah,GhaffÄt al ‘Uwayyirah 24.46667 55.31667 T DUNE AE 01 0 152 Asia/Dubai 2011-11-06
+290527 Jabal al ‘Uwayyin Jabal al `Uwayyin Jabal al `Uwayyin,Jabal al ‘Uwayyin 25.36083 56.32395 T MT AE 06 0 583 Asia/Dubai 2011-11-06
+290528 Ţawī ‘Uwayyah Tawi `Uwayyah Tawi `Uwayyah,Ţawī ‘Uwayyah 25.58472 55.76028 H WLL AE 07 0 26 Asia/Dubai 2011-11-06
+290529 ‘Uwayyah `Uwayyah `Uwayyah,‘Uwayyah 25.11278 55.30367 L LCTY AE 03 0 24 Asia/Dubai 2011-11-06
+290530 Qarn ‘Uwayşim Qarn `Uwaysim Qarn `Uwaysim,Qarn ‘Uwayşim 23.86667 53.43333 T HLL AE 01 0 70 Asia/Dubai 2011-11-06
+290531 ‘Uwayşim `Uwaysim `Uwaisim,`Uwaysim,‘Uwaisim,‘Uwayşim 23.88333 53.38333 H WLL AE 01 0 66 Asia/Dubai 2011-11-06
+290532 Sabkhat ‘Uwayrah Sabkhat `Uwayrah Sabkhat `Uwayrah,Sabkhat ‘Uwayrah 23.95006 55.3068 H SBKH AE 01 0 131 Asia/Dubai 2011-11-06
+290533 NiqyÄn ‘Uwayrah Niqyan `Uwayrah Niqyan `Uwayrah,NiqyÄn ‘Uwayrah 23.96497 55.28962 T DUNE AE 01 0 181 Asia/Dubai 2011-11-06
+290534 Ţawī al ‘Uwaynīyah Tawi al `Uwayniyah Tawi al `Uwayniyah,Ţawī al ‘Uwaynīyah 25.31667 55.76667 H WLL AE 06 0 86 Asia/Dubai 2011-11-06
+290535 ‘Uwayjir `Uwayjir `Uwayjir,‘Uwayjir 24.71667 55.16667 T HLL AE 03 0 87 Asia/Dubai 2011-11-06
+290536 ‘Uwayj al ‘Abīd `Uwayj al `Abid `Uwayj al `Abid,‘Uwayj al ‘Abīd 24.11667 55.06667 T DUNE AE 01 0 119 Asia/Dubai 2011-11-06
+290537 ‘Uwayj al ‘Abīd `Uwayj al `Abid `Uwayj al `Abid,‘Uwayj al ‘Abīd 24.13333 55.06667 T DPR AE 01 0 125 Asia/Dubai 2011-11-06
+290538 Jabal ‘Uwaybil Jabal `Uwaybil Jabal `Uwaybil,Jabal ‘Uwaybil 25.44021 55.96991 T DUNE AE 05 0 189 Asia/Dubai 2011-11-06
+290539 Jabal al ‘Uţayfah Jabal al `Utayfah Jabal al `Utayfah,Jabal al ‘Utayfah,Jabal al ‘Uţayfah 25.51024 56.02862 T HLL AE 04 0 278 Asia/Dubai 2011-11-06
+290540 ‘Ushsh `Ushsh 'Ishsh,Al Isha,Jazirat `Ish,Jazīrat ‘Ish,Ushsh,`Ashsh,`Ish,`Ushsh,‘Ashsh,‘Ish,‘Ushsh,’Ishsh 24.30488 52.87712 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290541 WÄdÄ« Ushayl Wadi Ushayl Wadi Ushail,Wadi Ushayl,WÄdÄ« Ushail,WÄdÄ« Ushayl 24.90515 56.08586 H WAD AE 05 0 287 Asia/Dubai 2011-11-06
+290542 WÄdÄ« ‘Usaylah Wadi `Usaylah Wadi `Usaylah,WÄdÄ« ‘Usaylah 25.69178 56.03764 H WAD AE 05 0 48 Asia/Dubai 2011-11-06
+290543 Sabkhat al ‘Urūq Sabkhat al `Uruq Sabkhat al `Uruq,Sabkhat al ‘Urūq 23.90811 54.18872 H SBKH AE 01 0 40 Asia/Dubai 2011-11-06
+290544 ‘UrqÅ«b SibÄ’ `Urqub Siba' Sbaa,Siba`,SibÄ‘,`Urqub Siba',‘UrqÅ«b SibÄ’ 25.27824 55.47043 L LCTY AE 03 0 24 Asia/Dubai 2011-11-06
+290545 ‘Urqūb Juwayzah `Urqub Juwayzah `Urqub Juwayzah,‘Urqūb Juwayzah 24.90586 55.4608 T SCRP AE 03 0 107 Asia/Dubai 2011-11-06
+290546 ‘Urqūb Juwayza `Urqub Juwayza `Urqub Juwayza,‘Urqūb Juwayza 24.93333 55.46667 P PPL AE 03 0 102 Asia/Dubai 2011-11-06
+290547 ‘Uraykah `Uraykah `Uraykah,‘Uraykah 24.36667 55.45 T TRGD AE 01 0 158 Asia/Dubai 2011-11-06
+290548 ‘Uraykah `Uraykah 'Ureika,`Uraykah,‘Uraykah,’Ureika 24.33333 55.46667 T HLL AE 01 0 199 Asia/Dubai 2011-11-06
+290549 WÄdÄ« ‘Urayf Wadi `Urayf Wadi `Urayf,WÄdÄ« ‘Urayf 25.49385 56.06209 H WAD AE 04 0 147 Asia/Dubai 2011-11-06
+290550 ‘Uraybī `Uraybi Araibi,`Uraybi,‘Uraybī 25.78945 55.97665 P PPLX AE 05 0 20 Asia/Dubai 2011-11-06
+290551 ‘Uqayyiq `Uqayyiq `Uqayyiq,‘Uqayyiq 24.53333 54.75 H WLL AE 01 0 33 Asia/Dubai 2011-11-06
+290552 ‘Uqayyiq `Uqayyiq `Uqayyiq,‘Uqayyiq 24.52079 54.66317 T DUNE AE 01 0 7 Asia/Dubai 2011-11-06
+290553 Ţawī ‘Uqayr Tawi `Uqayr Tawi `Uqayr,Ţawī ‘Uqayr 23.78033 55.4708 H WLL AE 01 0 146 Asia/Dubai 2011-11-06
+290554 ‘Uqayr `Uqayr `Uqayr,‘Uqayr 25.28302 56.36435 P PPL AE 06 0 23 Asia/Dubai 2011-11-06
+290555 ‘UqaydÄt `Uqaydat Al `Uqaydat,Al ‘UqaydÄt,`Uqaydat,‘UqaydÄt 24.78556 55.78972 V TREE AE 06 0 275 Asia/Dubai 2011-11-06
+290556 Jabal ‘UqaybÄt Jabal `Uqaybat Jabal `Uqaybat,Jabal ‘UqaybÄt 25.1275 56.31278 T HLL AE 04 0 155 Asia/Dubai 2011-11-06
+290557 United Arab Emirates United Arab Emirates Ab'adnanya Arabskia Emiraty,Al Imarat al `Arabiyah al Muttahidah,Al ImÄrÄt al ‘ArabÄ«yah al Muttaḩidah,Aontas na nEimiriochtai Arabacha,Aontas na nÉimÃrÃochtaà Arabacha,Apvienotie Arabu Emirati,Apvienotie ArÄbu EmirÄti,Araabia UEhendemiraadid,Araabia Ühendemiraadid,Arab Federation of Gulf States,Arab Gulf Federation,Arabiar Emirerri Batuak,Arabiar Emirrerri Batuak,Arabiemiirikunnat,Birlashgan Arab Amirliglar,Birlesik Arap Emirlikleri,BirleÅŸik Arap Emirlikleri,Cac Tieu Vuong quoc A-rap Thong nhat,Cac Tieu vuong quoc A rap Thong nhat,Các Tiểu Vương quốc A-ráºp Thống nhất,Các Tiểu vương quốc Ả ráºp Thống nhất,Dawlat Ittihad al Imarat al `Arabiyah,Dawlat IttiḩÄd al ImÄrÄt al ‘ArabÄ«yah,De forente arabiske emirater,Dei sameinte arabiske emirata,Egyesuelt Arab Emiratus,Egyesuelt Arab Emirsegek,Egyesült Arab Emirátus,Egyesült Arab EmÃrségek,Emira Arab Ini,Emirados Arabes Unidos,Emirados Ãrabes Unidos,Emiraethau Arabaidd Unedig,Emiratele Arabe Unite,Emiratet Arabe te Bashkuara,Emiratet e Bashkuara Arabe,Emirati Arabi Uniti,Emirati Gharab Maqghuda,Emirati Għarab Maqgħuda,Emiratos Arabe Unite,Emiratos Arabes Unidos,Emiratos Arabes Unitos,Emiratos Ãrabes Unidos,Emiratos Ãrabes Unidos - الإمارات العربيّة Ø§Ù„Ù…ØªÙ‘ØØ¯Ø©,Emirats Arabes Unis,Emirats Arabis Units,Emirats Arabs Units,Emirats arabes units,Emirats arabos unis,Emirats Àrabs Units,Emiratus Arabi Uniti,Emirelezhiou Arab Unanet,Emirelezhioù Arab Unanet,Emiriah Arab Bersatu,Enomena Arabika Emirata,Falme za Kiarabu,Federation of Arab Emirates,Federation of Arabian Emirates,Federation of Arabian Gulf Emirates,Feriene Arabyske Emiraten,Foerenade Arabemiraten,Forenede Arabiske Emirater,Förenade Arabemiraten,Imaaraadka Carabta ee Midoobay,Jungtiniai Arabu Emyratai,Jungtiniai Arabų Emyratai,Lemiraens Pebaloel Larabaenik,Lemiräns Pebalöl Larabänik,Mirnisinen Erebi yen Yekbuyi,Muugano wa Falme za Nchi za Kiarabu,Mîrnişînên Erebî yên Yekbûyî,OAEH,Ob"edinjonnye Arabskie Ehmiraty,Obedineni Arabski Emirstva,Obedineti Arapski Emirati,Obuedinennye Arabskie Ehmiraty,Ovttastuvvan Arabaemirahtat,Ovttastuvvan Arábaemiráhtat,Pennternasedh Unys Arabek,Sameindu Emirrikini,Sameindu EmirrÃkini,Sameinudu arabisku furstadaemin,Sameinuðu arabÃsku furstadæmin,Spojene arabske emiraty,Spojené arabské emiráty,Trucial States,UAE,Ujedineni Arapski Emirati,Ujedinjeni Arapski Emirati,Uni Emirat Arab,Unio dels Emirats Arabs,Union of Arab Emirates,Unionita Araba Emirati,United Arab Emirates,Unió dels Emirats Àrabs,Unuigintaj Arabaj Emirlandoj,Unuigintaj Arabaj Emirlandos,UnuiÄintaj Arabaj Emirlandoj,UnuiÄintaj Arabaj Emirlandos,Vereenegt Arabesch Emirater,Vereenigte Araabsche Emiraten,Vereinegde Arabische Emirate,Vereinigte Arabische Emirate,Verenigde Arabiese Emirate,Verenigde Arabische Emiraten,Zdruzeni arabski emirati,Združeni arabski emirati,Zjednocene Arabske Emiraty,Zjednoczone Emiraty Arabskie,Zjednoćene Arabske Emiraty,a la bo lian he qiu zhang guo,aikkiya arapu amirakam,aikkiya arapu kuttatci,alab-emiliteu,arabetis gaertianebuli saamiroebi,arabu shou zhang guo lian bang,sanyukta arab rastram,sanyukta araba amirata,shrath xahrab xe mi rets,Èmirats arabes units,Èmirats arabos unis,Émirats Arabes Unis,ΗνωμÎνα ΑÏαβικά ΕμιÏάτα,Ðб'ÑÐ´Ð½Ð°Ð½Ñ‹Ñ ÐрабÑÐºÑ–Ñ Ðміраты,Имороти Муттаҳидаи Ðраб,ОÐÐ,Об'єднані ÐрабÑькі Емірати,Обʼєднані ÐрабÑькі Емірати,Обединени ÐрабÑки ЕмирÑтва,Обединети ÐрапÑки Емирати,Объединенные ÐрабÑкие Ðмираты,Объединённые ÐрабÑкие Ðмираты,Уједињени ÐрапÑки Емирати,Õ„Õ«Õ¡ÖÕµÕ¡Õ¬ Ô±Ö€Õ¡Õ¢Õ¡Õ¯Õ¡Õ¶ Ô·Õ´Õ«Ö€Õ¡Õ©Õ¶Õ¥Ö€,×יחוד ×”×מירויות הערביות,×יחוד × ×¡×™×›×•×™×•×ª ערב,ברית ×”×מירויות הערביות,ئەرەب بىرلەشمە خەلىپىلىكى,الإمارات العربية Ø§Ù„Ù…ØªØØ¯Ø©,الامارات العربية Ø§Ù„Ù…ØªØØ¯Ø©,امارات Ù…ØªØØ¯Ù‡ عربی,امارات Ù…ØªØØ¯Ù‡Ù” عربی,عمارات Ù…ØªØØ¯Ù‡ ÛŒ عربی,Ù…ØªØØ¯Ù‡ عرب امارات,Ù…ØªØØ¯Û عرب امارات,ÜÜ¡ÜÜªÜ˜Ü¬Ü Ü¥ÜªÜ’ÜÜ¬Ü ÜšÜ•ÜܬÜ,संयà¥à¤•à¥à¤¤ अरब अमीरात,সংযà§à¦•à§à¦¤ আরব আমিরাত,à®à®•à¯à®•ிய அரப௠அமீரகமà¯,à®à®•à¯à®•ிய அரப௠கூடà¯à®Ÿà®¾à®Ÿà¯à®šà®¿,à´à´•àµà´¯ അറബൠഎമിരേറàµà´±àµà´•à´³àµâ€â€Œ,സംയàµà´•àµà´¤ അറബൠരാഷàµà´Ÿàµà´°à´‚,สหรัà¸à¸à¸²à¸«à¸£à¸±à¸šà¹€à¸à¸¡à¸´à¹€à¸£à¸•ส์,ສະຫະລັດàºàº²àº«àº¥àº±àºšà»€àºàº¡àº´à»€àº¥àº”,ཡུ་ནའི་ཊེཊ་ཨ་ར བ་ཨེ་མི་རེཊསི,ཨ་རབ༠ཨི་མི་རཊ྄༠ཆིག་སྒྲིལ་རྒྱལ་à½à½–à¼,áƒáƒ áƒáƒ‘ეთის გáƒáƒ”რთიáƒáƒœáƒ”ბული ემირáƒáƒ¢áƒ”ბი,áƒáƒ áƒáƒ‘ეთის გáƒáƒ”რთიáƒáƒœáƒ”ბული სáƒáƒáƒ›áƒ˜áƒ áƒáƒ”ბი,የተባበሩት አረብ ኤáˆáˆ¬á‰µáˆµ,អáŸáž˜áž¸ážšáŸ‰áŸ‚ទអារ៉ាប់រួម,アラブ首長国連邦,阿拉伯è”åˆé…‹é•¿å›½,ì•„ëžì—미리트 24 54 A PCLI AE 00 4975593 12 Asia/Dubai 2011-11-06
+290558 Umm Åžayd Umm Sayd Umm Sayd,Umm Åžayd 22.94932 53.78481 T DPR AE 01 0 44 Asia/Dubai 2011-11-06
+290559 Umm Qays Umm Qays Umm Qays,Umm Qayz,Umm Qayz̧,Umm Qaz,Umm Qaz̧ 22.92996 53.41542 H WLL AE 01 0 65 Asia/Dubai 2011-11-06
+290560 Umm Qays Umm Qays Umm Qays,Umm Qayz,Umm Qayz̧ 22.92593 53.4138 T DPR AE 01 0 65 Asia/Dubai 2011-11-06
+290561 Umm JaÅŸÅŸÄr Umm Jassar Umm Jassar,Umm JaÅŸÅŸÄr,Umm Qasar,Umm Qassar,Umm QaÅŸÄr,Umm QaÅŸÅŸÄr,`Umm Gassar,‘Umm GaṣṣÄr 24.3914 52.77794 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290562 Umm Minhad Umm Minhad Umm Minhad 24.99359 55.3636 T SAND AE 03 0 40 Asia/Dubai 2011-11-06
+290564 Fasht Umm Jannah Fasht Umm Jannah Fasht Umm Janna,Fasht Umm Jannah,Janna 24.56959 51.5546 H RF AE 01 0 -9999 Asia/Dubai 2011-11-06
+290565 Qarn Umm Ḩaşá Qarn Umm Hasa Qarn Umm Hasa,Qarn Umm Ḩaşá 25.11385 55.38284 T DUNE AE 03 0 49 Asia/Dubai 2011-11-06
+290566 Umm ḨafÄt Umm Hafat Umm Hafaf,Umm Hafat,Umm ḨafÄf,Umm ḨafÄt 23.84889 53.67889 H WLL AE AE 01 0 76 Asia/Dubai 2011-11-06
+290567 Umm Khafat Umm Khafat Umm Hafaf,Umm Hafat,Umm Kafat,Umm Khafat,Umm ḨafÄf,Umm ḨafÄt 23.85336 53.69666 T HLL AE 01 0 95 89 Asia/Dubai 2011-11-06
+290568 Umm ḨÄbil Umm Habil Umm Habil,Umm ḨÄbil 22.99242 54.08237 T DPR AE 01 0 213 Asia/Dubai 2011-11-06
+290569 Ţawī Umm Ghuwayr Tawi Umm Ghuwayr Tawi Umm Ghuwayr,Ţawī Umm Ghuwayr 24.25341 55.0425 H WLL AE 01 0 125 Asia/Dubai 2011-11-06
+290570 Umm Ghaythah Umm Ghaythah Umm Gaita,Umm Ghaythah 24.10556 55.67583 T DPR AE AE 01 0 244 Asia/Dubai 2011-11-06
+290571 JazÄ«rat Umm YafÄ«nah Jazirat Umm Yafinah Jazirat Umm Yafinah,JazÄ«rat Umm YafÄ«nah,Umm Fiyin,Umm FÄ«yÄ«n,jzyrt am yfynt,جزيرة أم ÙŠÙينة 24.48752 54.45009 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290572 Umm Dhuwaylah Umm Dhuwaylah Umm Dhuwaylah 24.45801 54.7294 H WLL AE 01 0 26 Asia/Dubai 2011-11-06
+290573 Umm Dhuwaylah Umm Dhuwaylah Umm Dhuwaylah 24.4692 54.723 T SAND AE 01 0 36 Asia/Dubai 2011-11-06
+290574 Umm Dasīs Umm Dasis Umm Dasis,Umm Dasīs,Umm Daysis,Umm Daysīs 23.81981 53.53876 L GVL AE 01 0 109 Asia/Dubai 2011-11-06
+290575 WÄdÄ« Umm ad Daqqayn Wadi Umm ad Daqqayn Wadi Umm Daqqayn,Wadi Umm ad Daqqayn,WÄdÄ« Umm Daqqayn,WÄdÄ« Umm ad Daqqayn 24.80556 56.20722 H WAD AE AE 00 0 271 Asia/Dubai 2011-11-06
+290576 Kharaj Umm BiyÄt Kharaj Umm Biyat Kharaj Umm Bayat,Kharaj Umm BayÄt,Kharaj Umm Biyat,Kharaj Umm BiyÄt 25.15 55.4 H WLL AE AE 03 0 7 Asia/Dubai 2011-11-06
+290577 Ţawī Umm Baraz Tawi Umm Baraz Tawi Umm Baraz,Ţawī Umm Baraz 22.94415 54.96468 H WLL AE 01 0 137 Asia/Dubai 2011-11-06
+290578 Sabkhat Umm Baraz Sabkhat Umm Baraz Sabkhat Umm Baraz 22.93458 55.03856 H SBKH AE 01 0 99 Asia/Dubai 2011-11-06
+290579 Umm Baraz Umm Baraz Umm Baraz 23.05 54.95 H WLL AE 01 0 125 Asia/Dubai 2011-11-06
+290580 Umm az Zumūl Umm az Zumul Umm Zamul,Umm Zamūl,Umm al Zamul,Umm az Zumul,Umm az Zumūl,Umm az-Zamul 22.70559 55.21205 H WLL AE 01 0 121 Asia/Dubai 2011-11-06
+290581 Umm Suqaym Umm Suqaym Umm Suqaym,Umm as Suqaym 25.15015 55.20587 P PPLX AE 03 0 26 Asia/Dubai 2011-11-06
+290582 Ḩaql Umm ash Shayf Haql Umm ash Shayf Haql Umm ash Shayf,Umm Shaif,Umm al-Shayf,Umm al-Sheif,Umm ash Sha'if,Umm ash ShÄ’if,Ḩaql Umm ash Shayf 25.2 53.2 L OILF AE AE 01 0 -9999 Asia/Dubai 2011-11-06
+290583 Umm ar Raml Umm ar Raml Umm ar Raml 25.19658 55.41034 L LCTY AE 03 0 22 Asia/Dubai 2011-11-06
+290584 Umm ar Raml Umm ar Raml Umm ar Raml 25.23333 55.38333 T DUNE AE 03 0 29 Asia/Dubai 2011-11-06
+290585 WÄdÄ« Umm an NughÅ«l Wadi Umm an Nughul Wadi Umm an Nughul,WÄdÄ« Umm an NughÅ«l 25.34985 55.84553 H WAD AE 07 0 111 Asia/Dubai 2011-11-06
+290586 Ţawī Umm an Nughūl Tawi Umm an Nughul Tawi Umm an Nughul,Ţawī Umm an Nughūl 25.36333 55.87019 H WLL AE 07 0 92 Asia/Dubai 2011-11-06
+290587 Umm an NÄr Umm an Nar Jazirat Umm an Nar,JazÄ«rat Umm an NÄr,Umm an Nar,Umm an NÄr,`Umm al-Nar,am alnar,أم النار,‘Umm al-NÄr 24.44248 54.50948 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290588 Umm ‘Amīm Umm `Amim Jazirat Umm `min,Jazīrat Umm ‘mīn,Umm `Amim,Umm ‘Amīm 24.24128 53.39448 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290589 Umm al Qurayn Umm al Qurayn Umm Grain,Umm al Qurayn,`Umm Qrein,‘Umm Qrein 23.1 53.71667 P PPL AE AE 01 0 181 Asia/Dubai 2011-11-06
+290590 Umm al Qurayn Umm al Qurayn Umm al Qurayn 23.09087 53.72477 L OAS AE 01 0 101 Asia/Dubai 2011-11-06
+290591 Umm al Qiţa‘ Umm al Qita` Umm al Qita`,Umm al Qiţa‘ 23.05522 53.53183 L OAS AE 01 0 127 Asia/Dubai 2011-11-06
+290592 Umm al Qird Umm al Qird Umm al Fa'iyah,Umm al FÄ’iyah,Umm al Qird 24.20527 54.79192 T SAND AE 01 0 66 Asia/Dubai 2011-11-06
+290593 Khawr Umm al Qaywayn Khawr Umm al Qaywayn Khawr Umm al Qaywayn 25.56 55.57972 H BAY AE 07 0 -9999 Asia/Dubai 2011-11-06
+290594 Umm al Qaywayn Umm al Qaywayn Um al Quweim,Umm al Qaiwain,Umm al Qawain,Umm al Qaywayn,Yumul al Quwain,am alqywyn,أم القيوين 25.56473 55.55517 P PPLA AE 07 44411 8 Asia/Dubai 2011-11-06
+290595 Umm al Qaywayn Umm al Qaywayn Oumm al Qaiwain,Oumm al Qaïwaïn,Skeikhdom of Umm al Qaiwain,Umm Al Quwain,Umm al Qawain,Umm al Qaywayn,Umm al Qiwain,am alqywyn,أم القيوين 25.5 55.75 A ADM1 AE 07 56253 65 Asia/Dubai 2011-11-05
+290596 Kharīmat Umm al Muwayghir Kharimat Umm al Muwayghir Kharimat Umm al Muwayghir,Kharīmat Umm al Muwayghir 24.10497 54.80762 T DPR AE 01 0 74 Asia/Dubai 2011-11-06
+290597 Umm al Kurkum Umm al Kurkum Umm Kirkum,Umm Kurkum,Umm al Kurkum 24.39273 52.76497 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290598 Umm al Ḩişn Umm al Hisn Umm al Hisn,Umm al Ḩişn 23.01691 53.41646 S FT AE 01 0 118 Asia/Dubai 2011-11-06
+290599 Umm al Ḩişn Umm al Hisn Umm al Hisn,Umm al Ḩişn 23.01994 53.44238 T DPR AE 01 0 89 Asia/Dubai 2011-11-06
+290600 Umm al HiryÄn Umm al Hiryan Umm al Hiryan,Umm al HiryÄn 24.31722 55.79722 H WLL AE 01 0 280 Asia/Dubai 2011-11-06
+290601 Umm al Ḩaţab Umm al Hatab Jazirat Umm al Hatab,Jazīrat Umm al Ḩaţab,Umm al Halab Island,Umm al Hatab,Umm al Ḩaţab,Umm el Halab 24.21623 51.86389 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290602 Umm al Ghurūl Umm al Ghurul Umm al Ghurul,Umm al Ghurūl 23.84161 53.21056 L GVL AE 01 0 97 Asia/Dubai 2011-11-06
+290603 Umm al GhirbÄn Umm al Ghirban Umm al Gharban,Umm al GharbÄn,Umm al Ghirban,Umm al GhirbÄn 23.03841 53.55597 T DPR AE 01 0 71 Asia/Dubai 2011-11-06
+290604 WÄdÄ« Umm al GhÄt Wadi Umm al Ghat Wadi Umm al Ghat,WÄdÄ« Umm al GhÄt 24.87975 56.2778 H WAD AE 05 0 89 Asia/Dubai 2011-11-06
+290605 Jabal Umm al FurfÄr Jabal Umm al Furfar Jabal Umm al Farfar,Jabal Umm al FarfÄr,Jabal Umm al Furfar,Jabal Umm al FurfÄr 25.12448 56.22754 T MT AE 04 0 735 Asia/Dubai 2011-11-06
+290606 Umm al Birak Umm al Birak Umm al Barak,Umm al BarÄk,Umm al Birak,Umm al-Berak 24.56699 54.58394 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290607 Umm al BanÄdÄ«q Umm al Banadiq Umm al Banadig,Umm al Banadiq,Umm al BanÄdÄ«q 24.08705 55.27527 H WLL AE 01 0 151 Asia/Dubai 2011-11-06
+290608 Umm al AshÅ£Än Umm al Ashtan Umm al Ashtan,Umm al AshÅ£Än 23.58333 52.48333 H WLL AE 01 0 75 Asia/Dubai 2011-11-06
+290609 Umm al AshÅ£Än Umm al Ashtan Umm al Ashtan,Umm al AshÅ£Än,Umm al Lishtan,Umm al LishtÄn 23.76667 52.66667 T SAND AE AE 01 0 79 Asia/Dubai 2011-11-06
+290610 Umm al AshÅ£Än Umm al Ashtan Umm al Ashtan,Umm al AshÅ£Än,Umm al Ishtan,Umm al IshÅ£Än 23.65248 52.45047 S CMPQ AE 01 0 64 Asia/Dubai 2011-11-06
+290611 Umm ‘Alaqah Umm `Alaqah Um `Alaqa,Um ‘Alaqa,Umm `Alaqah,Umm ‘Alaqah 24.0024 53.39048 T HLL AE 01 0 23 Asia/Dubai 2011-11-06
+290612 Ruqq Umm al ‘Anbar Ruqq Umm al `Anbar Ruqq Um el Umber,Ruqq Umm al `Anbar,Ruqq Umm al ‘Anbar,Umbar 24.60999 51.8836 H SHOL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290613 Sabkhat Umm al ‘Alqah Sabkhat Umm al `Alqah Sabkhat Umm al Alqa,Sabkhat Umm al `Alqah,Sabkhat Umm al ‘Alqah 23.63234 54.85535 H SBKH AE 01 0 115 Asia/Dubai 2011-11-06
+290614 Umm al ‘Alqah Umm al `Alqah Umm al Alqa,Umm al `Alqah,Umm al ‘Alqah 23.68333 54.83333 H WLL AE 01 0 105 Asia/Dubai 2011-11-06
+290615 Umm al Abyaḑ Umm al Abyad Umm al Abyad,Umm al Abyaḑ 25.09157 55.2481 H WLLS AE 03 0 26 Asia/Dubai 2011-11-06
+290616 Umm ad Dalkh Umm ad Dalkh Umm Addalkh,Umm ad Dalkh,Umm al Dalkh 24.53655 54.14598 L OILF AE 01 0 -9999 Asia/Dubai 2011-11-06
+290617 Ghuyūţ ‘Ulayyah Ghuyut `Ulayyah Ghuyut `Ulayyah,Ghuyūţ ‘Ulayyah 23.85 54.73333 T DPR AE 01 0 96 Asia/Dubai 2011-11-06
+290618 ‘Ūd Umm KhÄlid `Ud Umm Khalid `Ud Umm Khaldi,`Ud Umm Khalid,‘Ūd Umm KhaldÄ«,‘Ūd Umm KhÄlid 25.1375 55.39861 V TREE AE 03 0 35 Asia/Dubai 2011-11-06
+290619 Ra’s al ‘Udayd Ra's al `Udayd Al Odaid,Al `Udayd,Al ‘Udayd,Ra's al `Udayd,Ra’s al ‘Udayd 24.61667 51.43333 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+290620 Khawr al ‘Udayd Khawr al `Udayd Khawr al Wutayd,Khawr al Wuţayd,Khawr al `Udayd,Khawr al `Uwayd,Khawr al ‘Udayd,Khawr al ‘Uwayd,Khor al Odaid,Khor al Odeid,Khor al Ubeid,Khor al Wutaid,Khor al `Udaid,Khor al `Udeid,Khor al ‘Udaid,Khor al ‘Udeid 24.6 51.4 H INLT AE AE 06 0 -9999 Asia/Dubai 2011-11-06
+290622 ‘Ūd al Maţīnah `Ud al Matinah Aud al Matina,Awad al Matinah,Matina,Matinah,Matīna,Maţīnah,`Ud al Matinah,‘Ūd al Maţīnah 25.25583 55.44611 H WLLS AE 03 0 24 Asia/Dubai 2011-11-06
+290623 ‘Ūd al Maţīnah `Ud al Matinah `Ud al Matinah,‘Ūd al Maţīnah 25.23815 55.4741 L LCTY AE 03 0 39 Asia/Dubai 2011-11-06
+290624 ‘Ūd al BayḑÄ’ `Ud al Bayda' Ud al Beida,`Ud al Bayda',‘Ūd al BayḑÄ’ 25.01625 55.45533 P PPL AE 03 0 95 Asia/Dubai 2011-11-06
+290625 ‘Ūd al Atham `Ud al Atham `Ud al Atham,‘Ūd al Atham 25.15 55.71667 V TREE AE 06 0 124 Asia/Dubai 2011-11-06
+290626 ‘Ubaydil `Ubaydil `Ibeidil,`Ubaydhil,`Ubaydil,‘Ibeidil,‘Ubaydhil,‘Ubaydil 24.51667 51.33333 H WLL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290627 WÄdÄ« Å¢uwayyah Wadi Tuwayyah Wadi Tuwayyah,WÄdÄ« Å¢uwayyah 24.25722 55.68778 H WAD AE 01 0 267 Asia/Dubai 2011-11-06
+290628 Sayḩ Ţuwayyah Sayh Tuwayyah Sayh Tuwayyah,Sayḩ Ţuwayyah 24.39139 55.8275 T PLN AE 00 0 327 Asia/Dubai 2011-11-06
+290629 Sayḩ Tuwaysah Sayh Tuwaysah Sayh Tuways,Sayh Tuwaysah,Sayḩ Tuways,Sayḩ Tuwaysah 24.29278 55.505 T DPR AE AE 01 0 195 Asia/Dubai 2011-11-06
+290630 Ţawī Ţuwayli‘ Tawi Tuwayli` Tawi Tawaila,Tawi Tuwayli`,Tawi Tuwayyilah,Ţawī Tawaila,Ţawī Ţuwayli‘,Ţawī Ţuwayyilah 24.97679 55.7508 H WLL AE 06 0 171 Asia/Dubai 2011-11-06
+290631 Kharīmat Ţuwaylah Kharimat Tuwaylah Kharimat Tawaila,Kharimat Tuwaylah,Kharmat Tuwaylah,Kharmat Ţuwaylah,Kharīmat Ţuwaylah 24.1361 54.82632 T TRGD AE 01 0 92 Asia/Dubai 2011-11-06
+290632 Tuwayhil Tuwayhil Tuwayhil 23.4594 53.29148 H WLL AE 01 0 160 Asia/Dubai 2011-11-06
+290633 Jabal Tu‘ūs Jabal Tu`us Jabal Tu`us,Jabal Tus,Jabal Tu‘ūs 25.29958 56.11017 T HLL AE 04 0 550 Asia/Dubai 2011-11-06
+290634 Ţurayf Turayf Turayf,Ţurayf 23.0742 53.80161 T DPR AE 01 0 70 Asia/Dubai 2011-11-06
+290635 Ţunayq Tunayq Tanaij,Tanaiq,Tunaij,Tunayq,Ţunayq 25.86476 56.04169 L TRB AE 05 0 435 Asia/Dubai 2011-11-06
+290636 Tunayq Tunayq Tanaij,Tanaiq,Tunaij,Tunayq 25.26139 55.94944 L TRB AE 06 0 156 Asia/Dubai 2011-11-06
+290637 ‘Aqabat Tūmaytayn `Aqabat Tumaytayn `Aqabat Tumaytayn,‘Aqabat Tūmaytayn 25.47656 56.35033 T PASS AE 04 0 17 Asia/Dubai 2011-11-06
+290638 Ţawī Ţubūl Tawi Tubul Tawi Tubul,Ţawī Ţubūl 24.22687 55.03957 H WLL AE 01 0 133 Asia/Dubai 2011-11-06
+290639 Jabal Ţubayqah Jabal Tubayqah Jabal Tubayqah,Jabal Ţubayqah 24.07825 56.00668 T HLL AE 01 0 429 Asia/Dubai 2011-11-06
+290640 Trucial Coast Trucial Coast Al Sahil,Al SÄḩil,Arab Coast,Pirate Coast,Sahil `Oman,Sahil `Uman,Sahil as Sulh al Bahri,Shamal,ShamÄl,SÄhil ‘OmÄn,SÄhil ‘UmÄn,SÄḩil aÅŸ Åžulḩ al BaḩrÄ«,Trucial Coast,Trucial Oman,Trucial `Uman,Trucial ‘Uman 24 53 L RGN AE AE 00 0 37 Asia/Dubai 2011-11-06
+290641 Tina Tina Tina 23.81243 55.37423 T DUNE AE 01 0 182 Asia/Dubai 2011-11-06
+290642 Sabkhat Thuwaymah Sabkhat Thuwaymah Sabkhat Thuwaymah 24.0275 55.66806 L SALT AE 01 0 227 Asia/Dubai 2011-11-06
+290643 ‘UrqÅ«b ThurayyÄ `Urqub Thurayya `Urqub Thurayya,‘UrqÅ«b ThurayyÄ 24.9 55.46667 T DUNE AE 03 0 139 Asia/Dubai 2011-11-06
+290644 Ţawī ath Thuqbah Tawi ath Thuqbah Tawi Thuqbah,Tawi ath Thuqbah,Ţawī Thuqbah,Ţawī ath Thuqbah 25.34611 55.84722 H WLL AE AE 07 0 111 Asia/Dubai 2011-11-06
+290645 Ţawī Thuqaybah Tawi Thuqaybah Tawi Thuqaybah,Ţawī Thuqaybah 24.94829 55.81388 H WLL AE 06 0 190 Asia/Dubai 2011-11-06
+290646 Ţawī Thuqaybah Tawi Thuqaybah Tawi Thuqaybah,Ţawī Thuqaybah 23.55 55.28333 H WLL AE 01 0 181 Asia/Dubai 2011-11-06
+290647 Sabkhat Thuqaybah Sabkhat Thuqaybah Sabkhat Thuqaybah 23.58997 55.19693 H SBKH AE 01 0 114 Asia/Dubai 2011-11-06
+290648 Khawr Thumayrīyah Khawr Thumayriyah Khawr Thumayriyah,Khawr Thumayrīyah,Themairiyyah 24.15274 53.0014 H CHNM AE 01 0 -9999 Asia/Dubai 2011-11-06
+290649 Thumayrīyah Thumayriyah Themairiyya,Themeiriyyah,Thimairiyah,Thumayriyah,Thumayrīyah 24.15033 53.0169 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290650 Thubaybah Thubaybah Thubaybah 23.68333 54.61667 H WLL AE 01 0 117 Asia/Dubai 2011-11-06
+290651 WÄdÄ« Thayb Wadi Thayb Wadi Thayb,Wadi Theeb,WÄdÄ« Thayb 25.24914 56.35124 H WAD AE 04 0 96 Asia/Dubai 2011-11-06
+290652 Jabal Thayb Jabal Thayb Jabal Thaib,Jabal Thayb 25.25854 56.31202 T MT AE 04 0 386 Asia/Dubai 2011-11-06
+290653 Thawrīyah Thawriyah Thawriyah,Thawrīyah 23.02531 53.9109 T DPR AE 01 0 68 Asia/Dubai 2011-11-06
+290654 Thawrīyah Thawriyah Thawriyah,Thawrīyah 22.99797 53.76585 T DPR AE 01 0 60 Asia/Dubai 2011-11-06
+290655 Khabb ath Thawr Khabb ath Thawr Khabb ath Thawr 24.2597 54.64343 T SAND AE 01 0 17 Asia/Dubai 2011-11-06
+290656 WÄdÄ« ThawbÄn Wadi Thawban Wadi Thauban,Wadi Thawban,WÄdÄ« Thauban,WÄdÄ« ThawbÄn 25.28582 56.04058 H WAD AE 04 0 225 Asia/Dubai 2011-11-06
+290657 Jabal ThawbÄn Jabal Thawban Jabal Thawban,Jabal ThawbÄn 25.32944 56.10306 T MT AE 04 0 726 Asia/Dubai 2011-11-06
+290658 TharwÄnÄ«yah Tharwaniyah Tharwaniyah,Tharwaniyya,Tharwaniyyah,TharwÄniyya,TharwÄniyyah,TharwÄnÄ«yah 23.11088 54.01572 P PPL AE 01 0 191 Asia/Dubai 2011-11-06
+290659 Thara’awn Thara'awn Thara'awn,Thara’awn 22.91004 54.32293 T DPR AE 01 0 162 Asia/Dubai 2011-11-06
+290660 Jabal ThÄnÄ« Jabal Thani Jabal Thanais,Jabal Thani,Jabal ThÄnÄ« 25.02249 55.78912 T HLL AE 06 0 267 Asia/Dubai 2011-11-06
+290661 Thamūd Thamud Thamud,Thamūd 24.78333 55.28333 T TRGD AE 03 0 88 Asia/Dubai 2011-11-06
+290662 Barqat ThÄmir Barqat Thamir Barqat Thamir,Barqat ThÄmir 23.79619 52.66627 T DUNE AE 01 0 74 Asia/Dubai 2011-11-06
+290663 Bid‘at ThallÄb Bid`at Thallab Bid`at Thallab,Bid`ath Thalab,Bid‘at ThallÄb,Bid‘ath ThalÄb 23.83333 53.3 H WLL AE 01 0 89 Asia/Dubai 2011-11-06
+290664 Ţayyibah Tayyibah Taiyibah,Tayibah,Tayiban,Tayyibah,Ţayyibah 25.41228 56.17075 P PPL AE 04 0 430 Asia/Dubai 2011-11-06
+290665 Å¢awÄ« Å¢ayy Tawi Tayy Tawi Tai,Tawi Tayy,Å¢awÄ« Å¢ayy,Å¢ÄwÄ« Tai 25.23333 55.55 H WLL AE 03 0 42 Asia/Dubai 2011-11-06
+290666 Ţawī Ţayrī Tawi Tayri Tawi Tayri,Ţawī Ţayrī 25.45472 55.60611 H WLL AE 07 0 16 Asia/Dubai 2011-11-06
+290667 Ḩadd aţ Ţayr Hadd at Tayr Hadd at Tayr,Ḩadd aţ Ţayr 24.3713 51.83258 H RF AE 01 0 -9999 Asia/Dubai 2011-11-06
+290668 Nadd aÅ£ Å¢arÅ«sh Nadd at Tarush Nadd Tawsha,Nadd Tawshah,Nadd at Tarush,Nadd aÅ£ Å¢arÅ«sh,Nadd Å¢awshah,Nadd Å¢awshÄ 25.14929 55.37296 T DUNE AE 03 0 22 Asia/Dubai 2011-11-06
+290669 WÄdÄ« Å¢awÄ«yayn Wadi Tawiyayn Wadi Tawiyayn,WÄdÄ« Å¢awÄ«yayn 25.5575 56.07694 H WAD AE 04 0 185 Asia/Dubai 2011-11-06
+290670 Ţawīyayn Tawiyayn Tawiyain,Tawiyayn,Tawyayn,Tuwiyain,Ţawīyayn 25.55778 56.07667 H WLL AE AE 04 0 185 Asia/Dubai 2011-11-06
+290671 Ţawī Bin ‘Asīl Tawi Bin `Asil Tawi Bin `Asil,Ţawī Bin ‘Asīl 24.20368 54.6095 L LCTY AE 01 0 31 Asia/Dubai 2011-11-06
+290672 Ţawī Bid‘ Sa‘īd Tawi Bid` Sa`id Tawi Bid` Sa`id,Ţawī Bid‘ Sa‘īd 24.45084 54.74048 T SAND AE 01 0 33 Asia/Dubai 2011-11-06
+290673 WÄdÄ« TawÄh Wadi Tawah Wadi Tawah,WÄdÄ« TawÄh 24.98978 56.1243 H WAD AE 05 0 295 Asia/Dubai 2011-11-06
+290674 Jabal TawÄh Jabal Tawah Jabal Tawah,Jabal TawÄh 24.99639 56.12398 T MT AE 05 0 306 Asia/Dubai 2011-11-06
+290675 Ţawī Tasharawīyah Tawi Tasharawiyah Tawi Tasharawiyah,Ţawī Tasharawīyah 25.28942 55.89526 H WLL AE 06 0 105 Asia/Dubai 2011-11-06
+290676 Ţarūqah Taruqah Taruqah,Ţarūqah 23.02725 53.88404 T DPR AE 01 0 69 Asia/Dubai 2011-11-06
+290677 Ţarūfah Tarufah Tarufa,Tarufah,Ţarūfah 23.08936 53.83218 L OAS AE 01 0 86 Asia/Dubai 2011-11-06
+290678 Ţawī Tarish Tawi Tarish Tawi Tarish,Ţawī Tarish 23.58333 54.61667 H WLL AE 01 0 140 Asia/Dubai 2011-11-06
+290679 Ţarīqat Ja‘d Tariqat Ja`d Tariqat Ja`d,Ţarīqat Ja‘d 25.52852 56.15144 P PPL AE 04 0 357 Asia/Dubai 2011-11-06
+290680 Å¢arÄ«f KalbÄ Tarif Kalba Tarif Kalba,Å¢arÄ«f KalbÄ 25.0695 56.33115 P PPL AE 06 0 30 Asia/Dubai 2011-11-06
+290681 Ţarīf Tarif Al-Tarif,Al-Tarīf,At Tarif,At Turayf,Aţ Ţarīf,Aţ Ţurayf,Taraif,Tarif,Ţarīf 24.05399 53.76347 P PPL AE 01 0 24 Asia/Dubai 2011-11-06
+290682 Ţarīf Tarif Tarif,Ţarīf 24.03333 53.76667 T HLL AE 01 0 13 Asia/Dubai 2011-11-06
+290683 Qurayn aţ Ţarib Qurayn at Tarib Qurayn at Tarib,Qurayn aţ Ţarib 25.09414 55.81301 T HLL AE 06 0 216 Asia/Dubai 2011-11-06
+290684 Sayḩ Å¢arfÄ’ Sayh Tarfa' Sayh Tarfa',Sayḩ Å¢arfÄ’ 23.21358 55.18286 T DPR AE 01 0 154 Asia/Dubai 2011-11-06
+290685 MushÄsh Å¢arfÄ’ Mushash Tarfa' Mushash Tarfa',MushÄsh Å¢arfÄ’ 24.04622 51.69787 H WLL AE 01 0 34 Asia/Dubai 2011-11-06
+290686 Qarn at Tarb Qarn at Tarb Qarn al Tarab,Qarn at Tarb 24.45094 55.67408 T HLL AE 01 0 306 Asia/Dubai 2011-11-06
+290687 Ţaraq Taraq Taraq,Tereg,Ţaraq 23.11656 53.60697 P PPL AE 01 0 181 Asia/Dubai 2011-11-06
+290688 Å¢arÄhÄ«f Tarahif Tarahif,Å¢arÄhÄ«f 22.91769 53.33204 T DPR AE 01 0 179 Asia/Dubai 2011-11-06
+290689 WÄdÄ« aÅ£ Å¢araf Wadi at Taraf Wadi at Taraf,WÄdÄ« aÅ£ Å¢araf 25.41486 56.32874 H WAD AE 04 0 69 Asia/Dubai 2011-11-06
+290690 WÄdÄ« Tarabat Wadi Tarabat Wadi Tarabat,WÄdÄ« Tarabat 24.10167 55.71444 H WAD AE 01 0 235 Asia/Dubai 2011-11-06
+290691 Tall FÄḩah Tall Fahah Tall Fahah,Tall FÄḩah 23.95216 52.35299 L LCTY AE 01 0 14 Asia/Dubai 2011-11-06
+290692 Ḩadd aţ Ţallah Hadd at Tallah Hadd al Tahlei,Hadd at Tahli,Hadd at Tallah,Hadd at Thalei,Ḩadd at Tahlī,Ḩadd aţ Ţallah 24.66667 54.55 H SHOL AE AE 01 0 -9999 Asia/Dubai 2011-11-06
+290693 Dawḩat Tallah Dawhat Tallah Dawhat Tallah,Dawhat Tullah,Dawḩat Tallah,Dawḩat Tullah 24.42605 51.3286 H BGHT AE 01 0 1 Asia/Dubai 2011-11-06
+290694 Dawḩat Å¢allÄb Dawhat Tallab Dawhat Talab,Dawhat Tallab,Dawhat an Nakhlah,Dawḩat an Nakhlah,Dawḩat Å¢alab,Dawḩat Å¢allÄb,Dohat Tallab,Dohat Tullab,Dohat TullÄb,Dohat an Nakhala,Dohat ṬallÄb,Duhat an Nakhalah 24.2708 51.64849 H BAY AE 01 0 -9999 Asia/Dubai 2011-11-06
+290695 Sydney Hill Sydney Hill Sydney Hill 24.33333 52.6 T HLL AE 01 0 124 Asia/Dubai 2011-11-06
+290696 NaqÄ SuwayÅ£ah Naqa Suwaytah Naqa Suwaytah,NaqÄ SuwayÅ£ah 24.34939 55.59034 T DUNE AE 01 0 247 Asia/Dubai 2011-11-06
+290697 Å¢awÄ« SuwayḩÄn Tawi Suwayhan Tawi Suwayhan,Å¢awÄ« SuwayḩÄn 24.43584 55.25248 H WLL AE 01 0 123 Asia/Dubai 2011-11-06
+290698 Sayḩ SuwayḩÄn Sayh Suwayhan Sayh Suwayhan,Sayḩ SuwayḩÄn 24.44981 55.28275 H WAD AE 01 0 158 Asia/Dubai 2011-11-06
+290699 Ramlat SuwayḩÄn Ramlat Suwayhan Ramlat Suwaihan,Ramlat Suwayhan,Ramlat SuwayḩÄn 24.46087 55.26387 T DUNE AE 01 0 149 Asia/Dubai 2011-11-06
+290700 Ra’s Suwayfah Ra's Suwayfah Ra's Suwayfah,Ra’s Suwayfah 25.59594 56.35239 T PT AE 04 0 -9999 Asia/Dubai 2011-11-06
+290701 Farīq Suwayfah Fariq Suwayfah Fariq Suwayfah,Farīq Suwayfah 25.59256 56.34525 S CMP AE 04 0 60 Asia/Dubai 2011-11-06
+290702 Suwayfah Suwayfah Suwayfah 25.59043 56.36261 L LCTY AE 04 0 -9999 Asia/Dubai 2011-11-06
+290703 SuwaydÄn Suwaydan Suwaydan,SuwaydÄn 25.12436 55.7975 L AREA AE 06 0 138 Asia/Dubai 2011-11-06
+290704 WÄdÄ« SuwaydÄ’ Wadi Suwayda' Wadi Suwayda',WÄdÄ« SuwaydÄ’ 24.45696 55.54523 T TRGD AE 01 0 261 Asia/Dubai 2011-11-06
+290705 Ţawī SuwaydĒ Tawi Suwayda' Tawi Suwayda',Ţawī SuwaydĒ 25.14194 55.29972 H WLL AE 03 0 24 Asia/Dubai 2011-11-06
+290706 SuwaydÄ’ Suwayda' Suwaida,Suwayda',SuwaydÄ’ 25.11667 55.3 L LCTY AE 03 0 6 Asia/Dubai 2011-11-06
+290707 Sut Sut Sut 23.71667 54.53333 H WLL AE 01 0 133 Asia/Dubai 2011-11-06
+290708 Surayţ Surayt Serait,Surayt,Surayţ 23.12132 53.95075 L OAS AE 01 0 94 Asia/Dubai 2011-11-06
+290709 WÄdÄ« SÅ«r Wadi Sur Wadi Sur,WÄdÄ« SÅ«r 25.08333 56.36667 H WAD AE 06 0 -9999 Asia/Dubai 2011-11-06
+290710 Şūr Sur Sur,Şūr 25.09406 56.34827 P PPL AE 06 0 23 Asia/Dubai 2011-11-06
+290711 Suqayyah Suqayyah Suqayyah 24.57218 55.55891 L LCTY AE 01 0 253 Asia/Dubai 2011-11-06
+290712 Sunayyim Sunayyim Sunayyim 23.96236 55.40653 T DUNE AE 01 0 145 Asia/Dubai 2011-11-06
+290713 Baḩr Sunayţ Bahr Sunayt Bahr Sunayt,Baḩr Sunayţ 25.61667 56.25 H WAD AE 00 0 7 Asia/Dubai 2011-11-06
+290714 Sumbrair Sumbrair Sumbrair,Sumbrayir,Åžumbrayir 25.60082 56.2844 P PPL AE 04 0 21 Asia/Dubai 2011-11-06
+290715 Ra’s Sumayrah Ra's Sumayrah Ra's Sumayrah,Ras Semaira,Ras Sumaira,Ra’s Sumayrah,RÄs Semaira 24.32243 51.44653 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+290716 ImshÄsh as Sumayrah Imshash as Sumayrah Imshash Semaira,Imshash al-Semeirah,Imshash as Sumayrah,ImshÄsh Semaira,ImshÄsh al-Semeirah,ImshÄsh as Sumayrah,Tawi Sumayrah,Å¢awÄ« Sumayrah 24.27873 51.42068 H WLL AE 01 0 51 Asia/Dubai 2011-11-06
+290717 Dawḩat as Sumayrah Dawhat as Sumayrah Al Sumaira,Dawhat as Sumayrah,Dawḩat as Sumayrah 24.31884 51.54839 H BAY AE 01 0 -9999 Asia/Dubai 2011-11-06
+290718 Sayḩ as Sumayḩ Sayh as Sumayh Sayh as Sumayh,Sayḩ as Sumayḩ,Sih al-Semeih,Sih as Semeih,Sih as Sumayh,Sumayh,Sumayḩ,Sīḥ al-Semeiḥ,Sīḩ as Sumayḩ 24.7278 54.78697 T TRGD AE 01 0 16 Asia/Dubai 2011-11-06
+290719 Birkat Sumayḩ Birkat Sumayh Birkat Sumaih,Birkat Sumayh,Birkat Sumayḩ,Samaih,Semaih,Smeih 24.72276 54.77993 H WLL AE 01 0 18 Asia/Dubai 2011-11-06
+290720 Å¢awÄ« SulÅ£Än SÄlim Tawi Sultan Salim Tawi Sultan Salim,Å¢awÄ« SulÅ£Än SÄlim 25.02181 55.8195 H WLL AE 06 0 170 Asia/Dubai 2011-11-06
+290721 JazÄ«rat aÅŸ ŞīlÄ«yÄ Jazirat as Siliya Jazirat Sulayyah,Jazirat as Siliya,JazÄ«rat aÅŸ ŞīlÄ«yÄ,JazÄ«rat Åžulayyah 24.18286 52.8921 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290722 Å¢awÄ« SulaymÄt Tawi Sulaymat Al-Seleimat,Al-SeleimÄt,Tawi Sulaymat,Tawi Suleimat,TÄwÄ« Suleimat,Å¢awÄ« SulaymÄt 24.23064 55.59382 H WLL AE 01 0 251 Asia/Dubai 2011-11-06
+290723 Sayḩ SulaymÄt Sayh Sulaymat Sayh Sulaymat,Sayḩ SulaymÄt 24.19639 55.56278 T TRGD AE 01 0 224 Asia/Dubai 2011-11-06
+290724 Sayḩ SulaymÄn Sayh Sulayman Sayh Sulayman,Sayḩ SulaymÄn 24.67704 55.47051 T TRGD AE 03 0 149 Asia/Dubai 2011-11-06
+290725 Ḩadabat Sukhub Hadabat Sukhub Hadabat Sakhub,Hadabat Sukhub,Ḩadabat Sakhub,Ḩadabat Sukhub 24.82094 55.53095 T DUNE AE 03 0 157 Asia/Dubai 2011-11-06
+290726 Å¢awÄ« Suhaylah Tawi Suhaylah Tawi Saheila,Tawi Suhaylah,Tawi Suheila,TÄwÄ« Saheila,TÄwÄ« Suheila,Å¢awÄ« Suhaylah 25.36667 55.95 H WLL AE 07 0 184 Asia/Dubai 2011-11-06
+290727 Suḩaybah Suhaybah Suhaybah,Suḩaybah 24.93877 56.15754 P PPL AE 05 0 351 Asia/Dubai 2011-11-06
+290728 WÄdÄ« Suftah Wadi Suftah Wadi Suftah,WÄdÄ« Suftah 25.46364 56.11519 H WAD AE 05 0 297 Asia/Dubai 2011-11-06
+290729 Şufayrī Sufayri Sufayri,Şufayrī 24.81168 56.12646 P PPL AE 02 0 309 Asia/Dubai 2011-11-06
+290730 Sayḩ Şubrah Sayh Subrah Sayh Subrah,Sayḩ Şubrah 24.05023 55.49348 T TRGD AE 01 0 196 Asia/Dubai 2011-11-06
+290731 ‘Aqabat as Subaykhah `Aqabat as Subaykhah `Aqabat as Subaykhah,‘Aqabat as Subaykhah 25.5763 56.33881 T PASS AE 04 0 35 Asia/Dubai 2011-11-06
+290732 Subaykhah Subaykhah Subaykhah 25.5695 56.33907 L LCTY AE 04 0 256 Asia/Dubai 2011-11-06
+290733 Subayḩīyah Subayhiyah Subayhiyah,Subayḩīyah 25.40028 56.36417 V CULT AE 06 0 -9999 Asia/Dubai 2011-11-06
+290734 Stutter Shoal Stutter Shoal Stutter Shoal 24.23777 52.42787 H SHOL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290735 Stokes Bluff Stokes Bluff Stokes Bluff 24.3 52.63333 T CLF AE 01 0 6 Asia/Dubai 2011-11-06
+290736 Mount Stewart Mount Stewart Mount Stewart 24.33288 52.59674 T HLL AE 01 0 60 Asia/Dubai 2011-11-06
+290737 South YÄsÄt Channel South Yasat Channel South Yasat Channel,South YÄsÄt Channel 24.13797 51.98182 H CHNM AE 01 0 -9999 Asia/Dubai 2011-11-06
+290738 South Faraydat South Faraydat Faraijdat,Fereijid,South Faraydat,South Furayjidat,South FurayjidÄt 24.38333 51.71667 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290740 Å¢awÄ« Sirrah Tawi Sirrah Tawi Sirra,Tawi Sirrah,TÄwÄ« Sirra,Å¢awÄ« Sirrah 25.45114 55.61266 H WLL AE 07 0 31 Asia/Dubai 2011-11-06
+290741 Şīr Bū Nu‘ayr Sir Bu Nu`ayr Jazirat Siri Bu Naybar,Jazīrat Sīrī Bū Naybar,Jezirat Sir Bu Na`air,Jezirat Sir Bu Na‘air,Sir Abu Nu`air,Sir Abu Nu`ayr,Sir Abu Nu‘air,Sir Bu Nu`air,Sir Bu Nu`ayr,Sir bu Na`air Island,Sīr bu Na‘air Island,Şīr Abū Nu‘ayr,Şīr Bū Nu‘ayr,Ṣīr Bū Nu‘air 25.23305 54.2181 T ISL AE 01 0 15 Asia/Dubai 2011-11-06
+290742 Şīr BanÄ« YÄs Sir Bani Yas Al Yas,Al YÄs,Jezirat Yas,JezÄ«rat Yas,Sir Bani Yas,Sir Banias,Sir Beni Yas,Yas Island,Şīr BanÄ« YÄs 24.32589 52.60128 T ISL AE 01 0 124 Asia/Dubai 2011-11-06
+290743 Sīrat al Khawr Sirat al Khawr Sirat al Khawr,Sīrat al Khawr 25.35326 56.37784 T ISL AE 04 0 -9999 Asia/Dubai 2011-11-06
+290744 Suwayfat aş Şīr Suwayfat as Sir Suwayfat as Sir,Suwayfat aş Şīr 25.5188 56.36949 T PT AE 04 0 -9999 Asia/Dubai 2011-11-06
+290746 WÄdÄ« Sinnah Wadi Sinnah Wadi Sinnah,WÄdÄ« Sinnah 25.50796 56.19523 H WAD AE 04 0 124 Asia/Dubai 2011-11-06
+290747 Sinnah Sinnah Sinnah 25.50868 56.16772 P PPL AE 04 0 198 Asia/Dubai 2011-11-06
+290748 SinÄdil Sinadil Sinadil,SinÄdil 24.81095 56.02511 P PPL AE 02 0 424 Asia/Dubai 2011-11-06
+290749 Nada Sima Nada Sima Nada Sima 24.18333 55.36667 T DUNE AE 01 0 198 Asia/Dubai 2011-11-06
+290750 Ra’s as Silmīyah Ra's as Silmiyah Ra's as Silmiyah,Ra’s as Silmīyah 24.25278 54.52889 T DUNE AE 01 0 8 Asia/Dubai 2011-11-06
+290751 Silmīyah Silmiyah Silmiya,Silmiyah,Silmīya,Silmīyah 24.2 54.35 T HLL AE AE 01 0 6 Asia/Dubai 2011-11-06
+290752 Silmīyah Silmiyah Al-Silaimiyyah,Silmiyah,Silmīyah 24.22404 54.46374 T DUNE AE 01 0 13 Asia/Dubai 2011-11-06
+290753 Sayḩ Silm Sayh Silm Sayh Silm,Sayḩ Silm 24.41779 55.31025 T TRGD AE 01 0 158 Asia/Dubai 2011-11-06
+290754 Jabal Silḩū Bilḩū Jabal Silhu Bilhu Jabal Silhu Bilhu,Jabal Silḩū Bilḩū 25.15146 56.06771 T MT AE 05 0 607 Asia/Dubai 2011-11-06
+290755 Ra’s as Sila‘ Ra's as Sila` Ra's as Sil`,Ra's as Sila`,Ra’s as Sila‘,Ra’s as Sil‘ 24.05 51.78333 T PT AE 01 0 1 Asia/Dubai 2011-11-06
+290756 Dawḩat as Sila‘ Dawhat as Sila` Dawhat as Sila`,Dawḩat as Sila‘ 23.96667 51.93333 H INLT AE 01 0 -9999 Asia/Dubai 2011-11-06
+290757 WÄdÄ« SÄ«jÄ« Wadi Siji Wadi Nakh,Wadi Siji,Wadi as Siji,WÄdÄ« Nakh,WÄdÄ« SÄ«jÄ«,WÄdÄ« as SÄ«jÄ« 25.2736 56.03543 H WAD AE 00 0 225 Asia/Dubai 2011-11-06
+290758 Jabal Sījī Jabal Siji Jabal Siji,Jabal Sījī 25.2436 56.12194 T MT AE 04 0 664 Asia/Dubai 2011-11-06
+290759 Tawi Siji Tawi Siji Siji,Sījī,Tawi Siji,Tawi as Siji,Ţawī as Sījī 25.24764 56.07719 P PPL AE 04 0 324 Asia/Dubai 2011-11-06
+290760 Wadi Saha Wadi Saha Wadi Saha,Wadi Siha',WÄdÄ« ÅžihÄ’ 25.3828 56.29541 H WAD AE 00 0 320 Asia/Dubai 2011-11-06
+290761 Jabal ÅžihÄ’ Jabal Siha' Jabal Siha',Jabal ÅžihÄ’ 25.33931 56.276 T MT AE 00 0 788 Asia/Dubai 2011-11-06
+290762 WÄdÄ« SifÅ«nÄ« Wadi Sifuni Wadi Sifuni,WÄdÄ« SifÅ«nÄ« 25.19333 56.01389 H WAD AE 05 0 189 Asia/Dubai 2011-11-06
+290763 Sayḩ as Sidrah Sayh as Sidrah Sayh Sadrah,Sayh as Sidrah,Sayḩ Sadrah,Sayḩ as Sidrah,Sidra,Sih as Sidrah,Sīḩ as Sidrah 24.81824 54.9264 T TRGD AE 01 0 31 Asia/Dubai 2011-11-06
+290764 WÄdÄ« Sidr Wadi Sidr Wadi Sidr,WÄdÄ« Sidr 25.42182 56.09854 H WAD AE 04 0 290 Asia/Dubai 2011-11-06
+290765 Jabal Sidr Jabal Sidr Jabal Sidr 25.36833 56.34 T HLL AE 06 0 11 Asia/Dubai 2011-11-06
+290766 WÄdÄ« Sadakh Wadi Sadakh Wadi Sadakh,Wadi Sidakh,WÄdÄ« Sadakh,WÄdÄ« Sidakh 25.56929 56.06399 H WAD AE 04 0 132 Asia/Dubai 2011-11-06
+290767 SÄ«bat al AshkharÄt Sibat al Ashkharat Sibat al Ashkharat,SÄ«bat al AshkharÄt 25.61667 56.26667 S CMTY AE 04 0 19 Asia/Dubai 2011-11-06
+290768 WÄdÄ« Shuway Wadi Shuway Wadi Shuway,Wadi Shuwayy,WÄdÄ« Shuway,WÄdÄ« Shuwayy 25.23142 56.20244 H WAD AE 04 0 233 Asia/Dubai 2011-11-06
+290769 WÄdÄ« Shuways Wadi Shuways Wadi Shuways,WÄdÄ« Shuways 25.18957 56.33323 H WAD AE 04 0 347 Asia/Dubai 2011-11-06
+290770 Jabal Shuways Jabal Shuways Jabal Shuways 25.17617 56.3459 T HLL AE 04 0 171 Asia/Dubai 2011-11-06
+290771 Al Quwayz Al Quwayz Al Quwayz,Chuwais,Kuways,Shuways 25.78102 55.9787 P PPLX AE 05 0 24 Asia/Dubai 2011-11-06
+290772 JazÄ«rat ShuwayhÄt Jazirat Shuwayhat Jazirat Mashat,Jazirat Showayhat,Jazirat Shuwayhat,JazÄ«rat Mashat,JazÄ«rat ShowayhÄt,JazÄ«rat ShuwayhÄt,Shuwaihat,ShuwaihÄt,Shuweihat,ShuweihÄt 24.11391 52.43972 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290773 Bid‘ Shuwaybir Bid` Shuwaybir Bid` Shuwaybir,Bida` Shuwaibah,Bidau' Shuaibar,Bidau' Suaibar,Bidau’ Shuaibar,Bidau’ Suaibar,Bida‘ Shuwaibah,Bid‘ Shuwaybir,Budu` Shuaibar,Budu` Shuwaibir,Budu` Shuwaybir,Budu‘ Shuwaibir,Budū‘ Shuaibar,Budū‘ Shuwaybir,Shawaibir 24.0958 54.58919 H WLL AE 01 0 46 Asia/Dubai 2011-11-06
+290774 Shū Triyyah Shu Triyyah Shu Triyyah,Shū Triyyah 24.24938 54.29978 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290775 Shuray‘ah Shuray`ah Shuray`ah,Shuray‘ah 24.3 53.61667 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290776 Shunayyin Shunayyin Shunayyin 24.05604 54.46374 T SAND AE 01 0 39 Asia/Dubai 2011-11-06
+290777 Shunayyin Shunayyin Shunayyin 24.10632 54.39287 L LCTY AE 01 0 16 Asia/Dubai 2011-11-06
+290778 Shunayyin Shunayyin Shinaiyin,Shunaiyin,Shunayyin 24.21667 54.41667 T HLL AE AE 01 0 3 Asia/Dubai 2011-11-06
+290779 Buday‘ Shumrūkh Buday` Shumrukh Buday` Shumrukh,Buday‘ Shumrūkh,Shumrukh,Shumrūkh 24.77232 55.7849 H WLL AE 01 0 302 Asia/Dubai 2011-11-06
+290780 Sayḩ Shumayrīkh Sayh Shumayrikh Sayh Shumayrikh,Sayḩ Shumayrīkh 24.71557 55.4203 T TRGD AE 03 0 145 Asia/Dubai 2011-11-06
+290781 Shumaylīyah Shumayliyah Ash Shumailiya,Ash Shumailīya,Ash Shumayliyah,Ash Shumaylīyah,Shamailiya,Shamailiyah,Shamailīyah,Shamayliyah,Shamaylīyah,Shumailiyah,Shumayliyah,Shumaylīyah 25.33333 56.33333 L AREA AE AE 06 0 426 Asia/Dubai 2011-11-06
+290782 Jabal Shughayr Jabal Shughayr Jabal Shughayr 25.4 56.33333 T HLL AE 04 0 55 Asia/Dubai 2011-11-06
+290783 Ţawī Shubayşī Tawi Shubaysi Tawi Shubaysi,Ţawī Shubayşī 24.16278 55.05729 H WLL AE 01 0 120 Asia/Dubai 2011-11-06
+290784 Nadd ash ShibÄ Nadd ash Shiba Nad Shibeih,Nadd Shubayh,Nadd Shubayḩ,Nadd ash Shiba,Nadd ash ShibÄ 25.14583 55.33417 P PPL AE AE 03 0 23 Asia/Dubai 2011-11-06
+290785 Shu‘ayb ‘Ūd Shu`ayb `Ud Shu`ayb `Ud,Shu‘ayb ‘Ūd 24.74968 55.79034 V TREE AE 00 0 253 Asia/Dubai 2011-11-06
+290786 Shu‘ayb Şaghīr Shu`ayb Saghir Shu`ayb Saghir,Shu‘ayb Şaghīr 24.74944 55.82028 V TREE AE 01 0 311 Asia/Dubai 2011-11-06
+290787 Sayḩ Shu‘ayb Sayh Shu`ayb Sayh Shu`ayb,Sayh ash Shu`ayb,Sayḩ Shu‘ayb,Sayḩ ash Shu‘ayb,Shu`aib,Shu‘aib,Sih Shu`aib,Sīḩ Shu‘aib 24.89786 54.96311 T TRGD AE 00 0 17 Asia/Dubai 2011-11-06
+290788 Raml ash Shu‘ayb Raml ash Shu`ayb Raml ash Shu`ayb,Raml ash Shu‘ayb 24.8757 55.0429 T SAND AE 03 0 30 Asia/Dubai 2011-11-06
+290789 Shīshah Shishah Shisha,Shishah,Shīshah 24.33635 54.94465 T HLL AE 01 0 65 Asia/Dubai 2011-11-06
+290790 WÄdÄ« Shīşah Wadi Shisah Wadi Shisa,Wadi Shisah,WÄdÄ« Shīşah 25.84028 56.1 H WAD AE AE 05 0 321 Asia/Dubai 2011-11-06
+290791 Ramlat Shīşah Ramlat Shisah Ramlat Shisah,Ramlat Shīşah 25.38835 56.02995 T DUNE AE 04 0 304 Asia/Dubai 2011-11-06
+290792 WÄdÄ« ShÄ«s Wadi Shis Wadi Shis,WÄdÄ« ShÄ«s 25.29348 56.24396 H WAD AE 06 0 408 Asia/Dubai 2011-11-06
+290793 Shīs Shis Shis,Shīs 25.29138 56.24549 P PPL AE 06 0 365 Asia/Dubai 2011-11-06
+290794 WÄdÄ« ShimÄl Wadi Shimal Wadi Shimal,WÄdÄ« ShimÄl 25.50089 56.18876 H WAD AE 04 0 158 Asia/Dubai 2011-11-06
+290795 ShimÄl Shimal Shimal,Shimil,ShimÄl 25.8178 56.01088 P PPL AE 05 0 19 Asia/Dubai 2011-11-06
+290796 KhaÅ£mat ash Shiklah Khatmat ash Shiklah Khaim ash Shikla,Khatm al-Shiklah,Khatmat ash Shiklah,KhaÅ£mat ash Shiklah,Khaá¹m al-Shiklah 24.21583 55.95306 T SPUR AE AE 01 0 392 Asia/Dubai 2011-11-06
+290797 Shidq al Kalb Shidq al Kalb Shidq al Kalb,Shudeg al-Kalb,Shudq al Kalb 23.13385 53.73315 L OAS AE 01 0 93 Asia/Dubai 2011-11-06
+290798 WÄdÄ« ShibḩÄt Wadi Shibhat Wadi Shibhat,WÄdÄ« ShibḩÄt 24.45849 55.7685 T TRGD AE 01 0 321 Asia/Dubai 2011-11-06
+290799 Shabahanat al Majann Shabahanat al Majann Ash Shibhanah,Shabahanat al Majann,Shibhanat al Mijann,ShibhÄnat al Mijann 24.01997 51.735 T PLAT AE 01 0 48 Asia/Dubai 2011-11-06
+290800 ShibÄnÄt Shibanat Shibanat,ShibÄnÄt 24.91667 55.66667 L TRB AE 00 0 190 Asia/Dubai 2011-11-06
+290801 Shi‘b al GhÄf Shi`b al Ghaf Shi`b al Ghaf,Shib al Gat,Shi‘b al GhÄf 24.05278 55.71972 P PPL AE 01 0 237 Asia/Dubai 2011-11-06
+290802 ShibÄk Shibak Shibak,ShibÄk 24.59856 55.52346 L LCTY AE 01 0 208 Asia/Dubai 2011-11-06
+290803 Jabal Shi‘Äb ash Shaybah Jabal Shi`ab ash Shaybah Jabal Sha`ab ash Shaybah,Jabal Sha‘Äb ash Shaybah,Jabal Shi`ab ash Shaybah,Jabal Shi‘Äb ash Shaybah 25.40026 56.30431 T MT AE 04 0 410 Asia/Dubai 2011-11-06
+290804 WÄdÄ« ShÄ« Wadi Shi Wadi Shi,WÄdÄ« ShÄ« 25.34983 56.35746 H WAD AE 06 0 40 Asia/Dubai 2011-11-06
+290805 Shaykh ad DimÄth Shaykh ad Dimath Shaikh al Dimath,Shaykh ad Dimath,Shaykh ad DimÄth 24.01056 53.09528 L LCTY AE AE 01 0 29 Asia/Dubai 2011-11-06
+290806 Shaydad Bin Khanfūr Shaydad Bin Khanfur Shaydad Bin Khanfur,Shaydad Bin Khanfūr 24.6932 55.44164 T SAND AE 03 0 141 Asia/Dubai 2011-11-06
+290807 ‘Urūq ash Shaybah `Uruq ash Shaybah Uruq ash Shaibah,Uruq ash Sheibah,`Uruq ash Shaybah,‘Urūq ash Shaybah 22.35 54.91667 T DUNE AE 01 0 161 Asia/Dubai 2011-11-06
+290808 Sayḩ Shawyī Sayh Shawyi Sayh Shawyi,Sayḩ Shawyī 25.65293 56.01207 T PLN AE 05 0 17 Asia/Dubai 2011-11-06
+290809 WÄdÄ« Shawkah Wadi Shawkah Wadi Shaukha,Wadi Shawkah,WÄdÄ« Shawkah 25.26761 55.8668 H WAD AE 06 0 110 Asia/Dubai 2011-11-06
+290810 Ţawī Shawkah Tawi Shawkah Tawi Shawkah,Ţawī Shawkah 25.03056 56.10694 H WLL AE 05 0 699 Asia/Dubai 2011-11-06
+290811 Jabal Shawkah Jabal Shawkah Jabal Shauka,Jabal Shawkah 25.10014 56.04331 T HLL AE 05 0 442 Asia/Dubai 2011-11-06
+290812 Shawkah Shawkah Shauka,Shawkah,Shokah 25.1 56.03333 P PPL AE AE 05 0 283 Asia/Dubai 2011-11-06
+290813 WÄdÄ« Shawiyayn Wadi Shawiyayn Wadi Shawiyayn,WÄdÄ« Shawiyayn 25.27208 56.06068 H WAD AE 04 0 247 Asia/Dubai 2011-11-06
+290814 Shawīyah Shawiyah Shawiyah,Shawīyah 25.26792 56.08149 P PPL AE 04 0 304 Asia/Dubai 2011-11-06
+290815 Å¢awÄ« ShÄÅ£ Tawi Shat Tawi Shat,Å¢awÄ« ShÄÅ£ 24.48333 55.2 H WLLQ AE 01 0 139 Asia/Dubai 2011-11-06
+290816 ShÄt Shat Shat,ShÄt 24.48614 55.19806 T SAND AE 01 0 117 Asia/Dubai 2011-11-06
+290817 Sharqīyīn Sharqiyin Sharqiyan,Sharqiyin,Sharqīyīn 25.25 56.16667 L TRB AE AE 00 0 522 Asia/Dubai 2011-11-06
+290818 Å¢awÄ« SharqÄn Tawi Sharqan Tawi Sharqan,Å¢awÄ« SharqÄn 25.38333 55.4 H WLL AE 06 0 1 Asia/Dubai 2011-11-06
+290819 Jabal Sharmah Jabal Sharmah Jabal Sharmah 25.28406 56.12845 T MT AE 04 0 594 Asia/Dubai 2011-11-06
+290820 Sharm Sharm Sharam,Sharm 25.47078 56.35319 P PPL AE 04 0 14 Asia/Dubai 2011-11-06
+290821 Jabal Sharīyah Jabal Shariyah Jabal Shariyah,Jabal Sharīyah 25.29063 56.14987 T MT AE 04 0 415 Asia/Dubai 2011-11-06
+290822 Sharīyah Shariyah Shariyah,Sharīyah 25.1 56.03333 P PPL AE 05 0 283 Asia/Dubai 2011-11-06
+290823 Sharīyah Shariyah Shariyah,Sharīyah 24.81512 56.0986 P PPL AE 02 0 319 Asia/Dubai 2011-11-06
+290824 Khawr ash ShÄriqah Khawr ash Shariqah Khawr ash Shariqah,Khawr ash ShÄriqah,Khor Sharja,Khor Sharjah,Khor ShÄrja 25.33111 55.38222 H INLT AE AE 06 0 9 Asia/Dubai 2011-11-06
+290825 Sayḩ Sharbū Sayh Sharbu Sayh Sharbu,Sayḩ Sharbū 24.81667 55.41667 T TRGD AE 03 0 125 Asia/Dubai 2011-11-06
+290826 Raml Sharbū Raml Sharbu Raml Sharbu,Raml Sharbū 24.83333 55.41667 T SAND AE 03 0 130 Asia/Dubai 2011-11-06
+290827 Sharaf Tamī Sharaf Tami Sharaf Tami,Sharaf Tamī 23.62836 55.33692 T DUNE AE 01 0 207 Asia/Dubai 2011-11-06
+290828 Sharaf Sawqar Sharaf Sawqar Sharaf Sawqar 24.33333 55.7 L LCTY AE 01 0 275 Asia/Dubai 2011-11-06
+290829 Sharaf Mundassah Sharaf Mundassah Sharaf Mundassah 24.58472 55.73722 V TREE AE 01 0 308 Asia/Dubai 2011-11-06
+290830 Sharaf JabÄrah Sharaf Jabarah Sharaf Jabara,Sharaf Jabarah,Sharaf JabÄrah 24.62804 55.77334 L LCTY AE 01 0 337 Asia/Dubai 2011-11-06
+290831 Sharaf Ḩawrah Sharaf Hawrah Sharaf Hawrah,Sharaf Ḩawrah 24.54521 55.71675 T SAND AE 01 0 318 Asia/Dubai 2011-11-06
+290832 Sharaf á¸abbah Sharaf Dabbah Sharaf Dabbah,Sharaf á¸abbah 24.55321 55.65638 L LCTY AE 01 0 288 Asia/Dubai 2011-11-06
+290833 Sharaf Bin ‘Ubayd Sharaf Bin `Ubayd Sharaf Bin `Ubayd,Sharaf Bin ‘Ubayd 24.91667 55.4 L LCTY AE 03 0 100 Asia/Dubai 2011-11-06
+290834 Sharaf al ‘AfÄr Sharaf al `Afar Sharaf al `Afar,Sharaf al ‘AfÄr 24.9516 55.44056 L LCTY AE 03 0 85 Asia/Dubai 2011-11-06
+290835 WÄdÄ« SharabÄt Wadi Sharabat Wadi Sharabat,WÄdÄ« SharabÄt 24.88063 56.21317 H WAD AE 05 0 142 Asia/Dubai 2011-11-06
+290836 Jabal Sharab Jabal Sharab Jabal Sharab 25.0138 56.0337 T MT AE 05 0 664 Asia/Dubai 2011-11-06
+290837 Jabal Sha‘rÄ’ Jabal Sha`ra' Jabal Sha`arah,Jabal Sha`ra',Jabal Sha‘arah,Jabal Sha‘rÄ’ 24.06667 56.26667 T MT AE 01 0 995 Asia/Dubai 2011-11-06
+290838 WÄdÄ« Shaqq Wadi Shaqq Wadi Shaq,Wadi Shaqq,WÄdÄ« Shaqq 25.84389 56.02139 H WAD AE AE 05 0 26 Asia/Dubai 2011-11-06
+290839 Ţawī Shaqq Tawi Shaqq Tawi Shaqq,Ţawī Shaqq 24.38244 55.60894 H WLL AE 01 0 232 Asia/Dubai 2011-11-06
+290840 Ramlat Shanţūt Bin ‘UmÄn Ramlat Shantut Bin `Uman Ramlat Shantut Bin `Uman,Ramlat Shanţūt Bin ‘UmÄn 24.26667 55.4 T DUNE AE 01 0 195 Asia/Dubai 2011-11-06
+290841 Shanţūt Ibn ‘UmÄn Shantut Ibn `Uman Shantut Bin `Uman,Shantut Ibn `Uman,Shanţūt Bin ‘UmÄn,Shanţūt Ibn ‘UmÄn 24.25432 55.42166 T TRGD AE 01 0 183 Asia/Dubai 2011-11-06
+290842 Shanţūţ Shantut Shantut,Shanţūţ 23.38142 55.30225 T DUNE AE 01 0 154 Asia/Dubai 2011-11-06
+290843 Shantuba Shantuba Shantuba 24.54867 55.73806 L LCTY AE 01 0 322 Asia/Dubai 2011-11-06
+290844 Ra’s Shandaghah Ra's Shandaghah Ra's Shandaghah,Ra’s Shandaghah 25.26667 55.28333 T PT AE 03 0 -9999 Asia/Dubai 2011-11-06
+290845 Shandaghah Shandaghah Shandaghah 25.25833 55.28333 P PPLX AE 03 0 9 Asia/Dubai 2011-11-06
+290846 ‘Araj Shamsī `Araj Shamsi `Araj Shamsi,‘Araj Shamsī 24.18944 54.70361 T DPR AE 01 0 51 Asia/Dubai 2011-11-06
+290847 ShammÄmÄ«yah Shammamiyah Shammamiyah,ShammÄmÄ«yah 23.06374 53.68898 T DPR AE 01 0 76 Asia/Dubai 2011-11-06
+290848 ShÄmis Shamis Shamis,Shams,ShÄmis 23.93333 53.7 L GASF AE AE 01 0 21 Asia/Dubai 2011-11-06
+290849 ShÄmis Shamis Shamis,ShÄmis 23.93854 53.69593 S FCL AE 01 0 41 Asia/Dubai 2011-11-06
+290851 WÄdÄ« ash ShÄmah Wadi ash Shamah Wadi ash Shamah,WÄdÄ« ash ShÄmah 25.41592 56.33835 H WAD AE 00 0 36 Asia/Dubai 2011-11-06
+290852 WÄdÄ« ash Sha‘m Wadi ash Sha`m Wadi ash Sha`m,WÄdÄ« ash Sha‘m 26.03111 56.0975 H WAD AE 05 0 143 Asia/Dubai 2011-11-06
+290853 WÄdÄ« ash ShÄl Wadi ash Shal Wadi ash Shal,WÄdÄ« ash ShÄl 25.38365 55.95383 H WAD AE 07 0 155 Asia/Dubai 2011-11-06
+290854 WÄdÄ« Shakhkh Wadi Shakhkh Wadi Shakh,Wadi Shakhkh,WÄdÄ« Shakh,WÄdÄ« Shakhkh 25.58769 56.14287 H WAD AE 04 0 228 Asia/Dubai 2011-11-06
+290855 Haḑbat Shakhbūţ Hadbat Shakhbut Hadbat Shakhbut,Haḑbat Shakhbūţ 24.29583 54.65722 T HLL AE 01 0 27 Asia/Dubai 2011-11-06
+290856 Shaitham Shaitham Shaitham,Shaithan,ShaithÄn 24.29326 54.85926 H WLL AE 01 0 71 Asia/Dubai 2011-11-06
+290857 Sayḩ Sha‘īl Sayh Sha`il Sayh Sha`il,Sayḩ Sha‘īl 24.68333 55.45 T TRGD AE 03 0 160 Asia/Dubai 2011-11-06
+290858 Qarn ShÄ’i‘ Qarn Sha'i` Qarn Sha'i`,Qarn ShÄ’i‘ 23.83933 53.28344 T HLL AE 01 0 91 Asia/Dubai 2011-11-06
+290859 Å¢awÄ« ShÄhÄ«n Tawi Shahin Tawi Shahin,Å¢awÄ« ShÄhÄ«n 25.61667 55.88333 H WLL AE 05 0 93 Asia/Dubai 2011-11-06
+290860 ShahawÄt Shahawat Sahawat,Shahawat,ShahawÄt 25.96667 56.08333 P PPL AE AE 05 0 18 Asia/Dubai 2011-11-06
+290861 ShahÄ’irah Shaha'irah Shaha'irah,ShahÄ’irah 25.38333 56.13333 L TRB AE 05 0 501 Asia/Dubai 2011-11-06
+290862 WÄdÄ« ShÄh Wadi Shah Wadi Shah,WÄdÄ« ShÄh 25.82949 56.10949 H WAD AE 05 0 202 Asia/Dubai 2011-11-06
+290863 ShÄh Shah Shah,ShÄh 23.13561 53.91652 P PPL AE 01 0 170 Asia/Dubai 2011-11-06
+290864 ShÄh Shah Shah,ShÄh 22.85 53.91667 L OILF AE 01 0 170 Asia/Dubai 2011-11-06
+290865 ShÄh Shah Shah,ShÄh 23.13333 53.91667 T DPR AE 01 0 158 Asia/Dubai 2011-11-06
+290866 ShÄh Shah Shah,ShÄh 25.89861 56.12833 P PPL AE 05 0 715 Asia/Dubai 2011-11-06
+290867 Shabqatayn Shabqatayn Shabqatayn 23.91665 55.20997 T DUNE AE 01 0 178 Asia/Dubai 2011-11-06
+290868 Sha‘bīyah Sha`biyah Sha`biya,Sha`biyah,Sha‘biya,Sha‘bīyah 24.28333 54.48333 T HLL AE 01 0 12 Asia/Dubai 2011-11-06
+290869 Shabakah Shabakah Shabakah 25.03333 56 P PPL AE 05 0 322 Asia/Dubai 2011-11-06
+290870 WÄdÄ« Shabak Wadi Shabak Wadi Shabak,WÄdÄ« Shabak 25.15157 55.45444 T TRGD AE 03 0 22 Asia/Dubai 2011-11-06
+290871 Bid‘ Shabaan Bid` Shabaan Bid` Shabaan,Bid‘ Shabaan 23.35 54.36667 H WLL AE 01 0 133 Asia/Dubai 2011-11-06
+290872 Sha‘athÄn Sha`athan Sha`athan,Sha‘athÄn 24.27375 54.91292 T SAND AE 01 0 73 Asia/Dubai 2011-11-06
+290873 WÄdÄ« Sha‘arah Wadi Sha`arah Wadi Sha`arah,Wadi Sha`areh,WÄdÄ« Sha‘arah,WÄdÄ« Sha‘areh 25.06667 56.36667 H WAD AE 06 0 -9999 Asia/Dubai 2011-11-06
+290874 Sayyidah Sayyidah Sayyidah 24.6884 55.70747 V TREE AE 01 0 281 Asia/Dubai 2011-11-06
+290875 Ramlat Sayyid Ramlat Sayyid Ramlat Saiyin,Ramlat Sayyid,Ramle Sayyid 24.39496 55.60754 T DUNE AE 01 0 304 Asia/Dubai 2011-11-06
+290876 WÄdÄ« Sayraq Wadi Sayraq Wadi Sayraq,WÄdÄ« Sayraq 25.56185 56.05472 H WAD AE 04 0 127 Asia/Dubai 2011-11-06
+290877 Sayḩ MaḩÄrÄ« Sayh Mahari Sayh Mahari,Sayḩ MaḩÄrÄ« 25.38333 55.73333 T DUNE AE 06 0 51 Asia/Dubai 2011-11-06
+290878 Sayḩ aş Şaqlah Sayh as Saqlah Sayh as Saqlah,Sayḩ aş Şaqlah 24.88398 56.16847 P PPL AE 05 0 195 Asia/Dubai 2011-11-06
+290879 Sayḩ al Ḩalamah Sayh al Halamah Sayh al Halamah,Sayh al Halmah,Sayḩ al Ḩalamah,Sayḩ al Ḩalmah 24.25409 54.6919 T DPR AE 01 0 22 Asia/Dubai 2011-11-06
+290880 Sayḩ Sayh Sayh,Sayḩ 25.93333 56.06667 P PPL AE 05 0 196 Asia/Dubai 2011-11-06
+290881 Ţawī Sayfilman Tawi Sayfilman Tawi Sayfilman,Ţawī Sayfilman 23.6 54.6 H WLL AE 01 0 113 Asia/Dubai 2011-11-06
+290882 Ţawī Sayf Tawi Sayf Tawi Sayf,Ţawī Sayf 25.33472 55.81056 H WLL AE 06 0 79 Asia/Dubai 2011-11-06
+290883 Bid‘ Sayf Bid` Sayf Bid` Sayf,Bid‘ Sayf 25.06667 55.58333 H WLL AE 03 0 107 Asia/Dubai 2011-11-06
+290884 Bid‘ Sayf Bid` Sayf Bid` Sayf,Bid‘ Sayf 23.9195 54.93082 T DPR AE 01 0 105 Asia/Dubai 2011-11-06
+290885 Barqat Sayf Barqat Sayf Barqat Sayf,Burga Seif,Burqat Sayf 24.29874 52.58478 T HLL AE 01 0 34 Asia/Dubai 2011-11-06
+290886 Barqat Sayf Barqat Sayf Barqat Sayf 24.03307 53.27185 T HLL AE 01 0 14 Asia/Dubai 2011-11-06
+290887 WÄdÄ« SaybitalmÄ Wadi Saybitalma Wadi Saybitalma,WÄdÄ« SaybitalmÄ 25.23589 56.13664 H WAD AE 05 0 437 Asia/Dubai 2011-11-06
+290888 NaqÄ SawÅ£ah Naqa Sawtah Naqa Sawtah,NaqÄ SawÅ£ah 24.26611 55.55694 T DUNE AE 01 0 197 Asia/Dubai 2011-11-06
+290889 Jabal SawdÄ’ Jabal Sawda' Jaba Sawda',Jaba SawdÄ’,Jabal Sawda',Jabal SawdÄ’ 25.20083 56.33993 T HLL AE 04 0 377 Asia/Dubai 2011-11-06
+290890 Ra’s Abyaḑ QaÅ£Ä Ra's Abyad Qata Ra's Abyad Qata,Ra's Sawami`,Ra’s Abyaḑ QaÅ£Ä,Ra’s ÅžawÄmi‘ 24.14785 53.20015 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+290891 Jabal SÄÅ£if Jabal Satif Jabal Satif,Jabal SÄÅ£if 25.49887 56.04789 T MT AE 04 0 374 Asia/Dubai 2011-11-06
+290892 Saţḩ ar RÄzbÅ«t Sath ar Razbut Saath al Raazboot,Satah,Sath ar Razbut,Saţḩ ar RÄzbÅ«t 24.83333 53.16667 L OILF AE AE 01 0 -9999 Asia/Dubai 2011-11-06
+290893 SarÅ«q ZÄhir Saruq Zahir Saruq Zahir,SarÅ«q ZÄhir 24.21007 54.69939 H SBKH AE 01 0 32 Asia/Dubai 2011-11-06
+290894 Sarūq al Jasam Saruq al Jasam Saruq al Jasam,Sarūq al Jasam 24.55 55.43333 T DUNE AE 01 0 181 Asia/Dubai 2011-11-06
+290895 SÄrÅ«q Saruq Saruq,SÄrÅ«q 23.04905 53.73279 T DPR AE 01 0 75 Asia/Dubai 2011-11-06
+290896 Sarūj Saruj Saruj,Sarūj 25.26636 56.30934 P PPL AE 00 0 386 Asia/Dubai 2011-11-06
+290897 Sarūb Sarub Sarub,Sarūb 23.07561 53.69655 T DPR AE 01 0 164 Asia/Dubai 2011-11-06
+290898 Sayḩ Şarm Sayf Sayh Sarm Sayf Sayh Sarm Sayf,Sayḩ Şarm Sayf 23.68333 55.48333 T DPR AE 01 0 183 Asia/Dubai 2011-11-06
+290899 Ţawī ŞarmĒ Tawi Sarma' Tawi Saramin,Tawi Sarma',Tawi Sarmah,Ţawī Şaramin,Ţawī Şarmah,Ţawī ŞarmĒ 24.98018 55.88064 H WLL AE 06 0 203 Asia/Dubai 2011-11-06
+290900 Wuqnat Sarīj Wuqnat Sarij Wuqnat Sarij,Wuqnat Sarīj 22.97483 53.77287 T DPR AE 01 0 55 Asia/Dubai 2011-11-06
+290901 SarÄmÄ« Sarami Sarami,SarÄmÄ« 24.1 54.51667 T HLL AE 01 0 33 Asia/Dubai 2011-11-06
+290902 Ţawī Şaram Tawi Saram Sarm,Tawi Saram,Şarm,Ţawī Şaram 25.4925 56.02583 H WLL AE AE 05 0 114 Asia/Dubai 2011-11-06
+290903 Ra’s Sarab Ra's Sarab Ra's Sarab,Ras as-Sareb,Ra’s Sarab,RÄs as-Sareb 24.26252 51.78124 T PT AE 01 0 17 Asia/Dubai 2011-11-06
+290904 ÅžaqwÄn Saqwan Saqwan,ÅžaqwÄn 23.76608 53.49031 L GVL AE 01 0 105 Asia/Dubai 2011-11-06
+290905 MīnĒ Şaqr Mina' Saqr Mina Saqr Harbour,Mina' Saqr,MīnĒ Şaqr,Port Saqr 25.79167 55.95333 H HBR AE 05 0 -9999 Asia/Dubai 2011-11-06
+290906 WÄdÄ« Saqamqam Wadi Saqamqam Wadi Saqamqam,WÄdÄ« Saqamqam 25.16667 56.33333 H WAD AE 04 0 160 Asia/Dubai 2011-11-06
+290907 Sabkhat Saqamqam Sabkhat Saqamqam Sabkhat Saqamqam 25.15839 56.35618 H SBKH AE 04 0 16 Asia/Dubai 2011-11-06
+290908 Jabal Saqamqam Jabal Saqamqam Jabal Sakamkam,Jabal Saqamqam 25.18982 56.31074 T RDGE AE 04 0 145 Asia/Dubai 2011-11-06
+290909 Saqamqam Saqamqam As Saqamqam,Sakamkam,Saqamqam 25.17467 56.33039 P PPL AE 04 0 160 Asia/Dubai 2011-11-06
+290910 Saqal Saqal Saqal 25.27976 56.1859 V GRVP AE 05 0 386 Asia/Dubai 2011-11-06
+290911 Dawḩat ÅžÄniyah Dawhat Saniyah Dawhat Saniyah,Dawḩat ÅžÄniyah 24.15343 51.77192 H COVE AE 01 0 1 Asia/Dubai 2011-11-06
+290912 Khabrat Sanad Khabrat Sanad Khabrat Sanad 24.08333 51.7 H WLL AE 01 0 7 Asia/Dubai 2011-11-06
+290913 Sanad Sanad Sanad 24.1 51.7 L LCTY AE 01 0 14 Asia/Dubai 2011-11-06
+290914 WÄdÄ« SanÄbil Wadi Sanabil Wadi Sanabil,WÄdÄ« SanÄbil 24.66808 55.55881 T TRGD AE 00 0 221 Asia/Dubai 2011-11-06
+290915 SamnÄn Samnan Samnan,SamnÄn 25.3 55.45 L LCTY AE 00 0 13 Asia/Dubai 2011-11-06
+290916 Å¢awÄ« SamḩÄn Tawi Samhan Tawi Samhan,Tawi Semhan,Tawi Simhan,Å¢awÄ« SamḩÄn 24.82866 55.44335 H WLL AE 03 0 122 Asia/Dubai 2011-11-06
+290917 Raml SamḩÄn Raml Samhan Raml Samhan,Raml SamḩÄn 24.83333 55.46667 T SAND AE 03 0 129 Asia/Dubai 2011-11-06
+290918 SamḩÄn Samhan Al Irma,Samhan,SamḩÄn,Semhan 24.83333 55.46667 T HLL AE AE 03 0 129 Asia/Dubai 2011-11-06
+290919 Å¢awÄ« SamḩÄ’ Tawi Samha' Tawi Samha',Å¢awÄ« SamḩÄ’ 25.56444 55.82694 H WLL AE 07 0 46 Asia/Dubai 2011-11-06
+290920 Samarat Samarat Samarat 25.68907 56.13698 P PPL AE 01 0 1211 Asia/Dubai 2011-11-06
+290921 Sayh Samarah Sayh Samarah Sayh Samarah 24.21929 55.43518 T TRGD AE 01 0 205 Asia/Dubai 2011-11-06
+290922 WÄdÄ« SamÄḩ Wadi Samah Wadi Samah,WÄdÄ« SamÄḩ 25.44621 55.98338 H WAD AE 05 0 125 Asia/Dubai 2011-11-06
+290923 Jabal SamÄḩ Jabal Samah Jabal Samah,Jabal SamÄḩ 25.12605 56.17094 T MT AE 00 0 954 Asia/Dubai 2011-11-06
+290924 Jabal SamÄḩ Jabal Samah Jabal Samah,Jabal SamÄḩ 25.4165 55.96082 T DUNE AE 05 0 204 Asia/Dubai 2011-11-06
+290925 SamÄḩ Samah Samah,SamÄḩ 24.91667 56.3 L TRB AE 00 0 108 Asia/Dubai 2011-11-06
+290926 Salwá Salwa Salwa,Salwah,Salwá,Sawa,Sawá 25.02031 55.14772 L LCTY AE 03 0 20 Asia/Dubai 2011-11-06
+290927 Sayḩ Salmá Sayh Salma Sayh Salma,Sayḩ Salmá 24.16132 55.4748 T TRGD AE 01 0 237 Asia/Dubai 2011-11-06
+290928 Raml as Salmá Raml as Salma Raml as Salma,Raml as Salmá 24.16667 55.48333 T DUNE AE 01 0 243 Asia/Dubai 2011-11-06
+290929 Sayḩ as Salm Sayh as Salm Sayh as Salam,Sayh as Salm,Sayḩ as Salam,Sayḩ as Salm 24.83631 55.29693 T TRGD AE 03 0 88 Asia/Dubai 2011-11-06
+290930 Raml as Salm Raml as Salm Raml as Salam,Raml as Salm 24.85099 55.25704 T DUNE AE 03 0 55 Asia/Dubai 2011-11-06
+290931 WÄdÄ« Sallah Wadi Sallah Wadi Sallah,WÄdÄ« Sallah 24.56329 55.64776 T TRGD AE 01 0 279 Asia/Dubai 2011-11-06
+290932 Sayḩ Sallah Sayh Sallah Sayh Sallah,Sayḩ Sallah 23.93198 55.47704 T TRGD AE 01 0 186 Asia/Dubai 2011-11-06
+290933 Sallah Sallah Sallah 24.57314 55.61947 L LCTY AE 01 0 250 Asia/Dubai 2011-11-06
+290934 WÄdÄ« Sall Wadi Sall Wadi Sal,Wadi Sall,WÄdÄ« Sal,WÄdÄ« Sall 25.84083 56.06722 H WAD AE AE 05 0 75 Asia/Dubai 2011-11-06
+290935 Jabal Sall Jabal Sall Jabal Sal,Jabal Sall 25.76028 56.08861 T MT AE AE 05 0 799 Asia/Dubai 2011-11-06
+290936 Sall Sall Sal,Sall 25.74903 56.09217 V CULT AE 05 0 563 Asia/Dubai 2011-11-06
+290937 Falaj as Salj Falaj as Salj Falaj as Salj 25.44825 55.98449 H STMI AE 05 0 125 Asia/Dubai 2011-11-06
+290938 WÄdÄ« SalÄ«mah Wadi Salimah Wadi Salimah,Wadi Salum,WÄdÄ« SalÄ«mah,WÄdÄ« SalÅ«m 24.72335 55.66265 T TRGD AE 00 0 244 Asia/Dubai 2011-11-06
+290939 Shu‘bat Salīmah Shu`bat Salimah Shu`bat Salimah,Shu`bat Silima,Shu‘bat Salīmah,Shu‘bat Silima 24.0475 55.83278 H WAD AE 01 0 324 Asia/Dubai 2011-11-06
+290940 Baţn Salīmah Batn Salimah Batn Salima,Batn Salimah,Baţn Salima,Baţn Salīmah 24.91893 55.54126 T DPR AE 03 0 166 Asia/Dubai 2011-11-06
+290941 Salīmah Salimah Saleema,Salimah,Salīmah 22.9118 54.3397 T DPR AE 01 0 164 Asia/Dubai 2011-11-06
+290942 Ţawī Salīm Tawi Salim Tawi Salim,Ţawī Salīm 25.33896 55.82837 H WLL AE 07 0 79 Asia/Dubai 2011-11-06
+290943 Sayḩ SalÄ«l Sayh Salil Madiq al-Salil,Maá¸Ä«q al-SalÄ«l,Sayh Salil,Sayḩ SalÄ«l,Sih Salil 23.13333 55.41667 T TRGD AE 00 0 120 Asia/Dubai 2011-11-06
+290944 ÅžÄliḩīyah Salihiyah As Salihiyah,AÅŸ ÅžÄliḩīyah,Salihiyah,ÅžÄliḩīyah 25.72161 55.99081 P PPL AE 05 0 21 Asia/Dubai 2011-11-06
+290945 Saleh Saleh Saleh,Salih,ÅžÄliḩ 26.13333 55.75 L OILF AE 01 0 -9999 Asia/Dubai 2011-11-06
+290946 Jabal SalḩÄl Jabal Salhal Jabal Salhal,Jabal SalḩÄl 25.25969 56.18567 T MT AE 05 0 634 Asia/Dubai 2011-11-06
+290947 Salbūk Salbuk Salbuk,Salbūk 23.87316 53.76852 T DUNE AE 01 0 55 Asia/Dubai 2011-11-06
+290948 SalÄn KhalfÄn Salan Khalfan Salan Khalfan,SalÄn KhalfÄn 23.93776 53.52864 T DUNE AE 01 0 43 Asia/Dubai 2011-11-06
+290949 Sabkhat as Salamīyah Sabkhat as Salamiyah As Salamiyah Sabkhat,Sabkhat as Salamiyah,Sabkhat as Salamīyah 23.99347 53.76611 H SBKH AE 01 0 12 Asia/Dubai 2011-11-06
+290950 SalÄm Salam Salam,SalÄm 25.66583 55.8525 H WLL AE 07 0 66 Asia/Dubai 2011-11-06
+290951 Sabkhat Salal Sabkhat Salal Sabkhat Salal,Sabkhat Salil,Sabkhat Salīl 23.74706 55.12282 H SBKH AE 01 0 103 Asia/Dubai 2011-11-06
+290952 Salala Salala As Salahah,AÅŸ ÅžalÄḩah,Salahah,Salala,ÅžalÄḩah 24.1955 53.53829 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+290953 Bid‘ Salaal Bid` Salaal Bid` Salaal,Bid‘ Salaal 23.9 54.7 H WLL AE 01 0 90 Asia/Dubai 2011-11-06
+290954 Sala Sala Sala 24.16667 55.4 T DUNE AE 01 0 198 Asia/Dubai 2011-11-06
+290955 Jabal Sakhbīrī Jabal Sakhbiri Jabal Sakhbiri,Jabal Sakhbīrī 25.10639 56.03417 T HLL AE 05 0 366 Asia/Dubai 2011-11-06
+290956 SÄ’if Sa'if Sa'if,SÄ’if 25.1842 56.24205 P PPL AE 04 0 415 Asia/Dubai 2011-11-06
+290957 Å¢awÄ« Sa‘īdÄ« Tawi Sa`idi Tawi Sa`idi,Tawi Zaid,Tawi Zayd,Å¢awÄ« Sa‘īdÄ«,Å¢awÄ« Zayd,Å¢ÄwÄ« Zaid 25.54241 55.89153 H WLL AE 05 0 26 Asia/Dubai 2011-11-06
+290958 Å¢awÄ« Sa‘īd Bin HuwaydÄn Tawi Sa`id Bin Huwaydan Tawi Sa`id Bin Huwaydan,Å¢awÄ« Sa‘īd Bin HuwaydÄn 24.97961 55.78755 H WLL AE 06 0 172 Asia/Dubai 2011-11-06
+290959 Bid‘ Sa‘īd Bid` Sa`id Bada Sa`id,Bada Sa‘īd,Bid` Sa`id,Bid‘ Sa‘īd 23.65747 52.49121 H WLL AE 01 0 68 Asia/Dubai 2011-11-06
+290960 SÄ’ibah Sa'ibah Sa'iba,Sa'ibah,Sa’iba,SÄ’ibah 22.95698 54.27151 T DPR AE 01 0 98 Asia/Dubai 2011-11-06
+290961 GhaffÄt SaḩmÄ« Ghaffat Sahmi Ghaffat Sahmi,GhaffÄt SaḩmÄ« 24.5 55.26667 T DUNE AE 01 0 123 Asia/Dubai 2011-11-06
+290962 Sayḩ Sahmah Sayh Sahmah Sayh Sahmah,Sayḩ Sahmah 23.15 55.25 T DPR AE 00 0 104 Asia/Dubai 2011-11-06
+290963 Ḩaql Sahl Haql Sahl Haql Sahl,Sahal,Sahel,Sahil,Ḩaql Sahl 23.70594 54.32028 L OILF AE 01 0 114 Asia/Dubai 2011-11-06
+290964 WÄdÄ« Åžaḩanah Wadi Sahanah Wadi Sahanah,WÄdÄ« Åžaḩanah 25.28025 56.32255 H WAD AE 00 0 63 Asia/Dubai 2011-11-06
+290965 Saḩanah Sahanah Sahanah,Saḩanah 25.28556 56.3084 P PPL AE 06 0 99 Asia/Dubai 2011-11-06
+290966 SaḩÄb Sahab Sahab,SaḩÄb 25.15848 56.08929 L LCTY AE 05 0 286 Asia/Dubai 2011-11-06
+290967 Å¢awÄ« SÄji‘ah Tawi Saji`ah Tawi Saghyah,Tawi Saja'a,Tawi Saja`ah,Tawi Saji`ah,TÄwÄ« Saja’a,Å¢awÄ« Saghyah,Å¢awÄ« SÄja‘ah,Å¢awÄ« SÄji‘ah 25.30552 55.58034 H WLL AE 06 0 39 Asia/Dubai 2011-11-06
+290968 Saghyah Saghyah Saghyah,Sajaa 25.25 55.78333 L GASF AE AE 06 0 113 Asia/Dubai 2011-11-06
+290969 Sagh‘ayn Sagh`ayn Sagh'ain,Sagh`ayn,Sagh‘ayn,Sagh’ain 24.53333 51.11667 H WLL AE 01 0 3 Asia/Dubai 2011-11-06
+290970 WÄdÄ« aÅŸ ÅžafÅŸaf Wadi as Safsaf Wadi as Safsaf,WÄdÄ« aÅŸ ÅžafÅŸaf 25.33999 56.02183 H WAD AE 00 0 201 Asia/Dubai 2011-11-06
+290971 Ţawī Şafşaf Tawi Safsaf Tawi Safsaf,Ţawī Şafşaf 25.32833 56.01056 H WLL AE 02 0 219 Asia/Dubai 2011-11-06
+290972 Jabal ÅžafÅŸaf Jabal Safsaf Jabal Safsaf,Jabal ÅžafÅŸaf 25.33111 56.02306 T HLL AE 02 0 198 Asia/Dubai 2011-11-06
+290973 WÄdÄ« Åžafad Wadi Safad Wadi Safad,WÄdÄ« Åžafad 25.22915 56.35145 H WAD AE 04 0 12 Asia/Dubai 2011-11-06
+290974 Jabal Åžafad Jabal Safad Jabal Safad,Jabal Åžafad 25.23315 56.28695 T MT AE 04 0 640 Asia/Dubai 2011-11-06
+290975 Åžafad Safad Safad,Sufad,Åžafad 25.22165 56.3239 P PPL AE 04 0 71 Asia/Dubai 2011-11-06
+290976 WÄdÄ« ÅžafÄ Wadi Safa Wadi Safa,WÄdÄ« ÅžafÄ 25.06554 55.26203 T TRGD AE 03 0 30 Asia/Dubai 2011-11-06
+290977 Ra’s aş Şadr Ra's as Sadr Ra's as Sadr,Ra’s aş Şadr 24.6753 54.65663 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+290978 Ra’s as Sa‘dÄ«yÄt Ra's as Sa`diyat Ra's as Sa`diyat,Ras al Sa`diya,Ras al Sa‘diya,Ra’s as Sa‘dÄ«yÄt 24.58542 54.47465 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+290979 Khawr as Sa‘dÄ«yÄt Khawr as Sa`diyat Khawr Essadiyat,Khawr EssÄdiyÄt,Khawr as Sa`diyat,Khawr as Sa‘dÄ«yÄt 24.62296 54.46416 H CHNM AE 01 0 -9999 Asia/Dubai 2011-11-06
+290980 Ţawī Sa‘dīyah Tawi Sa`diyah Tawi Sa`diyah,Ţawī Sa‘dīyah 24.87867 56.16899 H WLL AE 05 0 191 Asia/Dubai 2011-11-06
+290981 Sa‘dīyah Sa`diyah Sa`diyah,Sa‘dīyah 23.69426 54.10408 H WLL AE 01 0 97 Asia/Dubai 2011-11-06
+290982 Sa‘dÄ«yah Sa`diyah Sa`diyah,Sa‘dÄ«yah,Tawi Sa'adiya,TÄwÄ« Sa’adiya 25.1 55.7 V TREE AE 06 0 156 Asia/Dubai 2011-11-06
+290983 Å¢awÄ« as SÄdd Tawi as Sadd Sa`ad,Sa‘ad,Tawi Sa`d,Tawi as Sadd,Å¢awÄ« Sa‘d,Å¢awÄ« as SÄdd 24.20806 55.50417 H WLLS AE AE 01 0 211 Asia/Dubai 2011-11-06
+290984 Raml as SÄdd Raml as Sadd Raml Sa`d,Raml Sa‘d,Raml as Sadd,Raml as SÄdd 24.2 55.48333 T DUNE AE AE 01 0 213 Asia/Dubai 2011-11-06
+290985 Jabal Sa‘d Jabal Sa`d Jabal Sa`d,Jabal Sa‘d 25.11355 56.11717 T MT AE 05 0 427 Asia/Dubai 2011-11-06
+290986 Sa‘d Sa`d Sa`d,Sa‘d 25.26667 56.28333 P PPL AE 06 0 213 Asia/Dubai 2011-11-06
+290987 Ramlat Åžabr Ramlat Sabr Ramlat Sabar,Ramlat Sabr,Ramlat Åžabr 24.03866 55.4161 T DUNE AE 01 0 202 Asia/Dubai 2011-11-06
+290988 Sabkhah Sabkhah Sabakha,Sabkhah 23.3 53.85 P PPL AE AE 01 0 136 Asia/Dubai 2011-11-06
+290989 Sabkhah Sabkhah Sabkha,Sabkhah,Sabukhah 23.12917 53.98181 P PPL AE 01 0 166 Asia/Dubai 2011-11-06
+290990 Sabkah Sabkah Sabka,Sabkah 25.29255 55.39472 H WLL AE 03 0 8 Asia/Dubai 2011-11-06
+290991 Ţawī Sabarad Tawi Sabarad Tawi Sabarad,Ţawī Sabarad 25.29389 55.89944 H WLL AE 06 0 144 Asia/Dubai 2011-11-06
+290992 Sabalah Sabalah Sabalah,Sablah 22.92596 53.45262 T DPR AE 01 0 61 Asia/Dubai 2011-11-06
+290993 SabÄ’is Saba'is Saba'is,SabÄ’is 24.83333 55.5 L TRB AE 03 0 169 Asia/Dubai 2011-11-06
+290994 Qarn Şa‘bah Qarn Sa`bah Qarn Sa`bah,Qarn Şa‘bah 24.46867 55.72733 T HLL AE 01 0 330 Asia/Dubai 2011-11-06
+290995 Sa‘abah Sa`abah Sa`abah,Sa`bah,Sa‘abah,Sa‘bah 24.99417 56.025 P PPLQ AE 05 0 306 Asia/Dubai 2011-11-06
+290996 NaqÄ Sa‘Ädah Naqa Sa`adah Naqa Sa`adah,NaqÄ Sa‘Ädah 23.42818 55.38215 T DUNE AE 01 0 195 Asia/Dubai 2011-11-06
+290997 Ţawī Ruwayyah Tawi Ruwayyah Tawi Mirdif,Tawi Ruwayyah,Ţawī Mirdif,Ţawī Ruwayyah 24.89283 55.66387 H WLL AE 03 0 209 Asia/Dubai 2011-11-06
+290998 Ruwayyah Ruwayyah Ruwayyah 24.8871 55.66016 L LCTY AE 03 0 205 Asia/Dubai 2011-11-06
+290999 Ra’s Ruwaysīyah Ra's Ruwaysiyah Ra's Harmiyah,Ra's Ru'aysiyah,Ra's Ruwaysiyah,Ra’s Harmīyah,Ra’s Ruwaysīyah,Ra’s Ru’aysīyah 24.12591 53.44243 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+291000 Ruwaysīyah Ruwaysiyah Ru'aysiyah,Ruwaisiyya,Ruwaysiyah,Ruwaysīyah,Ruweisiya,Ru’aysīyah 24.11927 53.4354 L LCTY AE 01 0 1 Asia/Dubai 2011-11-06
+291001 Sayḩ Ruwayshid Sayh Ruwayshid Sayh Ruwayshid,Sayḩ Ruwayshid 25.3 55.66667 T TRGD AE 06 0 76 Asia/Dubai 2011-11-06
+291002 Khawr ar Ruways Khawr ar Ruways Khawr ar Ru'ays,Khawr ar Ruways,Khawr ar Ru’ays 24.13966 52.67385 H INLT AE 01 0 1 Asia/Dubai 2011-11-06
+291003 Ruwaymayah Ruwaymayah Ruwaymayah 24.80098 55.64042 L LCTY AE 03 0 230 Asia/Dubai 2011-11-06
+291004 Jabal Ruwayḑah Jabal Ruwaydah Jabal Ruwaydah,Jabal Ruwayḑah 25.50556 56.08844 T MT AE 04 0 493 Asia/Dubai 2011-11-06
+291005 Ruwayḑah Ruwaydah Riweidah,Riweiá¸ah,Ruwaidha,Ruwaydah,Ruwayḑah 23.1039 53.97789 L OAS AE 01 0 98 Asia/Dubai 2011-11-06
+291006 RuwÄlÄ Ruwala Ruwala,Ruwalla,RuwÄlÄ 23.2956 54.20134 H SPNG AE 01 0 158 Asia/Dubai 2011-11-06
+291007 Ruqayyib Ruqayyib Ruqayyib 23.93586 52.88188 H WLL AE 01 0 59 Asia/Dubai 2011-11-06
+291008 Ruqayyah Ruqayyah Ruqayyah 23.28333 54.65 H WLL AE 01 0 126 Asia/Dubai 2011-11-06
+291009 Ruqayyah Ruqayyah Ruqayyah 23.03532 53.60933 L OAS AE 01 0 159 Asia/Dubai 2011-11-06
+291010 Shaqqat ar Ruqayţuwah Shaqqat ar Ruqaytuwah Ruqaytuwwah,Ruqayţuwwah,Shaqqat ar Ruqaytuwah,Shaqqat ar Ruqaytuwwah,Shaqqat ar Ruqayţuwah,Shaqqat ar Ruqayţuwwah 23.38663 55.25216 T TRGD AE 01 0 121 Asia/Dubai 2011-11-06
+291011 Sabkhat ar Ruqayţuwah Sabkhat ar Ruqaytuwah Sabkhat Bu Satma,Sabkhat Bū Satma,Sabkhat ar Ruqaytuwah,Sabkhat ar Ruqaytuwwah,Sabkhat ar Ruqayţuwah,Sabkhat ar Ruqayţūwwah 23.44808 55.11346 H SBKH AE 01 0 110 Asia/Dubai 2011-11-06
+291012 ‘Irq Ruqayţuwah `Irq Ruqaytuwah `Irq Ruqaytuwah,`Irq Ruqaytuwwah,‘Irq Ruqayţuwah,‘Irq Ruqayţuwwah 23.38449 55.20788 T DUNE AE 01 0 187 Asia/Dubai 2011-11-06
+291013 Ţawī Ruqayshah Tawi Ruqayshah Tawi Ruqayshah,Ţawī Ruqayshah 24.98485 55.78776 H WLL AE 06 0 163 Asia/Dubai 2011-11-06
+291014 Ruq‘at Ya‘Äribah Ruq`at Ya`aribah Raq`al al Jarba,Raq‘al al Jarba,Ruq`at Ya`aribah,Ruq‘at Ya‘Äribah 23.89652 55.48556 T TRGD AE 01 0 145 Asia/Dubai 2011-11-06
+291015 Ruq‘at Tulūl Ruq`at Tulul Ruq`at Tulul,Ruq‘at Tulūl 22.95827 55.20907 T DPR AE 01 0 126 Asia/Dubai 2011-11-06
+291016 Ruq‘at Sunayyim Ruq`at Sunayyim Ruq`at Sunayyim,Ruq‘at Sunayyim 23.65155 55.29791 T TRGD AE 01 0 114 Asia/Dubai 2011-11-06
+291017 Ruq‘at Ruqayţuwah Ruq`at Ruqaytuwah Ruq`at Ruqatuwwah,Ruq`at Ruqaytuwah,Ruq`at Ruqaytuwwah,Ruq‘at Ruqayţuwah,Ruq‘at Ruqayţuwwah,Ruq‘at Ruqaţuwwah 23.40594 55.28253 T DPR AE 01 0 121 Asia/Dubai 2011-11-06
+291018 Ruq‘at Nuşb Ruq`at Nusb Ruq`at Nusb,Ruq`at Nusub,Ruq‘at Nuşb,Ruq‘at Nuşub 24.05 55.41667 T SAND AE 01 0 173 Asia/Dubai 2011-11-06
+291019 Ruq‘at Muşallī Ruq`at Musalli Ruq`at Musalli,Ruq‘at Muşallī 24.85889 55.57612 T DUNE AE 03 0 195 Asia/Dubai 2011-11-06
+291020 Ruq‘at Musajira Ruq`at Musajira Ruq`at Musajira,Ruq‘at Musajira 24.84075 55.65624 T DUNE AE 03 0 228 Asia/Dubai 2011-11-06
+291021 Muḩayşinah Muhaysinah Muhaysinah,Muḩayşinah,Ruq`ah Muhassanah,Ruq`at Muhassanah,Ruq‘ah Muḩaşşanah,Ruq‘at Muḩaşşanah 25.25528 55.39278 L LCTY AE AE 03 0 15 Asia/Dubai 2011-11-06
+291022 Ruq‘at Maghis Ruq`at Maghis Ruq`at Maghis,Ruq‘at Maghis 23.61802 55.4466 T TRGD AE 01 0 139 Asia/Dubai 2011-11-06
+291023 Ruq‘at Ḩumaydī Ruq`at Humaydi Hameidha,Humaydah,Ruq`at Humaydi,Ruq‘at Ḩumaydī,Ḩumayḑah 24.79964 55.70185 T DUNE AE 03 0 260 Asia/Dubai 2011-11-06
+291024 Ruq‘at Ḩulayw Ruq`at Hulayw Ruq`at Hulayw,Ruq‘at Ḩulayw 23.63585 55.36908 T TRGD AE 01 0 157 Asia/Dubai 2011-11-06
+291025 Rafadah Rafadah Rafadah,Ruq`at Bu Zafidah,Ruq‘at Bū Z̧afīdah 24.86014 55.65022 T DUNE AE 03 0 217 Asia/Dubai 2011-11-06
+291026 Ruq‘at al Mīr Ruq`at al Mir Ruq`at al Mir,Ruq‘at al Mīr 22.98574 55.19227 H SBKH AE 01 0 100 Asia/Dubai 2011-11-06
+291027 Ruq‘at al ḨÄdh Ruq`at al Hadh Ruq`at al Hadh,Ruq‘at al ḨÄdh 22.94468 55.1886 H SBKH AE 01 0 105 Asia/Dubai 2011-11-06
+291028 Ruq‘at al ‘ArÄd Ruq`at al `Arad Raq`at al-`Arad,Raq‘at al-‘ArÄd,Ruq`at al Ard,Ruq`at al `Arad,Ruq‘at al Ard,Ruq‘at al ‘ArÄd 23.86265 55.50198 T TRGD AE 01 0 191 Asia/Dubai 2011-11-06
+291029 WÄdÄ« Rumḩ Wadi Rumh Wadi Rumh,WÄdÄ« Rumḩ 25.04006 56.32907 H WAD AE 06 0 24 Asia/Dubai 2011-11-06
+291030 Ţawī Rumḩ Tawi Rumh Tawi Rumh,Ţawī Rumḩ 25.01667 56.35 H WLL AE 06 0 7 Asia/Dubai 2011-11-06
+291031 Jabal Rumḩ Jabal Rumh Jabal Rumh,Jabal Rumḩ 25.02953 56.2807 T MT AE 04 0 369 Asia/Dubai 2011-11-06
+291032 Rumaylī Rumayli Rumayli,Rumaylī 26.02472 56.11083 V CULT AE 05 0 565 Asia/Dubai 2011-11-06
+291033 Jabal ar Rumaylah Jabal ar Rumaylah Jabal ar Rumaylah 25.43333 56.35 T HLL AE 04 0 34 Asia/Dubai 2011-11-06
+291034 Jabal Umayliḩ Jabal Umaylih Jabal Rumaylah,Jabal Umaylah,Jabal Umaylih,Jabal Umayliḩ 25.0503 55.82244 T HLL AE 06 0 253 Asia/Dubai 2011-11-06
+291035 RÅ«l á¸adnÄ Rul Dadna Rol Dadnah,Rol Ḍadnah,Rul Dadna,Rul Dadnah,Rul Dhadna,RÅ«l á¸adnah,RÅ«l á¸adnÄ 25.55481 56.34546 P PPL AE 04 0 26 Asia/Dubai 2011-11-06
+291036 Ţawī ar Rūl Tawi ar Rul Tawi ar Rul,Ţawī ar Rūl 25.57602 56.35214 H WLLQ AE 04 0 -9999 Asia/Dubai 2011-11-06
+291037 RukbÄt Rukbat Rukbat,RukbÄt 24.74428 54.65006 T SPIT AE 01 0 7 Asia/Dubai 2011-11-06
+291038 Ramlat ar Ruhayl Ramlat ar Ruhayl Ramlat ar Ruhayl 23.55 55.46667 T DUNE AE 00 0 188 Asia/Dubai 2011-11-06
+291039 Jabal Ruḩam Jabal Ruham Jabal Ruham,Jabal Ruḩam 25.13333 56.26667 T HLL AE 04 0 93 Asia/Dubai 2011-11-06
+291040 RughaylÄt Rughaylat Righeilat,RigheilÄt,Rughaylat,RughaylÄt,Rugheilat 25.10552 56.3563 P PPL AE 04 0 23 Asia/Dubai 2011-11-06
+291041 ‘Ayn ar Rufayşah `Ayn ar Rufaysah `Ayn ar Rufaysah,‘Ayn ar Rufayşah 25.35 56.31667 H SPNG AE 06 0 77 Asia/Dubai 2011-11-06
+291042 RufayÅŸah Rufaysah Rufaysah,RufayÅŸah 25.16778 56.17722 V CULT AE 01 0 389 Asia/Dubai 2011-11-06
+291043 Bandar Rudaym Bandar Rudaym Bandar Rudaym 24.06323 53.69867 H COVE AE 01 0 -9999 Asia/Dubai 2011-11-06
+291044 Å¢awÄ« Rubayyah Tawi Rubayyah Tawi Rabaiya,Tawi Rubayyah,TÄwÄ« Rabaiya,Å¢awÄ« Rubayyah 25.73333 56.05 H WLL AE AE 05 0 361 Asia/Dubai 2011-11-06
+291045 Rubay‘ah Rubay`ah Rubay`ah,Rubay‘ah 24.25 55.08333 T DPR AE 01 0 123 Asia/Dubai 2011-11-06
+291046 WÄdÄ« RiyÄmah Wadi Riyamah Wadi Riyama,Wadi Riyamah,WÄdÄ« RiyÄmah 25.55023 56.04815 H WAD AE 04 0 237 Asia/Dubai 2011-11-06
+291047 RiyÄmah Riyamah Riyama,Riyamah,RiyÄmah 25.54564 56.05836 P PPL AE 04 0 179 Asia/Dubai 2011-11-06
+291048 Jabal RiyÄdir Jabal Riyadir Jabal Riyadir,Jabal RiyÄdir 25.3 56.33333 T MT AE 06 0 42 Asia/Dubai 2011-11-06
+291049 Å¢awÄ« RiqÄnah Tawi Riqanah Riqana,Tawi Riqanah,Å¢awÄ« RiqÄnah 24.3418 55.69959 H WLL AE 01 0 310 Asia/Dubai 2011-11-06
+291050 ‘Uqlat ar Rimth `Uqlat ar Rimth `Uqlat ar Rimth,‘Uqlat ar Rimth 24.43333 51.11667 H WLL AE 01 0 6 Asia/Dubai 2011-11-06
+291051 RimÄḩ Rimah Al Ramah,Rimah,RimÄḩ 24.79667 55.61194 V TREE AE AE 03 0 224 Asia/Dubai 2011-11-06
+291052 Rimá Rima Rima,Rimá 25.16784 56.11795 P PPL AE 05 0 519 Asia/Dubai 2011-11-06
+291053 RiḩyÄt Rihyat Rihyat,RiḩyÄt 24.88151 55.44885 T SAND AE 03 0 152 Asia/Dubai 2011-11-06
+291054 Ţawī Rifđ Tawi Rifa` Tawi Rifa`,Ţawī Rifđ 24.96961 55.78658 H WLL AE 06 0 173 Asia/Dubai 2011-11-06
+291055 GhalÄ«lat RÄziqÄ« Ghalilat Raziqi Ghalilat Raziqi,GhalÄ«lat RÄziqÄ« 25.6 56.28333 H WADM AE 04 0 95 Asia/Dubai 2011-11-06
+291056 WÄdÄ« Rayj Wadi Rayj Wadi Rayj,WÄdÄ« Rayj 25.0183 55.2567 T TRGD AE 03 0 37 Asia/Dubai 2011-11-06
+291057 WÄdÄ« RaybÄ«yah Wadi Raybiyah Wadi Raibiya,Wadi Raybiyah,WÄdÄ« Raibiya,WÄdÄ« RaybÄ«yah 25.73472 56.02722 H WAD AE AE 05 0 9 Asia/Dubai 2011-11-06
+291058 Rawḑah Rawdah Rawdah,Rawḑah 24.09028 55.56194 T DPR AE 01 0 215 Asia/Dubai 2011-11-06
+291059 Ra’s SiyÄd Ra's Siyad Ra's Seyad,Ra's Siyad,Ra’s SeyÄd,Ra’s SiyÄd 24.55 51.65 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+291060 Ra’s Qal‘ah Ra's Qal`ah Ra's Qal`ah,Ra’s Qal‘ah 24.19222 55.58333 T DUNE AE 01 0 235 Asia/Dubai 2011-11-06
+291061 Ar RÄshidÄ«yah Ar Rashidiyah Ar Rashidiyah,Ar RÄshidÄ«yah,Rashdia,Rashidiyah,RashÄ«dÄ«yah 25.22365 55.38894 P PPLX AE 03 0 20 Asia/Dubai 2011-11-06
+291062 Å¢awÄ« RÄshid Bin Sa‘īd Tawi Rashid Bin Sa`id Tawi Rashid Bin Sa`id,Å¢awÄ« RÄshid Bin Sa‘īd 25.65528 55.95861 H WLL AE 05 0 22 Asia/Dubai 2011-11-06
+291063 Å¢awÄ« RÄshid Bin Ḩamad Tawi Rashid Bin Hamad Tawi Rashid Bin Hamad,Å¢awÄ« RÄshid Bin Ḩamad 25.24 55.91278 H WLL AE 06 0 136 Asia/Dubai 2011-11-06
+291064 RÄshid al GharbÄ« Rashid al Gharbi Rashid al Gharbi,RÄshid al GharbÄ«,Tawi Rashid Gharbiyah,Tawi Rashid al Gharbiyah,Å¢awÄ« RÄshid GharbÄ«yah,Å¢awÄ« RÄshid al GharbÄ«yah 25.35833 55.70528 H WLL AE AE 06 0 74 Asia/Dubai 2011-11-06
+291065 Å¢awÄ« RÄshidah Tawi Rashidah Tawi Rashidah,Å¢awÄ« RÄshidah 25.16667 55.96667 H WLL AE 05 0 234 Asia/Dubai 2011-11-06
+291066 Å¢awÄ« RÄshid Tawi Rashid Tawi Rashid,Å¢awÄ« RÄshid 25.33333 55.78333 H WLL AE 06 0 105 Asia/Dubai 2011-11-06
+291067 Nadd RÄshid Nadd Rashid Nadd Rashid,Nadd RÄshid 25.21944 55.40556 T DUNE AE 03 0 24 Asia/Dubai 2011-11-06
+291068 MÄ«nÄ’ RÄshid Mina' Rashid Mina' Rashid,MÄ«nÄ’ RÄshid,Port Rashid,Rachid Port 25.27056 55.29083 L PRT AE 03 0 -9999 Asia/Dubai 2011-11-06
+291069 Ḩaql RÄshid Haql Rashid Haql Rashid,Rashid Oilfield,Ḩaql RÄshid 25.23333 55.2 L OILF AE AE 03 0 -9999 Asia/Dubai 2011-11-06
+291070 Barqat RashÄ«d Barqat Rashid Bada` Rashid,Bada‘ Rashid,Barqat Rashid,Barqat RashÄ«d,Barqat RÄshid 24.00562 54.01885 T HLL AE 01 0 18 Asia/Dubai 2011-11-06
+291071 Raseen Raseen Raseen 23.68333 54.78333 H WLL AE 01 0 130 Asia/Dubai 2011-11-06
+291072 Rasan Rasan Rasan 24.15 55.21667 T DUNE AE 01 0 151 Asia/Dubai 2011-11-06
+291073 Khawr Ra’s al Khaymah Khawr Ra's al Khaymah Khawr Khaymah,Khawr Ra's al Khaymah,Khawr Ra’s al Khaymah 25.78333 55.95 H LGN AE 05 0 -9999 Asia/Dubai 2011-11-06
+291074 Ra’s al Khaymah Ra's al Khaymah Julfa,Khaimah,Ra's al Khaymah,Ras al Khaima,Ras al Khaimah,Ra’s al Khaymah,RÄs al Khaima,ras alkhymt,رأس الخيمة 25.78953 55.9432 P PPLA AE 05 115949 27 Asia/Dubai 2011-11-06
+291075 Ra’s al Khaymah Ra's al Khaymah Jasimi Shaikhdom of Ras al-Khaimah,Ra's al Khaymah,Ran al Khaima,Ras al Khaima,Ras el Khaimah,Ras el Khaïmah,Raʼs al Khaymah,Ra’s al Khaymah,Sheikhdom of Ras al Khaimah,State of Ras Al Khaima,ras alkhymt,رأس الخيمة 25.66667 56 A ADM1 AE 05 187535 12 Asia/Dubai 2011-11-05
+291076 WÄdÄ« Ra’s Wadi Ra's Wadi Ra's,WÄdÄ« Ra’s 25.10156 56.35801 H WAD AE 04 0 23 Asia/Dubai 2011-11-06
+291077 Jabal Ra’s Jabal Ra's Jabal Ra's,Jabal Ra’s 25.32634 56.3712 T MT AE 00 0 331 Asia/Dubai 2011-11-06
+291078 Sayḩ ar Raqq Sayh ar Raqq Sayh ar Raq,Sayh ar Raqq,Sayḩ ar Raq,Sayḩ ar Raqq 24.06667 55.83333 T TRGD AE AE 01 0 316 Asia/Dubai 2011-11-06
+291079 Jabal Rams Jabal Rams Jabal Rams 25.86917 56.06917 T MT AE 05 0 774 Asia/Dubai 2011-11-06
+291080 RamrÄmÄ«yÄt Ramramiyat Birkat al Ju,Ramramiyah,Ramramiyat,Ramrammiyat,RamrammiyÄt,RamramÄ«yah,RamrÄmÄ«yÄt 23.98231 53.7953 T HLL AE 01 0 68 69 Asia/Dubai 2011-11-06
+291081 Ḩabl RamrÄm Habl Ramram Habl Ramram,Ḩabl RamrÄm 24.9 55.3 T SAND AE 03 0 62 Asia/Dubai 2011-11-06
+291082 Ḩabl RamrÄm Habl Ramram Habl Ramram,Ḩabl RamrÄm 24.91667 55.3 T DUNE AE 03 0 74 Asia/Dubai 2011-11-06
+291083 Za‘bīl Za`bil Raml Sabil,Raml Za`bil,Raml Za‘bīl,Za`bil,Za‘bīl 25.20408 55.2875 P PPLX AE 03 0 6 Asia/Dubai 2011-11-06
+291084 Ramlat ZarÄrah Ramlat Zararah Ramlat Zarara,Ramlat Zararah,Ramlat ZarÄrah 22.90077 54.49992 L AREA AE 01 0 131 Asia/Dubai 2011-11-06
+291085 Ramlah Ramlah Ramla,Ramlah,RamlÄ 25.35979 56.04452 P PPL AE 04 0 264 Asia/Dubai 2011-11-06
+291086 JazÄ«rat RamḩÄn Jazirat Ramhan Jazirat Ramhan,JazÄ«rat RamḩÄn,Ramhan,RamḩÄn,jzyrt rmhan,جزيرة Ø±Ù…ØØ§Ù† 24.53778 54.52419 T ISL AE 01 0 10 Asia/Dubai 2011-11-06
+291087 Å¢awÄ« RamÄrÄ«m Tawi Ramarim Tawi Ramarim,Tawi Rimarim,Tawi Rumayram,TÄwÄ« Rimarim,Å¢awÄ« RamÄrÄ«m,Å¢awÄ« Rumayram 25.36083 55.535 H WLL AE 02 0 12 Asia/Dubai 2011-11-06
+291088 Ramaq Ramaq Ramaq 24.93333 55.13333 T SAND AE 03 0 42 Asia/Dubai 2011-11-06
+291089 QÅ«r ar RÄkibÄ« Qur ar Rakibi Qur ar Rakibi,QÅ«r ar RÄkibÄ« 25.5841 56.32873 T MT AE 04 0 357 Asia/Dubai 2011-11-06
+291090 NaqÄ RÄkah Naqa Rakah Naqa Rakah,NaqÄ RÄkah 23.95296 55.43484 T DUNE AE 01 0 232 Asia/Dubai 2011-11-06
+291091 GhÄf Rakaḑ Ghaf Rakad Ghaf Rakad,GhÄf Rakaḑ 24.51667 55.41667 T HLL AE 01 0 179 Asia/Dubai 2011-11-06
+291092 Sayḩ ar Rajībah Sayh ar Rajibah Sayh ar Rajibah,Sayḩ ar Rajībah 24.8 55.26667 T TRGD AE 03 0 71 Asia/Dubai 2011-11-06
+291093 Ḩadabat ar Rajībah Hadabat ar Rajibah Hadabat ar Rajibah,Ḩadabat ar Rajībah 24.8042 55.2656 T HLL AE 03 0 67 Asia/Dubai 2011-11-06
+291094 Raighnaan Raighnaan Raighnaan 23.16667 54.83333 H WLL AE 01 0 114 Asia/Dubai 2011-11-06
+291095 Raighan Raighan Raighan 23.26667 54.73333 H WLL AE 01 0 129 Asia/Dubai 2011-11-06
+291096 Raigha Raigha Raigha 24.44577 55.15561 T SAND AE 01 0 128 Asia/Dubai 2011-11-06
+291097 Raḩmah Rahmah Rahmah,Raḩmah 25.68 55.86361 H WLL AE 07 0 61 Asia/Dubai 2011-11-06
+291098 Rahfīyah Rahfiyah Rahfiya,Rahfiyah,Rahfīyah 22.79933 54.90797 T DPR AE 01 0 173 Asia/Dubai 2011-11-06
+291099 Jabal ar Raḩraḩ Jabal ar Rahrah Jabal Rahah,Jabal RÄḩah,Jabal ar Rahrah,Jabal ar Raḩraḩ 25.94833 56.14194 T MT AE AE 05 0 1586 Asia/Dubai 2011-11-06
+291100 WÄdÄ« Raḩbah Wadi Rahbah Wadi Rahabah,Wadi Rahbah,WÄdÄ« Raḩabah,WÄdÄ« Raḩbah 25.92472 56.07167 H WAD AE AE 05 0 12 Asia/Dubai 2011-11-06
+291101 Jabal Raḩabah Jabal Rahabah Jabal Rahabah,Jabal Raḩabah,Jabal ar Ra`aylah,Jabal ar Ra‘aylah 25.92583 56.11667 T MT AE AE 05 0 1539 Asia/Dubai 2011-11-06
+291102 Raḩabah Rahabah Rahabah,Raḩabah 25.98333 56.08333 L TRB AE 05 0 37 Asia/Dubai 2011-11-06
+291103 Raḩá Raha Raha,Raḩá 24.87639 56.2175 V CULT AE 05 0 140 Asia/Dubai 2011-11-06
+291105 NaqÄ Raghwah Naqa Raghwah Naqa Raghawa,Naqa Raghwah,NaqÄ Raghawa,NaqÄ Raghwah 24.44834 55.23357 T DUNE AE 01 0 141 Asia/Dubai 2011-11-06
+291106 Å¢awÄ« Rafī‘ah Tawi Rafi`ah Tawi Rafi`ah,Tawi Rafiya,Tawi Rayifah,TÄwÄ« Rafiya,TÄwÄ« Rayifah,Å¢awÄ« Rafī‘ah 25.31972 55.73583 H WLL AE 06 0 79 Asia/Dubai 2011-11-06
+291107 Rafaq Rafaq Rafaq 24.87471 56.24559 P PPL AE 05 0 321 Asia/Dubai 2011-11-06
+291108 Å¢awÄ« RafÄh Tawi Rafah Tawi Rafah,Å¢awÄ« RafÄh 25.28333 55.88333 H WLL AE 06 0 110 Asia/Dubai 2011-11-06
+291109 Å¢awÄ« RafÄdah Tawi Rafadah Tawi Rafaada,Tawi Rafadah,Å¢awÄ« Rafaada,Å¢awÄ« RafÄdah 24.89462 55.74432 H WLL AE 06 0 185 Asia/Dubai 2011-11-06
+291110 Rafđ Rafa` Rafa`,Rafđ 25.34701 56.35014 P PPL AE 06 0 40 Asia/Dubai 2011-11-06
+291111 Radūm Radum Radum,Radūm 23.09185 53.61473 L OAS AE 01 0 172 Asia/Dubai 2011-11-06
+291112 RadÄt Ramz Radat Ramz Radat Ramz,Radath Ramz,RadÄt Ramz 25.5 56.1 H WLL AE AE 05 0 218 Asia/Dubai 2011-11-06
+291113 Jabal ar Rabbah Jabal ar Rabbah Jabal ar Rabbah 25.39071 56.06641 T HLL AE 05 0 322 Asia/Dubai 2011-11-06
+291114 Sayḩ Rab‘ Sayh Rab` Sayh Rab`,Sayḩ Rab‘ 24.61506 54.86516 T TRGD AE 01 0 48 Asia/Dubai 2011-11-06
+291115 Quwayd Quwayd Quwayd 25.13333 56.05 L TRB AE 05 0 488 Asia/Dubai 2011-11-06
+291116 Quţūf Qutuf Qatuf,Qaá¹Å«f,Qutuf,Quţūf 23.10971 53.72738 P PPL AE 01 0 87 Asia/Dubai 2011-11-06
+291117 Quţūf Qutuf Qutuf,Quţūf 23.10794 53.69333 T DPR AE 01 0 80 Asia/Dubai 2011-11-06
+291118 Jabal Qutayyib Jabal Qutayyib Jabal Qutayyib 25.94139 56.09333 T HLL AE 05 0 881 Asia/Dubai 2011-11-06
+291119 Jabal al Qusīy Jabal al Qusiy Jabal al Qusiy,Jabal al Qusīy 25.06667 56.31667 T HLL AE 06 0 90 Asia/Dubai 2011-11-06
+291120 QushÄsh Qushash Qushash,QushÄsh 23.95539 53.62028 T SAND AE 01 0 14 Asia/Dubai 2011-11-06
+291121 Sabkhat Qusaywirah Sabkhat Qusaywirah Sabkhat Qusaywirah 22.77956 55.04968 H SBKH AE 01 0 76 Asia/Dubai 2011-11-06
+291122 Qusaywirah Qusaywirah Geseiwra,Jiseiwrah,Qusaywirah 22.81918 54.79949 L LCTY AE 01 0 117 Asia/Dubai 2011-11-06
+291124 QuÅŸaydÄt Qusaydat Qusaidat,Qusaydat,QuÅŸaydÄt 25.77461 55.97002 P PPLX AE 05 0 21 Asia/Dubai 2011-11-06
+291125 Qurmidah Qurmidah Garmada,GarmadÄ,Jarmada,JarmadÄ,Qarmadah,Qurmidah 23.11957 53.82855 P PPL AE 01 0 154 Asia/Dubai 2011-11-06
+291126 Qurmidah Qurmidah Qarmadah,Qurmidah 23.11234 53.82152 L OAS AE 01 0 86 Asia/Dubai 2011-11-06
+291127 Jazīrat Qurmah Jazirat Qurmah Jazirat Qurmah,Jazīrat Qurmah 25.76667 55.95 T ISL AE 05 0 20 Asia/Dubai 2011-11-06
+291128 Khawr Qurm Khawr Qurm Khawr Qurm 25.72056 55.84667 H LGN AE 05 0 -9999 Asia/Dubai 2011-11-06
+291129 Qurm Qurm Qurm 25.91139 56.06417 P PPL AE 05 0 29 Asia/Dubai 2011-11-06
+291130 WÄdÄ« Qurayyah Wadi Qurayyah Wadi Qaraiyat,Wadi Qurayyah,WÄdÄ« Qurayyah 25.24214 56.34825 H WAD AE 04 0 16 Asia/Dubai 2011-11-06
+291131 Jabal Qurayyah Jabal Qurayyah Jabal Qurayyah 25.25531 56.3374 T HLL AE 04 0 40 Asia/Dubai 2011-11-06
+291132 Quraytisah Quraytisah Quraytisah 23.57867 54.87448 H WLL AE 01 0 147 Asia/Dubai 2011-11-06
+291133 Ţawī Qurayn Tawi Qurayn Tawi Qurayn,Ţawī Qurayn 23.66667 54.7 H WLL AE 01 0 143 Asia/Dubai 2011-11-06
+291134 Sayḩ Qurayḩah Sayh Qurayhah Sayh Qurayhah,Sayḩ Qurayḩah 25.41667 55.7 T TRGD AE 07 0 77 Asia/Dubai 2011-11-06
+291135 Quraydah Quraydah Quraydah 23.13464 53.86398 L OAS AE 01 0 167 Asia/Dubai 2011-11-06
+291136 Qūr Qur Al Qawr,Al Qor,Quor,Qur,Qūr 24.91214 56.09566 P PPL AE 05 0 340 Asia/Dubai 2011-11-06
+291137 Qunayy Qunayy Qunayy 23.16667 54.38333 H WLL AE 01 0 151 Asia/Dubai 2011-11-06
+291138 Bid‘ al QumzÄn Bid` al Qumzan Bid` al Qumzan,Bid‘ al QumzÄn 23.78619 53.42782 H WLL AE 01 0 102 Asia/Dubai 2011-11-06
+291139 Qulaydah Qulaydah Qulaydah 25.1968 55.96372 T RDGE AE 06 0 213 Asia/Dubai 2011-11-06
+291140 Qufayyid Qufayyid Qiff Rajajil,Qiff RajÄjÄ«l,Qufayyid 23.7573 52.87017 T SAND AE 01 0 94 Asia/Dubai 2011-11-06
+291141 Sih al Qubua Sih al Qubua Sayh al Qubua,Sayḩ al Qubua,Sih al Qubua 24.31519 54.88739 T DPR AE 01 0 75 Asia/Dubai 2011-11-06
+291142 Qubbat BÅ« Lisail Qubbat Bu Lisail Qubbat Bu Lisail,Qubbat BÅ« Lisail 24.36204 55.64738 L LCTY AE 01 0 278 Asia/Dubai 2011-11-06
+291143 WÄdÄ« Qubbah Wadi Qubbah Wadi Qubbah,WÄdÄ« Qubbah 25.3444 56.13202 H WAD AE 00 0 760 Asia/Dubai 2011-11-06
+291144 Sayḩ QubaysÄt Sayh Qubaysat Sayh Qubaysat,Sayḩ QubaysÄt 24.31489 55.05401 T TRGD AE 01 0 95 Asia/Dubai 2011-11-06
+291145 GhaḑÄ’ QÅ«bah Ghada' Qubah Ghada' Qubah,GhaḑÄ’ QÅ«bah,Sayh as Salab,Sayḩ as Salab 24.26986 55.30432 T TRGD AE 01 0 141 Asia/Dubai 2011-11-06
+291146 Qu‘aysah Qu`aysah Ge'aisa,Ge’aisa,Je`eisah,Je‘eiṣah,Qu`aysah,Qu‘aysah 22.9941 54.22184 P PPL AE 01 0 151 Asia/Dubai 2011-11-06
+291147 Quar Ah Qahlish Quar Ah Qahlish Quar Ah Qahlish 25.985 56.08639 P PPL AE 05 0 10 Asia/Dubai 2011-11-06
+291148 Jabal Qitab Jabal Qitab Jabal Kitas,Jabal Qitab 25.02017 56.2411 T MT AE 00 0 1029 1004 Asia/Dubai 2011-11-06
+291149 Khawr QirqishÄn Khawr Qirqishan Khawr Qirqishan,Khawr QirqishÄn 24.37884 54.36899 H BAY AE 01 0 -9999 Asia/Dubai 2011-11-06
+291150 WÄdÄ« Qimah Wadi Qimah Wadi Qamh,Wadi Qima,Wadi Qimah,Wadi Qimh,WÄdÄ« Qamḩ,WÄdÄ« Qimah,WÄdÄ« Qimh,WÄdÄ« QÄ«má 24.79862 56.13736 H WAD AE 03 0 306 Asia/Dubai 2011-11-06
+291151 Jabal Qimah Jabal Qimah Jabal Qima,Jabal Qimah,Jabal Qīmá 24.79495 56.15749 T HLL AE 03 0 609 Asia/Dubai 2011-11-06
+291152 Qimah Qimah Qamh,Qamḩ,Qima,Qimah,Qimh,Qīmá 24.78668 56.14143 P PPL AE 03 0 322 Asia/Dubai 2011-11-06
+291153 Ra’s al QilÄ‘ Ra's al Qila` Qala`,Qala‘,Ra's al Qila`,Ras Djaliya,Ras Ijla,Ras Jaliya,Ras Jaliyah,Ras al Jala'a,Ras al Jala’a,Ras al-Qela',Ra’s al QilÄ‘,RÄs al-QelÄ’ 24.15507 52.97896 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+291154 Jabal Qidfa‘ Jabal Qidfa` Jabal Qidfa`,Jabal Qidfa‘ 25.33201 56.32246 T MT AE 06 0 473 Asia/Dubai 2011-11-06
+291155 Qidfa‘ Qidfa` Qidfa,Qidfa`,Qidfa‘,Qidfi`,Qidfi‘ 25.30426 56.37034 P PPL AE 06 0 45 Asia/Dubai 2011-11-06
+291156 Qayyat al Bawl Qayyat al Bawl Qayyat al Bawl 24.27465 54.62206 T DUNE AE 01 0 15 Asia/Dubai 2011-11-06
+291157 NaqÄ Qaydah Naqa Qaydah Naqa Qaydah,NaqÄ Qaydah 23.36667 55.4 T DUNE AE 00 0 174 Asia/Dubai 2011-11-06
+291158 Qawm ‘AnÄ«yÄt Qawm `Aniyat Qaum `Aniyat,Qaum ‘AnÄ«yÄt,Qawm `Aniyat,Qawm ‘AnÄ«yÄt 24.11544 54.34195 T HLL AE 01 0 21 Asia/Dubai 2011-11-06
+291159 QaţţÄrah Qattarah Al-Qatarah,Qatara,Qatarah,Qattara,Qattarah,QatÄrah,QaţţÄrah,Qaá¹á¹Ära 24.25993 55.75566 P PPL AE 01 0 292 Asia/Dubai 2011-11-06
+291160 Ţawī Qaţam Tawi Qatam Tawi Qatam,Ţawī Qaţam 24.0525 54.74417 H WLL AE 01 0 74 Asia/Dubai 2011-11-06
+291161 Qaţam Qatam Qatam,Qaţam 24.0532 54.73458 T DUNE AE 01 0 74 Asia/Dubai 2011-11-06
+291162 Qataiwa Qataiwa Qataiwa 23.41667 55.28333 H WLL AE 01 0 155 Asia/Dubai 2011-11-06
+291163 Qassīs Ḩayy Qassis Hayy Qassis Haiy,Qassis Hayy,Qassīs Haiy,Qassīs Ḩayy 24.1 55.63333 T DPR AE AE 01 0 250 Asia/Dubai 2011-11-06
+291164 Ramlat al Qassīs Ramlat al Qassis Ramlat al Qassis,Ramlat al Qassīs 24.08333 55.58333 T DUNE AE 01 0 238 Asia/Dubai 2011-11-06
+291165 QaÅŸÅŸÄbÄ« Qassabi Gassabi,GassÄbi,Jassabi,JaÅŸÅŸÄbÄ«,Qassabi,QaÅŸÅŸÄbÄ«,Qusabi,QuÅŸÄbÄ«,Umm al Majarib,Umm al MajÄrib 24.21093 54.1017 T ISL AE 01 0 11 Asia/Dubai 2011-11-06
+291166 QaÅŸr ÅžÄliḩ Qasr Salih Qasr Salih,QaÅŸr ÅžÄliḩ 23.92871 52.78831 T SAND AE 01 0 43 Asia/Dubai 2011-11-06
+291167 WÄdÄ« Qasmah Wadi Qasmah Wadi Qasma,Wadi Qasmah,WÄdÄ« Qasma,WÄdÄ« Qasmah 24.48968 55.53049 H WAD AE 01 0 251 Asia/Dubai 2011-11-06
+291168 Kharīmat Qaşīmah Kharimat Qasimah Kharimat Qasimah,Kharmat Qasimah,Kharmat Qaşīmah,Kharīmat Qaşīmah,Qasimah,Qaşimah 24.07524 54.89174 T TRGD AE 01 0 90 Asia/Dubai 2011-11-06
+291169 Qaşīmah Qasimah Qasimah,Qaşīmah 24.05154 54.90686 H WLL AE 01 0 83 Asia/Dubai 2011-11-06
+291170 Qarn Qashash Qarn Qashash Qarn Qashash 24.42261 55.59755 T HLL AE 01 0 275 Asia/Dubai 2011-11-06
+291171 Qarqarī Qarqari Qarqari,Qarqarī 24.31889 55.61222 T DPR AE 01 0 221 Asia/Dubai 2011-11-06
+291172 Qarnayn Qarnayn Al Qarnain,Al Qarnayn,Jazirat al Qarnayn,Jazīrat al Qarnayn,Jezirat Qarnain,Karnain Island,Qarnayn,Qarnein 24.93528 52.84972 T ISL AE AE 01 0 -9999 Asia/Dubai 2011-11-06
+291173 Qarḩat Suwayd Qarhat Suwayd Qarhat Suwaid,Qarhat Suwayd,Qarḩat Suwayd 24.5 55.68333 V TREE AE AE 01 0 286 Asia/Dubai 2011-11-06
+291174 WÄdÄ« Qarḩah Wadi Qarhah Wadi Qarhah,WÄdÄ« Qarḩah 25.36471 55.8084 T DPR AE 06 0 80 Asia/Dubai 2011-11-06
+291175 Sayḩ QarÄţīs Sayh Qaratis Sayh Qaratis,Sayḩ QarÄţīs 24.86667 55.55 T DPR AE 03 0 148 Asia/Dubai 2011-11-06
+291176 Ţawī al Qaran Tawi al Qaran Tawi al Qaran,Ţawī al Qaran 25.53317 55.74746 H WLL AE 07 0 49 Asia/Dubai 2011-11-06
+291177 QarÄmidah Qaramidah Qaramidah,QarÄmidah 23.14517 53.71552 L OAS AE 01 0 160 Asia/Dubai 2011-11-06
+291178 Å¢awÄ« al QÄrah Tawi al Qarah Alcara,Tawi al Qarah,Å¢awÄ« al QÄrah 25.69146 56.00238 H WLL AE 05 0 15 Asia/Dubai 2011-11-06
+291179 Qar‘ah Qar`ah Qar'a,Qar`ah,Qara'a,Qara’a,Qar‘ah,Qar’a 24.9 55.03333 T SAND AE 03 0 16 Asia/Dubai 2011-11-06
+291180 Å¢awÄ« Qarad Tawi Qarad Tawi Garhad,Tawi Qarad,Å¢awÄ« Qarad,Å¢ÄwÄ« Garhad 25.62735 55.75506 H WLL AE 07 0 23 Asia/Dubai 2011-11-06
+291181 Khawr Qanţūr Khawr Qantur Khawr Qantur,Khawr Qanţūr 24.15843 54.06167 H CHNM AE 01 0 -9999 Asia/Dubai 2011-11-06
+291182 Qamshī Qamshi Qamshi,Qamshī,Qemshi 24.74458 54.77306 T HLL AE 01 0 12 Asia/Dubai 2011-11-06
+291183 QamrÄ’ Qamra' Qamra',QamrÄ’ 23.03861 52.60583 H WLL AE 01 0 103 Asia/Dubai 2011-11-06
+291184 QamrÄ’ Qamra' Qamra',QamrÄ’ 23.01254 52.48934 L LCTY AE 01 0 85 Asia/Dubai 2011-11-06
+291185 Qamarah Qamarah Qamarah 23.71667 53.4 T HLL AE 01 0 81 Asia/Dubai 2011-11-06
+291186 Jabal al Qumr ash ShamÄlÄ« Jabal al Qumr ash Shamali Jabal al Qamar,Jabal al Qumr ash Shamali,Jabal al Qumr ash ShamÄlÄ« 25.46168 56.04501 T MT AE 05 0 561 Asia/Dubai 2011-11-06
+291187 Jabal al Qumr al Janūbī Jabal al Qumr al Janubi Jabal al Qamar,Jabal al Qumr al Janubi,Jabal al Qumr al Janūbī 25.43354 56.044 T MT AE 05 0 455 Asia/Dubai 2011-11-06
+291188 WÄdÄ« al QildÄ« Wadi al Qildi Wadi al Qaliddi,Wadi al Qildi,WÄdÄ« al QaliddÄ«,WÄdÄ« al QildÄ« 25.59763 55.98951 H WAD AE 04 0 36 Asia/Dubai 2011-11-06
+291189 WÄdÄ« al QaliddÄ« Wadi al Qaliddi Wadi al Qaliddi,WÄdÄ« al QaliddÄ« 25.56667 56.05 H WAD AE 04 0 346 Asia/Dubai 2011-11-06
+291190 ‘Aqabat al QaliddÄ« `Aqabat al Qaliddi Qalidda Pass,Qaliddi Pass,QÄliddi Pass,`Aqabat al Qaliddi,‘Aqabat al QaliddÄ« 25.53387 56.12393 T PASS AE 04 0 512 Asia/Dubai 2011-11-06
+291191 Qaiyaisha Qaiyaisha Qaiyaisha 25.88333 56.11667 L TRB AE 05 0 853 Asia/Dubai 2011-11-06
+291192 Ḩabl QÄ’imah Habl Qa'imah Habl Qa'im,Habl Qa'imah,Habl Qa’im,Ḩabl QÄ’imah 24.35345 54.73194 T SAND AE 01 0 46 Asia/Dubai 2011-11-06
+291193 QÄ’imah Qa'imah Gaimo,Qa'imah,QÄ’imah 24.32635 54.7658 H WLLS AE 01 0 53 Asia/Dubai 2011-11-06
+291194 Raml al Qaḩţ Raml al Qaht Raml al Qaht,Raml al Qaḩţ 24.73333 55.28333 T SAND AE 03 0 115 Asia/Dubai 2011-11-06
+291195 Ţawī Qafan Tawi Qafan Tawi Qafan,Ţawī Qafan 25.29778 55.88472 H WLL AE 06 0 118 Asia/Dubai 2011-11-06
+291196 WÄdÄ« Qada‘ah Wadi Qada`ah Wadi Ghayl,Wadi Qada`a,Wadi Qada`ah,Wadi Qadda`a,WÄdÄ« Qada‘a,WÄdÄ« Qada‘ah,WÄdÄ« Qadda‘a 25.79083 56.07842 H WAD AE 05 0 203 Asia/Dubai 2011-11-06
+291197 Jabal Qada‘ah Jabal Qada`ah Jabal Qada`a,Jabal Qada`ah,Jabal Qada‘a,Jabal Qada‘ah 25.77944 56.13973 T MT AE 05 0 1374 Asia/Dubai 2011-11-06
+291198 Qada‘ah Qada`ah Qada`a,Qada`ah,Qada‘a,Qada‘ah 25.76667 56.08333 V CULT AE 05 0 379 Asia/Dubai 2011-11-06
+291199 Qabas Qabas Qabas 26.03435 56.12302 P PPL AE 05 0 419 Asia/Dubai 2011-11-06
+291200 Qubaythah Qubaythah Qabaitha,Qubaythah 24.41472 55.69599 L LCTY AE 01 0 318 Asia/Dubai 2011-11-06
+291201 Petty Patches Petty Patches Petty Patches 24.48392 52.4424 H SHOL AE 01 0 -9999 Asia/Dubai 2011-11-06
+291204 NuwayrÄn Nuwayran Nuwayran,NuwayrÄn 24.0781 54.66495 T DUNE AE 01 0 56 Asia/Dubai 2011-11-06
+291205 Ţawī Nuşaylī Tawi Nusayli Tawi Nusayli,Ţawī Nuşaylī 24.34467 55.22058 H WLLS AE 01 0 133 Asia/Dubai 2011-11-06
+291206 Sabkhat Nuşaylī Sabkhat Nusayli Sabkhat Nusayli,Sabkhat Nuşaylī 24.32398 55.1741 H SBKH AE 01 0 124 Asia/Dubai 2011-11-06
+291207 Qarn NuÅŸaylah Qarn Nusaylah Al-Neseilah,Qarn Nusaylah,Qarn NuÅŸaylah 24.46222 54.63361 T DUNE AE AE 01 0 14 Asia/Dubai 2011-11-06
+291208 NuÅŸayb Nusayb Nusayb,NuÅŸayb 24.45 55.11667 T TRGD AE 01 0 123 Asia/Dubai 2011-11-06
+291209 Nuqayrah Nuqayrah Nuqayrah 23.30021 54.09107 P PPL AE 01 0 172 Asia/Dubai 2011-11-06
+291210 Nuqaydhah Nuqaydhah Nuqaydhah 23.02418 53.56632 T DPR AE 01 0 77 Asia/Dubai 2011-11-06
+291211 Ţawī Numayrīyah Tawi Numayriyah Nimairiya,Numairiya,Tawi Nimairiyyah,Tawi Numayriyah,Ţawī Numayrīyah 23.75469 54.42187 H WLL AE 01 0 104 Asia/Dubai 2011-11-06
+291212 Numayl Numayl Nemail,Numayl 23.13487 53.88304 L OAS AE 01 0 106 Asia/Dubai 2011-11-06
+291213 Å¢awÄ« an Nukharah Tawi an Nukharah Tawi Nakh,Tawi an Nakharah,Tawi an Nukharah,Å¢awÄ« an Nakharah,Å¢awÄ« an Nukharah,ṬÄwÄ« Nakh 24.93857 55.39779 H WLL AE 03 0 83 Asia/Dubai 2011-11-06
+291214 Sayḩ an Nukharah Sayh an Nukharah Sayh an Nakharah,Sayh an Nukharah,Sayḩ an Nakharah,Sayḩ an Nukharah 24.93333 55.35 T TRGD AE AE 03 0 71 Asia/Dubai 2011-11-06
+291215 Nuhayy Nuhayy Nahai,Nuhayy 25.27074 56.36242 P PPL AE 06 0 22 Asia/Dubai 2011-11-06
+291216 Ţawī Nubayy Tawi Nubayy Tawi Nubayy,Ţawī Nubayy 25.48333 55.71667 H WLL AE 07 0 45 Asia/Dubai 2011-11-06
+291217 Ţawī Nubaybigh Tawi Nubaybigh Nebeibighat,Tawi Nubaybigh,Tawi Nubaybighah,Ţawī Nubaybigh,Ţawī Nubaybighah 25.26667 55.81667 H WLL AE AE 06 0 125 Asia/Dubai 2011-11-06
+291218 WÄdÄ« NiyÄm Wadi Niyam Wadi Niyam,WÄdÄ« NiyÄm 25.08412 55.92606 H WAD AE 05 0 185 Asia/Dubai 2011-11-06
+291219 QurÅ«n NiÅŸÄb Qurun Nisab Qurun Nisab,QurÅ«n NiÅŸÄb 23.80045 54.33433 T DUNE AE 01 0 100 Asia/Dubai 2011-11-06
+291220 NisÄb Nisab Nisab,NisÄb 23.79429 54.35987 H WLL AE 01 0 90 Asia/Dubai 2011-11-06
+291221 Nibrī Nibri Nibri,Nibrī 25.35176 55.84985 S FT AE 07 0 75 Asia/Dubai 2011-11-06
+291222 Jabal Nibah Jabal Nibah Jabal Nibah 24.76861 56.15111 T MT AE 00 0 522 Asia/Dubai 2011-11-06
+291223 WÄdÄ« Nazwá Wadi Nazwa Wadi Nazwa,WÄdÄ« Nazwá 25.01163 55.65579 T TRGD AE 06 0 176 Asia/Dubai 2011-11-06
+291224 Å¢awÄ« Nazwá Tawi Nazwa Nazwa,NazwÄ,Tawi Nazwa,Tawi Nezwa,Tawi Nizwa,Å¢awÄ« Nazwá,Å¢ÄwÄ« Nezwa 25.0325 55.68361 H WLL AE AE 06 0 168 Asia/Dubai 2011-11-06
+291225 Qarn Nazwá Qarn Nazwa Qarn Nazwa,Qarn Nazwá,Qarn Nizwa 24.98371 55.66235 T HLL AE 00 0 173 Asia/Dubai 2011-11-06
+291226 WÄdÄ« NayÄs Wadi Nayas Wadi Nayas,Wadi Nayassa,WÄdÄ« Nayassa,WÄdÄ« NayÄs 25.02082 55.9848 H WAD AE 05 0 247 Asia/Dubai 2011-11-06
+291227 NaqÄ an NawÄ Naqa an Nawa Naqa Nawi,Naqa an Nawa,NaqÄ NÄwÄ«,NaqÄ an NawÄ 25.14487 55.42354 T DUNE AE 03 0 55 Asia/Dubai 2011-11-06
+291228 Sayḩ Naşūrīyah Ghafanī Sayh Nasuriyah Ghafani Sayh Nasuriyah Ghafani,Sayh Nisuriyah,Sayḩ Naşūrīyah Ghafanī,Sayḩ Nişūrīyah 24.39117 55.2855 T TRGD AE 01 0 155 Asia/Dubai 2011-11-06
+291229 Ţawī Naşūrīyah Tawi Nasuriyah Tawi Nasuriyah,Tawi Nisuriyah,Ţawī Naşūrīyah,Ţawī Nişūrīyah 24.32521 55.35583 H WLL AE 01 0 146 Asia/Dubai 2011-11-06
+291230 Sayḩ Naşūrīyah Sayh Nasuriyah Sayh Nasuriyah,Sayh Nisuriyah,Sayḩ Naşūrīyah,Sayḩ Nişūrīyah 24.36214 55.35931 T TRGD AE 01 0 179 Asia/Dubai 2011-11-06
+291231 Naşūrīyah Nasuriyah Nasuriyah,Naşūrīyah,Ryah Nisuh 24.4 55.31667 H WLL AE AE 01 0 100 Asia/Dubai 2011-11-06
+291232 Naşūrīyah Nasuriyah Nasuriyah,Naşūrīyah,Nisuriyah,Nişūrīyah 24.36667 55.41667 T DUNE AE AE 01 0 184 Asia/Dubai 2011-11-06
+291233 NaÅŸlah Naslah Naslah,NaÅŸlah 23.82234 52.6032 T SAND AE 01 0 62 Asia/Dubai 2011-11-06
+291234 Naşīb Nasib Nasib,Naşīb 24.56667 55.16667 T DUNE AE 01 0 92 Asia/Dubai 2011-11-06
+291235 Nashimah Nashimah Nashimah 23.09103 53.80754 T DPR AE 01 0 81 Asia/Dubai 2011-11-06
+291236 Sayḩ an Nashash Sayh an Nashash Sayh an Nashash,Sayḩ an Nashash,Sih al Nashash,Sih an Nashash,Sīḩ an NashÄsh 24.08865 55.71184 T TRGD AE 01 0 245 Asia/Dubai 2011-11-06
+291237 Milhala Milhala Milhala,Tawi Nasas,Å¢awÄ« Nasas,Å¢awÄ« NaÅŸÄÅŸ 25.02694 55.97 H WLL AE 05 0 265 Asia/Dubai 2011-11-06
+291238 Ramlat NasÄs Ramlat Nasas Ramlat Nasas,Ramlat NasÄs 24.51593 55.168 T DUNE AE 01 0 106 Asia/Dubai 2011-11-06
+291239 Nasas Nasas Nasas 24.5 55.18333 T TRGD AE 01 0 125 Asia/Dubai 2011-11-06
+291240 Ţawī Naqrah Tawi Naqrah Tawi Nagarah,Tawi Naqara,Tawi Naqrah,Ţawī Naqara,Ţawī Naqrah 24.3762 55.53108 H WLLS AE 01 0 194 Asia/Dubai 2011-11-06
+291241 Qarn Naqrah Qarn Naqrah Qarn Naqrah 24.37348 55.52974 T DUNE AE 01 0 235 Asia/Dubai 2011-11-06
+291242 Naqrah Naqrah Naqrah 24.37644 55.56032 P PPL AE 01 0 247 Asia/Dubai 2011-11-06
+291243 Naqrah Naqrah Naqrah 24.38333 55.45 T DUNE AE 01 0 204 Asia/Dubai 2011-11-06
+291244 Naqbīyīn Naqbiyin Naqbiyin,Naqbīyīn 25.66667 56 L TRB AE 00 0 12 Asia/Dubai 2011-11-06
+291245 Naqbiyīn Naqbiyin Naqbiyin,Naqbiyīn 25.33333 56.33333 L TRB AE 00 0 426 Asia/Dubai 2011-11-06
+291246 WÄdÄ« Naqat Wadi Naqat Wadi Naqat,WÄdÄ« Naqat 25.86667 56.1 H WAD AE 05 0 205 Asia/Dubai 2011-11-06
+291247 Jaww an NÄqah Jaww an Naqah Jaww an Naqah,Jaww an NÄqah 22.96387 54.14931 T DPR AE 01 0 95 Asia/Dubai 2011-11-06
+291248 Naqab Sha‘rÄn Naqab Sha`ran Naqab Sha`ran,Naqab Sha‘rÄn 24.17121 54.66632 T SAND AE 01 0 44 Asia/Dubai 2011-11-06
+291249 WÄdÄ« Naqab Wadi Naqab Wadi Naqab,WÄdÄ« Naqab 25.70384 56.0662 H WAD AE 05 0 85 Asia/Dubai 2011-11-06
+291250 Namlīyah Namliyah Namliyah,Namlīyah 23.05915 53.958 T DPR AE 01 0 170 Asia/Dubai 2011-11-06
+291251 Namlīyah Namliyah Namliyah,Namlīyah 23.03453 54.02193 T DPR AE 01 0 87 Asia/Dubai 2011-11-06
+291252 Namlah Namlah Namlah 23.06497 53.97529 H WLL AE 01 0 76 Asia/Dubai 2011-11-06
+291253 Namlah Namlah Namalah,Namlah 23.01393 53.47494 T DPR AE 01 0 172 Asia/Dubai 2011-11-06
+291254 Nakhl Subayyis Nakhl Subayyis Nakhl Sibaiyis,Nakhl Sibaylis,Nakhl Subaiyis,Nakhl Subayyis 24.3575 55.20611 V TREE AE AE 01 0 107 Asia/Dubai 2011-11-06
+291255 Nakhl Bin JirwÄn Nakhl Bin Jirwan Nakhl Bin Jirwan,Nakhl Bin JirwÄn,Nakhl bin Jerwan 24.18333 55.11667 H WLLS AE AE 01 0 123 Asia/Dubai 2011-11-06
+291256 ‘Uqlat an Nakhlah `Uqlat an Nakhlah An Nakhlah,Nakhla,`Uglat Nakhlan,`Uqlat al-Nakhlah,`Uqlat an Nakhlah,‘Uglat Nakhlan,‘Uqlat al-Nakhlah,‘Uqlat an Nakhlah 24.31667 51.2 H WLL AE 01 0 7 Asia/Dubai 2011-11-06
+291257 Ramlat an Nakhlah Ramlat an Nakhlah Ramlat an Nakhlah 24.35861 51.20632 T DUNE AE 01 0 10 Asia/Dubai 2011-11-06
+291258 Nakhalai Nakhalai Nakhalai 25.09528 55.57653 L LCTY AE 03 0 89 Asia/Dubai 2011-11-06
+291259 Jabal Najla‘ Jabal Najla` Jabal Najla`,Jabal Najla‘ 25.43593 55.99124 T HLL AE 05 0 208 Asia/Dubai 2011-11-06
+291260 Jabal Najdayn Jabal Najdayn Jabal Najdayn,Jabal Naydayn 25.17988 56.17201 T MT AE 00 0 880 Asia/Dubai 2011-11-06
+291261 Najdayn Najdayn Najdayn 25.16667 56.2 V CULT AE 04 0 639 Asia/Dubai 2011-11-06
+291262 NajdÄt Najdat Najadat,NajadÄt,Najdat,NajdÄt 24.13333 55.91667 L TRB AE AE 01 0 378 Asia/Dubai 2011-11-06
+291263 Na‘ītah Na`itah Jazirat Na`itah,Jazīrat Na‘ītah,Na`itah,Naita,Na‘ītah,Ne'ita,Ne’īta,Ni`eitah,Ni‘eitah 24.29085 51.79455 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+291264 NaqÄ NÄ’if Naqa Na'if Naqa Na'if,NaqÄ NÄ’if 23.08742 55.22844 T DUNE AE 01 0 171 Asia/Dubai 2011-11-06
+291265 Nahwá Nahwa Nahawa,Nahwa,Nahwá 25.26823 56.28002 P PPL AE 06 0 213 Asia/Dubai 2011-11-06
+291266 Ţawī Nahshīlah Tawi Nahshilah Tawi Nahshilah,Ţawī Nahshīlah 24.40832 55.10681 H WLL AE 01 0 103 Asia/Dubai 2011-11-06
+291267 Nahshīlah Nahshilah Nahshilah,Nahshīlah 24.4032 55.05936 T SAND AE 01 0 100 Asia/Dubai 2011-11-06
+291268 Nahdayn Nahdayn Nahdain,Nahdayn,Nahdein 23.68333 52.3 S OILW AE AE 01 0 41 Asia/Dubai 2011-11-06
+291269 Nahdayn Nahdayn Nahadain,Nahdayn 23.68725 52.30246 T HLL AE 01 0 49 Asia/Dubai 2011-11-06
+291270 Nafīr Nafir Nafir,Nafīr,Nefair 23.10393 53.78919 P PPL AE 01 0 86 Asia/Dubai 2011-11-06
+291271 Nafīr Nafir Nafir,Nafīr 23.10444 53.78667 T DPR AE 01 0 86 Asia/Dubai 2011-11-06
+291272 NaqÄ Nadh Naqa Nadh Naqa Nadh,NaqÄ Nadh 23.79851 55.49041 T DUNE AE 01 0 194 Asia/Dubai 2011-11-06
+291273 Nadd Umm Ḩaşá Nadd Umm Hasa Nadd Umm Hasa,Nadd Umm Ḩaşá 25.13954 55.38266 L LCTY AE 03 0 20 Asia/Dubai 2011-11-06
+291274 Nadd MÄni‘ Nadd Mani` Nadd Mani`,Nadd MÄni‘,Ned Bin Tamana,Nidd Mani`,Nidd MÄni‘ 25.55306 55.5475 T PEN AE 07 0 18 Asia/Dubai 2011-11-06
+291275 Nadd BayḑÄ’ Nadd Bayda' Nad Beidha,Nadd Bayda',Nadd BayḑÄ’ 25.35352 55.43762 L LCTY AE 06 0 15 Asia/Dubai 2011-11-06
+291276 Ramlat Nadd Ramlat Nadd Ramlat Nadd 23.97641 55.12876 T DUNE AE 01 0 143 Asia/Dubai 2011-11-06
+291277 Naban Naban Naban 24.23824 54.90422 H WLL AE 01 0 77 Asia/Dubai 2011-11-06
+291278 Na‘adhal Na`adhal Na`adhal,Na‘adhal 22.98454 53.54606 T DPR AE 01 0 46 Asia/Dubai 2011-11-06
+291279 Muzayri‘ Muzayri` Mezaira'a,Mezaira’a,Mizeir`ah,Mizeir‘ah,Mozayri`,Mozayri‘,Muzayri`,Muzayri‘ 23.14355 53.7881 P PPL AE 01 10000 181 Asia/Dubai 2011-11-06
+291280 Mughaylat MuzÄri‘ Mughaylat Muzari` Maghailat Muzaria,Mughaylat Muzari`,Mughaylat MuzÄri‘ 24.03333 54.95 H WLL AE 01 0 90 Asia/Dubai 2011-11-06
+291281 GhÄfat al Muyayridah Ghafat al Muyayridah Ghafat al Muyayridah,GhÄfat al Muyayridah 25.6 56.3 H WLL AE 04 0 45 Asia/Dubai 2011-11-06
+291282 Sayḩ Muyaydirah Sayh Muyaydirah Sayh Muyaydirah,Sayḩ Muyaydirah 24.68333 55.4 T TRGD AE 03 0 155 Asia/Dubai 2011-11-06
+291283 Muyaydirah Muyaydirah Muyaydirah 24.7 55.38333 T SAND AE 03 0 118 Asia/Dubai 2011-11-06
+291284 NaqÄ Muwayzah Naqa Muwayzah Naqa Muwayzah,NaqÄ Muwayzah 24.6 55.31667 T SAND AE 01 0 135 Asia/Dubai 2011-11-06
+291285 Ḩişn al Muwayqi‘ī Hisn al Muwayqi`i Hisn al Muwayqi`i,Ḩişn al Muwayqi‘ī 24.21667 55.71667 S FT AE 01 0 266 Asia/Dubai 2011-11-06
+291286 Muwayliḩ Muwaylih Muailah,Muwaylih,Muwayliḩ 24.61506 54.7728 H WLL AE 01 0 24 Asia/Dubai 2011-11-06
+291287 WÄdÄ« al Muwayji‘ah Wadi al Muwayji`ah Wadi al Muwayji`ah,WÄdÄ« al Muwayji‘ah 24.95 55.4 T DPR AE 03 0 85 Asia/Dubai 2011-11-06
+291288 Muwayh Arnab Muwayh Arnab Muwayh Arnab 24.27467 55.05049 T SAND AE 01 0 123 Asia/Dubai 2011-11-06
+291289 Muwayh al Ju’Äbir Muwayh al Ju'abir Muwayh al Ju'abir,Muwayh al Ju’Äbir 23.82211 55.46188 T DUNE AE 01 0 181 Asia/Dubai 2011-11-06
+291290 Muwayh al Huḑayb Muwayh al Hudayb Muwayh al Hudayb,Muwayh al Huḑayb 24.06076 55.35169 T SAND AE 01 0 191 Asia/Dubai 2011-11-06
+291291 Bi’r Muwayfiqah Bi'r Muwayfiqah Bi'r Muwayfiqah,Bi’r Muwayfiqah,Muwafiqa,Muwafiqah,MuwÄfiqa,MuwÄfÄ«qah 24.13194 55.68139 H WLL AE 01 0 247 Asia/Dubai 2011-11-06
+291292 Jabal Muthrad Jabal Muthrad Jabal Muthrad 25.15731 56.28963 T MT AE 04 0 387 Asia/Dubai 2011-11-06
+291293 Ţawī Mutayn Tawi Mutayn Tawi Mutayn,Ţawī Mutayn 23.2725 53.60472 H WLL AE 01 0 141 Asia/Dubai 2011-11-06
+291294 MuÅ£aylÄn Mutaylan Mitailan,Mutailan,Mutaylan,MuÅ£aylÄn 23.84996 53.82125 L AREA AE 01 0 69 Asia/Dubai 2011-11-06
+291295 Mughayyil Muţawwá Mughayyil Mutawwa Mughayyil Mutawwa,Mughayyil Muţawwá 24.06546 54.89046 H WLL AE 01 0 107 Asia/Dubai 2011-11-06
+291296 Bid‘ al MuÅ£Äwa‘ah Bid` al Mutawa`ah Bid' al-Mataw'a,Bid` al Mutawa`ah,Bid` al Mutawwa`,Bid` al-Mataw`ah,Bid‘ al MuÅ£awwa‘,Bid‘ al MuÅ£Äwa‘ah,Bid‘ al-Maá¹Äw‘ah,Bid’ al-Mataw’a 23.75972 52.51917 H WLL AE 01 0 68 Asia/Dubai 2011-11-06
+291297 WÄdÄ« Mu‘tariḑah Wadi Mu`taridah Wadi Mu`taridah,WÄdÄ« Mu‘tariḑah 25.5165 56.00264 H WAD AE 00 0 97 Asia/Dubai 2011-11-06
+291298 Mu‘tariḑah Mu`taridah Mu`taridah,Mu`taridat,Mu‘tariḑah,Mu‘tariḑat 23.93178 52.41949 T RKS AE 01 0 58 Asia/Dubai 2011-11-06
+291299 Mu‘tariḑah Mu`taridah Mu`taridah,Mu‘tariḑah 25.50841 56.11046 P PPL AE 04 0 350 Asia/Dubai 2011-11-06
+291300 Mu‘tariḑah Mu`taridah Mu`taridah,Mu‘tariḑah 24.03558 53.34833 T HLLS AE 01 0 18 Asia/Dubai 2011-11-06
+291301 Mu‘tariḑah Mu`taridah Mu`taridah,Mu‘tariḑah 23.92444 52.43222 T DUNE AE 01 0 38 Asia/Dubai 2011-11-06
+291302 Jabal Mu‘tariḑ Jabal Mu`tarid Jabal Ma'atridh,Jabal Ma’atridh,Jabal Mu`tarid,Jabal Mu‘tariḑ 25.45641 55.99319 T HLL AE 05 0 233 Asia/Dubai 2011-11-06
+291303 Bid‘ Mussama Bid` Mussama Bid` Mussama,Bid‘ Mussama 23.55 53.61667 H WLL AE 01 0 128 Asia/Dubai 2011-11-06
+291304 Musaqab Musaqab Musaqab,Musqab 25.16778 56.14167 V GRVP AE AE 05 0 363 Asia/Dubai 2011-11-06
+291305 Mushrif Mushrif Mushrif 25.21667 55.45 H WLL AE 03 0 34 Asia/Dubai 2011-11-06
+291306 Ţawī Mushayrif Tawi Mushayrif Mashairif,Mushairif,Tawi Mushayrif,Ţawī Mushayrif 24.08689 54.81192 H WLL AE 01 0 76 Asia/Dubai 2011-11-06
+291307 Sabkhat Mushayrif Sabkhat Mushayrif Sabkhat Mushayrif 24.15149 54.66116 H SBKH AE 01 0 40 Asia/Dubai 2011-11-06
+291308 Mushayrif Mushayrif Mushayrif 23.92507 54.31447 T TRGD AE 01 0 81 Asia/Dubai 2011-11-06
+291309 Mushayrif Mushayrif Mushairif,Mushayrif 24.11525 54.81618 L LCTY AE 01 0 84 Asia/Dubai 2011-11-06
+291310 Mushayrif Mushayrif Mushayrif 24.11489 54.66871 T DUNE AE 01 0 57 Asia/Dubai 2011-11-06
+291311 Ra’s Mushayrib Ra's Mushayrib Ra's Mashayrib,Ra's Mushayrib,Ras Mashairif,Ras Masheirib,Ras Masherib,Ras Mushairib,Ras Musheirib,Ra’s Mashayrib,Ra’s Mushayrib,RÄs Mushairib 24.29292 51.74242 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+291312 Mushayrib Mushayrib Mushayrib 24.46667 54.41667 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+291313 MushÄshÄ« Mushashi Mashashi,Mushashi,MushÄshÄ« 23.11124 52.5636 S OILW AE 01 0 114 Asia/Dubai 2011-11-06
+291314 MushÄsh Mushash Imshash,Mushash,MushÄsh 24.55769 51.35768 H WLL AE 01 0 -9999 Asia/Dubai 2011-11-06
+291315 MushÄjir Mushajir Mushajir,MushÄjir 23.08605 53.99163 L OAS AE 01 0 178 Asia/Dubai 2011-11-06
+291316 MushÄjir Mushajir Mushajir,MushÄjir 23.07049 53.93374 T DPR AE 01 0 73 Asia/Dubai 2011-11-06
+291317 Bid‘ Muşfir Bid` Musfir Bid` Musfir,Bid‘ Muşfir,Bir Misfar 24.0619 55.29648 H WLL AE 01 0 175 Asia/Dubai 2011-11-06
+291318 Ruqq Musfayr Ruqq Musfayr Ruqq Musfayr 24.27875 51.88656 H RF AE 01 0 -9999 Asia/Dubai 2011-11-06
+291319 Musayyibah Musayyibah Musayyibah 23.82699 55.2768 T TRGD AE 01 0 122 Asia/Dubai 2011-11-06
+291320 Ţawī Musannad Tawi Musannad Tawi Musannad,Tawi Mussarad,Ţawī Musannad,Ţawī Mussarad 25.25 55.7 H WLL AE AE 06 0 111 Asia/Dubai 2011-11-06
+291321 Sayḩ Musannad Sayh Musannad Sayh Musannad,Sayḩ Musannad 25.25104 55.67154 T TRGD AE 06 0 71 Asia/Dubai 2011-11-06
+291322 WÄdÄ« MusallÄ Wadi Musalla Wadi Musalla,WÄdÄ« MusallÄ 25.51575 56.00944 H WAD AE 04 0 142 Asia/Dubai 2011-11-06
+291323 Ḩadd Musafsif Hadd Musafsif Hadd Musafsif,Ḩadd Musafsif 24.10083 52.06417 H SHOL AE 01 0 -9999 Asia/Dubai 2011-11-06
+291324 Khawr Musá Khawr Musa Khawr Musa,Khawr Musá 22.725 53.51671 T DPR AE 01 0 62 Asia/Dubai 2011-11-06
+291325 Sayḩ Muruq Sayh Muruq Sayh Muruq,Sayḩ Muruq 25.56617 56.0731 T DPR AE 04 0 211 Asia/Dubai 2011-11-06
+291326 WÄdÄ« Murtaqam Wadi Murtaqam Wadi Murtaqam,Wadi Murtaqau,WÄdÄ« Murtaqam,WÄdÄ« Murtaqau 25.36727 56.25448 H WAD AE 04 0 277 Asia/Dubai 2011-11-06
+291327 WÄdÄ« Murrah Wadi Murrah Wadi Murrah,WÄdÄ« Murrah 25.19803 56.22673 H WAD AE 04 0 321 Asia/Dubai 2011-11-06
+291328 WÄdÄ« Murrah Wadi Murrah Wadi Murrah,WÄdÄ« Murrah 24.9 55.43333 T TRGD AE 03 0 105 Asia/Dubai 2011-11-06
+291329 Å¢awÄ« Murrah Tawi Murrah Al-Murrah,Qa`r Murra,Qa‘r Murra,Tawi Murra,Tawi Murrah,Å¢awÄ« Murrah,Å¢ÄwÄ« Murra 25.13443 55.74992 H WLL AE 06 0 117 Asia/Dubai 2011-11-06
+291330 Ţawī Murrah Tawi Murrah Tawi Morro,Tawi Motto,Tawi Murra,Tawi Murrah,Ţawī Murrah 24.90846 55.44146 H WLL AE 03 0 113 Asia/Dubai 2011-11-06
+291331 Raml Murrah Raml Murrah Raml Murrah 24.86266 55.38085 T SAND AE 03 0 80 Asia/Dubai 2011-11-06
+291332 Qarn Murrah Qarn Murrah Qarat Murra,Qarat Murrah,Qarn Murrah,QÄrat Murra,QÄrat Murrah 25.13349 55.7703 T DUNE AE 06 0 150 Asia/Dubai 2011-11-06
+291333 Murrah Murrah Murrah 25.03333 55.6 V TREE AE 03 0 162 Asia/Dubai 2011-11-06
+291334 Murrah Murrah Murrah 25.19903 56.22165 P PPL AE 04 0 223 Asia/Dubai 2011-11-06
+291335 Muriya HammÄmah Muriya Hammamah Muriya Hammamah,Muriya HammÄmah 24.93333 54.26667 T RKS AE 01 0 -9999 Asia/Dubai 2011-11-06
+291336 Ḩaql MurbÄn Haql Murban Haql Murban,Mirban,Murban Field,Ḩaql MurbÄn 23.83333 53.75 L OILF AE AE 01 0 76 Asia/Dubai 2011-11-06
+291337 MurbÄn Murban Mirban,MirbÄn,Murban,MurbÄn 23.95342 53.69134 T HLL AE 01 0 3 Asia/Dubai 2011-11-06
+291338 Sabkhat Murbaḩ Sabkhat Murbah Sabkhat Murbah,Sabkhat Murbaḩ 25.25 56.36667 H SBKH AE 04 0 -9999 Asia/Dubai 2011-11-06
+291339 Murbaḩ Murbah Marbah,Mirba,Mirbih,Mirbiḥ,Murbah,Murbaḩ 25.27623 56.36256 P PPL AE 06 0 23 Asia/Dubai 2011-11-06
+291340 Murbaḑ Murbad Marbad,Murbad,Murbaḑ 25.3254 56.13311 P PPL AE 04 0 487 Asia/Dubai 2011-11-06
+291341 Murayyil Murayyil Murayyil 25.11667 55.51667 V TREE AE 06 0 82 Asia/Dubai 2011-11-06
+291342 Muraytah Muraytah Muraytah 25.40032 56.05844 P PPL AE 05 0 240 Asia/Dubai 2011-11-06
+291343 WÄdÄ« Murayshid Wadi Murayshid Wadi Murayshid,WÄdÄ« Murayshid 25.1 56.36667 H WAD AE 04 0 -9999 Asia/Dubai 2011-11-06
+291344 Jabal Murayshid Jabal Murayshid Jabal Murayshid 25.11667 56.33333 T HLL AE 04 0 35 Asia/Dubai 2011-11-06
+291345 Barqat Muraymīth Barqat Muraymith Barqat Muraymith,Barqat Muraymīth 23.88902 54.47119 T RKS AE 01 0 88 Asia/Dubai 2011-11-06
+291346 Muraykh Muraykh Maraikh,Muraykh 24 55.41667 H WLL AE AE 01 0 188 Asia/Dubai 2011-11-06
+291347 Ţawī Muray Tawi Muray Tawi Muray,Ţawī Muray 23.86667 54.61667 H WLL AE 01 0 86 Asia/Dubai 2011-11-06
+291348 Å¢awÄ« MuraqqibÄt Tawi Muraqqibat Muraghabat,Muraqabat,Muraqqibat,Tawi Muraqqibat,Å¢awÄ« MuraqqibÄt 25.32611 55.89944 H WLL AE AE 07 0 170 Asia/Dubai 2011-11-06
+291349 Ţawī Muraqqab Tawi Muraqqab Tawi Muraqqab,Ţawī Muraqqab 24.82029 55.58435 H WLL AE 03 0 182 Asia/Dubai 2011-11-06
+291350 MurÄqabÄt Muraqabat Muraqabat,MurÄqabÄt 25.16361 55.34111 H WLL AE 03 0 13 Asia/Dubai 2011-11-06
+291351 Murabba‘ah Murabba`ah Murabba`ah,Murabba‘ah 25.88333 56.03333 S TOWR AE 05 0 1 Asia/Dubai 2011-11-06
+291352 Ra’s Muqayshiţ Ra's Muqayshit Mughhaishat,Ra's Muqayshit,Ra's al Ibrah,Ra’s Muqayshiţ,Ra’s al Ibrah 24.16778 53.6236 T PT AE 01 0 -9999 Asia/Dubai 2011-11-06
+291353 Muqayshiţ Muqayshit Megaishit,Megeshit,Megeshiţ,Muqayshit,Muqayshiţ 24.18908 53.75226 T ISLX AE 01 0 10 Asia/Dubai 2011-11-06
+291354 Muqaţţarah Muqattarah Miqattarah,Miqaá¹á¹arah,Mugatara,Mugattara,Mugaţţara,Muqatirah,Muqattarah,Muqaţţarah,MuqÄtirah 24.2694 54.51333 T HLL AE 01 0 10 Asia/Dubai 2011-11-06
+291355 Muqala Muqala Muqala 23.53333 54.43333 H WLL AE 01 0 131 Asia/Dubai 2011-11-06
+291356 Muntahá Muntaha Mntaha,Muntaha,Muntahá 23.84263 55.41381 T DPR AE 01 0 187 Asia/Dubai 2011-11-06
+291357 Munfatrah Munfatrah Mufatra,Munfatrah 23.96667 53.03333 T HLL AE AE 01 0 39 Asia/Dubai 2011-11-06
+291358 Mundafinah Mundafinah Mondafanah,Mondafinah,Mundafinah,Mundafnah 24.11667 55.8 P PPL AE AE 01 0 320 Asia/Dubai 2011-11-06
+291359 Khawr ManÄ’if Khawr Mana'if Khawr Mana'if,Khawr ManÄ’if,Khawr Munayyif,Khor Manayef,Khor ManÄyef 24.14456 52.91494 H COVE AE 01 0 -9999 Asia/Dubai 2011-11-06
+291360 Munayyif Munayyif Munayyif 24.07278 52.94833 L LCTY AE 01 0 22 Asia/Dubai 2011-11-06
+291361 Munayyif Munayyif Munaiyif,Munayyif 24.09324 52.93446 T HLL AE 01 0 51 Asia/Dubai 2011-11-06
+291362 WÄdÄ« Munay‘ī Wadi Munay`i Wadi Manai,Wadi Munay`,Wadi Munay`i,WÄdÄ« Munay‘,WÄdÄ« Munay‘ī 24.88227 56.20944 H WAD AE 05 0 142 Asia/Dubai 2011-11-06
+291363 Munay‘ī Munay`i Manai,Manai'i,Manai’i,Munay`i,Munay‘ī 24.95135 56.14997 P PPL AE 05 0 370 Asia/Dubai 2011-11-06
+291364 Jabal Mulfīrah Jabal Mulfirah Jabal Mulfirah,Jabal Mulfīrah 25.15716 56.31601 T HLL AE 04 0 229 Asia/Dubai 2011-11-06
+291365 Mulaysah Mulaysah Mulaysah 23.91634 53.03505 H WLL AE 01 0 63 Asia/Dubai 2011-11-06
+291366 Mulaysah Mulaysah Mulaysah 23.9 53.05 H WLL AE 01 0 51 Asia/Dubai 2011-11-06
+291367 Mulayhim Mulayhim Malayham,Mulayhim 22.90126 53.43345 T DPR AE 01 0 54 Asia/Dubai 2011-11-06
+291368 WÄdÄ« Mulayḩah Wadi Mulayhah Wadi Mulayhah,WÄdÄ« Mulayḩah 25.73333 56.01667 H WAD AE 05 0 28 Asia/Dubai 2011-11-06
+291369 WÄdÄ« Mulayḩah Wadi Mulayhah Wadi Mulayhah,WÄdÄ« Mulayḩah 25.23504 55.90669 H WAD AE 06 0 121 Asia/Dubai 2011-11-06
+291370 Ţawī Mulayḩah Tawi Mulayhah Tawi Mulaiha,Tawi Mulayhah,Ţawī Mulaiha,Ţawī Mulayḩah 25.02722 55.795 H WLL AE AE 06 0 161 Asia/Dubai 2011-11-06
+291371 Jabal Mulayḩah Jabal Mulayhah Jabal Milaiha,Jabal Mileihah,Jabal Mileiḥah,Jabal Mulayhah,Jabal Mulayḩah,Jibal Mulayhah,JibÄl Mulayḩah,Mulaiha 25.14818 55.83716 T HLL AE 06 0 362 Asia/Dubai 2011-11-06
+291372 Mulayḩah Mulayhah Mileihah,Mileiḥah,Miliaha,Mulayhah,Mulayḩah 25.16667 55.8 H WLLS AE AE 06 0 158 Asia/Dubai 2011-11-06
+291373 Qarn Mulayḩ Qarn Mulayh Qarn Mileih,Qarn Mulayh,Qarn Mulayḩ 25.02286 55.72966 T HLL AE 06 0 274 236 Asia/Dubai 2011-11-06
+291374 Mukhtaraqah Mukhtaraqah Mahtarraqah,Muhtarqah,Mukhtaraja,Mukhtaraqah 25.54092 56.13066 P PPL AE 04 0 272 Asia/Dubai 2011-11-06
+291375 Mukhayūs Mukhayus Mukhayus,Mukhayūs 25.73222 55.95389 H WLL AE 05 0 47 Asia/Dubai 2011-11-06
+291376 Mūjib Mujib Mujib,Mūjib 23.11667 53.68333 P PPL AE 01 0 84 Asia/Dubai 2011-11-06
+291377 Kharmat al Muhayn Kharmat al Muhayn Kharimat al Mahain,Kharmat al Mahaim,Kharmat al Muhayn 23.01796 55.10773 T DPR AE 01 0 109 Asia/Dubai 2011-11-06
+291378 WÄdÄ« MuhaylÄ« Wadi Muhayli Wadi Muhayli,WÄdÄ« MuhaylÄ« 25.7044 56.06564 H WAD AE 05 0 85 Asia/Dubai 2011-11-06
+291379 Ţawī Muḩayjir Tawi Muhayjir Tawi Muhayjir,Ţawī Muḩayjir 24.5511 55.76381 H WLL AE 01 0 319 Asia/Dubai 2011-11-06
+291380 Jabal Muḩayjir Jabal Muhayjir Jabal Muhayjir,Jabal Muḩayjir,Qarn Muhaiyir 24.53819 55.73512 T HLL AE 01 0 293 Asia/Dubai 2011-11-06
+291381 Muhammalīyah Muhammaliyah Al Mahammaliyah,Jazirat Muhammaliyah,Jazīrat Muḩammalīyah,Mahamaliya,Mehammaliyya,Mehammaliyyah,Muhammaliya,Muhammaliyah,Muhammalīyah 24.11616 51.89603 T ISL AE 01 0 -9999 Asia/Dubai 2011-11-06
+291382 Ţawī Muḩammad Tawi Muhammad Tawi Muhammad,Ţawī Muḩammad 24.94636 55.75046 H WLL AE 06 0 183 Asia/Dubai 2011-11-06
+291383 Bid‘ Muḩammad Bid` Muhammad Bid` Muhammad,Bid‘ Muḩammad 24.25 55.1 H WLL AE 01 0 125 Asia/Dubai 2011-11-06
+291384 Qullat MuḩÄfiz̧ Qullat Muhafiz Qal`eh Mahafidh,Qallah Mahafidh,Qal‘eh MahÄfidh,Qullat Mahafiz,Qullat MaḩÄfiz̧,Qullat Muhafiz,Qullat MuḩÄfiz̧ 25.15848 55.93008 T PLN AE 06 0 164 Asia/Dubai 2011-11-06
+291385 Mughīlah Mughilah Mughila,Mughilah,Mughīlah 23.78842 55.18178 T DPR AE 01 0 113 Asia/Dubai 2011-11-06
+291386 Mughayyin Mughayyin Mughaiyin,Mughayyin 23.89196 52.78213 T SAND AE 01 0 69 Asia/Dubai 2011-11-06
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/data/people.csv Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,3 @@
+# uri,name,knows
+http://www.example.org/alice,Alice,
+http://www.example.org/bob,Bob,http://www.example.org/alice
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/data/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,29 @@
+# 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/>.
+
+from yams.buildobjs import EntityType, String, SubjectRelation
+
+from cubicweb.schema import RQLConstraint
+
+
+class Personne(EntityType):
+ nom = String(required=True)
+ prenom = String()
+ enfant = SubjectRelation('Personne', inlined=True, cardinality='?*')
+ connait = SubjectRelation('Personne', symmetric=True,
+ constraints=[RQLConstraint('NOT S identity O')])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/data/timeZones.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,410 @@
+Africa/Abidjan 0.0 0.0 0.0
+Africa/Accra 0.0 0.0 0.0
+Africa/Addis_Ababa 3.0 3.0 3.0
+Africa/Algiers 1.0 1.0 1.0
+Africa/Asmara 3.0 3.0 3.0
+Africa/Bamako 0.0 0.0 0.0
+Africa/Bangui 1.0 1.0 1.0
+Africa/Banjul 0.0 0.0 0.0
+Africa/Bissau 0.0 0.0 0.0
+Africa/Blantyre 2.0 2.0 2.0
+Africa/Brazzaville 1.0 1.0 1.0
+Africa/Bujumbura 2.0 2.0 2.0
+Africa/Cairo 2.0 2.0 2.0
+Africa/Casablanca 0.0 1.0 0.0
+Africa/Ceuta 1.0 2.0 1.0
+Africa/Conakry 0.0 0.0 0.0
+Africa/Dakar 0.0 0.0 0.0
+Africa/Dar_es_Salaam 3.0 3.0 3.0
+Africa/Djibouti 3.0 3.0 3.0
+Africa/Douala 1.0 1.0 1.0
+Africa/El_Aaiun 0.0 0.0 0.0
+Africa/Freetown 0.0 0.0 0.0
+Africa/Gaborone 2.0 2.0 2.0
+Africa/Harare 2.0 2.0 2.0
+Africa/Johannesburg 2.0 2.0 2.0
+Africa/Kampala 3.0 3.0 3.0
+Africa/Khartoum 3.0 3.0 3.0
+Africa/Kigali 2.0 2.0 2.0
+Africa/Kinshasa 1.0 1.0 1.0
+Africa/Lagos 1.0 1.0 1.0
+Africa/Libreville 1.0 1.0 1.0
+Africa/Lome 0.0 0.0 0.0
+Africa/Luanda 1.0 1.0 1.0
+Africa/Lubumbashi 2.0 2.0 2.0
+Africa/Lusaka 2.0 2.0 2.0
+Africa/Malabo 1.0 1.0 1.0
+Africa/Maputo 2.0 2.0 2.0
+Africa/Maseru 2.0 2.0 2.0
+Africa/Mbabane 2.0 2.0 2.0
+Africa/Mogadishu 3.0 3.0 3.0
+Africa/Monrovia 0.0 0.0 0.0
+Africa/Nairobi 3.0 3.0 3.0
+Africa/Ndjamena 1.0 1.0 1.0
+Africa/Niamey 1.0 1.0 1.0
+Africa/Nouakchott 0.0 0.0 0.0
+Africa/Ouagadougou 0.0 0.0 0.0
+Africa/Porto-Novo 1.0 1.0 1.0
+Africa/Sao_Tome 0.0 0.0 0.0
+Africa/Tripoli 2.0 2.0 2.0
+Africa/Tunis 1.0 1.0 1.0
+Africa/Windhoek 2.0 1.0 1.0
+America/Adak -10.0 -9.0 -10.0
+America/Anchorage -9.0 -8.0 -9.0
+America/Anguilla -4.0 -4.0 -4.0
+America/Antigua -4.0 -4.0 -4.0
+America/Araguaina -3.0 -3.0 -3.0
+America/Argentina/Buenos_Aires -3.0 -3.0 -3.0
+America/Argentina/Catamarca -3.0 -3.0 -3.0
+America/Argentina/Cordoba -3.0 -3.0 -3.0
+America/Argentina/Jujuy -3.0 -3.0 -3.0
+America/Argentina/La_Rioja -3.0 -3.0 -3.0
+America/Argentina/Mendoza -3.0 -3.0 -3.0
+America/Argentina/Rio_Gallegos -3.0 -3.0 -3.0
+America/Argentina/Salta -3.0 -3.0 -3.0
+America/Argentina/San_Juan -3.0 -3.0 -3.0
+America/Argentina/San_Luis -3.0 -3.0 -4.0
+America/Argentina/Tucuman -3.0 -3.0 -3.0
+America/Argentina/Ushuaia -3.0 -3.0 -3.0
+America/Aruba -4.0 -4.0 -4.0
+America/Asuncion -3.0 -4.0 -4.0
+America/Atikokan -5.0 -5.0 -5.0
+America/Bahia -3.0 -3.0 -3.0
+America/Bahia_Banderas -6.0 -5.0 -6.0
+America/Barbados -4.0 -4.0 -4.0
+America/Belem -3.0 -3.0 -3.0
+America/Belize -6.0 -6.0 -6.0
+America/Blanc-Sablon -4.0 -4.0 -4.0
+America/Boa_Vista -4.0 -4.0 -4.0
+America/Bogota -5.0 -5.0 -5.0
+America/Boise -7.0 -6.0 -7.0
+America/Cambridge_Bay -7.0 -6.0 -7.0
+America/Campo_Grande -3.0 -4.0 -4.0
+America/Cancun -6.0 -5.0 -6.0
+America/Caracas -4.5 -4.5 -4.5
+America/Cayenne -3.0 -3.0 -3.0
+America/Cayman -5.0 -5.0 -5.0
+America/Chicago -6.0 -5.0 -6.0
+America/Chihuahua -7.0 -6.0 -7.0
+America/Costa_Rica -6.0 -6.0 -6.0
+America/Cuiaba -3.0 -4.0 -4.0
+America/Curacao -4.0 -4.0 -4.0
+America/Danmarkshavn 0.0 0.0 0.0
+America/Dawson -8.0 -7.0 -8.0
+America/Dawson_Creek -7.0 -7.0 -7.0
+America/Denver -7.0 -6.0 -7.0
+America/Detroit -5.0 -4.0 -5.0
+America/Dominica -4.0 -4.0 -4.0
+America/Edmonton -7.0 -6.0 -7.0
+America/Eirunepe -4.0 -4.0 -4.0
+America/El_Salvador -6.0 -6.0 -6.0
+America/Fortaleza -3.0 -3.0 -3.0
+America/Glace_Bay -4.0 -3.0 -4.0
+America/Godthab -3.0 -2.0 -3.0
+America/Goose_Bay -4.0 -3.0 -4.0
+America/Grand_Turk -5.0 -4.0 -5.0
+America/Grenada -4.0 -4.0 -4.0
+America/Guadeloupe -4.0 -4.0 -4.0
+America/Guatemala -6.0 -6.0 -6.0
+America/Guayaquil -5.0 -5.0 -5.0
+America/Guyana -4.0 -4.0 -4.0
+America/Halifax -4.0 -3.0 -4.0
+America/Havana -5.0 -4.0 -5.0
+America/Hermosillo -7.0 -7.0 -7.0
+America/Indiana/Indianapolis -5.0 -4.0 -5.0
+America/Indiana/Knox -6.0 -5.0 -6.0
+America/Indiana/Marengo -5.0 -4.0 -5.0
+America/Indiana/Petersburg -5.0 -4.0 -5.0
+America/Indiana/Tell_City -6.0 -5.0 -6.0
+America/Indiana/Vevay -5.0 -4.0 -5.0
+America/Indiana/Vincennes -5.0 -4.0 -5.0
+America/Indiana/Winamac -5.0 -4.0 -5.0
+America/Inuvik -7.0 -6.0 -7.0
+America/Iqaluit -5.0 -4.0 -5.0
+America/Jamaica -5.0 -5.0 -5.0
+America/Juneau -9.0 -8.0 -9.0
+America/Kentucky/Louisville -5.0 -4.0 -5.0
+America/Kentucky/Monticello -5.0 -4.0 -5.0
+America/La_Paz -4.0 -4.0 -4.0
+America/Lima -5.0 -5.0 -5.0
+America/Los_Angeles -8.0 -7.0 -8.0
+America/Maceio -3.0 -3.0 -3.0
+America/Managua -6.0 -6.0 -6.0
+America/Manaus -4.0 -4.0 -4.0
+America/Marigot -4.0 -4.0 -4.0
+America/Martinique -4.0 -4.0 -4.0
+America/Matamoros -6.0 -5.0 -6.0
+America/Mazatlan -7.0 -6.0 -7.0
+America/Menominee -6.0 -5.0 -6.0
+America/Merida -6.0 -5.0 -6.0
+America/Metlakatla -8.0 -8.0 -8.0
+America/Mexico_City -6.0 -5.0 -6.0
+America/Miquelon -3.0 -2.0 -3.0
+America/Moncton -4.0 -3.0 -4.0
+America/Monterrey -6.0 -5.0 -6.0
+America/Montevideo -2.0 -3.0 -3.0
+America/Montreal -5.0 -4.0 -5.0
+America/Montserrat -4.0 -4.0 -4.0
+America/Nassau -5.0 -4.0 -5.0
+America/New_York -5.0 -4.0 -5.0
+America/Nipigon -5.0 -4.0 -5.0
+America/Nome -9.0 -8.0 -9.0
+America/Noronha -2.0 -2.0 -2.0
+America/North_Dakota/Beulah -6.0 -5.0 -6.0
+America/North_Dakota/Center -6.0 -5.0 -6.0
+America/North_Dakota/New_Salem -6.0 -5.0 -6.0
+America/Ojinaga -7.0 -6.0 -7.0
+America/Panama -5.0 -5.0 -5.0
+America/Pangnirtung -5.0 -4.0 -5.0
+America/Paramaribo -3.0 -3.0 -3.0
+America/Phoenix -7.0 -7.0 -7.0
+America/Port-au-Prince -5.0 -5.0 -5.0
+America/Port_of_Spain -4.0 -4.0 -4.0
+America/Porto_Velho -4.0 -4.0 -4.0
+America/Puerto_Rico -4.0 -4.0 -4.0
+America/Rainy_River -6.0 -5.0 -6.0
+America/Rankin_Inlet -6.0 -5.0 -6.0
+America/Recife -3.0 -3.0 -3.0
+America/Regina -6.0 -6.0 -6.0
+America/Resolute -6.0 -5.0 -6.0
+America/Rio_Branco -4.0 -4.0 -4.0
+America/Santa_Isabel -8.0 -7.0 -8.0
+America/Santarem -3.0 -3.0 -3.0
+America/Santiago -3.0 -4.0 -4.0
+America/Santo_Domingo -4.0 -4.0 -4.0
+America/Sao_Paulo -2.0 -3.0 -3.0
+America/Scoresbysund -1.0 0.0 -1.0
+America/Shiprock -7.0 -6.0 -7.0
+America/Sitka -9.0 -8.0 -9.0
+America/St_Barthelemy -4.0 -4.0 -4.0
+America/St_Johns -3.5 -2.5 -3.5
+America/St_Kitts -4.0 -4.0 -4.0
+America/St_Lucia -4.0 -4.0 -4.0
+America/St_Thomas -4.0 -4.0 -4.0
+America/St_Vincent -4.0 -4.0 -4.0
+America/Swift_Current -6.0 -6.0 -6.0
+America/Tegucigalpa -6.0 -6.0 -6.0
+America/Thule -4.0 -3.0 -4.0
+America/Thunder_Bay -5.0 -4.0 -5.0
+America/Tijuana -8.0 -7.0 -8.0
+America/Toronto -5.0 -4.0 -5.0
+America/Tortola -4.0 -4.0 -4.0
+America/Vancouver -8.0 -7.0 -8.0
+America/Whitehorse -8.0 -7.0 -8.0
+America/Winnipeg -6.0 -5.0 -6.0
+America/Yakutat -9.0 -8.0 -9.0
+America/Yellowknife -7.0 -6.0 -7.0
+Antarctica/Casey 8.0 8.0 8.0
+Antarctica/Davis 7.0 7.0 7.0
+Antarctica/DumontDUrville 10.0 10.0 10.0
+Antarctica/Macquarie 11.0 11.0 11.0
+Antarctica/Mawson 5.0 5.0 5.0
+Antarctica/McMurdo 13.0 12.0 12.0
+Antarctica/Palmer -3.0 -4.0 -4.0
+Antarctica/Rothera -3.0 -3.0 -3.0
+Antarctica/South_Pole 13.0 12.0 12.0
+Antarctica/Syowa 3.0 3.0 3.0
+Antarctica/Vostok 6.0 6.0 6.0
+Arctic/Longyearbyen 1.0 2.0 1.0
+Asia/Aden 3.0 3.0 3.0
+Asia/Almaty 6.0 6.0 6.0
+Asia/Amman 2.0 3.0 2.0
+Asia/Anadyr 11.0 12.0 12.0
+Asia/Aqtau 5.0 5.0 5.0
+Asia/Aqtobe 5.0 5.0 5.0
+Asia/Ashgabat 5.0 5.0 5.0
+Asia/Baghdad 3.0 3.0 3.0
+Asia/Bahrain 3.0 3.0 3.0
+Asia/Baku 4.0 5.0 4.0
+Asia/Bangkok 7.0 7.0 7.0
+Asia/Beirut 2.0 3.0 2.0
+Asia/Bishkek 6.0 6.0 6.0
+Asia/Brunei 8.0 8.0 8.0
+Asia/Choibalsan 8.0 8.0 8.0
+Asia/Chongqing 8.0 8.0 8.0
+Asia/Colombo 5.5 5.5 5.5
+Asia/Damascus 2.0 3.0 2.0
+Asia/Dhaka 6.0 6.0 6.0
+Asia/Dili 9.0 9.0 9.0
+Asia/Dubai 4.0 4.0 4.0
+Asia/Dushanbe 5.0 5.0 5.0
+Asia/Gaza 2.0 3.0 2.0
+Asia/Harbin 8.0 8.0 8.0
+Asia/Ho_Chi_Minh 7.0 7.0 7.0
+Asia/Hong_Kong 8.0 8.0 8.0
+Asia/Hovd 7.0 7.0 7.0
+Asia/Irkutsk 8.0 9.0 9.0
+Asia/Jakarta 7.0 7.0 7.0
+Asia/Jayapura 9.0 9.0 9.0
+Asia/Jerusalem 2.0 3.0 2.0
+Asia/Kabul 4.5 4.5 4.5
+Asia/Kamchatka 11.0 12.0 12.0
+Asia/Karachi 5.0 5.0 5.0
+Asia/Kashgar 8.0 8.0 8.0
+Asia/Kathmandu 5.75 5.75 5.75
+Asia/Kolkata 5.5 5.5 5.5
+Asia/Krasnoyarsk 7.0 8.0 8.0
+Asia/Kuala_Lumpur 8.0 8.0 8.0
+Asia/Kuching 8.0 8.0 8.0
+Asia/Kuwait 3.0 3.0 3.0
+Asia/Macau 8.0 8.0 8.0
+Asia/Magadan 11.0 12.0 12.0
+Asia/Makassar 8.0 8.0 8.0
+Asia/Manila 8.0 8.0 8.0
+Asia/Muscat 4.0 4.0 4.0
+Asia/Nicosia 2.0 3.0 2.0
+Asia/Novokuznetsk 6.0 7.0 7.0
+Asia/Novosibirsk 6.0 7.0 7.0
+Asia/Omsk 6.0 7.0 7.0
+Asia/Oral 5.0 5.0 5.0
+Asia/Phnom_Penh 7.0 7.0 7.0
+Asia/Pontianak 7.0 7.0 7.0
+Asia/Pyongyang 9.0 9.0 9.0
+Asia/Qatar 3.0 3.0 3.0
+Asia/Qyzylorda 6.0 6.0 6.0
+Asia/Rangoon 6.5 6.5 6.5
+Asia/Riyadh 3.0 3.0 3.0
+Asia/Sakhalin 10.0 11.0 11.0
+Asia/Samarkand 5.0 5.0 5.0
+Asia/Seoul 9.0 9.0 9.0
+Asia/Shanghai 8.0 8.0 8.0
+Asia/Singapore 8.0 8.0 8.0
+Asia/Taipei 8.0 8.0 8.0
+Asia/Tashkent 5.0 5.0 5.0
+Asia/Tbilisi 4.0 4.0 4.0
+Asia/Tehran 3.5 4.5 3.5
+Asia/Thimphu 6.0 6.0 6.0
+Asia/Tokyo 9.0 9.0 9.0
+Asia/Ulaanbaatar 8.0 8.0 8.0
+Asia/Urumqi 8.0 8.0 8.0
+Asia/Vientiane 7.0 7.0 7.0
+Asia/Vladivostok 10.0 11.0 11.0
+Asia/Yakutsk 9.0 10.0 10.0
+Asia/Yekaterinburg 5.0 6.0 6.0
+Asia/Yerevan 4.0 5.0 4.0
+Atlantic/Azores -1.0 0.0 -1.0
+Atlantic/Bermuda -4.0 -3.0 -4.0
+Atlantic/Canary 0.0 1.0 0.0
+Atlantic/Cape_Verde -1.0 -1.0 -1.0
+Atlantic/Faroe 0.0 1.0 0.0
+Atlantic/Madeira 0.0 1.0 0.0
+Atlantic/Reykjavik 0.0 0.0 0.0
+Atlantic/South_Georgia -2.0 -2.0 -2.0
+Atlantic/St_Helena 0.0 0.0 0.0
+Atlantic/Stanley -3.0 -3.0 -4.0
+Australia/Adelaide 10.5 9.5 9.5
+Australia/Brisbane 10.0 10.0 10.0
+Australia/Broken_Hill 10.5 9.5 9.5
+Australia/Currie 11.0 10.0 10.0
+Australia/Darwin 9.5 9.5 9.5
+Australia/Eucla 8.75 8.75 8.75
+Australia/Hobart 11.0 10.0 10.0
+Australia/Lindeman 10.0 10.0 10.0
+Australia/Lord_Howe 11.0 10.5 10.5
+Australia/Melbourne 11.0 10.0 10.0
+Australia/Perth 8.0 8.0 8.0
+Australia/Sydney 11.0 10.0 10.0
+Europe/Amsterdam 1.0 2.0 1.0
+Europe/Andorra 1.0 2.0 1.0
+Europe/Athens 2.0 3.0 2.0
+Europe/Belgrade 1.0 2.0 1.0
+Europe/Berlin 1.0 2.0 1.0
+Europe/Bratislava 1.0 2.0 1.0
+Europe/Brussels 1.0 2.0 1.0
+Europe/Bucharest 2.0 3.0 2.0
+Europe/Budapest 1.0 2.0 1.0
+Europe/Chisinau 2.0 3.0 2.0
+Europe/Copenhagen 1.0 2.0 1.0
+Europe/Dublin 0.0 1.0 0.0
+Europe/Gibraltar 1.0 2.0 1.0
+Europe/Guernsey 0.0 1.0 0.0
+Europe/Helsinki 2.0 3.0 2.0
+Europe/Isle_of_Man 0.0 1.0 0.0
+Europe/Istanbul 2.0 3.0 2.0
+Europe/Jersey 0.0 1.0 0.0
+Europe/Kaliningrad 2.0 3.0 3.0
+Europe/Kiev 2.0 3.0 3.0
+Europe/Lisbon 0.0 1.0 0.0
+Europe/Ljubljana 1.0 2.0 1.0
+Europe/London 0.0 1.0 0.0
+Europe/Luxembourg 1.0 2.0 1.0
+Europe/Madrid 1.0 2.0 1.0
+Europe/Malta 1.0 2.0 1.0
+Europe/Mariehamn 2.0 3.0 2.0
+Europe/Minsk 2.0 3.0 3.0
+Europe/Monaco 1.0 2.0 1.0
+Europe/Moscow 3.0 4.0 4.0
+Europe/Oslo 1.0 2.0 1.0
+Europe/Paris 1.0 2.0 1.0
+Europe/Podgorica 1.0 2.0 1.0
+Europe/Prague 1.0 2.0 1.0
+Europe/Riga 2.0 3.0 2.0
+Europe/Rome 1.0 2.0 1.0
+Europe/Samara 3.0 4.0 4.0
+Europe/San_Marino 1.0 2.0 1.0
+Europe/Sarajevo 1.0 2.0 1.0
+Europe/Simferopol 2.0 3.0 3.0
+Europe/Skopje 1.0 2.0 1.0
+Europe/Sofia 2.0 3.0 2.0
+Europe/Stockholm 1.0 2.0 1.0
+Europe/Tallinn 2.0 3.0 2.0
+Europe/Tirane 1.0 2.0 1.0
+Europe/Uzhgorod 2.0 3.0 3.0
+Europe/Vaduz 1.0 2.0 1.0
+Europe/Vatican 1.0 2.0 1.0
+Europe/Vienna 1.0 2.0 1.0
+Europe/Vilnius 2.0 3.0 2.0
+Europe/Volgograd 3.0 4.0 4.0
+Europe/Warsaw 1.0 2.0 1.0
+Europe/Zagreb 1.0 2.0 1.0
+Europe/Zaporozhye 2.0 3.0 3.0
+Europe/Zurich 1.0 2.0 1.0
+Indian/Antananarivo 3.0 3.0 3.0
+Indian/Chagos 6.0 6.0 6.0
+Indian/Christmas 7.0 7.0 7.0
+Indian/Cocos 6.5 6.5 6.5
+Indian/Comoro 3.0 3.0 3.0
+Indian/Kerguelen 5.0 5.0 5.0
+Indian/Mahe 4.0 4.0 4.0
+Indian/Maldives 5.0 5.0 5.0
+Indian/Mauritius 4.0 4.0 4.0
+Indian/Mayotte 3.0 3.0 3.0
+Indian/Reunion 4.0 4.0 4.0
+Pacific/Apia -10.0 -11.0 -11.0
+Pacific/Auckland 13.0 12.0 12.0
+Pacific/Chatham 13.75 12.75 12.75
+Pacific/Chuuk 10.0 10.0 10.0
+Pacific/Easter -5.0 -6.0 -6.0
+Pacific/Efate 11.0 11.0 11.0
+Pacific/Enderbury 13.0 13.0 13.0
+Pacific/Fakaofo -10.0 -10.0 -10.0
+Pacific/Fiji 13.0 12.0 12.0
+Pacific/Funafuti 12.0 12.0 12.0
+Pacific/Galapagos -6.0 -6.0 -6.0
+Pacific/Gambier -9.0 -9.0 -9.0
+Pacific/Guadalcanal 11.0 11.0 11.0
+Pacific/Guam 10.0 10.0 10.0
+Pacific/Honolulu -10.0 -10.0 -10.0
+Pacific/Johnston -10.0 -10.0 -10.0
+Pacific/Kiritimati 14.0 14.0 14.0
+Pacific/Kosrae 11.0 11.0 11.0
+Pacific/Kwajalein 12.0 12.0 12.0
+Pacific/Majuro 12.0 12.0 12.0
+Pacific/Marquesas -9.5 -9.5 -9.5
+Pacific/Midway -11.0 -11.0 -11.0
+Pacific/Nauru 12.0 12.0 12.0
+Pacific/Niue -11.0 -11.0 -11.0
+Pacific/Norfolk 11.5 11.5 11.5
+Pacific/Noumea 11.0 11.0 11.0
+Pacific/Pago_Pago -11.0 -11.0 -11.0
+Pacific/Palau 9.0 9.0 9.0
+Pacific/Pitcairn -8.0 -8.0 -8.0
+Pacific/Pohnpei 11.0 11.0 11.0
+Pacific/Port_Moresby 10.0 10.0 10.0
+Pacific/Rarotonga -10.0 -10.0 -10.0
+Pacific/Saipan 10.0 10.0 10.0
+Pacific/Tahiti -10.0 -10.0 -10.0
+Pacific/Tarawa 12.0 12.0 12.0
+Pacific/Tongatapu 13.0 13.0 13.0
+Pacific/Wake 12.0 12.0 12.0
+Pacific/Wallis 12.0 12.0 12.0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/test_csv.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,72 @@
+# copyright 2003-2015 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/>.
+"""unittest for cubicweb.dataimport.csv"""
+
+from StringIO import StringIO
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.dataimport import csv
+
+
+class UcsvreaderTC(TestCase):
+
+ def test_empty_lines_skipped(self):
+ stream = StringIO('''a,b,c,d,
+1,2,3,4,
+,,,,
+,,,,
+''')
+ self.assertEqual([[u'a', u'b', u'c', u'd', u''],
+ [u'1', u'2', u'3', u'4', u''],
+ ],
+ list(csv.ucsvreader(stream)))
+ stream.seek(0)
+ self.assertEqual([[u'a', u'b', u'c', u'd', u''],
+ [u'1', u'2', u'3', u'4', u''],
+ [u'', u'', u'', u'', u''],
+ [u'', u'', u'', u'', u'']
+ ],
+ list(csv.ucsvreader(stream, skip_empty=False)))
+
+ def test_skip_first(self):
+ stream = StringIO('a,b,c,d,\n1,2,3,4,\n')
+ reader = csv.ucsvreader(stream, skipfirst=True, ignore_errors=True)
+ self.assertEqual(list(reader),
+ [[u'1', u'2', u'3', u'4', u'']])
+
+ stream.seek(0)
+ reader = csv.ucsvreader(stream, skipfirst=True, ignore_errors=False)
+ self.assertEqual(list(reader),
+ [[u'1', u'2', u'3', u'4', u'']])
+
+ stream.seek(0)
+ reader = csv.ucsvreader(stream, skipfirst=False, ignore_errors=True)
+ self.assertEqual(list(reader),
+ [[u'a', u'b', u'c', u'd', u''],
+ [u'1', u'2', u'3', u'4', u'']])
+
+ stream.seek(0)
+ reader = csv.ucsvreader(stream, skipfirst=False, ignore_errors=False)
+ self.assertEqual(list(reader),
+ [[u'a', u'b', u'c', u'd', u''],
+ [u'1', u'2', u'3', u'4', u'']])
+
+
+if __name__ == '__main__':
+ unittest_main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/test_pgstore.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,93 @@
+# coding: utf-8
+# copyright 2003-2015 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/>.
+"""unittest for cubicweb.dataimport.pgstore"""
+
+import datetime as DT
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.dataimport import pgstore
+
+
+class CreateCopyFromBufferTC(TestCase):
+
+ # test converters
+
+ def test_convert_none(self):
+ cnvt = pgstore._copyfrom_buffer_convert_None
+ self.assertEqual('NULL', cnvt(None))
+
+ def test_convert_number(self):
+ cnvt = pgstore._copyfrom_buffer_convert_number
+ self.assertEqual('42', cnvt(42))
+ self.assertEqual('42', cnvt(42L))
+ self.assertEqual('42.42', cnvt(42.42))
+
+ def test_convert_string(self):
+ cnvt = pgstore._copyfrom_buffer_convert_string
+ # simple
+ self.assertEqual('babar', cnvt('babar'))
+ # unicode
+ self.assertEqual('\xc3\xa9l\xc3\xa9phant', cnvt(u'éléphant'))
+ self.assertEqual('\xe9l\xe9phant', cnvt(u'éléphant', encoding='latin1'))
+ # escaping
+ self.assertEqual('babar\\tceleste\\n', cnvt('babar\tceleste\n'))
+ self.assertEqual(r'C:\\new\tC:\\test', cnvt('C:\\new\tC:\\test'))
+
+ def test_convert_date(self):
+ cnvt = pgstore._copyfrom_buffer_convert_date
+ self.assertEqual('0666-01-13', cnvt(DT.date(666, 1, 13)))
+
+ def test_convert_time(self):
+ cnvt = pgstore._copyfrom_buffer_convert_time
+ self.assertEqual('06:06:06.000100', cnvt(DT.time(6, 6, 6, 100)))
+
+ def test_convert_datetime(self):
+ cnvt = pgstore._copyfrom_buffer_convert_datetime
+ self.assertEqual('0666-06-13 06:06:06.000000', cnvt(DT.datetime(666, 6, 13, 6, 6, 6)))
+
+ # test buffer
+ def test_create_copyfrom_buffer_tuple(self):
+ data = ((42, 42L, 42.42, u'éléphant', DT.date(666, 1, 13), DT.time(6, 6, 6),
+ DT.datetime(666, 6, 13, 6, 6, 6)),
+ (6, 6L, 6.6, u'babar', DT.date(2014, 1, 14), DT.time(4, 2, 1),
+ DT.datetime(2014, 1, 1, 0, 0, 0)))
+ results = pgstore._create_copyfrom_buffer(data)
+ # all columns
+ expected = '''42\t42\t42.42\téléphant\t0666-01-13\t06:06:06.000000\t0666-06-13 06:06:06.000000
+6\t6\t6.6\tbabar\t2014-01-14\t04:02:01.000000\t2014-01-01 00:00:00.000000'''
+ self.assertMultiLineEqual(expected, results.getvalue())
+ # selected columns
+ results = pgstore._create_copyfrom_buffer(data, columns=(1, 3, 6))
+ expected = '''42\téléphant\t0666-06-13 06:06:06.000000
+6\tbabar\t2014-01-01 00:00:00.000000'''
+ self.assertMultiLineEqual(expected, results.getvalue())
+
+ def test_create_copyfrom_buffer_dict(self):
+ data = (dict(integer=42, double=42.42, text=u'éléphant',
+ date=DT.datetime(666, 6, 13, 6, 6, 6)),
+ dict(integer=6, double=6.6, text=u'babar',
+ date=DT.datetime(2014, 1, 1, 0, 0, 0)))
+ results = pgstore._create_copyfrom_buffer(data, ('integer', 'text'))
+ expected = '''42\téléphant\n6\tbabar'''
+ self.assertMultiLineEqual(expected, results.getvalue())
+
+
+if __name__ == '__main__':
+ unittest_main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/test_sqlgenstore.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact@logilab.fr
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+"""SQL object store test case"""
+
+import itertools
+
+from cubicweb.dataimport import ucsvreader
+from cubicweb.devtools import testlib, PostgresApptestConfiguration
+from cubicweb.devtools import startpgcluster, stoppgcluster
+from cubicweb.dataimport.pgstore import SQLGenObjectStore
+
+
+def setUpModule():
+ startpgcluster(__file__)
+
+
+def tearDownModule(*args):
+ stoppgcluster(__file__)
+
+
+class SQLGenImportSimpleTC(testlib.CubicWebTC):
+ configcls = PostgresApptestConfiguration
+ appid = 'data-massimport'
+
+ def cast(self, _type, value):
+ try:
+ return _type(value)
+ except ValueError:
+ return None
+
+ def push_geonames_data(self, dumpname, store):
+ # Push timezones
+ cnx = store._cnx
+ for code, gmt, dst, raw_offset in ucsvreader(open(self.datapath('timeZones.txt'), 'rb'),
+ delimiter='\t'):
+ cnx.create_entity('TimeZone', code=code, gmt=float(gmt),
+ dst=float(dst), raw_offset=float(raw_offset))
+ timezone_code = dict(cnx.execute('Any C, X WHERE X is TimeZone, X code C'))
+ cnx.commit()
+ # Push data
+ for ind, infos in enumerate(ucsvreader(open(dumpname, 'rb'),
+ delimiter='\t',
+ ignore_errors=True)):
+ if ind > 99:
+ break
+ latitude = self.cast(float, infos[4])
+ longitude = self.cast(float, infos[5])
+ population = self.cast(int, infos[14])
+ elevation = self.cast(int, infos[15])
+ gtopo = self.cast(int, infos[16])
+ feature_class = infos[6]
+ if len(infos[6]) != 1:
+ feature_class = None
+ entity = {'name': infos[1],
+ 'asciiname': infos[2],
+ 'alternatenames': infos[3],
+ 'latitude': latitude, 'longitude': longitude,
+ 'feature_class': feature_class,
+ 'alternate_country_code':infos[9],
+ 'admin_code_3': infos[12],
+ 'admin_code_4': infos[13],
+ 'population': population, 'elevation': elevation,
+ 'gtopo30': gtopo, 'timezone': timezone_code.get(infos[17]),
+ 'cwuri': u'http://sws.geonames.org/%s/' % int(infos[0]),
+ 'geonameid': int(infos[0]),
+ }
+ store.prepare_insert_entity('Location', **entity)
+
+ def test_autoflush_metadata(self):
+ with self.admin_access.repo_cnx() as cnx:
+ crs = cnx.system_sql('SELECT * FROM entities WHERE type=%(t)s',
+ {'t': 'Location'})
+ self.assertEqual(len(crs.fetchall()), 0)
+ store = SQLGenObjectStore(cnx)
+ store.prepare_insert_entity('Location', name=u'toto')
+ store.flush()
+ store.commit()
+ cnx.commit()
+ with self.admin_access.repo_cnx() as cnx:
+ crs = cnx.system_sql('SELECT * FROM entities WHERE type=%(t)s',
+ {'t': 'Location'})
+ self.assertEqual(len(crs.fetchall()), 1)
+
+ def test_sqlgenstore_etype_metadata(self):
+ with self.admin_access.repo_cnx() as cnx:
+ store = SQLGenObjectStore(cnx)
+ timezone_eid = store.prepare_insert_entity('TimeZone')
+ store.prepare_insert_entity('Location', timezone=timezone_eid)
+ store.flush()
+ store.commit()
+ eid, etname = cnx.execute('Any X, TN WHERE X timezone TZ, X is T, '
+ 'T name TN')[0]
+ self.assertEqual(cnx.entity_from_eid(eid).cw_etype, etname)
+
+ def test_simple_insert(self):
+ with self.admin_access.repo_cnx() as cnx:
+ store = SQLGenObjectStore(cnx)
+ self.push_geonames_data(self.datapath('geonames.csv'), store)
+ store.flush()
+ store.commit()
+ with self.admin_access.repo_cnx() as cnx:
+ rset = cnx.execute('Any X WHERE X is Location')
+ self.assertEqual(len(rset), 100)
+ rset = cnx.execute('Any X WHERE X is Location, X timezone T')
+ self.assertEqual(len(rset), 100)
+
+
+if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/test_stores.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,88 @@
+# copyright 2003-2015 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/>.
+"""unittest for cubicweb.dataimport.stores"""
+
+import datetime as DT
+
+from cubicweb.dataimport import stores
+from cubicweb.devtools.testlib import CubicWebTC
+
+
+class RQLObjectStoreTC(CubicWebTC):
+
+ def test_all(self):
+ with self.admin_access.repo_cnx() as cnx:
+ store = stores.RQLObjectStore(cnx)
+ # Check data insertion
+ group_eid = store.prepare_insert_entity('CWGroup', name=u'grp')
+ user_eid = store.prepare_insert_entity('CWUser', login=u'lgn',
+ upassword=u'pwd')
+ store.prepare_insert_relation(user_eid, 'in_group', group_eid)
+ cnx.commit()
+ users = cnx.execute('CWUser X WHERE X login "lgn"')
+ self.assertEqual(1, len(users))
+ self.assertEqual(user_eid, users.one().eid)
+ groups = cnx.execute('CWGroup X WHERE U in_group X, U login "lgn"')
+ self.assertEqual(1, len(users))
+ self.assertEqual(group_eid, groups.one().eid)
+ # Check data update
+ self.set_description('Check data update')
+ store.prepare_update_entity('CWGroup', group_eid, name=u'new_grp')
+ cnx.commit()
+ group = cnx.execute('CWGroup X WHERE X name "grp"')
+ self.assertEqual(len(group), 0)
+ group = cnx.execute('CWGroup X WHERE X name "new_grp"')
+ self.assertEqual, len(group), 1
+ # Check data update with wrong type
+ with self.assertRaises(AssertionError):
+ store.prepare_update_entity('CWUser', group_eid, name=u'new_user')
+ cnx.commit()
+ group = cnx.execute('CWGroup X WHERE X name "new_user"')
+ self.assertEqual(len(group), 0)
+ group = cnx.execute('CWGroup X WHERE X name "new_grp"')
+ self.assertEqual(len(group), 1)
+
+
+class MetaGeneratorTC(CubicWebTC):
+
+ def test_dont_generate_relation_to_internal_manager(self):
+ with self.admin_access.repo_cnx() as cnx:
+ metagen = stores.MetaGenerator(cnx)
+ self.assertIn('created_by', metagen.etype_rels)
+ self.assertIn('owned_by', metagen.etype_rels)
+ with self.repo.internal_cnx() as cnx:
+ metagen = stores.MetaGenerator(cnx)
+ self.assertNotIn('created_by', metagen.etype_rels)
+ self.assertNotIn('owned_by', metagen.etype_rels)
+
+ def test_dont_generate_specified_values(self):
+ with self.admin_access.repo_cnx() as cnx:
+ metagen = stores.MetaGenerator(cnx)
+ # hijack gen_modification_date to ensure we don't go through it
+ metagen.gen_modification_date = None
+ md = DT.datetime.now() - DT.timedelta(days=1)
+ entity, rels = metagen.base_etype_dicts('CWUser')
+ entity.cw_edited.update(dict(modification_date=md))
+ with cnx.ensure_cnx_set:
+ metagen.init_entity(entity)
+ self.assertEqual(entity.cw_edited['modification_date'], md)
+
+
+if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport/test/unittest_importer.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# copyright 2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact@logilab.fr
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+"""Tests for cubicweb.dataimport.importer"""
+
+from collections import defaultdict
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb import ValidationError
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.dataimport import RQLObjectStore, ucsvreader
+from cubicweb.dataimport.importer import (ExtEntity, ExtEntitiesImporter, SimpleImportLog,
+ RelationMapping, use_extid_as_cwuri)
+
+
+class RelationMappingTC(CubicWebTC):
+
+ def test_nosource(self):
+ with self.admin_access.repo_cnx() as cnx:
+ alice_eid = cnx.create_entity('Personne', nom=u'alice').eid
+ bob_eid = cnx.create_entity('Personne', nom=u'bob', connait=alice_eid).eid
+ cnx.commit()
+ mapping = RelationMapping(cnx)
+ self.assertEqual(mapping['connait'],
+ set([(bob_eid, alice_eid), (alice_eid, bob_eid)]))
+
+ def test_with_source(self):
+ with self.admin_access.repo_cnx() as cnx:
+ alice_eid = cnx.create_entity('Personne', nom=u'alice').eid
+ bob_eid = cnx.create_entity('Personne', nom=u'bob', connait=alice_eid).eid
+ cnx.commit()
+ mapping = RelationMapping(cnx, cnx.find('CWSource', name=u'system').one())
+ self.assertEqual(mapping['connait'],
+ set([(bob_eid, alice_eid), (alice_eid, bob_eid)]))
+
+
+class ExtEntitiesImporterTC(CubicWebTC):
+
+ def importer(self, cnx):
+ store = RQLObjectStore(cnx)
+ return ExtEntitiesImporter(self.schema, store, raise_on_error=True)
+
+ def test_simple_import(self):
+ with self.admin_access.repo_cnx() as cnx:
+ importer = self.importer(cnx)
+ personne = ExtEntity('Personne', 1, {'nom': set([u'de la lune']),
+ 'prenom': set([u'Jean'])})
+ importer.import_entities([personne])
+ cnx.commit()
+ rset = cnx.execute('Any X WHERE X is Personne')
+ entity = rset.get_entity(0, 0)
+ self.assertEqual(entity.nom, u'de la lune')
+ self.assertEqual(entity.prenom, u'Jean')
+
+ def test_import_missing_required_attribute(self):
+ """Check import of ext entity with missing required attribute"""
+ with self.admin_access.repo_cnx() as cnx:
+ importer = self.importer(cnx)
+ tag = ExtEntity('Personne', 2, {'prenom': set([u'Jean'])})
+ self.assertRaises(ValidationError, importer.import_entities, [tag])
+
+ def test_import_inlined_relation(self):
+ """Check import of ext entities with inlined relation"""
+ with self.admin_access.repo_cnx() as cnx:
+ importer = self.importer(cnx)
+ richelieu = ExtEntity('Personne', 3, {'nom': set([u'Richelieu']),
+ 'enfant': set([4])})
+ athos = ExtEntity('Personne', 4, {'nom': set([u'Athos'])})
+ importer.import_entities([athos, richelieu])
+ cnx.commit()
+ rset = cnx.execute('Any X WHERE X is Personne, X nom "Richelieu"')
+ entity = rset.get_entity(0, 0)
+ self.assertEqual(entity.enfant[0].nom, 'Athos')
+
+ def test_import_non_inlined_relation(self):
+ """Check import of ext entities with non inlined relation"""
+ with self.admin_access.repo_cnx() as cnx:
+ importer = self.importer(cnx)
+ richelieu = ExtEntity('Personne', 5, {'nom': set([u'Richelieu']),
+ 'connait': set([6])})
+ athos = ExtEntity('Personne', 6, {'nom': set([u'Athos'])})
+ importer.import_entities([athos, richelieu])
+ cnx.commit()
+ rset = cnx.execute('Any X WHERE X is Personne, X nom "Richelieu"')
+ entity = rset.get_entity(0, 0)
+ self.assertEqual(entity.connait[0].nom, 'Athos')
+ rset = cnx.execute('Any X WHERE X is Personne, X nom "Athos"')
+ entity = rset.get_entity(0, 0)
+ self.assertEqual(entity.connait[0].nom, 'Richelieu')
+
+ def test_import_missing_inlined_relation(self):
+ """Check import of ext entity with missing inlined relation"""
+ with self.admin_access.repo_cnx() as cnx:
+ importer = self.importer(cnx)
+ richelieu = ExtEntity('Personne', 7,
+ {'nom': set([u'Richelieu']), 'enfant': set([8])})
+ self.assertRaises(Exception, importer.import_entities, [richelieu])
+ cnx.commit()
+ rset = cnx.execute('Any X WHERE X is Personne, X nom "Richelieu"')
+ self.assertEqual(len(rset), 0)
+
+ def test_import_missing_non_inlined_relation(self):
+ """Check import of ext entity with missing non-inlined relation"""
+ with self.admin_access.repo_cnx() as cnx:
+ importer = self.importer(cnx)
+ richelieu = ExtEntity('Personne', 9,
+ {'nom': set([u'Richelieu']), 'connait': set([10])})
+ self.assertRaises(Exception, importer.import_entities, [richelieu])
+ cnx.commit()
+ rset = cnx.execute('Any X WHERE X is Personne, X nom "Richelieu"')
+ entity = rset.get_entity(0, 0)
+ self.assertEqual(entity.nom, u'Richelieu')
+ self.assertEqual(len(entity.connait), 0)
+
+ def test_update(self):
+ """Check update of ext entity"""
+ with self.admin_access.repo_cnx() as cnx:
+ importer = self.importer(cnx)
+ # First import
+ richelieu = ExtEntity('Personne', 11,
+ {'nom': {u'Richelieu Diacre'}})
+ importer.import_entities([richelieu])
+ cnx.commit()
+ rset = cnx.execute('Any X WHERE X is Personne')
+ entity = rset.get_entity(0, 0)
+ self.assertEqual(entity.nom, u'Richelieu Diacre')
+ # Second import
+ richelieu = ExtEntity('Personne', 11,
+ {'nom': {u'Richelieu Cardinal'}})
+ importer.import_entities([richelieu])
+ cnx.commit()
+ rset = cnx.execute('Any X WHERE X is Personne')
+ self.assertEqual(len(rset), 1)
+ entity = rset.get_entity(0, 0)
+ self.assertEqual(entity.nom, u'Richelieu Cardinal')
+
+
+class UseExtidAsCwuriTC(TestCase):
+
+ def test(self):
+ personne = ExtEntity('Personne', 1, {'nom': set([u'de la lune']),
+ 'prenom': set([u'Jean'])})
+ mapping = {}
+ set_cwuri = use_extid_as_cwuri(mapping)
+ list(set_cwuri((personne,)))
+ self.assertIn('cwuri', personne.values)
+ self.assertEqual(personne.values['cwuri'], set(['1']))
+ mapping[1] = 'whatever'
+ personne.values.pop('cwuri')
+ list(set_cwuri((personne,)))
+ self.assertNotIn('cwuri', personne.values)
+
+
+def extentities_from_csv(fpath):
+ """Yield ExtEntity read from `fpath` CSV file."""
+ with open(fpath) as f:
+ for uri, name, knows in ucsvreader(f, skipfirst=True, skip_empty=False):
+ yield ExtEntity('Personne', uri,
+ {'nom': set([name]), 'connait': set([knows])})
+
+
+class DataimportFunctionalTC(CubicWebTC):
+
+ def test_csv(self):
+ extenties = extentities_from_csv(self.datapath('people.csv'))
+ with self.admin_access.repo_cnx() as cnx:
+ store = RQLObjectStore(cnx)
+ importer = ExtEntitiesImporter(self.schema, store)
+ importer.import_entities(extenties)
+ cnx.commit()
+ rset = cnx.execute('String N WHERE X nom N, X connait Y, Y nom "Alice"')
+ self.assertEqual(rset[0][0], u'Bob')
+
+
+if __name__ == '__main__':
+ unittest_main()
--- a/dbapi.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,836 +0,0 @@
-# copyright 2003-2013 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/>.
-"""DB-API 2.0 compliant module
-
-Take a look at http://www.python.org/peps/pep-0249.html
-
-(most parts of this document are reported here in docstrings)
-"""
-
-__docformat__ = "restructuredtext en"
-
-from threading import currentThread
-from logging import getLogger
-from time import time, clock
-from itertools import count
-from warnings import warn
-from os.path import join
-from uuid import uuid4
-from urlparse import urlparse
-
-from logilab.common.logging_ext import set_log_methods
-from logilab.common.decorators import monkeypatch, cachedproperty
-from logilab.common.deprecation import deprecated
-
-from cubicweb import (ETYPE_NAME_MAP, AuthenticationError, ProgrammingError,
- cwvreg, cwconfig)
-from cubicweb.repoapi import get_repository
-from cubicweb.req import RequestSessionBase
-
-
-_MARKER = object()
-
-def _fake_property_value(self, name):
- try:
- return super(DBAPIRequest, self).property_value(name)
- except KeyError:
- return ''
-
-def fake(*args, **kwargs):
- return None
-
-def multiple_connections_fix():
- """some monkey patching necessary when an application has to deal with
- several connections to different repositories. It tries to hide buggy class
- attributes since classes are not designed to be shared among multiple
- registries.
- """
- defaultcls = cwvreg.CWRegistryStore.REGISTRY_FACTORY[None]
-
- etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
- orig_etype_class = etypescls.orig_etype_class = etypescls.etype_class
- @monkeypatch(defaultcls)
- def etype_class(self, etype):
- """return an entity class for the given entity type.
- Try to find out a specific class for this kind of entity or
- default to a dump of the class registered for 'Any'
- """
- usercls = orig_etype_class(self, etype)
- if etype == 'Any':
- return usercls
- usercls.e_schema = self.schema.eschema(etype)
- return usercls
-
-def multiple_connections_unfix():
- etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
- etypescls.etype_class = etypescls.orig_etype_class
-
-
-class ConnectionProperties(object):
- def __init__(self, cnxtype=None, close=True, log=False):
- if cnxtype is not None:
- warn('[3.16] cnxtype argument is deprecated', DeprecationWarning,
- stacklevel=2)
- self.cnxtype = cnxtype
- self.log_queries = log
- self.close_on_del = close
-
-
-@deprecated('[3.19] the dbapi is deprecated. Have a look at the new repoapi.')
-def _repo_connect(repo, login, **kwargs):
- """Constructor to create a new connection to the given CubicWeb repository.
-
- Returns a Connection instance.
-
- Raises AuthenticationError if authentication failed
- """
- cnxid = repo.connect(unicode(login), **kwargs)
- cnx = Connection(repo, cnxid, kwargs.get('cnxprops'))
- if cnx.is_repo_in_memory:
- cnx.vreg = repo.vreg
- return cnx
-
-def connect(database, login=None,
- cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs):
- """Constructor for creating a connection to the CubicWeb repository.
- Returns a :class:`Connection` object.
-
- Typical usage::
-
- cnx = connect('myinstance', login='me', password='toto')
-
- `database` may be:
-
- * a simple instance id for in-memory connection
-
- * a uri like scheme://host:port/instanceid where scheme may be one of
- 'pyro', 'inmemory' or 'zmqpickle'
-
- * if scheme is 'pyro', <host:port> determine the name server address. If
- not specified (e.g. 'pyro:///instanceid'), it will be detected through a
- broadcast query. The instance id is the name of the instance in the name
- server and may be prefixed by a group (e.g.
- 'pyro:///:cubicweb.instanceid')
-
- * if scheme is handled by ZMQ (eg 'tcp'), you should not specify an
- instance id
-
- Other arguments:
-
- :login:
- the user login to use to authenticate.
-
- :cnxprops:
- a :class:`ConnectionProperties` instance, allowing to specify
- the connection method (eg in memory or pyro). A Pyro connection will be
- established if you don't specify that argument.
-
- :setvreg:
- flag telling if a registry should be initialized for the connection.
- Don't change this unless you know what you're doing.
-
- :mulcnx:
- Will disappear at some point. Try to deal with connections to differents
- instances in the same process unless specified otherwise by setting this
- flag to False. Don't change this unless you know what you're doing.
-
- :initlog:
- flag telling if logging should be initialized. You usually don't want
- logging initialization when establishing the connection from a process
- where it's already initialized.
-
- :kwargs:
- there goes authentication tokens. You usually have to specify a password
- for the given user, using a named 'password' argument.
- """
- if not urlparse(database).scheme:
- warn('[3.16] give an qualified URI as database instead of using '
- 'host/cnxprops to specify the connection method',
- DeprecationWarning, stacklevel=2)
- if cnxprops and cnxprops.cnxtype == 'zmq':
- database = kwargs.pop('host')
- elif cnxprops and cnxprops.cnxtype == 'inmemory':
- database = 'inmemory://' + database
- else:
- host = kwargs.pop('host', None)
- if host is None:
- host = ''
- group = kwargs.pop('group', None)
- if group is None:
- group = 'cubicweb'
- database = 'pyro://%s/%s.%s' % (host, group, database)
- puri = urlparse(database)
- method = puri.scheme.lower()
- if method == 'inmemory':
- config = cwconfig.instance_configuration(puri.netloc)
- else:
- config = cwconfig.CubicWebNoAppConfiguration()
- repo = get_repository(database, config=config)
- if method == 'inmemory':
- vreg = repo.vreg
- elif setvreg:
- if mulcnx:
- multiple_connections_fix()
- vreg = cwvreg.CWRegistryStore(config, initlog=initlog)
- schema = repo.get_schema()
- for oldetype, newetype in ETYPE_NAME_MAP.items():
- if oldetype in schema:
- print 'aliasing', newetype, 'to', oldetype
- schema._entities[newetype] = schema._entities[oldetype]
- vreg.set_schema(schema)
- else:
- vreg = None
- cnx = _repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
- cnx.vreg = vreg
- return cnx
-
-def in_memory_repo(config):
- """Return and in_memory Repository object from a config (or vreg)"""
- if isinstance(config, cwvreg.CWRegistryStore):
- vreg = config
- config = None
- else:
- vreg = None
- # get local access to the repository
- return get_repository('inmemory://', config=config, vreg=vreg)
-
-def in_memory_repo_cnx(config, login, **kwargs):
- """useful method for testing and scripting to get a dbapi.Connection
- object connected to an in-memory repository instance
- """
- # connection to the CubicWeb repository
- repo = in_memory_repo(config)
- return repo, _repo_connect(repo, login, **kwargs)
-
-# XXX web only method, move to webconfig?
-def anonymous_session(vreg):
- """return a new anonymous session
-
- raises an AuthenticationError if anonymous usage is not allowed
- """
- anoninfo = vreg.config.anonymous_user()
- if anoninfo[0] is None: # no anonymous user
- raise AuthenticationError('anonymous access is not authorized')
- anon_login, anon_password = anoninfo
- # use vreg's repository cache
- repo = vreg.config.repository(vreg)
- anon_cnx = _repo_connect(repo, anon_login, password=anon_password)
- anon_cnx.vreg = vreg
- return DBAPISession(anon_cnx, anon_login)
-
-
-class _NeedAuthAccessMock(object):
- def __getattribute__(self, attr):
- raise AuthenticationError()
- def __nonzero__(self):
- return False
-
-class DBAPISession(object):
- def __init__(self, cnx, login=None):
- self.cnx = cnx
- self.data = {}
- self.login = login
- # 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)
- if cnx is not None:
- self.sessionid = cnx.sessionid
- else:
- self.sessionid = uuid4().hex
-
- @property
- def anonymous_session(self):
- return not self.cnx or self.cnx.anonymous_connection
-
- def __repr__(self):
- return '<DBAPISession %r>' % self.sessionid
-
-
-class DBAPIRequest(RequestSessionBase):
- #: Request language identifier eg: 'en'
- lang = None
-
- def __init__(self, vreg, session=None):
- super(DBAPIRequest, self).__init__(vreg)
- #: 'language' => translation_function() mapping
- try:
- # no vreg or config which doesn't handle translations
- self.translations = vreg.config.translations
- except AttributeError:
- self.translations = {}
- #: cache entities built during the request
- self._eid_cache = {}
- if session is not None:
- self.set_session(session)
- else:
- # these args are initialized after a connection is
- # established
- self.session = DBAPISession(None)
- self.cnx = self.user = _NeedAuthAccessMock()
- self.set_default_language(vreg)
-
- def get_option_value(self, option, foreid=None):
- if foreid is not None:
- warn('[3.19] foreid argument is deprecated', DeprecationWarning,
- stacklevel=2)
- return self.cnx.get_option_value(option)
-
- def set_session(self, session):
- """method called by the session handler when the user is authenticated
- or an anonymous connection is open
- """
- self.session = session
- if session.cnx:
- self.cnx = session.cnx
- self.execute = session.cnx.cursor(self).execute
- self.user = self.cnx.user(self)
- self.set_entity_cache(self.user)
-
- def execute(self, *args, **kwargs): # pylint: disable=E0202
- """overriden when session is set. By default raise authentication error
- so authentication is requested.
- """
- raise AuthenticationError()
-
- def set_default_language(self, vreg):
- try:
- lang = vreg.property_value('ui.language')
- except Exception: # property may not be registered
- lang = 'en'
- try:
- self.set_language(lang)
- except KeyError:
- # this occurs usually during test execution
- self._ = self.__ = unicode
- self.pgettext = lambda x, y: unicode(y)
-
- # server-side service call #################################################
-
- def call_service(self, regid, **kwargs):
- return self.cnx.call_service(regid, **kwargs)
-
- # entities cache management ###############################################
-
- def entity_cache(self, eid):
- return self._eid_cache[eid]
-
- def set_entity_cache(self, entity):
- self._eid_cache[entity.eid] = entity
-
- def cached_entities(self):
- return self._eid_cache.values()
-
- def drop_entity_cache(self, eid=None):
- if eid is None:
- self._eid_cache = {}
- else:
- del self._eid_cache[eid]
-
- # low level session data management #######################################
-
- @deprecated('[3.19] use session or transaction data')
- def get_shared_data(self, key, default=None, pop=False, txdata=False):
- """see :meth:`Connection.get_shared_data`"""
- return self.cnx.get_shared_data(key, default, pop, txdata)
-
- @deprecated('[3.19] use session or transaction data')
- def set_shared_data(self, key, value, txdata=False, querydata=None):
- """see :meth:`Connection.set_shared_data`"""
- if querydata is not None:
- txdata = querydata
- warn('[3.10] querydata argument has been renamed to txdata',
- DeprecationWarning, stacklevel=2)
- return self.cnx.set_shared_data(key, value, txdata)
-
- # server session compat layer #############################################
-
- def entity_metas(self, eid):
- """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
- return self.cnx.entity_metas(eid)
-
- def source_defs(self):
- """return the definition of sources used by the repository."""
- return self.cnx.source_defs()
-
- @deprecated('[3.19] use .entity_metas(eid) instead')
- def describe(self, eid, asdict=False):
- """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
- return self.cnx.describe(eid, asdict)
-
- # these are overridden by set_log_methods below
- # only defining here to prevent pylint from complaining
- info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-
-set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi'))
-
-
-
-# cursor / connection objects ##################################################
-
-class Cursor(object):
- """These objects represent a database cursor, which is used to manage the
- context of a fetch operation. Cursors created from the same connection are
- not isolated, i.e., any changes done to the database by a cursor are
- immediately visible by the other cursors. Cursors created from different
- connections are isolated.
- """
-
- def __init__(self, connection, repo, req=None):
- """This read-only attribute return a reference to the Connection
- object on which the cursor was created.
- """
- self.connection = connection
- """optionnal issuing request instance"""
- self.req = req
- self._repo = repo
- self._sessid = connection.sessionid
-
- def close(self):
- """no effect"""
- pass
-
- def _txid(self):
- return self.connection._txid(self)
-
- def execute(self, rql, args=None, build_descr=True):
- """execute a rql query, return resulting rows and their description in
- a :class:`~cubicweb.rset.ResultSet` object
-
- * `rql` should be a Unicode string or a plain ASCII string, containing
- the rql query
-
- * `args` the optional args dictionary associated to the query, with key
- matching named substitution in `rql`
-
- * `build_descr` is a boolean flag indicating if the description should
- be built on select queries (if false, the description will be en empty
- list)
-
- on INSERT queries, there will be one row for each inserted entity,
- containing its eid
-
- on SET queries, XXX describe
-
- DELETE queries returns no result.
-
- .. Note::
- to maximize the rql parsing/analyzing cache performance, you should
- always use substitute arguments in queries, i.e. avoid query such as::
-
- execute('Any X WHERE X eid 123')
-
- use::
-
- execute('Any X WHERE X eid %(x)s', {'x': 123})
- """
- rset = self._repo.execute(self._sessid, rql, args,
- build_descr=build_descr, **self._txid())
- rset.req = self.req
- return rset
-
-
-class LogCursor(Cursor):
- """override the standard cursor to log executed queries"""
-
- def execute(self, operation, parameters=None, build_descr=True):
- """override the standard cursor to log executed queries"""
- tstart, cstart = time(), clock()
- rset = Cursor.execute(self, operation, parameters, build_descr=build_descr)
- self.connection.executed_queries.append((operation, parameters,
- time() - tstart, clock() - cstart))
- return rset
-
-def check_not_closed(func):
- def decorator(self, *args, **kwargs):
- if self._closed is not None:
- raise ProgrammingError('Closed connection %s' % self.sessionid)
- return func(self, *args, **kwargs)
- return decorator
-
-class Connection(object):
- """DB-API 2.0 compatible Connection object for CubicWeb
- """
- # make exceptions available through the connection object
- ProgrammingError = ProgrammingError
- # attributes that may be overriden per connection instance
- cursor_class = Cursor
- vreg = None
- _closed = None
-
- def __init__(self, repo, cnxid, cnxprops=None):
- self._repo = repo
- self.sessionid = cnxid
- self._close_on_del = getattr(cnxprops, 'close_on_del', True)
- self._web_request = False
- if cnxprops and cnxprops.log_queries:
- self.executed_queries = []
- self.cursor_class = LogCursor
-
- @property
- def is_repo_in_memory(self):
- """return True if this is a local, aka in-memory, connection to the
- repository
- """
- try:
- from cubicweb.server.repository import Repository
- except ImportError:
- # code not available, no way
- return False
- return isinstance(self._repo, Repository)
-
- @property # could be a cached property but we want to prevent assigment to
- # catch potential programming error.
- def anonymous_connection(self):
- login = self._repo.user_info(self.sessionid)[1]
- anon_login = self.vreg.config.get('anonymous-user')
- return login == anon_login
-
- def __repr__(self):
- if self.anonymous_connection:
- return '<Connection %s (anonymous)>' % self.sessionid
- return '<Connection %s>' % self.sessionid
-
- def __enter__(self):
- return self.cursor()
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- if exc_type is None:
- self.commit()
- else:
- self.rollback()
- return False #propagate the exception
-
- def __del__(self):
- """close the remote connection if necessary"""
- if self._closed is None and self._close_on_del:
- try:
- self.close()
- except Exception:
- pass
-
- # server-side service call #################################################
-
- @check_not_closed
- def call_service(self, regid, **kwargs):
- return self._repo.call_service(self.sessionid, regid, **kwargs)
-
- # connection initialization methods ########################################
-
- def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
- config = self.vreg.config
- if cubes is _MARKER:
- cubes = self._repo.get_cubes()
- elif cubes is None:
- cubes = ()
- else:
- if not isinstance(cubes, (list, tuple)):
- cubes = (cubes,)
- if expand:
- cubes = config.expand_cubes(cubes)
- if subpath is None:
- subpath = esubpath = ('entities', 'views')
- else:
- esubpath = subpath
- if 'views' in subpath:
- esubpath = list(subpath)
- esubpath.remove('views')
- esubpath.append(join('web', 'views'))
- # first load available configs, necessary for proper persistent
- # properties initialization
- config.load_available_configs()
- # then init cubes
- config.init_cubes(cubes)
- # then load appobjects into the registry
- vpath = config.build_appobjects_path(reversed(config.cubes_path()),
- evobjpath=esubpath,
- tvobjpath=subpath)
- self.vreg.register_objects(vpath)
-
- def use_web_compatible_requests(self, baseurl, sitetitle=None):
- """monkey patch DBAPIRequest to fake a cw.web.request, so you should
- able to call html views using rset from a simple dbapi connection.
-
- You should call `load_appobjects` at some point to register those views.
- """
- DBAPIRequest.property_value = _fake_property_value
- DBAPIRequest.next_tabindex = count().next
- DBAPIRequest.relative_path = fake
- DBAPIRequest.url = fake
- DBAPIRequest.get_page_data = fake
- DBAPIRequest.set_page_data = fake
- # XXX could ask the repo for it's base-url configuration
- self.vreg.config.set_option('base-url', baseurl)
- self.vreg.config.uiprops = {}
- self.vreg.config.datadir_url = baseurl + '/data'
- # XXX why is this needed? if really needed, could be fetched by a query
- if sitetitle is not None:
- self.vreg['propertydefs']['ui.site-title'] = {'default': sitetitle}
- self._web_request = True
-
- def request(self):
- if self._web_request:
- from cubicweb.web.request import DBAPICubicWebRequestBase
- req = DBAPICubicWebRequestBase(self.vreg, False)
- req.get_header = lambda x, default=None: default
- req.set_session = lambda session: DBAPIRequest.set_session(
- req, session)
- req.relative_path = lambda includeparams=True: ''
- else:
- req = DBAPIRequest(self.vreg)
- req.set_session(DBAPISession(self))
- return req
-
- @check_not_closed
- def user(self, req=None, props=None):
- """return the User object associated to this connection"""
- # cnx validity is checked by the call to .user_info
- eid, login, groups, properties = self._repo.user_info(self.sessionid,
- props)
- if req is None:
- 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)
- else:
- from cubicweb.entity import Entity
- user = Entity(req, rset, row=0)
- user.cw_attr_cache['login'] = login # cache login
- return user
-
- @check_not_closed
- def check(self):
- """raise `BadConnectionId` if the connection is no more valid, else
- return its latest activity timestamp.
- """
- return self._repo.check_session(self.sessionid)
-
- def _txid(self, cursor=None): # pylint: disable=E0202
- # XXX could now handle various isolation level!
- # return a dict as bw compat trick
- return {'txid': currentThread().getName()}
-
- # session data methods #####################################################
-
- @check_not_closed
- def get_shared_data(self, key, default=None, pop=False, txdata=False):
- """return value associated to key in the session's data dictionary or
- session's transaction's data if `txdata` is true.
-
- If pop is True, value will be removed from the dictionary.
-
- If key isn't defined in the dictionary, value specified by the
- `default` argument will be returned.
- """
- return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
-
- @check_not_closed
- def set_shared_data(self, key, value, txdata=False):
- """set value associated to `key` in shared data
-
- if `txdata` is true, the value will be added to the repository
- session's query data which are cleared on commit/rollback of the current
- transaction.
- """
- return self._repo.set_shared_data(self.sessionid, key, value, txdata)
-
- # meta-data accessors ######################################################
-
- @check_not_closed
- def source_defs(self):
- """Return the definition of sources used by the repository."""
- return self._repo.source_defs()
-
- @check_not_closed
- def get_schema(self):
- """Return the schema currently used by the repository."""
- return self._repo.get_schema()
-
- @check_not_closed
- def get_option_value(self, option, foreid=None):
- """Return the value for `option` in the configuration.
-
- `foreid` argument is deprecated and now useless (as of 3.19).
- """
- if foreid is not None:
- warn('[3.19] foreid argument is deprecated', DeprecationWarning,
- stacklevel=2)
- return self._repo.get_option_value(option)
-
-
- @check_not_closed
- def entity_metas(self, eid):
- """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
- try:
- return self._repo.entity_metas(self.sessionid, eid, **self._txid())
- except AttributeError:
- # talking to pre 3.19 repository
- metas = self._repo.describe(self.sessionid, eid, **self._txid())
- if len(metas) == 3: # even older backward compat
- metas = list(metas)
- metas.append(metas[1])
- return dict(zip(('type', 'source', 'extid', 'asource'), metas))
-
-
- @deprecated('[3.19] use .entity_metas(eid) instead')
- @check_not_closed
- def describe(self, eid, asdict=False):
- try:
- metas = self._repo.entity_metas(self.sessionid, eid, **self._txid())
- except AttributeError:
- metas = self._repo.describe(self.sessionid, eid, **self._txid())
- # talking to pre 3.19 repository
- if len(metas) == 3: # even older backward compat
- metas = list(metas)
- metas.append(metas[1])
- if asdict:
- return dict(zip(('type', 'source', 'extid', 'asource'), metas))
- return metas[:-1]
- if asdict:
- metas['asource'] = meta['source'] # XXX pre 3.19 client compat
- return metas
- return metas['type'], metas['source'], metas['extid']
-
-
- # db-api like interface ####################################################
-
- @check_not_closed
- def commit(self):
- """Commit pending transaction for this connection to the repository.
-
- may raises `Unauthorized` or `ValidationError` if we attempted to do
- something we're not allowed to for security or integrity reason.
-
- If the transaction is undoable, a transaction id will be returned.
- """
- return self._repo.commit(self.sessionid, **self._txid())
-
- @check_not_closed
- def rollback(self):
- """This method is optional since not all databases provide transaction
- support.
-
- In case a database does provide transactions this method causes the the
- database to roll back to the start of any pending transaction. Closing
- a connection without committing the changes first will cause an implicit
- rollback to be performed.
- """
- self._repo.rollback(self.sessionid, **self._txid())
-
- @check_not_closed
- def cursor(self, req=None):
- """Return a new Cursor Object using the connection.
-
- On pyro connection, you should get cursor after calling if
- load_appobjects method if desired (which you should call if you intend
- to use ORM abilities).
- """
- if req is None:
- req = self.request()
- return self.cursor_class(self, self._repo, req=req)
-
- @check_not_closed
- def close(self):
- """Close the connection now (rather than whenever __del__ is called).
-
- The connection will be unusable from this point forward; an Error (or
- subclass) exception will be raised if any operation is attempted with
- the connection. The same applies to all cursor objects trying to use the
- connection. Note that closing a connection without committing the
- changes first will cause an implicit rollback to be performed.
- """
- self._repo.close(self.sessionid, **self._txid())
- del self._repo # necessary for proper garbage collection
- self._closed = 1
-
- # undo support ############################################################
-
- @check_not_closed
- def undoable_transactions(self, ueid=None, req=None, **actionfilters):
- """Return a list of undoable transaction objects by the connection's
- user, ordered by descendant transaction time.
-
- Managers may filter according to user (eid) who has done the transaction
- using the `ueid` argument. Others will only see their own transactions.
-
- Additional filtering capabilities is provided by using the following
- named arguments:
-
- * `etype` to get only transactions creating/updating/deleting entities
- of the given type
-
- * `eid` to get only transactions applied to entity of the given eid
-
- * `action` to get only transactions doing the given action (action in
- 'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
- 'D'.
-
- * `public`: when additional filtering is provided, their are by default
- only searched in 'public' actions, unless a `public` argument is given
- and set to false.
- """
- actionfilters.update(self._txid())
- txinfos = self._repo.undoable_transactions(self.sessionid, ueid,
- **actionfilters)
- if req is None:
- req = self.request()
- for txinfo in txinfos:
- txinfo.req = req
- return txinfos
-
- @check_not_closed
- def transaction_info(self, txuuid, req=None):
- """Return transaction object for the given uid.
-
- raise `NoSuchTransaction` if not found or if session's user is not
- allowed (eg not in managers group and the transaction doesn't belong to
- him).
- """
- txinfo = self._repo.transaction_info(self.sessionid, txuuid,
- **self._txid())
- if req is None:
- req = self.request()
- txinfo.req = req
- return txinfo
-
- @check_not_closed
- def transaction_actions(self, txuuid, public=True):
- """Return an ordered list of action effectued during that transaction.
-
- If public is true, return only 'public' actions, eg not ones triggered
- under the cover by hooks, else return all actions.
-
- raise `NoSuchTransaction` if the transaction is not found or if
- session's user is not allowed (eg not in managers group and the
- transaction doesn't belong to him).
- """
- return self._repo.transaction_actions(self.sessionid, txuuid, public,
- **self._txid())
-
- @check_not_closed
- def undo_transaction(self, txuuid):
- """Undo the given transaction. Return potential restoration errors.
-
- raise `NoSuchTransaction` if not found or if session's user is not
- allowed (eg not in managers group and the transaction doesn't belong to
- him).
- """
- return self._repo.undo_transaction(self.sessionid, txuuid,
- **self._txid())
-
-in_memory_cnx = deprecated('[3.16] use _repo_connect instead)')(_repo_connect)
--- a/debian/changelog Tue Jul 19 15:59:02 2016 +0200
+++ b/debian/changelog Tue Jul 19 16:13:12 2016 +0200
@@ -1,3 +1,51 @@
+cubicweb (3.21.6-1) unstable; urgency=medium
+
+ * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr> Tue, 16 Feb 2016 18:53:15 +0100
+
+cubicweb (3.21.5-2) unstable; urgency=medium
+
+ * Fix conflict between cubicweb-server and cubicweb-dev.
+
+ -- Julien Cristau <julien.cristau@logilab.fr> Thu, 17 Dec 2015 14:56:07 +0100
+
+cubicweb (3.21.5-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Rémi Cardona <remi.cardona@logilab.fr> Tue, 15 Dec 2015 17:33:05 +0100
+
+cubicweb (3.21.4-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Rémi Cardona <remi.cardona@logilab.fr> Tue, 15 Dec 2015 11:59:58 +0100
+
+cubicweb (3.21.3-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Rémi Cardona <remi.cardona@logilab.fr> Wed, 09 Dec 2015 18:29:39 +0100
+
+cubicweb (3.21.2-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Rémi Cardona <remi.cardona@logilab.fr> Fri, 09 Oct 2015 18:00:39 +0200
+
+cubicweb (3.21.1-1) unstable; urgency=medium
+
+ * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr> Tue, 28 Jul 2015 18:05:55 +0200
+
+cubicweb (3.21.0-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Julien Cristau <julien.cristau@logilab.fr> Fri, 10 Jul 2015 17:04:11 +0200
+
cubicweb (3.20.16-1) unstable; urgency=medium
* new upstream release
--- a/debian/control Tue Jul 19 15:59:02 2016 +0200
+++ b/debian/control Tue Jul 19 16:13:12 2016 +0200
@@ -20,7 +20,7 @@
python-lxml,
Standards-Version: 3.9.1
Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.6
+X-Python-Version: >= 2.6
Package: cubicweb
Architecture: all
@@ -58,10 +58,10 @@
| python-pysqlite2,
python-passlib
Recommends:
- pyro (<< 4.0.0),
- cubicweb-documentation (= ${source:Version})
+ cubicweb-documentation (= ${source:Version}),
Suggests:
- python-zmq
+ python-zmq,
+ python-cwclientlib (>= 0.4.0),
Description: server part of the CubicWeb framework
CubicWeb is a semantic web application framework.
.
@@ -109,7 +109,6 @@
cubicweb-ctl (= ${source:Version}),
python-twisted-web
Recommends:
- pyro (<< 4.0.0),
cubicweb-documentation (= ${source:Version})
Description: twisted-based web interface for the CubicWeb framework
CubicWeb is a semantic web application framework.
@@ -137,6 +136,7 @@
Breaks:
cubicweb-inlinedit (<< 1.1.1),
cubicweb-bootstrap (<< 0.6.6),
+ cubicweb-folder (<< 1.10.0),
Description: web interface library for the CubicWeb framework
CubicWeb is a semantic web application framework.
.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-common.install Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,4 @@
+usr/lib/python2*/*-packages/cubicweb/entities/
+usr/lib/python2*/*-packages/cubicweb/ext/
+usr/share/cubicweb/cubes/
+usr/lib/python2*/*-packages/cubicweb/*.py
--- a/debian/cubicweb-common.install.in Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/entities/
-usr/lib/PY_VERSION/*-packages/cubicweb/ext/
-usr/share/cubicweb/cubes/
-usr/lib/PY_VERSION/*-packages/cubicweb/*.py
--- a/debian/cubicweb-ctl.cubicweb.init Tue Jul 19 15:59:02 2016 +0200
+++ b/debian/cubicweb-ctl.cubicweb.init Tue Jul 19 16:13:12 2016 +0200
@@ -4,16 +4,14 @@
# Provides: cubicweb
# Required-Start: $remote_fs $syslog $local_fs $network
# Required-Stop: $remote_fs $syslog $local_fs $network
-# Should-Start: postgresql pyro-nsd
-# Should-Stop: postgresql pyro-nsd
+# Should-Start: postgresql
+# Should-Stop: postgresql
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start cubicweb application at boot time
### END INIT INFO
# FIXME Seems to be inadequate here
-# FIXME If related to pyro, try instead:
-# export PYRO_STORAGE="/tmp"
cd /tmp
# FIXME Work-around about the following lintian error
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-ctl.install Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,3 @@
+usr/bin/cubicweb-ctl usr/bin/
+usr/lib/python2*/*-packages/cubicweb/cwctl.py
+../cubicweb-ctl.bash_completion etc/bash_completion.d/cubicweb-ctl
--- a/debian/cubicweb-ctl.install.in Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-usr/bin/cubicweb-ctl usr/bin/
-usr/lib/PY_VERSION/*-packages/cubicweb/cwctl.py
-../cubicweb-ctl.bash_completion etc/bash_completion.d/cubicweb-ctl
--- a/debian/cubicweb-ctl.postinst Tue Jul 19 15:59:02 2016 +0200
+++ b/debian/cubicweb-ctl.postinst Tue Jul 19 16:13:12 2016 +0200
@@ -10,32 +10,6 @@
;;
esac
-if [ "$1" = configure ]; then
- # XXX bw compat: erudi -> cubicweb migration
- if [ -e "/etc/erudi.d/" ]; then
- mv /etc/erudi.d/* /etc/cubicweb.d/ && (
- echo 'moved /etc/erudi.d/* to /etc/cubicweb.d/'
- sed -i s/ginco/cubicweb/g /etc/*/*.py
- sed -i s/erudi/cubicweb/ */*.conf
- ) || true # empty dir
- fi
- if [ -e "/var/log/erudi/" ]; then
- mv /var/log/erudi/* /var/log/cubicweb/ && (
- echo 'moved /var/log/erudi/* to /var/log/cubicweb/'
- ) || true # empty dir
- fi
- if [ -e "/var/lib/erudi/backup" ]; then
- mv /var/lib/erudi/backup/* /var/lib/cubicweb/backup/ && (
- echo 'moved /var/lib/erudi/backup/* to /var/lib/cubicweb/backup/'
- ) || true # empty dir
- fi
- if [ -e "/var/lib/erudi/instances" ]; then
- mv /var/lib/erudi/instances/* /var/lib/cubicweb/instances/ && (
- echo 'moved /var/lib/erudi/instances/* to /var/lib/cubicweb/instances/'
- ) || true # empty dir
- fi
-fi
-
#DEBHELPER#
exit 0
--- a/debian/cubicweb-ctl.postrm Tue Jul 19 15:59:02 2016 +0200
+++ b/debian/cubicweb-ctl.postrm Tue Jul 19 16:13:12 2016 +0200
@@ -1,8 +1,15 @@
#!/bin/sh -e
-if [ "$1" = "purge" ] ; then
+
+if [ "$1" = "remove" ]; then
update-rc.d cubicweb remove >/dev/null
fi
+if [ "$1" = "purge" ] ; then
+ rm -rf /etc/cubicweb.d/
+ rm -rf /var/log/cubicweb/
+ rm -rf /var/lib/cubicweb/
+fi
+
#DEBHELPER#
exit 0
--- a/debian/cubicweb-ctl.prerm Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-#! /bin/sh -e
-
-case "$1" in
- purge)
- rm -rf /etc/cubicweb.d/
- rm -rf /var/log/cubicweb/
- rm -rf /var/lib/cubicweb/
- ;;
-esac
-
-#DEBHELPER#
-
-exit 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-dev.install Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,11 @@
+usr/lib/python2*/*-packages/cubicweb/devtools/
+usr/lib/python2*/*-packages/cubicweb/skeleton/
+usr/lib/python2*/*-packages/cubicweb/test
+usr/lib/python2*/*-packages/cubicweb/dataimport/test
+usr/lib/python2*/*-packages/cubicweb/entities/test
+usr/lib/python2*/*-packages/cubicweb/ext/test
+usr/lib/python2*/*-packages/cubicweb/server/test
+usr/lib/python2*/*-packages/cubicweb/sobjects/test
+usr/lib/python2*/*-packages/cubicweb/hooks/test
+usr/lib/python2*/*-packages/cubicweb/web/test
+usr/lib/python2*/*-packages/cubicweb/etwist/test
--- a/debian/cubicweb-dev.install.in Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/devtools/
-usr/lib/PY_VERSION/*-packages/cubicweb/skeleton/
-usr/lib/PY_VERSION/*-packages/cubicweb/test
-usr/lib/PY_VERSION/*-packages/cubicweb/entities/test
-usr/lib/PY_VERSION/*-packages/cubicweb/ext/test
-usr/lib/PY_VERSION/*-packages/cubicweb/server/test
-usr/lib/PY_VERSION/*-packages/cubicweb/sobjects/test
-usr/lib/PY_VERSION/*-packages/cubicweb/hooks/test
-usr/lib/PY_VERSION/*-packages/cubicweb/web/test
-usr/lib/PY_VERSION/*-packages/cubicweb/etwist/test
--- a/debian/cubicweb-doc Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-Document: cubicweb-doc
-Title: CubicWeb documentation
-Author: Logilab
-Abstract: Some base documentation for CubicWeb users and developpers
-Section: Apps/Programming
-
-Format: HTML
-Index: /usr/share/doc/cubicweb-documentation/index.html
-Files: /usr/share/doc/cubicweb-documentation/*.html
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-documentation.doc-base Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,9 @@
+Document: cubicweb-doc
+Title: CubicWeb documentation
+Author: Logilab
+Abstract: Some base documentation for CubicWeb users and developpers
+Section: Apps/Programming
+
+Format: HTML
+Index: /usr/share/doc/cubicweb-documentation/index.html
+Files: /usr/share/doc/cubicweb-documentation/*.html
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-documentation.install Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,2 @@
+../../doc/book usr/share/doc/cubicweb-documentation
+../../doc/_build/html usr/share/doc/cubicweb-documentation
--- a/debian/cubicweb-documentation.install.in Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-../../doc/book usr/share/doc/cubicweb-documentation
-../../doc/html usr/share/doc/cubicweb-documentation
-../../debian/cubicweb-doc usr/share/doc-base/cubicweb-doc
--- a/debian/cubicweb-documentation.postinst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-#! /bin/sh -e
-#
-
-if [ "$1" = configure ]; then
- if which install-docs >/dev/null 2>&1; then
- install-docs -i /usr/share/doc-base/cubicweb-doc
- fi
-fi
-
-
-#DEBHELPER#
-
-exit 0
--- a/debian/cubicweb-documentation.prerm Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-#! /bin/sh -e
-#
-
-if [ "$1" = remove -o "$1" = upgrade ]; then
- if which install-docs >/dev/null 2>&1; then
- install-docs -r cubicweb-doc
- fi
-fi
-
-#DEBHELPER#
-
-exit 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-server.install Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,6 @@
+usr/lib/python2*/*-packages/cubicweb/dataimport/
+usr/lib/python2*/*-packages/cubicweb/server/
+usr/lib/python2*/*-packages/cubicweb/hooks/
+usr/lib/python2*/*-packages/cubicweb/sobjects/
+usr/lib/python2*/*-packages/cubicweb/schemas/
+usr/share/cubicweb/migration/
--- a/debian/cubicweb-server.install.in Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/server/
-usr/lib/PY_VERSION/*-packages/cubicweb/hooks/
-usr/lib/PY_VERSION/*-packages/cubicweb/sobjects/
-usr/lib/PY_VERSION/*-packages/cubicweb/schemas/
-usr/share/cubicweb/migration/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-twisted.install Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+usr/lib/python2*/*-packages/cubicweb/etwist/
--- a/debian/cubicweb-twisted.install.in Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/etwist/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/cubicweb-web.install Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,4 @@
+usr/lib/python2*/*-packages/cubicweb/web
+usr/lib/python2*/*-packages/cubicweb/wsgi
+usr/share/cubicweb/cubes/shared/data
+usr/share/cubicweb/cubes/shared/wdoc
--- a/debian/cubicweb-web.install.in Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-usr/lib/PY_VERSION/*-packages/cubicweb/web
-usr/lib/PY_VERSION/*-packages/cubicweb/wsgi
-usr/share/cubicweb/cubes/shared/data
-usr/share/cubicweb/cubes/shared/wdoc
--- a/debian/pycompat Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-2
--- a/debian/rules Tue Jul 19 15:59:02 2016 +0200
+++ b/debian/rules Tue Jul 19 16:13:12 2016 +0200
@@ -5,8 +5,6 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
-PY_VERSION:=$(shell pyversions -d)
-
build: build-stamp
build-stamp:
dh_testdir
@@ -17,7 +15,7 @@
# documentation build is now made optional since it can break for old
# distributions and we don't want to block a new release of Cubicweb
# because of documentation issues.
- -PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc/book/en all
+ -PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc all
rm -rf debian/pythonpath
touch build-stamp
@@ -27,10 +25,10 @@
rm -rf build
#rm -rf debian/cubicweb-*/
find . -name "*.pyc" -delete
- rm -f $(basename $(wildcard debian/*.in))
+ -$(MAKE) -C doc clean
dh_clean
-install: build $(basename $(wildcard debian/*.in))
+install: build
dh_testdir
dh_testroot
dh_clean
@@ -49,30 +47,29 @@
dh_lintian
# Remove unittests directory (should be available in cubicweb-dev only)
- rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/server/test
- rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/hooks/test
- rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/sobjects/test
- rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/*-packages/cubicweb/web/test
- rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/*-packages/cubicweb/etwist/test
- rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/*-packages/cubicweb/ext/test
- rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/*-packages/cubicweb/entities/test
+ rm -rf debian/cubicweb-server/usr/lib/python2*/*-packages/cubicweb/dataimport/test
+ rm -rf debian/cubicweb-server/usr/lib/python2*/*-packages/cubicweb/server/test
+ rm -rf debian/cubicweb-server/usr/lib/python2*/*-packages/cubicweb/hooks/test
+ rm -rf debian/cubicweb-server/usr/lib/python2*/*-packages/cubicweb/sobjects/test
+ rm -rf debian/cubicweb-web/usr/lib/python2*/*-packages/cubicweb/web/test
+ rm -rf debian/cubicweb-twisted/usr/lib/python2*/*-packages/cubicweb/etwist/test
+ rm -rf debian/cubicweb-common/usr/lib/python2*/*-packages/cubicweb/ext/test
+ rm -rf debian/cubicweb-common/usr/lib/python2*/*-packages/cubicweb/entities/test
-%: %.in
- sed "s/PY_VERSION/${PY_VERSION}/g" < $< > $@
-
# Build architecture-independent files here.
binary-indep: build install
dh_testdir
dh_testroot -i
dh_python2 -i
+ dh_python2 -i /usr/share/cubicweb
dh_installinit -i -n --name cubicweb -u"defaults 99"
dh_installlogrotate -i
dh_installdocs -i -A README
dh_installman -i
- dh_installchangelogs -i
+ dh_installchangelogs -i -Xdoc/changes
dh_link -i
- dh_compress -i -X.py -X.ini -X.xml -X.js -X.rst -X.txt
+ dh_compress -i -X.py -X.ini -X.xml -X.js -X.rst -X.txt -Xchangelog.html
dh_fixperms -i
dh_installdeb -i
dh_gencontrol -i
--- a/devtools/__init__.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/__init__.py Tue Jul 19 16:13:12 2016 +0200
@@ -26,7 +26,6 @@
import shutil
import pickle
import glob
-import random
import subprocess
import warnings
import tempfile
@@ -93,8 +92,6 @@
DEFAULT_PSQL_SOURCES = DEFAULT_SOURCES.copy()
DEFAULT_PSQL_SOURCES['system'] = DEFAULT_SOURCES['system'].copy()
DEFAULT_PSQL_SOURCES['system']['db-driver'] = 'postgres'
-DEFAULT_PSQL_SOURCES['system']['db-host'] = '/tmp'
-DEFAULT_PSQL_SOURCES['system']['db-port'] = str(random.randrange(5432, 2**16))
DEFAULT_PSQL_SOURCES['system']['db-user'] = unicode(getpass.getuser())
DEFAULT_PSQL_SOURCES['system']['db-password'] = None
@@ -176,8 +173,8 @@
return self._apphome
appdatahome = apphome
- def load_configuration(self):
- super(TestServerConfiguration, self).load_configuration()
+ def load_configuration(self, **kw):
+ super(TestServerConfiguration, self).load_configuration(**kw)
# no undo support in tests
self.global_set_option('undo-enabled', 'n')
@@ -237,10 +234,6 @@
def available_languages(self, *args):
return self.cw_languages()
- def pyro_enabled(self):
- # but export PYRO_MULTITHREAD=0 or you get problems with sqlite and
- # threads
- return True
# XXX merge with BaseApptestConfiguration ?
class ApptestConfiguration(BaseApptestConfiguration):
@@ -251,7 +244,7 @@
skip_db_create_and_restore = False
def __init__(self, appid, apphome=None,
- log_threshold=logging.CRITICAL, sourcefile=None):
+ log_threshold=logging.WARNING, sourcefile=None):
BaseApptestConfiguration.__init__(self, appid, apphome,
log_threshold=log_threshold)
self.init_repository = sourcefile is None
@@ -303,6 +296,14 @@
# pure consistency check
assert self.system_source['db-driver'] == self.DRIVER
+ # some handlers want to store info here, avoid a warning
+ from cubicweb.server.sources.native import NativeSQLSource
+ NativeSQLSource.options += (
+ ('global-db-name',
+ {'type': 'string', 'help': 'for internal use only'
+ }),
+ )
+
def _ensure_test_backup_db_dir(self):
"""Return path of directory for database backup.
@@ -398,9 +399,9 @@
def _new_repo(self, config):
"""Factory method to create a new Repository Instance"""
- from cubicweb.dbapi import in_memory_repo
+ from cubicweb.repoapi import _get_inmemory_repo
config._cubes = None
- repo = in_memory_repo(config)
+ repo = _get_inmemory_repo(config)
config.repository = lambda x=None: repo
# extending Repository class
repo._has_started = False
@@ -499,7 +500,7 @@
repo = self.get_repo(startup=True)
cnx = self.get_cnx()
with cnx:
- pre_setup_func(cnx._cnx, self.config)
+ pre_setup_func(cnx, self.config)
cnx.commit()
self.backup_database(test_db_id)
@@ -534,6 +535,48 @@
### postgres test database handling ############################################
+def startpgcluster(pyfile):
+ """Start a postgresql cluster next to pyfile"""
+ datadir = join(os.path.dirname(pyfile), 'data',
+ 'pgdb-%s' % os.path.splitext(os.path.basename(pyfile))[0])
+ if not exists(datadir):
+ try:
+ subprocess.check_call(['initdb', '-D', datadir, '-E', 'utf-8', '--locale=C'])
+
+ except OSError, err:
+ if err.errno == errno.ENOENT:
+ raise OSError('"initdb" could not be found. '
+ 'You should add the postgresql bin folder to your PATH '
+ '(/usr/lib/postgresql/9.1/bin for example).')
+ raise
+ datadir = os.path.abspath(datadir)
+ pgport = '5432'
+ env = os.environ.copy()
+ sockdir = tempfile.mkdtemp(prefix='cwpg')
+ DEFAULT_PSQL_SOURCES['system']['db-host'] = sockdir
+ DEFAULT_PSQL_SOURCES['system']['db-port'] = pgport
+ options = '-h "" -k %s -p %s' % (sockdir, pgport)
+ options += ' -c fsync=off -c full_page_writes=off'
+ options += ' -c synchronous_commit=off'
+ try:
+ subprocess.check_call(['pg_ctl', 'start', '-w', '-D', datadir,
+ '-o', options],
+ env=env)
+ except OSError, err:
+ if err.errno == errno.ENOENT:
+ raise OSError('"pg_ctl" could not be found. '
+ 'You should add the postgresql bin folder to your PATH '
+ '(/usr/lib/postgresql/9.1/bin for example).')
+ raise
+
+
+def stoppgcluster(pyfile):
+ """Kill the postgresql cluster running next to pyfile"""
+ datadir = join(os.path.dirname(pyfile), 'data',
+ 'pgdb-%s' % os.path.splitext(os.path.basename(pyfile))[0])
+ subprocess.call(['pg_ctl', 'stop', '-D', datadir, '-m', 'fast'])
+
+
class PostgresTestDataBaseHandler(TestDataBaseHandler):
DRIVER = 'postgres'
@@ -543,45 +586,11 @@
__CTL = set()
- @classmethod
- def killall(cls):
- for datadir in cls.__CTL:
- subprocess.call(['pg_ctl', 'stop', '-D', datadir, '-m', 'fast'])
-
def __init__(self, *args, **kwargs):
super(PostgresTestDataBaseHandler, self).__init__(*args, **kwargs)
- datadir = realpath(join(self.config.apphome, 'pgdb'))
- if datadir in self.__CTL:
- return
- if not exists(datadir):
- try:
- subprocess.check_call(['initdb', '-D', datadir, '-E', 'utf-8', '--locale=C'])
-
- except OSError, err:
- if err.errno == errno.ENOENT:
- raise OSError('"initdb" could not be found. '
- 'You should add the postgresql bin folder to your PATH '
- '(/usr/lib/postgresql/9.1/bin for example).')
- raise
- port = self.system_source['db-port']
- directory = self.system_source['db-host']
- env = os.environ.copy()
- env['PGPORT'] = str(port)
- env['PGHOST'] = str(directory)
- options = '-h "" -k %s -p %s' % (directory, port)
- options += ' -c fsync=off -c full_page_writes=off'
- options += ' -c synchronous_commit=off'
- try:
- subprocess.check_call(['pg_ctl', 'start', '-w', '-D', datadir,
- '-o', options],
- env=env)
- except OSError, err:
- if err.errno == errno.ENOENT:
- raise OSError('"pg_ctl" could not be found. '
- 'You should add the postgresql bin folder to your PATH '
- '(/usr/lib/postgresql/9.1/bin for example).')
- raise
- self.__CTL.add(datadir)
+ if 'global-db-name' not in self.system_source:
+ self.system_source['global-db-name'] = self.system_source['db-name']
+ self.system_source['db-name'] = self.system_source['db-name'] + str(os.getpid())
@property
@cached
@@ -590,6 +599,10 @@
return get_db_helper('postgres')
@property
+ def dbname(self):
+ return self.system_source['global-db-name']
+
+ @property
def dbcnx(self):
try:
return self._cnx
@@ -615,13 +628,18 @@
return backup_name
return None
+ def has_cache(self, db_id):
+ backup_name = self._backup_name(db_id)
+ return (super(PostgresTestDataBaseHandler, self).has_cache(db_id)
+ and backup_name in self.helper.list_databases(self.cursor))
+
def init_test_database(self):
"""initialize a fresh postgresql database used for testing purpose"""
from cubicweb.server import init_repository
from cubicweb.server.serverctl import system_source_cnx, createdb
# connect on the dbms system base to create our base
try:
- self._drop(self.dbname)
+ self._drop(self.system_source['db-name'])
createdb(self.helper, self.system_source, self.dbcnx, self.cursor)
self.dbcnx.commit()
cnx = system_source_cnx(self.system_source, special_privs='LANGUAGE C',
@@ -699,7 +717,7 @@
"""Actual restore of the current database.
Use the value tostored in db_cache as input """
- self._drop(self.dbname)
+ self._drop(self.system_source['db-name'])
createdb(self.helper, self.system_source, self.dbcnx, self.cursor,
template=backup_coordinates)
self.dbcnx.commit()
@@ -771,7 +789,7 @@
dbfile = self.absolute_dbfile()
backup_file = self.absolute_backup_file(db_id, 'sqlite')
shutil.copy(dbfile, backup_file)
- # Usefull to debug WHO write a database
+ # Useful to debug WHO writes a database
# backup_stack = self.absolute_backup_file(db_id, '.stack')
#with open(backup_stack, 'w') as backup_stack_file:
# import traceback
@@ -800,7 +818,6 @@
import atexit
atexit.register(SQLiteTestDataBaseHandler._cleanup_all_tmpdb)
-atexit.register(PostgresTestDataBaseHandler.killall)
def install_sqlite_patch(querier):
--- a/devtools/data/qunit.css Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/data/qunit.css Tue Jul 19 16:13:12 2016 +0200
@@ -1,119 +1,291 @@
+/*!
+ * QUnit 1.18.0
+ * http://qunitjs.com/
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2015-04-03T10:23Z
+ */
-ol#qunit-tests {
- font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
- margin:0;
- padding:0;
- list-style-position:inside;
+/** Font Family and Sizes */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+}
+
+#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-tests { font-size: smaller; }
+
+
+/** Resets */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
+ margin: 0;
+ padding: 0;
+}
+
+
+/** Header */
+
+#qunit-header {
+ padding: 0.5em 0 0.5em 1em;
- font-size: smaller;
+ color: #8699A4;
+ background-color: #0D3349;
+
+ font-size: 1.5em;
+ line-height: 1em;
+ font-weight: 400;
+
+ border-radius: 5px 5px 0 0;
+}
+
+#qunit-header a {
+ text-decoration: none;
+ color: #C2CCD1;
}
-ol#qunit-tests li{
- padding:0.4em 0.5em 0.4em 2.5em;
- border-bottom:1px solid #fff;
- font-size:small;
- list-style-position:inside;
+
+#qunit-header a:hover,
+#qunit-header a:focus {
+ color: #FFF;
+}
+
+#qunit-testrunner-toolbar label {
+ display: inline-block;
+ padding: 0 0.5em 0 0.1em;
+}
+
+#qunit-banner {
+ height: 5px;
+}
+
+#qunit-testrunner-toolbar {
+ padding: 0.5em 1em 0.5em 1em;
+ color: #5E740B;
+ background-color: #EEE;
+ overflow: hidden;
}
-ol#qunit-tests li ol{
- box-shadow: inset 0px 2px 13px #999;
- -moz-box-shadow: inset 0px 2px 13px #999;
- -webkit-box-shadow: inset 0px 2px 13px #999;
- margin-top:0.5em;
- margin-left:0;
- padding:0.5em;
- background-color:#fff;
- border-radius:15px;
- -moz-border-radius: 15px;
- -webkit-border-radius: 15px;
+
+#qunit-userAgent {
+ padding: 0.5em 1em 0.5em 1em;
+ background-color: #2B81AF;
+ color: #FFF;
+ text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+
+#qunit-modulefilter-container {
+ float: right;
+ padding: 0.2em;
+}
+
+.qunit-url-config {
+ display: inline-block;
+ padding: 0.1em;
+}
+
+.qunit-filter {
+ display: block;
+ float: right;
+ margin-left: 1em;
+}
+
+/** Tests: Pass/Fail */
+
+#qunit-tests {
+ list-style-position: inside;
+}
+
+#qunit-tests li {
+ padding: 0.4em 1em 0.4em 1em;
+ border-bottom: 1px solid #FFF;
+ list-style-position: inside;
}
-ol#qunit-tests li li{
- border-bottom:none;
- margin:0.5em;
- background-color:#fff;
- list-style-position: inside;
- padding:0.4em 0.5em 0.4em 0.5em;
+
+#qunit-tests > li {
+ display: none;
+}
+
+#qunit-tests li.running,
+#qunit-tests li.pass,
+#qunit-tests li.fail,
+#qunit-tests li.skipped {
+ display: list-item;
+}
+
+#qunit-tests.hidepass li.running,
+#qunit-tests.hidepass li.pass {
+ visibility: hidden;
+ position: absolute;
+ width: 0px;
+ height: 0px;
+ padding: 0;
+ border: 0;
+ margin: 0;
+}
+
+#qunit-tests li strong {
+ cursor: pointer;
+}
+
+#qunit-tests li.skipped strong {
+ cursor: default;
+}
+
+#qunit-tests li a {
+ padding: 0.5em;
+ color: #C2CCD1;
+ text-decoration: none;
}
-ol#qunit-tests li li.pass{
- border-left:26px solid #C6E746;
- background-color:#fff;
- color:#5E740B;
- }
-ol#qunit-tests li li.fail{
- border-left:26px solid #EE5757;
- background-color:#fff;
- color:#710909;
+#qunit-tests li p a {
+ padding: 0.25em;
+ color: #6B6464;
+}
+#qunit-tests li a:hover,
+#qunit-tests li a:focus {
+ color: #000;
+}
+
+#qunit-tests li .runtime {
+ float: right;
+ font-size: smaller;
}
-ol#qunit-tests li.pass{
- background-color:#D2E0E6;
- color:#528CE0;
+
+.qunit-assert-list {
+ margin-top: 0.5em;
+ padding: 0.5em;
+
+ background-color: #FFF;
+
+ border-radius: 5px;
}
-ol#qunit-tests li.fail{
- background-color:#EE5757;
- color:#000;
+
+.qunit-collapsed {
+ display: none;
+}
+
+#qunit-tests table {
+ border-collapse: collapse;
+ margin-top: 0.2em;
}
-ol#qunit-tests li strong {
- cursor:pointer;
+
+#qunit-tests th {
+ text-align: right;
+ vertical-align: top;
+ padding: 0 0.5em 0 0;
+}
+
+#qunit-tests td {
+ vertical-align: top;
+}
+
+#qunit-tests pre {
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
}
-h1#qunit-header{
- background-color:#0d3349;
- margin:0;
- padding:0.5em 0 0.5em 1em;
- color:#fff;
- font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
- border-top-right-radius:15px;
- border-top-left-radius:15px;
- -moz-border-radius-topright:15px;
- -moz-border-radius-topleft:15px;
- -webkit-border-top-right-radius:15px;
- -webkit-border-top-left-radius:15px;
- text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px;
+
+#qunit-tests del {
+ background-color: #E0F2BE;
+ color: #374E0C;
+ text-decoration: none;
+}
+
+#qunit-tests ins {
+ background-color: #FFCACA;
+ color: #500;
+ text-decoration: none;
}
-h2#qunit-banner{
- font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
- height:5px;
- margin:0;
- padding:0;
+
+/*** Test Counts */
+
+#qunit-tests b.counts { color: #000; }
+#qunit-tests b.passed { color: #5E740B; }
+#qunit-tests b.failed { color: #710909; }
+
+#qunit-tests li li {
+ padding: 5px;
+ background-color: #FFF;
+ border-bottom: none;
+ list-style-position: inside;
}
-h2#qunit-banner.qunit-pass{
- background-color:#C6E746;
-}
-h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar {
- background-color:#EE5757;
+
+/*** Passing Styles */
+
+#qunit-tests li li.pass {
+ color: #3C510C;
+ background-color: #FFF;
+ border-left: 10px solid #C6E746;
}
-#qunit-testrunner-toolbar {
- font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
- padding:0;
- /*width:80%;*/
- padding:0em 0 0.5em 2em;
- font-size: small;
+
+#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
+#qunit-tests .pass .test-name { color: #366097; }
+
+#qunit-tests .pass .test-actual,
+#qunit-tests .pass .test-expected { color: #999; }
+
+#qunit-banner.qunit-pass { background-color: #C6E746; }
+
+/*** Failing Styles */
+
+#qunit-tests li li.fail {
+ color: #710909;
+ background-color: #FFF;
+ border-left: 10px solid #EE5757;
+ white-space: pre;
+}
+
+#qunit-tests > li:last-child {
+ border-radius: 0 0 5px 5px;
}
-h2#qunit-userAgent {
- font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
- background-color:#2b81af;
- margin:0;
- padding:0;
- color:#fff;
- font-size: small;
- padding:0.5em 0 0.5em 2.5em;
- text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+
+#qunit-tests .fail { color: #000; background-color: #EE5757; }
+#qunit-tests .fail .test-name,
+#qunit-tests .fail .module-name { color: #000; }
+
+#qunit-tests .fail .test-actual { color: #EE5757; }
+#qunit-tests .fail .test-expected { color: #008000; }
+
+#qunit-banner.qunit-fail { background-color: #EE5757; }
+
+/*** Skipped tests */
+
+#qunit-tests .skipped {
+ background-color: #EBECE9;
+}
+
+#qunit-tests .qunit-skipped-label {
+ background-color: #F4FF77;
+ display: inline-block;
+ font-style: normal;
+ color: #366097;
+ line-height: 1.8em;
+ padding: 0 0.5em;
+ margin: -0.4em 0.4em -0.4em 0;
}
-p#qunit-testresult{
- font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
- margin:0;
- font-size: small;
- color:#2b81af;
- border-bottom-right-radius:15px;
- border-bottom-left-radius:15px;
- -moz-border-radius-bottomright:15px;
- -moz-border-radius-bottomleft:15px;
- -webkit-border-bottom-right-radius:15px;
- -webkit-border-bottom-left-radius:15px;
- background-color:#D2E0E6;
- padding:0.5em 0.5em 0.5em 2.5em;
+
+/** Result */
+
+#qunit-testresult {
+ padding: 0.5em 1em 0.5em 1em;
+
+ color: #2B81AF;
+ background-color: #D2E0E6;
+
+ border-bottom: 1px solid #FFF;
}
-strong b.fail{
- color:#710909;
- }
-strong b.pass{
- color:#5E740B;
- }
+#qunit-testresult .module-name {
+ font-weight: 700;
+}
+
+/** Fixture */
+
+#qunit-fixture {
+ position: absolute;
+ top: -10000px;
+ left: -10000px;
+ width: 1000px;
+ height: 1000px;
+}
--- a/devtools/data/qunit.js Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/data/qunit.js Tue Jul 19 16:13:12 2016 +0200
@@ -1,643 +1,713 @@
-/*
- * QUnit - A JavaScript Unit Testing Framework
- *
- * http://docs.jquery.com/QUnit
+/*!
+ * QUnit 1.18.0
+ * http://qunitjs.com/
*
- * Copyright (c) 2009 John Resig, Jörn Zaefferer
- * Dual licensed under the MIT (MIT-LICENSE.txt)
- * and GPL (GPL-LICENSE.txt) licenses.
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2015-04-03T10:23Z
*/
-(function(window) {
-
-var QUnit = {
-
- // Initialize the configuration options
- init: function() {
- config = {
- stats: { all: 0, bad: 0 },
- moduleStats: { all: 0, bad: 0 },
- started: +new Date,
- updateRate: 1000,
- blocking: false,
- autorun: false,
- assertions: [],
- filters: [],
- queue: []
- };
-
- var tests = id("qunit-tests"),
- banner = id("qunit-banner"),
- result = id("qunit-testresult");
-
- if ( tests ) {
- tests.innerHTML = "";
- }
-
- if ( banner ) {
- banner.className = "";
- }
-
- if ( result ) {
- result.parentNode.removeChild( result );
+(function( window ) {
+
+var QUnit,
+ config,
+ onErrorFnPrev,
+ loggingCallbacks = {},
+ fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ),
+ toString = Object.prototype.toString,
+ hasOwn = Object.prototype.hasOwnProperty,
+ // Keep a local reference to Date (GH-283)
+ Date = window.Date,
+ now = Date.now || function() {
+ return new Date().getTime();
+ },
+ globalStartCalled = false,
+ runStarted = false,
+ setTimeout = window.setTimeout,
+ clearTimeout = window.clearTimeout,
+ defined = {
+ document: window.document !== undefined,
+ setTimeout: window.setTimeout !== undefined,
+ sessionStorage: (function() {
+ var x = "qunit-test-string";
+ try {
+ sessionStorage.setItem( x, x );
+ sessionStorage.removeItem( x );
+ return true;
+ } catch ( e ) {
+ return false;
+ }
+ }())
+ },
+ /**
+ * Provides a normalized error string, correcting an issue
+ * with IE 7 (and prior) where Error.prototype.toString is
+ * not properly implemented
+ *
+ * Based on http://es5.github.com/#x15.11.4.4
+ *
+ * @param {String|Error} error
+ * @return {String} error message
+ */
+ errorString = function( error ) {
+ var name, message,
+ errorString = error.toString();
+ if ( errorString.substring( 0, 7 ) === "[object" ) {
+ name = error.name ? error.name.toString() : "Error";
+ message = error.message ? error.message.toString() : "";
+ if ( name && message ) {
+ return name + ": " + message;
+ } else if ( name ) {
+ return name;
+ } else if ( message ) {
+ return message;
+ } else {
+ return "Error";
+ }
+ } else {
+ return errorString;
}
},
-
- // call on start of module test to prepend name to all tests
- module: function(name, testEnvironment) {
- config.currentModule = name;
-
- synchronize(function() {
- if ( config.currentModule ) {
- QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
+ /**
+ * Makes a clone of an object using only Array or Object as base,
+ * and copies over the own enumerable properties.
+ *
+ * @param {Object} obj
+ * @return {Object} New object with only the own properties (recursively).
+ */
+ objectValues = function( obj ) {
+ var key, val,
+ vals = QUnit.is( "array", obj ) ? [] : {};
+ for ( key in obj ) {
+ if ( hasOwn.call( obj, key ) ) {
+ val = obj[ key ];
+ vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
}
-
- config.currentModule = name;
- config.moduleTestEnvironment = testEnvironment;
- config.moduleStats = { all: 0, bad: 0 };
-
- QUnit.moduleStart( name, testEnvironment );
- });
+ }
+ return vals;
+ };
+
+QUnit = {};
+
+/**
+ * Config object: Maintain internal state
+ * Later exposed as QUnit.config
+ * `config` initialized at top of scope
+ */
+config = {
+ // The queue of tests to run
+ queue: [],
+
+ // block until document ready
+ blocking: true,
+
+ // by default, run previously failed tests first
+ // very useful in combination with "Hide passed tests" checked
+ reorder: true,
+
+ // by default, modify document.title when suite is done
+ altertitle: true,
+
+ // by default, scroll to top of the page when suite is done
+ scrolltop: true,
+
+ // when enabled, all tests must call expect()
+ requireExpects: false,
+
+ // depth up-to which object will be dumped
+ maxDepth: 5,
+
+ // add checkboxes that are persisted in the query-string
+ // when enabled, the id is set to `true` as a `QUnit.config` property
+ urlConfig: [
+ {
+ id: "hidepassed",
+ label: "Hide passed tests",
+ tooltip: "Only show tests and assertions that fail. Stored as query-strings."
+ },
+ {
+ id: "noglobals",
+ label: "Check for Globals",
+ tooltip: "Enabling this will test if any test introduces new properties on the " +
+ "`window` object. Stored as query-strings."
+ },
+ {
+ id: "notrycatch",
+ label: "No try-catch",
+ tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " +
+ "exceptions in IE reasonable. Stored as query-strings."
+ }
+ ],
+
+ // Set of all modules.
+ modules: [],
+
+ // The first unnamed module
+ currentModule: {
+ name: "",
+ tests: []
},
- asyncTest: function(testName, expected, callback) {
+ callbacks: {}
+};
+
+// Push a loose unnamed module to the modules collection
+config.modules.push( config.currentModule );
+
+// Initialize more QUnit.config and QUnit.urlParams
+(function() {
+ var i, current,
+ location = window.location || { search: "", protocol: "file:" },
+ params = location.search.slice( 1 ).split( "&" ),
+ length = params.length,
+ urlParams = {};
+
+ if ( params[ 0 ] ) {
+ for ( i = 0; i < length; i++ ) {
+ current = params[ i ].split( "=" );
+ current[ 0 ] = decodeURIComponent( current[ 0 ] );
+
+ // allow just a key to turn on a flag, e.g., test.html?noglobals
+ current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
+ if ( urlParams[ current[ 0 ] ] ) {
+ urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
+ } else {
+ urlParams[ current[ 0 ] ] = current[ 1 ];
+ }
+ }
+ }
+
+ if ( urlParams.filter === true ) {
+ delete urlParams.filter;
+ }
+
+ QUnit.urlParams = urlParams;
+
+ // String search anywhere in moduleName+testName
+ config.filter = urlParams.filter;
+
+ if ( urlParams.maxDepth ) {
+ config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ?
+ Number.POSITIVE_INFINITY :
+ urlParams.maxDepth;
+ }
+
+ config.testId = [];
+ if ( urlParams.testId ) {
+
+ // Ensure that urlParams.testId is an array
+ urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," );
+ for ( i = 0; i < urlParams.testId.length; i++ ) {
+ config.testId.push( urlParams.testId[ i ] );
+ }
+ }
+
+ // Figure out if we're running the tests from a server or not
+ QUnit.isLocal = location.protocol === "file:";
+
+ // Expose the current QUnit version
+ QUnit.version = "1.18.0";
+}());
+
+// Root QUnit object.
+// `QUnit` initialized at top of scope
+extend( QUnit, {
+
+ // call on start of module test to prepend name to all tests
+ module: function( name, testEnvironment ) {
+ var currentModule = {
+ name: name,
+ testEnvironment: testEnvironment,
+ tests: []
+ };
+
+ // DEPRECATED: handles setup/teardown functions,
+ // beforeEach and afterEach should be used instead
+ if ( testEnvironment && testEnvironment.setup ) {
+ testEnvironment.beforeEach = testEnvironment.setup;
+ delete testEnvironment.setup;
+ }
+ if ( testEnvironment && testEnvironment.teardown ) {
+ testEnvironment.afterEach = testEnvironment.teardown;
+ delete testEnvironment.teardown;
+ }
+
+ config.modules.push( currentModule );
+ config.currentModule = currentModule;
+ },
+
+ // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
+ asyncTest: function( testName, expected, callback ) {
if ( arguments.length === 2 ) {
callback = expected;
- expected = 0;
+ expected = null;
}
- QUnit.test(testName, expected, callback, true);
+ QUnit.test( testName, expected, callback, true );
},
-
- test: function(testName, expected, callback, async) {
- var name = testName, testEnvironment, testEnvironmentArg;
+
+ test: function( testName, expected, callback, async ) {
+ var test;
if ( arguments.length === 2 ) {
callback = expected;
expected = null;
}
- // is 2nd argument a testEnvironment?
- if ( expected && typeof expected === 'object') {
- testEnvironmentArg = expected;
- expected = null;
- }
-
- if ( config.currentModule ) {
- name = config.currentModule + " module: " + name;
- }
-
- if ( !validTest(name) ) {
- return;
- }
-
- synchronize(function() {
- QUnit.testStart( testName );
-
- testEnvironment = extend({
- setup: function() {},
- teardown: function() {}
- }, config.moduleTestEnvironment);
- if (testEnvironmentArg) {
- extend(testEnvironment,testEnvironmentArg);
- }
-
- // allow utility functions to access the current test environment
- QUnit.current_testEnvironment = testEnvironment;
-
- config.assertions = [];
- config.expected = expected;
-
- try {
- if ( !config.pollution ) {
- saveGlobal();
- }
-
- testEnvironment.setup.call(testEnvironment);
- } catch(e) {
- QUnit.ok( false, "Setup failed on " + name + ": " + e.message );
- }
-
- if ( async ) {
- QUnit.stop();
- }
-
- try {
- callback.call(testEnvironment);
- } catch(e) {
- fail("Test " + name + " died, exception and test follows", e, callback);
- QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message );
- // else next test will carry the responsibility
- saveGlobal();
-
- // Restart the tests if they're blocking
- if ( config.blocking ) {
- start();
- }
- }
+
+ test = new Test({
+ testName: testName,
+ expected: expected,
+ async: async,
+ callback: callback
+ });
+
+ test.queue();
+ },
+
+ skip: function( testName ) {
+ var test = new Test({
+ testName: testName,
+ skip: true
});
- synchronize(function() {
- try {
- checkPollution();
- testEnvironment.teardown.call(testEnvironment);
- } catch(e) {
- QUnit.ok( false, "Teardown failed on " + name + ": " + e.message );
- }
-
- try {
- QUnit.reset();
- } catch(e) {
- fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset);
- }
-
- if ( config.expected && config.expected != config.assertions.length ) {
- QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" );
+ test.queue();
+ },
+
+ // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0.
+ // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior.
+ start: function( count ) {
+ var globalStartAlreadyCalled = globalStartCalled;
+
+ if ( !config.current ) {
+ globalStartCalled = true;
+
+ if ( runStarted ) {
+ throw new Error( "Called start() outside of a test context while already started" );
+ } else if ( globalStartAlreadyCalled || count > 1 ) {
+ throw new Error( "Called start() outside of a test context too many times" );
+ } else if ( config.autostart ) {
+ throw new Error( "Called start() outside of a test context when " +
+ "QUnit.config.autostart was true" );
+ } else if ( !config.pageLoaded ) {
+
+ // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
+ config.autostart = true;
+ return;
}
-
- var good = 0, bad = 0,
- tests = id("qunit-tests");
-
- config.stats.all += config.assertions.length;
- config.moduleStats.all += config.assertions.length;
-
- if ( tests ) {
- var ol = document.createElement("ol");
- ol.style.display = "none";
-
- for ( var i = 0; i < config.assertions.length; i++ ) {
- var assertion = config.assertions[i];
-
- var li = document.createElement("li");
- li.className = assertion.result ? "pass" : "fail";
- li.appendChild(document.createTextNode(assertion.message || "(no message)"));
- ol.appendChild( li );
-
- if ( assertion.result ) {
- good++;
- } else {
- bad++;
- config.stats.bad++;
- config.moduleStats.bad++;
- }
- }
-
- var b = document.createElement("strong");
- b.innerHTML = name + " <b style='color:black;'>(<b class='fail'>" + bad + "</b>, <b class='pass'>" + good + "</b>, " + config.assertions.length + ")</b>";
-
- addEvent(b, "click", function() {
- var next = b.nextSibling, display = next.style.display;
- next.style.display = display === "none" ? "block" : "none";
- });
-
- addEvent(b, "dblclick", function(e) {
- var target = e && e.target ? e.target : window.event.srcElement;
- if ( target.nodeName.toLowerCase() === "strong" ) {
- var text = "", node = target.firstChild;
-
- while ( node.nodeType === 3 ) {
- text += node.nodeValue;
- node = node.nextSibling;
- }
-
- text = text.replace(/(^\s*|\s*$)/g, "");
-
- if ( window.location ) {
- window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text);
- }
- }
- });
-
- var li = document.createElement("li");
- li.className = bad ? "fail" : "pass";
- li.appendChild( b );
- li.appendChild( ol );
- tests.appendChild( li );
-
- if ( bad ) {
- var toolbar = id("qunit-testrunner-toolbar");
- if ( toolbar ) {
- toolbar.style.display = "block";
- id("qunit-filter-pass").disabled = null;
- id("qunit-filter-missing").disabled = null;
- }
- }
-
- } else {
- for ( var i = 0; i < config.assertions.length; i++ ) {
- if ( !config.assertions[i].result ) {
- bad++;
- config.stats.bad++;
- config.moduleStats.bad++;
- }
- }
+ } else {
+
+ // If a test is running, adjust its semaphore
+ config.current.semaphore -= count || 1;
+
+ // Don't start until equal number of stop-calls
+ if ( config.current.semaphore > 0 ) {
+ return;
}
- QUnit.testDone( testName, bad, config.assertions.length );
-
- if ( !window.setTimeout && !config.queue.length ) {
- done();
+ // throw an Error if start is called more often than stop
+ if ( config.current.semaphore < 0 ) {
+ config.current.semaphore = 0;
+
+ QUnit.pushFailure(
+ "Called start() while already started (test's semaphore was 0 already)",
+ sourceFromStacktrace( 2 )
+ );
+ return;
}
- });
-
- if ( window.setTimeout && !config.doneTimer ) {
- config.doneTimer = window.setTimeout(function(){
- if ( !config.queue.length ) {
- done();
- } else {
- synchronize( done );
- }
- }, 13);
}
- },
-
- /**
- * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
- */
- expect: function(asserts) {
- config.expected = asserts;
+
+ resumeProcessing();
},
- /**
- * Asserts true.
- * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
- */
- ok: function(a, msg) {
- QUnit.log(a, msg);
-
- config.assertions.push({
- result: !!a,
- message: msg
- });
- },
-
- /**
- * Checks that the first two arguments are equal, with an optional message.
- * Prints out both actual and expected values.
- *
- * Prefered to ok( actual == expected, message )
- *
- * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
- *
- * @param Object actual
- * @param Object expected
- * @param String message (optional)
- */
- equal: function(actual, expected, message) {
- push(expected == actual, actual, expected, message);
- },
-
- notEqual: function(actual, expected, message) {
- push(expected != actual, actual, expected, message);
- },
-
- deepEqual: function(a, b, message) {
- push(QUnit.equiv(a, b), a, b, message);
- },
-
- notDeepEqual: function(a, b, message) {
- push(!QUnit.equiv(a, b), a, b, message);
- },
-
- strictEqual: function(actual, expected, message) {
- push(expected === actual, actual, expected, message);
- },
-
- notStrictEqual: function(actual, expected, message) {
- push(expected !== actual, actual, expected, message);
+ // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0.
+ stop: function( count ) {
+
+ // If there isn't a test running, don't allow QUnit.stop() to be called
+ if ( !config.current ) {
+ throw new Error( "Called stop() outside of a test context" );
+ }
+
+ // If a test is running, adjust its semaphore
+ config.current.semaphore += count || 1;
+
+ pauseProcessing();
},
-
- start: function() {
- // A slight delay, to avoid any current callbacks
- if ( window.setTimeout ) {
- window.setTimeout(function() {
- if ( config.timeout ) {
- clearTimeout(config.timeout);
- }
-
- config.blocking = false;
- process();
- }, 13);
- } else {
- config.blocking = false;
- process();
- }
- },
-
- stop: function(timeout) {
- config.blocking = true;
-
- if ( timeout && window.setTimeout ) {
- config.timeout = window.setTimeout(function() {
- QUnit.ok( false, "Test timed out" );
- QUnit.start();
- }, timeout);
- }
- },
-
- /**
- * Resets the test setup. Useful for tests that modify the DOM.
- */
- reset: function() {
- if ( window.jQuery ) {
- jQuery("#main").html( config.fixture );
- jQuery.event.global = {};
- jQuery.ajaxSettings = extend({}, config.ajaxSettings);
- }
- },
-
- /**
- * Trigger an event on an element.
- *
- * @example triggerEvent( document.body, "click" );
- *
- * @param DOMElement elem
- * @param String type
- */
- triggerEvent: function( elem, type, event ) {
- if ( document.createEvent ) {
- event = document.createEvent("MouseEvents");
- event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
- 0, 0, 0, 0, 0, false, false, false, false, 0, null);
- elem.dispatchEvent( event );
-
- } else if ( elem.fireEvent ) {
- elem.fireEvent("on"+type);
- }
- },
-
+
+ config: config,
+
// Safe object type checking
is: function( type, obj ) {
- return Object.prototype.toString.call( obj ) === "[object "+ type +"]";
+ return QUnit.objectType( obj ) === type;
},
-
- // Logging callbacks
- done: function(failures, total) {},
- log: function(result, message) {},
- testStart: function(name) {},
- testDone: function(name, failures, total) {},
- moduleStart: function(name, testEnvironment) {},
- moduleDone: function(name, failures, total) {}
-};
-
-// Backwards compatibility, deprecated
-QUnit.equals = QUnit.equal;
-QUnit.same = QUnit.deepEqual;
-
-// Maintain internal state
-var config = {
- // The queue of tests to run
- queue: [],
-
- // block until document ready
- blocking: true
-};
-
-// Load paramaters
-(function() {
- var location = window.location || { search: "", protocol: "file:" },
- GETParams = location.search.slice(1).split('&');
-
- for ( var i = 0; i < GETParams.length; i++ ) {
- GETParams[i] = decodeURIComponent( GETParams[i] );
- if ( GETParams[i] === "noglobals" ) {
- GETParams.splice( i, 1 );
- i--;
- config.noglobals = true;
- } else if ( GETParams[i].search('=') > -1 ) {
- GETParams.splice( i, 1 );
- i--;
+
+ objectType: function( obj ) {
+ if ( typeof obj === "undefined" ) {
+ return "undefined";
+ }
+
+ // Consider: typeof null === object
+ if ( obj === null ) {
+ return "null";
+ }
+
+ var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ),
+ type = match && match[ 1 ] || "";
+
+ switch ( type ) {
+ case "Number":
+ if ( isNaN( obj ) ) {
+ return "nan";
+ }
+ return "number";
+ case "String":
+ case "Boolean":
+ case "Array":
+ case "Date":
+ case "RegExp":
+ case "Function":
+ return type.toLowerCase();
+ }
+ if ( typeof obj === "object" ) {
+ return "object";
+ }
+ return undefined;
+ },
+
+ extend: extend,
+
+ load: function() {
+ config.pageLoaded = true;
+
+ // Initialize the configuration options
+ extend( config, {
+ stats: { all: 0, bad: 0 },
+ moduleStats: { all: 0, bad: 0 },
+ started: 0,
+ updateRate: 1000,
+ autostart: true,
+ filter: ""
+ }, true );
+
+ config.blocking = false;
+
+ if ( config.autostart ) {
+ resumeProcessing();
}
}
-
- // restrict modules/tests by get parameters
- config.filters = GETParams;
-
- // Figure out if we're running the tests from a server or not
- QUnit.isLocal = !!(location.protocol === 'file:');
-})();
-
-// Expose the API as global variables, unless an 'exports'
-// object exists, in that case we assume we're in CommonJS
-if ( typeof exports === "undefined" || typeof require === "undefined" ) {
- extend(window, QUnit);
- window.QUnit = QUnit;
-} else {
- extend(exports, QUnit);
- exports.QUnit = QUnit;
-}
-
-if ( typeof document === "undefined" || document.readyState === "complete" ) {
- config.autorun = true;
-}
-
-addEvent(window, "load", function() {
- // Initialize the config, saving the execution queue
- var oldconfig = extend({}, config);
- QUnit.init();
- extend(config, oldconfig);
-
- config.blocking = false;
-
- var userAgent = id("qunit-userAgent");
- if ( userAgent ) {
- userAgent.innerHTML = navigator.userAgent;
+});
+
+// Register logging callbacks
+(function() {
+ var i, l, key,
+ callbacks = [ "begin", "done", "log", "testStart", "testDone",
+ "moduleStart", "moduleDone" ];
+
+ function registerLoggingCallback( key ) {
+ var loggingCallback = function( callback ) {
+ if ( QUnit.objectType( callback ) !== "function" ) {
+ throw new Error(
+ "QUnit logging methods require a callback function as their first parameters."
+ );
+ }
+
+ config.callbacks[ key ].push( callback );
+ };
+
+ // DEPRECATED: This will be removed on QUnit 2.0.0+
+ // Stores the registered functions allowing restoring
+ // at verifyLoggingCallbacks() if modified
+ loggingCallbacks[ key ] = loggingCallback;
+
+ return loggingCallback;
}
-
- var toolbar = id("qunit-testrunner-toolbar");
- if ( toolbar ) {
- toolbar.style.display = "none";
-
- var filter = document.createElement("input");
- filter.type = "checkbox";
- filter.id = "qunit-filter-pass";
- filter.disabled = true;
- addEvent( filter, "click", function() {
- var li = document.getElementsByTagName("li");
- for ( var i = 0; i < li.length; i++ ) {
- if ( li[i].className.indexOf("pass") > -1 ) {
- li[i].style.display = filter.checked ? "none" : "";
- }
+
+ for ( i = 0, l = callbacks.length; i < l; i++ ) {
+ key = callbacks[ i ];
+
+ // Initialize key collection of logging callback
+ if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) {
+ config.callbacks[ key ] = [];
+ }
+
+ QUnit[ key ] = registerLoggingCallback( key );
+ }
+})();
+
+// `onErrorFnPrev` initialized at top of scope
+// Preserve other handlers
+onErrorFnPrev = window.onerror;
+
+// Cover uncaught exceptions
+// Returning true will suppress the default browser handler,
+// returning false will let it run.
+window.onerror = function( error, filePath, linerNr ) {
+ var ret = false;
+ if ( onErrorFnPrev ) {
+ ret = onErrorFnPrev( error, filePath, linerNr );
+ }
+
+ // Treat return value as window.onerror itself does,
+ // Only do our handling if not suppressed.
+ if ( ret !== true ) {
+ if ( QUnit.config.current ) {
+ if ( QUnit.config.current.ignoreGlobalErrors ) {
+ return true;
}
- });
- toolbar.appendChild( filter );
-
- var label = document.createElement("label");
- label.setAttribute("for", "qunit-filter-pass");
- label.innerHTML = "Hide passed tests";
- toolbar.appendChild( label );
-
- var missing = document.createElement("input");
- missing.type = "checkbox";
- missing.id = "qunit-filter-missing";
- missing.disabled = true;
- addEvent( missing, "click", function() {
- var li = document.getElementsByTagName("li");
- for ( var i = 0; i < li.length; i++ ) {
- if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) {
- li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block";
- }
- }
- });
- toolbar.appendChild( missing );
-
- label = document.createElement("label");
- label.setAttribute("for", "qunit-filter-missing");
- label.innerHTML = "Hide missing tests (untested code is broken code)";
- toolbar.appendChild( label );
+ QUnit.pushFailure( error, filePath + ":" + linerNr );
+ } else {
+ QUnit.test( "global failure", extend(function() {
+ QUnit.pushFailure( error, filePath + ":" + linerNr );
+ }, { validTest: true } ) );
+ }
+ return false;
}
- var main = id('main');
- if ( main ) {
- config.fixture = main.innerHTML;
- }
-
- if ( window.jQuery ) {
- config.ajaxSettings = window.jQuery.ajaxSettings;
- }
-
- QUnit.start();
-});
+ return ret;
+};
function done() {
- if ( config.doneTimer && window.clearTimeout ) {
- window.clearTimeout( config.doneTimer );
- config.doneTimer = null;
- }
-
- if ( config.queue.length ) {
- config.doneTimer = window.setTimeout(function(){
- if ( !config.queue.length ) {
- done();
- } else {
- synchronize( done );
- }
- }, 13);
-
- return;
- }
+ var runtime, passed;
config.autorun = true;
// Log the last module results
- if ( config.currentModule ) {
- QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
- }
-
- var banner = id("qunit-banner"),
- tests = id("qunit-tests"),
- html = ['Tests completed in ',
- +new Date - config.started, ' milliseconds.<br/>',
- '<span class="passed">', config.stats.all - config.stats.bad, '</span> tests of <span class="total">', config.stats.all, '</span> passed, <span class="failed">', config.stats.bad,'</span> failed.'].join('');
-
- if ( banner ) {
- banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
- }
-
- if ( tests ) {
- var result = id("qunit-testresult");
-
- if ( !result ) {
- result = document.createElement("p");
- result.id = "qunit-testresult";
- result.className = "result";
- tests.parentNode.insertBefore( result, tests.nextSibling );
- }
-
- result.innerHTML = html;
+ if ( config.previousModule ) {
+ runLoggingCallbacks( "moduleDone", {
+ name: config.previousModule.name,
+ tests: config.previousModule.tests,
+ failed: config.moduleStats.bad,
+ passed: config.moduleStats.all - config.moduleStats.bad,
+ total: config.moduleStats.all,
+ runtime: now() - config.moduleStats.started
+ });
}
-
- QUnit.done( config.stats.bad, config.stats.all );
+ delete config.previousModule;
+
+ runtime = now() - config.started;
+ passed = config.stats.all - config.stats.bad;
+
+ runLoggingCallbacks( "done", {
+ failed: config.stats.bad,
+ passed: passed,
+ total: config.stats.all,
+ runtime: runtime
+ });
}
-function validTest( name ) {
- var i = config.filters.length,
- run = false;
-
- if ( !i ) {
- return true;
+// Doesn't support IE6 to IE9, it will return undefined on these browsers
+// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
+function extractStacktrace( e, offset ) {
+ offset = offset === undefined ? 4 : offset;
+
+ var stack, include, i;
+
+ if ( e.stack ) {
+ stack = e.stack.split( "\n" );
+ if ( /^error$/i.test( stack[ 0 ] ) ) {
+ stack.shift();
+ }
+ if ( fileName ) {
+ include = [];
+ for ( i = offset; i < stack.length; i++ ) {
+ if ( stack[ i ].indexOf( fileName ) !== -1 ) {
+ break;
+ }
+ include.push( stack[ i ] );
+ }
+ if ( include.length ) {
+ return include.join( "\n" );
+ }
+ }
+ return stack[ offset ];
+
+ // Support: Safari <=6 only
+ } else if ( e.sourceURL ) {
+
+ // exclude useless self-reference for generated Error objects
+ if ( /qunit.js$/.test( e.sourceURL ) ) {
+ return;
+ }
+
+ // for actual exceptions, this is useful
+ return e.sourceURL + ":" + e.line;
}
-
- while ( i-- ) {
- var filter = config.filters[i],
- not = filter.charAt(0) == '!';
-
- if ( not ) {
- filter = filter.slice(1);
- }
-
- if ( name.indexOf(filter) !== -1 ) {
- return !not;
- }
-
- if ( not ) {
- run = true;
+}
+
+function sourceFromStacktrace( offset ) {
+ var error = new Error();
+
+ // Support: Safari <=7 only, IE <=10 - 11 only
+ // Not all browsers generate the `stack` property for `new Error()`, see also #636
+ if ( !error.stack ) {
+ try {
+ throw error;
+ } catch ( err ) {
+ error = err;
}
}
- return run;
+ return extractStacktrace( error, offset );
}
-function push(result, actual, expected, message) {
- message = message || (result ? "okay" : "failed");
- QUnit.ok( result, result ? message + ": " + QUnit.jsDump.parse(expected) : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) );
-}
-
-function synchronize( callback ) {
+function synchronize( callback, last ) {
+ if ( QUnit.objectType( callback ) === "array" ) {
+ while ( callback.length ) {
+ synchronize( callback.shift() );
+ }
+ return;
+ }
config.queue.push( callback );
if ( config.autorun && !config.blocking ) {
- process();
+ process( last );
}
}
-function process() {
- var start = (new Date()).getTime();
+function process( last ) {
+ function next() {
+ process( last );
+ }
+ var start = now();
+ config.depth = ( config.depth || 0 ) + 1;
while ( config.queue.length && !config.blocking ) {
- if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
+ if ( !defined.setTimeout || config.updateRate <= 0 ||
+ ( ( now() - start ) < config.updateRate ) ) {
+ if ( config.current ) {
+
+ // Reset async tracking for each phase of the Test lifecycle
+ config.current.usedAsync = false;
+ }
config.queue.shift()();
-
} else {
- setTimeout( process, 13 );
+ setTimeout( next, 13 );
break;
}
}
+ config.depth--;
+ if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
+ done();
+ }
+}
+
+function begin() {
+ var i, l,
+ modulesLog = [];
+
+ // If the test run hasn't officially begun yet
+ if ( !config.started ) {
+
+ // Record the time of the test run's beginning
+ config.started = now();
+
+ verifyLoggingCallbacks();
+
+ // Delete the loose unnamed module if unused.
+ if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
+ config.modules.shift();
+ }
+
+ // Avoid unnecessary information by not logging modules' test environments
+ for ( i = 0, l = config.modules.length; i < l; i++ ) {
+ modulesLog.push({
+ name: config.modules[ i ].name,
+ tests: config.modules[ i ].tests
+ });
+ }
+
+ // The test run is officially beginning now
+ runLoggingCallbacks( "begin", {
+ totalTests: Test.count,
+ modules: modulesLog
+ });
+ }
+
+ config.blocking = false;
+ process( true );
+}
+
+function resumeProcessing() {
+ runStarted = true;
+
+ // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.)
+ if ( defined.setTimeout ) {
+ setTimeout(function() {
+ if ( config.current && config.current.semaphore > 0 ) {
+ return;
+ }
+ if ( config.timeout ) {
+ clearTimeout( config.timeout );
+ }
+
+ begin();
+ }, 13 );
+ } else {
+ begin();
+ }
+}
+
+function pauseProcessing() {
+ config.blocking = true;
+
+ if ( config.testTimeout && defined.setTimeout ) {
+ clearTimeout( config.timeout );
+ config.timeout = setTimeout(function() {
+ if ( config.current ) {
+ config.current.semaphore = 0;
+ QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
+ } else {
+ throw new Error( "Test timed out" );
+ }
+ resumeProcessing();
+ }, config.testTimeout );
+ }
}
function saveGlobal() {
config.pollution = [];
-
+
if ( config.noglobals ) {
for ( var key in window ) {
- config.pollution.push( key );
+ if ( hasOwn.call( window, key ) ) {
+ // in Opera sometimes DOM element ids show up here, ignore them
+ if ( /^qunit-test-output/.test( key ) ) {
+ continue;
+ }
+ config.pollution.push( key );
+ }
}
}
}
-function checkPollution( name ) {
- var old = config.pollution;
+function checkPollution() {
+ var newGlobals,
+ deletedGlobals,
+ old = config.pollution;
+
saveGlobal();
-
- var newGlobals = diff( old, config.pollution );
+
+ newGlobals = diff( config.pollution, old );
if ( newGlobals.length > 0 ) {
- ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
- config.expected++;
+ QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
}
- var deletedGlobals = diff( config.pollution, old );
+ deletedGlobals = diff( old, config.pollution );
if ( deletedGlobals.length > 0 ) {
- ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
- config.expected++;
+ QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
}
}
// returns a new Array with the elements that are in a but not in b
function diff( a, b ) {
- var result = a.slice();
- for ( var i = 0; i < result.length; i++ ) {
- for ( var j = 0; j < b.length; j++ ) {
- if ( result[i] === b[j] ) {
- result.splice(i, 1);
+ var i, j,
+ result = a.slice();
+
+ for ( i = 0; i < result.length; i++ ) {
+ for ( j = 0; j < b.length; j++ ) {
+ if ( result[ i ] === b[ j ] ) {
+ result.splice( i, 1 );
i--;
break;
}
@@ -646,424 +716,3113 @@
return result;
}
-function fail(message, exception, callback) {
- if ( typeof console !== "undefined" && console.error && console.warn ) {
- console.error(message);
- console.error(exception);
- console.warn(callback.toString());
-
- } else if ( window.opera && opera.postError ) {
- opera.postError(message, exception, callback.toString);
- }
-}
-
-function extend(a, b) {
+function extend( a, b, undefOnly ) {
for ( var prop in b ) {
- a[prop] = b[prop];
+ if ( hasOwn.call( b, prop ) ) {
+
+ // Avoid "Member not found" error in IE8 caused by messing with window.constructor
+ if ( !( prop === "constructor" && a === window ) ) {
+ if ( b[ prop ] === undefined ) {
+ delete a[ prop ];
+ } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
+ a[ prop ] = b[ prop ];
+ }
+ }
+ }
}
return a;
}
-function addEvent(elem, type, fn) {
- if ( elem.addEventListener ) {
- elem.addEventListener( type, fn, false );
- } else if ( elem.attachEvent ) {
- elem.attachEvent( "on" + type, fn );
+function runLoggingCallbacks( key, args ) {
+ var i, l, callbacks;
+
+ callbacks = config.callbacks[ key ];
+ for ( i = 0, l = callbacks.length; i < l; i++ ) {
+ callbacks[ i ]( args );
+ }
+}
+
+// DEPRECATED: This will be removed on 2.0.0+
+// This function verifies if the loggingCallbacks were modified by the user
+// If so, it will restore it, assign the given callback and print a console warning
+function verifyLoggingCallbacks() {
+ var loggingCallback, userCallback;
+
+ for ( loggingCallback in loggingCallbacks ) {
+ if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) {
+
+ userCallback = QUnit[ loggingCallback ];
+
+ // Restore the callback function
+ QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ];
+
+ // Assign the deprecated given callback
+ QUnit[ loggingCallback ]( userCallback );
+
+ if ( window.console && window.console.warn ) {
+ window.console.warn(
+ "QUnit." + loggingCallback + " was replaced with a new value.\n" +
+ "Please, check out the documentation on how to apply logging callbacks.\n" +
+ "Reference: http://api.qunitjs.com/category/callbacks/"
+ );
+ }
+ }
+ }
+}
+
+// from jquery.js
+function inArray( elem, array ) {
+ if ( array.indexOf ) {
+ return array.indexOf( elem );
+ }
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ if ( array[ i ] === elem ) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+function Test( settings ) {
+ var i, l;
+
+ ++Test.count;
+
+ extend( this, settings );
+ this.assertions = [];
+ this.semaphore = 0;
+ this.usedAsync = false;
+ this.module = config.currentModule;
+ this.stack = sourceFromStacktrace( 3 );
+
+ // Register unique strings
+ for ( i = 0, l = this.module.tests; i < l.length; i++ ) {
+ if ( this.module.tests[ i ].name === this.testName ) {
+ this.testName += " ";
+ }
+ }
+
+ this.testId = generateHash( this.module.name, this.testName );
+
+ this.module.tests.push({
+ name: this.testName,
+ testId: this.testId
+ });
+
+ if ( settings.skip ) {
+
+ // Skipped tests will fully ignore any sent callback
+ this.callback = function() {};
+ this.async = false;
+ this.expected = 0;
} else {
- fn();
+ this.assert = new Assert( this );
}
}
-function id(name) {
- return !!(typeof document !== "undefined" && document && document.getElementById) &&
- document.getElementById( name );
+Test.count = 0;
+
+Test.prototype = {
+ before: function() {
+ if (
+
+ // Emit moduleStart when we're switching from one module to another
+ this.module !== config.previousModule ||
+
+ // They could be equal (both undefined) but if the previousModule property doesn't
+ // yet exist it means this is the first test in a suite that isn't wrapped in a
+ // module, in which case we'll just emit a moduleStart event for 'undefined'.
+ // Without this, reporters can get testStart before moduleStart which is a problem.
+ !hasOwn.call( config, "previousModule" )
+ ) {
+ if ( hasOwn.call( config, "previousModule" ) ) {
+ runLoggingCallbacks( "moduleDone", {
+ name: config.previousModule.name,
+ tests: config.previousModule.tests,
+ failed: config.moduleStats.bad,
+ passed: config.moduleStats.all - config.moduleStats.bad,
+ total: config.moduleStats.all,
+ runtime: now() - config.moduleStats.started
+ });
+ }
+ config.previousModule = this.module;
+ config.moduleStats = { all: 0, bad: 0, started: now() };
+ runLoggingCallbacks( "moduleStart", {
+ name: this.module.name,
+ tests: this.module.tests
+ });
+ }
+
+ config.current = this;
+
+ this.testEnvironment = extend( {}, this.module.testEnvironment );
+ delete this.testEnvironment.beforeEach;
+ delete this.testEnvironment.afterEach;
+
+ this.started = now();
+ runLoggingCallbacks( "testStart", {
+ name: this.testName,
+ module: this.module.name,
+ testId: this.testId
+ });
+
+ if ( !config.pollution ) {
+ saveGlobal();
+ }
+ },
+
+ run: function() {
+ var promise;
+
+ config.current = this;
+
+ if ( this.async ) {
+ QUnit.stop();
+ }
+
+ this.callbackStarted = now();
+
+ if ( config.notrycatch ) {
+ promise = this.callback.call( this.testEnvironment, this.assert );
+ this.resolvePromise( promise );
+ return;
+ }
+
+ try {
+ promise = this.callback.call( this.testEnvironment, this.assert );
+ this.resolvePromise( promise );
+ } catch ( e ) {
+ this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
+ this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
+
+ // else next test will carry the responsibility
+ saveGlobal();
+
+ // Restart the tests if they're blocking
+ if ( config.blocking ) {
+ QUnit.start();
+ }
+ }
+ },
+
+ after: function() {
+ checkPollution();
+ },
+
+ queueHook: function( hook, hookName ) {
+ var promise,
+ test = this;
+ return function runHook() {
+ config.current = test;
+ if ( config.notrycatch ) {
+ promise = hook.call( test.testEnvironment, test.assert );
+ test.resolvePromise( promise, hookName );
+ return;
+ }
+ try {
+ promise = hook.call( test.testEnvironment, test.assert );
+ test.resolvePromise( promise, hookName );
+ } catch ( error ) {
+ test.pushFailure( hookName + " failed on " + test.testName + ": " +
+ ( error.message || error ), extractStacktrace( error, 0 ) );
+ }
+ };
+ },
+
+ // Currently only used for module level hooks, can be used to add global level ones
+ hooks: function( handler ) {
+ var hooks = [];
+
+ // Hooks are ignored on skipped tests
+ if ( this.skip ) {
+ return hooks;
+ }
+
+ if ( this.module.testEnvironment &&
+ QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) {
+ hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) );
+ }
+
+ return hooks;
+ },
+
+ finish: function() {
+ config.current = this;
+ if ( config.requireExpects && this.expected === null ) {
+ this.pushFailure( "Expected number of assertions to be defined, but expect() was " +
+ "not called.", this.stack );
+ } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
+ this.pushFailure( "Expected " + this.expected + " assertions, but " +
+ this.assertions.length + " were run", this.stack );
+ } else if ( this.expected === null && !this.assertions.length ) {
+ this.pushFailure( "Expected at least one assertion, but none were run - call " +
+ "expect(0) to accept zero assertions.", this.stack );
+ }
+
+ var i,
+ bad = 0;
+
+ this.runtime = now() - this.started;
+ config.stats.all += this.assertions.length;
+ config.moduleStats.all += this.assertions.length;
+
+ for ( i = 0; i < this.assertions.length; i++ ) {
+ if ( !this.assertions[ i ].result ) {
+ bad++;
+ config.stats.bad++;
+ config.moduleStats.bad++;
+ }
+ }
+
+ runLoggingCallbacks( "testDone", {
+ name: this.testName,
+ module: this.module.name,
+ skipped: !!this.skip,
+ failed: bad,
+ passed: this.assertions.length - bad,
+ total: this.assertions.length,
+ runtime: this.runtime,
+
+ // HTML Reporter use
+ assertions: this.assertions,
+ testId: this.testId,
+
+ // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
+ duration: this.runtime
+ });
+
+ // QUnit.reset() is deprecated and will be replaced for a new
+ // fixture reset function on QUnit 2.0/2.1.
+ // It's still called here for backwards compatibility handling
+ QUnit.reset();
+
+ config.current = undefined;
+ },
+
+ queue: function() {
+ var bad,
+ test = this;
+
+ if ( !this.valid() ) {
+ return;
+ }
+
+ function run() {
+
+ // each of these can by async
+ synchronize([
+ function() {
+ test.before();
+ },
+
+ test.hooks( "beforeEach" ),
+
+ function() {
+ test.run();
+ },
+
+ test.hooks( "afterEach" ).reverse(),
+
+ function() {
+ test.after();
+ },
+ function() {
+ test.finish();
+ }
+ ]);
+ }
+
+ // `bad` initialized at top of scope
+ // defer when previous test run passed, if storage is available
+ bad = QUnit.config.reorder && defined.sessionStorage &&
+ +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
+
+ if ( bad ) {
+ run();
+ } else {
+ synchronize( run, true );
+ }
+ },
+
+ push: function( result, actual, expected, message ) {
+ var source,
+ details = {
+ module: this.module.name,
+ name: this.testName,
+ result: result,
+ message: message,
+ actual: actual,
+ expected: expected,
+ testId: this.testId,
+ runtime: now() - this.started
+ };
+
+ if ( !result ) {
+ source = sourceFromStacktrace();
+
+ if ( source ) {
+ details.source = source;
+ }
+ }
+
+ runLoggingCallbacks( "log", details );
+
+ this.assertions.push({
+ result: !!result,
+ message: message
+ });
+ },
+
+ pushFailure: function( message, source, actual ) {
+ if ( !this instanceof Test ) {
+ throw new Error( "pushFailure() assertion outside test context, was " +
+ sourceFromStacktrace( 2 ) );
+ }
+
+ var details = {
+ module: this.module.name,
+ name: this.testName,
+ result: false,
+ message: message || "error",
+ actual: actual || null,
+ testId: this.testId,
+ runtime: now() - this.started
+ };
+
+ if ( source ) {
+ details.source = source;
+ }
+
+ runLoggingCallbacks( "log", details );
+
+ this.assertions.push({
+ result: false,
+ message: message
+ });
+ },
+
+ resolvePromise: function( promise, phase ) {
+ var then, message,
+ test = this;
+ if ( promise != null ) {
+ then = promise.then;
+ if ( QUnit.objectType( then ) === "function" ) {
+ QUnit.stop();
+ then.call(
+ promise,
+ QUnit.start,
+ function( error ) {
+ message = "Promise rejected " +
+ ( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
+ " " + test.testName + ": " + ( error.message || error );
+ test.pushFailure( message, extractStacktrace( error, 0 ) );
+
+ // else next test will carry the responsibility
+ saveGlobal();
+
+ // Unblock
+ QUnit.start();
+ }
+ );
+ }
+ }
+ },
+
+ valid: function() {
+ var include,
+ filter = config.filter && config.filter.toLowerCase(),
+ module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(),
+ fullName = ( this.module.name + ": " + this.testName ).toLowerCase();
+
+ // Internally-generated tests are always valid
+ if ( this.callback && this.callback.validTest ) {
+ return true;
+ }
+
+ if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) {
+ return false;
+ }
+
+ if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) {
+ return false;
+ }
+
+ if ( !filter ) {
+ return true;
+ }
+
+ include = filter.charAt( 0 ) !== "!";
+ if ( !include ) {
+ filter = filter.slice( 1 );
+ }
+
+ // If the filter matches, we need to honour include
+ if ( fullName.indexOf( filter ) !== -1 ) {
+ return include;
+ }
+
+ // Otherwise, do the opposite
+ return !include;
+ }
+
+};
+
+// Resets the test setup. Useful for tests that modify the DOM.
+/*
+DEPRECATED: Use multiple tests instead of resetting inside a test.
+Use testStart or testDone for custom cleanup.
+This method will throw an error in 2.0, and will be removed in 2.1
+*/
+QUnit.reset = function() {
+
+ // Return on non-browser environments
+ // This is necessary to not break on node tests
+ if ( typeof window === "undefined" ) {
+ return;
+ }
+
+ var fixture = defined.document && document.getElementById &&
+ document.getElementById( "qunit-fixture" );
+
+ if ( fixture ) {
+ fixture.innerHTML = config.fixture;
+ }
+};
+
+QUnit.pushFailure = function() {
+ if ( !QUnit.config.current ) {
+ throw new Error( "pushFailure() assertion outside test context, in " +
+ sourceFromStacktrace( 2 ) );
+ }
+
+ // Gets current test obj
+ var currentTest = QUnit.config.current;
+
+ return currentTest.pushFailure.apply( currentTest, arguments );
+};
+
+// Based on Java's String.hashCode, a simple but not
+// rigorously collision resistant hashing function
+function generateHash( module, testName ) {
+ var hex,
+ i = 0,
+ hash = 0,
+ str = module + "\x1C" + testName,
+ len = str.length;
+
+ for ( ; i < len; i++ ) {
+ hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i );
+ hash |= 0;
+ }
+
+ // Convert the possibly negative integer hash code into an 8 character hex string, which isn't
+ // strictly necessary but increases user understanding that the id is a SHA-like hash
+ hex = ( 0x100000000 + hash ).toString( 16 );
+ if ( hex.length < 8 ) {
+ hex = "0000000" + hex;
+ }
+
+ return hex.slice( -8 );
}
+function Assert( testContext ) {
+ this.test = testContext;
+}
+
+// Assert helpers
+QUnit.assert = Assert.prototype = {
+
+ // Specify the number of expected assertions to guarantee that failed test
+ // (no assertions are run at all) don't slip through.
+ expect: function( asserts ) {
+ if ( arguments.length === 1 ) {
+ this.test.expected = asserts;
+ } else {
+ return this.test.expected;
+ }
+ },
+
+ // Increment this Test's semaphore counter, then return a single-use function that
+ // decrements that counter a maximum of once.
+ async: function() {
+ var test = this.test,
+ popped = false;
+
+ test.semaphore += 1;
+ test.usedAsync = true;
+ pauseProcessing();
+
+ return function done() {
+ if ( !popped ) {
+ test.semaphore -= 1;
+ popped = true;
+ resumeProcessing();
+ } else {
+ test.pushFailure( "Called the callback returned from `assert.async` more than once",
+ sourceFromStacktrace( 2 ) );
+ }
+ };
+ },
+
+ // Exports test.push() to the user API
+ push: function( /* result, actual, expected, message */ ) {
+ var assert = this,
+ currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current;
+
+ // Backwards compatibility fix.
+ // Allows the direct use of global exported assertions and QUnit.assert.*
+ // Although, it's use is not recommended as it can leak assertions
+ // to other tests from async tests, because we only get a reference to the current test,
+ // not exactly the test where assertion were intended to be called.
+ if ( !currentTest ) {
+ throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) );
+ }
+
+ if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) {
+ currentTest.pushFailure( "Assertion after the final `assert.async` was resolved",
+ sourceFromStacktrace( 2 ) );
+
+ // Allow this assertion to continue running anyway...
+ }
+
+ if ( !( assert instanceof Assert ) ) {
+ assert = currentTest.assert;
+ }
+ return assert.test.push.apply( assert.test, arguments );
+ },
+
+ ok: function( result, message ) {
+ message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " +
+ QUnit.dump.parse( result ) );
+ this.push( !!result, result, true, message );
+ },
+
+ notOk: function( result, message ) {
+ message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " +
+ QUnit.dump.parse( result ) );
+ this.push( !result, result, false, message );
+ },
+
+ equal: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
+ this.push( expected == actual, actual, expected, message );
+ },
+
+ notEqual: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
+ this.push( expected != actual, actual, expected, message );
+ },
+
+ propEqual: function( actual, expected, message ) {
+ actual = objectValues( actual );
+ expected = objectValues( expected );
+ this.push( QUnit.equiv( actual, expected ), actual, expected, message );
+ },
+
+ notPropEqual: function( actual, expected, message ) {
+ actual = objectValues( actual );
+ expected = objectValues( expected );
+ this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
+ },
+
+ deepEqual: function( actual, expected, message ) {
+ this.push( QUnit.equiv( actual, expected ), actual, expected, message );
+ },
+
+ notDeepEqual: function( actual, expected, message ) {
+ this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
+ },
+
+ strictEqual: function( actual, expected, message ) {
+ this.push( expected === actual, actual, expected, message );
+ },
+
+ notStrictEqual: function( actual, expected, message ) {
+ this.push( expected !== actual, actual, expected, message );
+ },
+
+ "throws": function( block, expected, message ) {
+ var actual, expectedType,
+ expectedOutput = expected,
+ ok = false,
+ currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current;
+
+ // 'expected' is optional unless doing string comparison
+ if ( message == null && typeof expected === "string" ) {
+ message = expected;
+ expected = null;
+ }
+
+ currentTest.ignoreGlobalErrors = true;
+ try {
+ block.call( currentTest.testEnvironment );
+ } catch (e) {
+ actual = e;
+ }
+ currentTest.ignoreGlobalErrors = false;
+
+ if ( actual ) {
+ expectedType = QUnit.objectType( expected );
+
+ // we don't want to validate thrown error
+ if ( !expected ) {
+ ok = true;
+ expectedOutput = null;
+
+ // expected is a regexp
+ } else if ( expectedType === "regexp" ) {
+ ok = expected.test( errorString( actual ) );
+
+ // expected is a string
+ } else if ( expectedType === "string" ) {
+ ok = expected === errorString( actual );
+
+ // expected is a constructor, maybe an Error constructor
+ } else if ( expectedType === "function" && actual instanceof expected ) {
+ ok = true;
+
+ // expected is an Error object
+ } else if ( expectedType === "object" ) {
+ ok = actual instanceof expected.constructor &&
+ actual.name === expected.name &&
+ actual.message === expected.message;
+
+ // expected is a validation function which returns true if validation passed
+ } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) {
+ expectedOutput = null;
+ ok = true;
+ }
+ }
+
+ currentTest.assert.push( ok, actual, expectedOutput, message );
+ }
+};
+
+// Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word
+// Known to us are: Closure Compiler, Narwhal
+(function() {
+ /*jshint sub:true */
+ Assert.prototype.raises = Assert.prototype[ "throws" ];
+}());
+
// Test for equality any JavaScript type.
-// Discussions and reference: http://philrathe.com/articles/equiv
-// Test suites: http://philrathe.com/tests/equiv
// Author: Philippe Rathé <prathe@gmail.com>
-QUnit.equiv = function () {
-
- var innerEquiv; // the real equiv function
- var callers = []; // stack to decide between skip/abort functions
- var parents = []; // stack to avoiding loops from circular referencing
-
-
- // Determine what is o.
- function hoozit(o) {
- if (QUnit.is("String", o)) {
- return "string";
-
- } else if (QUnit.is("Boolean", o)) {
- return "boolean";
-
- } else if (QUnit.is("Number", o)) {
-
- if (isNaN(o)) {
- return "nan";
+QUnit.equiv = (function() {
+
+ // Call the o related callback with the given arguments.
+ function bindCallbacks( o, callbacks, args ) {
+ var prop = QUnit.objectType( o );
+ if ( prop ) {
+ if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
+ return callbacks[ prop ].apply( callbacks, args );
+ } else {
+ return callbacks[ prop ]; // or undefined
+ }
+ }
+ }
+
+ // the real equiv function
+ var innerEquiv,
+
+ // stack to decide between skip/abort functions
+ callers = [],
+
+ // stack to avoiding loops from circular referencing
+ parents = [],
+ parentsB = [],
+
+ getProto = Object.getPrototypeOf || function( obj ) {
+ /* jshint camelcase: false, proto: true */
+ return obj.__proto__;
+ },
+ callbacks = (function() {
+
+ // for string, boolean, number and null
+ function useStrictEquality( b, a ) {
+
+ /*jshint eqeqeq:false */
+ if ( b instanceof a.constructor || a instanceof b.constructor ) {
+
+ // to catch short annotation VS 'new' annotation of a
+ // declaration
+ // e.g. var i = 1;
+ // var j = new Number(1);
+ return a == b;
+ } else {
+ return a === b;
+ }
+ }
+
+ return {
+ "string": useStrictEquality,
+ "boolean": useStrictEquality,
+ "number": useStrictEquality,
+ "null": useStrictEquality,
+ "undefined": useStrictEquality,
+
+ "nan": function( b ) {
+ return isNaN( b );
+ },
+
+ "date": function( b, a ) {
+ return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
+ },
+
+ "regexp": function( b, a ) {
+ return QUnit.objectType( b ) === "regexp" &&
+
+ // the regex itself
+ a.source === b.source &&
+
+ // and its modifiers
+ a.global === b.global &&
+
+ // (gmi) ...
+ a.ignoreCase === b.ignoreCase &&
+ a.multiline === b.multiline &&
+ a.sticky === b.sticky;
+ },
+
+ // - skip when the property is a method of an instance (OOP)
+ // - abort otherwise,
+ // initial === would have catch identical references anyway
+ "function": function() {
+ var caller = callers[ callers.length - 1 ];
+ return caller !== Object && typeof caller !== "undefined";
+ },
+
+ "array": function( b, a ) {
+ var i, j, len, loop, aCircular, bCircular;
+
+ // b could be an object literal here
+ if ( QUnit.objectType( b ) !== "array" ) {
+ return false;
+ }
+
+ len = a.length;
+ if ( len !== b.length ) {
+ // safe and faster
+ return false;
+ }
+
+ // track reference to avoid circular references
+ parents.push( a );
+ parentsB.push( b );
+ for ( i = 0; i < len; i++ ) {
+ loop = false;
+ for ( j = 0; j < parents.length; j++ ) {
+ aCircular = parents[ j ] === a[ i ];
+ bCircular = parentsB[ j ] === b[ i ];
+ if ( aCircular || bCircular ) {
+ if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
+ loop = true;
+ } else {
+ parents.pop();
+ parentsB.pop();
+ return false;
+ }
+ }
+ }
+ if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
+ parents.pop();
+ parentsB.pop();
+ return false;
+ }
+ }
+ parents.pop();
+ parentsB.pop();
+ return true;
+ },
+
+ "object": function( b, a ) {
+
+ /*jshint forin:false */
+ var i, j, loop, aCircular, bCircular,
+ // Default to true
+ eq = true,
+ aProperties = [],
+ bProperties = [];
+
+ // comparing constructors is more strict than using
+ // instanceof
+ if ( a.constructor !== b.constructor ) {
+
+ // Allow objects with no prototype to be equivalent to
+ // objects with Object as their constructor.
+ if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) ||
+ ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) {
+ return false;
+ }
+ }
+
+ // stack constructor before traversing properties
+ callers.push( a.constructor );
+
+ // track reference to avoid circular references
+ parents.push( a );
+ parentsB.push( b );
+
+ // be strict: don't ensure hasOwnProperty and go deep
+ for ( i in a ) {
+ loop = false;
+ for ( j = 0; j < parents.length; j++ ) {
+ aCircular = parents[ j ] === a[ i ];
+ bCircular = parentsB[ j ] === b[ i ];
+ if ( aCircular || bCircular ) {
+ if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
+ loop = true;
+ } else {
+ eq = false;
+ break;
+ }
+ }
+ }
+ aProperties.push( i );
+ if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
+ eq = false;
+ break;
+ }
+ }
+
+ parents.pop();
+ parentsB.pop();
+ callers.pop(); // unstack, we are done
+
+ for ( i in b ) {
+ bProperties.push( i ); // collect b's properties
+ }
+
+ // Ensures identical properties name
+ return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
+ }
+ };
+ }());
+
+ innerEquiv = function() { // can take multiple arguments
+ var args = [].slice.apply( arguments );
+ if ( args.length < 2 ) {
+ return true; // end transition
+ }
+
+ return ( (function( a, b ) {
+ if ( a === b ) {
+ return true; // catch the most you can
+ } else if ( a === null || b === null || typeof a === "undefined" ||
+ typeof b === "undefined" ||
+ QUnit.objectType( a ) !== QUnit.objectType( b ) ) {
+
+ // don't lose time with error prone cases
+ return false;
+ } else {
+ return bindCallbacks( a, callbacks, [ b, a ] );
+ }
+
+ // apply transition with (1..n) arguments
+ }( args[ 0 ], args[ 1 ] ) ) &&
+ innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) );
+ };
+
+ return innerEquiv;
+}());
+
+// Based on jsDump by Ariel Flesler
+// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
+QUnit.dump = (function() {
+ function quote( str ) {
+ return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
+ }
+ function literal( o ) {
+ return o + "";
+ }
+ function join( pre, arr, post ) {
+ var s = dump.separator(),
+ base = dump.indent(),
+ inner = dump.indent( 1 );
+ if ( arr.join ) {
+ arr = arr.join( "," + s + inner );
+ }
+ if ( !arr ) {
+ return pre + post;
+ }
+ return [ pre, inner + arr, base + post ].join( s );
+ }
+ function array( arr, stack ) {
+ var i = arr.length,
+ ret = new Array( i );
+
+ if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
+ return "[object Array]";
+ }
+
+ this.up();
+ while ( i-- ) {
+ ret[ i ] = this.parse( arr[ i ], undefined, stack );
+ }
+ this.down();
+ return join( "[", ret, "]" );
+ }
+
+ var reName = /^function (\w+)/,
+ dump = {
+
+ // objType is used mostly internally, you can fix a (custom) type in advance
+ parse: function( obj, objType, stack ) {
+ stack = stack || [];
+ var res, parser, parserType,
+ inStack = inArray( obj, stack );
+
+ if ( inStack !== -1 ) {
+ return "recursion(" + ( inStack - stack.length ) + ")";
+ }
+
+ objType = objType || this.typeOf( obj );
+ parser = this.parsers[ objType ];
+ parserType = typeof parser;
+
+ if ( parserType === "function" ) {
+ stack.push( obj );
+ res = parser.call( this, obj, stack );
+ stack.pop();
+ return res;
+ }
+ return ( parserType === "string" ) ? parser : this.parsers.error;
+ },
+ typeOf: function( obj ) {
+ var type;
+ if ( obj === null ) {
+ type = "null";
+ } else if ( typeof obj === "undefined" ) {
+ type = "undefined";
+ } else if ( QUnit.is( "regexp", obj ) ) {
+ type = "regexp";
+ } else if ( QUnit.is( "date", obj ) ) {
+ type = "date";
+ } else if ( QUnit.is( "function", obj ) ) {
+ type = "function";
+ } else if ( obj.setInterval !== undefined &&
+ obj.document !== undefined &&
+ obj.nodeType === undefined ) {
+ type = "window";
+ } else if ( obj.nodeType === 9 ) {
+ type = "document";
+ } else if ( obj.nodeType ) {
+ type = "node";
+ } else if (
+
+ // native arrays
+ toString.call( obj ) === "[object Array]" ||
+
+ // NodeList objects
+ ( typeof obj.length === "number" && obj.item !== undefined &&
+ ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null &&
+ obj[ 0 ] === undefined ) ) )
+ ) {
+ type = "array";
+ } else if ( obj.constructor === Error.prototype.constructor ) {
+ type = "error";
+ } else {
+ type = typeof obj;
+ }
+ return type;
+ },
+ separator: function() {
+ return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? " " : " ";
+ },
+ // extra can be a number, shortcut for increasing-calling-decreasing
+ indent: function( extra ) {
+ if ( !this.multiline ) {
+ return "";
+ }
+ var chr = this.indentChar;
+ if ( this.HTML ) {
+ chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
+ }
+ return new Array( this.depth + ( extra || 0 ) ).join( chr );
+ },
+ up: function( a ) {
+ this.depth += a || 1;
+ },
+ down: function( a ) {
+ this.depth -= a || 1;
+ },
+ setParser: function( name, parser ) {
+ this.parsers[ name ] = parser;
+ },
+ // The next 3 are exposed so you can use them
+ quote: quote,
+ literal: literal,
+ join: join,
+ //
+ depth: 1,
+ maxDepth: QUnit.config.maxDepth,
+
+ // This is the list of parsers, to modify them, use dump.setParser
+ parsers: {
+ window: "[Window]",
+ document: "[Document]",
+ error: function( error ) {
+ return "Error(\"" + error.message + "\")";
+ },
+ unknown: "[Unknown]",
+ "null": "null",
+ "undefined": "undefined",
+ "function": function( fn ) {
+ var ret = "function",
+
+ // functions never have name in IE
+ name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ];
+
+ if ( name ) {
+ ret += " " + name;
+ }
+ ret += "( ";
+
+ ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" );
+ return join( ret, dump.parse( fn, "functionCode" ), "}" );
+ },
+ array: array,
+ nodelist: array,
+ "arguments": array,
+ object: function( map, stack ) {
+ var keys, key, val, i, nonEnumerableProperties,
+ ret = [];
+
+ if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
+ return "[object Object]";
+ }
+
+ dump.up();
+ keys = [];
+ for ( key in map ) {
+ keys.push( key );
+ }
+
+ // Some properties are not always enumerable on Error objects.
+ nonEnumerableProperties = [ "message", "name" ];
+ for ( i in nonEnumerableProperties ) {
+ key = nonEnumerableProperties[ i ];
+ if ( key in map && inArray( key, keys ) < 0 ) {
+ keys.push( key );
+ }
+ }
+ keys.sort();
+ for ( i = 0; i < keys.length; i++ ) {
+ key = keys[ i ];
+ val = map[ key ];
+ ret.push( dump.parse( key, "key" ) + ": " +
+ dump.parse( val, undefined, stack ) );
+ }
+ dump.down();
+ return join( "{", ret, "}" );
+ },
+ node: function( node ) {
+ var len, i, val,
+ open = dump.HTML ? "<" : "<",
+ close = dump.HTML ? ">" : ">",
+ tag = node.nodeName.toLowerCase(),
+ ret = open + tag,
+ attrs = node.attributes;
+
+ if ( attrs ) {
+ for ( i = 0, len = attrs.length; i < len; i++ ) {
+ val = attrs[ i ].nodeValue;
+
+ // IE6 includes all attributes in .attributes, even ones not explicitly
+ // set. Those have values like undefined, null, 0, false, "" or
+ // "inherit".
+ if ( val && val !== "inherit" ) {
+ ret += " " + attrs[ i ].nodeName + "=" +
+ dump.parse( val, "attribute" );
+ }
+ }
+ }
+ ret += close;
+
+ // Show content of TextNode or CDATASection
+ if ( node.nodeType === 3 || node.nodeType === 4 ) {
+ ret += node.nodeValue;
+ }
+
+ return ret + open + "/" + tag + close;
+ },
+
+ // function calls it internally, it's the arguments part of the function
+ functionArgs: function( fn ) {
+ var args,
+ l = fn.length;
+
+ if ( !l ) {
+ return "";
+ }
+
+ args = new Array( l );
+ while ( l-- ) {
+
+ // 97 is 'a'
+ args[ l ] = String.fromCharCode( 97 + l );
+ }
+ return " " + args.join( ", " ) + " ";
+ },
+ // object calls it internally, the key part of an item in a map
+ key: quote,
+ // function calls it internally, it's the content of the function
+ functionCode: "[code]",
+ // node calls it internally, it's an html attribute value
+ attribute: quote,
+ string: quote,
+ date: quote,
+ regexp: literal,
+ number: literal,
+ "boolean": literal
+ },
+ // if true, entities are escaped ( <, >, \t, space and \n )
+ HTML: false,
+ // indentation unit
+ indentChar: " ",
+ // if true, items in a collection, are separated by a \n, else just a space.
+ multiline: true
+ };
+
+ return dump;
+}());
+
+// back compat
+QUnit.jsDump = QUnit.dump;
+
+// For browser, export only select globals
+if ( typeof window !== "undefined" ) {
+
+ // Deprecated
+ // Extend assert methods to QUnit and Global scope through Backwards compatibility
+ (function() {
+ var i,
+ assertions = Assert.prototype;
+
+ function applyCurrent( current ) {
+ return function() {
+ var assert = new Assert( QUnit.config.current );
+ current.apply( assert, arguments );
+ };
+ }
+
+ for ( i in assertions ) {
+ QUnit[ i ] = applyCurrent( assertions[ i ] );
+ }
+ })();
+
+ (function() {
+ var i, l,
+ keys = [
+ "test",
+ "module",
+ "expect",
+ "asyncTest",
+ "start",
+ "stop",
+ "ok",
+ "notOk",
+ "equal",
+ "notEqual",
+ "propEqual",
+ "notPropEqual",
+ "deepEqual",
+ "notDeepEqual",
+ "strictEqual",
+ "notStrictEqual",
+ "throws"
+ ];
+
+ for ( i = 0, l = keys.length; i < l; i++ ) {
+ window[ keys[ i ] ] = QUnit[ keys[ i ] ];
+ }
+ })();
+
+ window.QUnit = QUnit;
+}
+
+// For nodejs
+if ( typeof module !== "undefined" && module && module.exports ) {
+ module.exports = QUnit;
+
+ // For consistency with CommonJS environments' exports
+ module.exports.QUnit = QUnit;
+}
+
+// For CommonJS with exports, but without module.exports, like Rhino
+if ( typeof exports !== "undefined" && exports ) {
+ exports.QUnit = QUnit;
+}
+
+if ( typeof define === "function" && define.amd ) {
+ define( function() {
+ return QUnit;
+ } );
+ QUnit.config.autostart = false;
+}
+
+// Get a reference to the global object, like window in browsers
+}( (function() {
+ return this;
+})() ));
+
+/*istanbul ignore next */
+// jscs:disable maximumLineLength
+/*
+ * This file is a modified version of google-diff-match-patch's JavaScript implementation
+ * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js),
+ * modifications are licensed as more fully set forth in LICENSE.txt.
+ *
+ * The original source of google-diff-match-patch is attributable and licensed as follows:
+ *
+ * Copyright 2006 Google Inc.
+ * http://code.google.com/p/google-diff-match-patch/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * More Info:
+ * https://code.google.com/p/google-diff-match-patch/
+ *
+ * Usage: QUnit.diff(expected, actual)
+ *
+ * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) === "the quick <del>brown </del> fox jump<ins>s</ins><del>ed</del over"
+ */
+QUnit.diff = (function() {
+
+ function DiffMatchPatch() {
+
+ // Defaults.
+ // Redefine these in your program to override the defaults.
+
+ // Number of seconds to map a diff before giving up (0 for infinity).
+ this.DiffTimeout = 1.0;
+ // Cost of an empty edit operation in terms of edit characters.
+ this.DiffEditCost = 4;
+ }
+
+ // DIFF FUNCTIONS
+
+ /**
+ * The data structure representing a diff is an array of tuples:
+ * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
+ * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
+ */
+ var DIFF_DELETE = -1,
+ DIFF_INSERT = 1,
+ DIFF_EQUAL = 0;
+
+ /**
+ * Find the differences between two texts. Simplifies the problem by stripping
+ * any common prefix or suffix off the texts before diffing.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {boolean=} optChecklines Optional speedup flag. If present and false,
+ * then don't run a line-level diff first to identify the changed areas.
+ * Defaults to true, which does a faster, slightly less optimal diff.
+ * @param {number} optDeadline Optional time when the diff should be complete
+ * by. Used internally for recursive calls. Users should set DiffTimeout
+ * instead.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ */
+ DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines, optDeadline ) {
+ var deadline, checklines, commonlength,
+ commonprefix, commonsuffix, diffs;
+ // Set a deadline by which time the diff must be complete.
+ if ( typeof optDeadline === "undefined" ) {
+ if ( this.DiffTimeout <= 0 ) {
+ optDeadline = Number.MAX_VALUE;
} else {
- return "number";
- }
-
- } else if (typeof o === "undefined") {
- return "undefined";
-
- // consider: typeof null === object
- } else if (o === null) {
- return "null";
-
- // consider: typeof [] === object
- } else if (QUnit.is( "Array", o)) {
- return "array";
-
- // consider: typeof new Date() === object
- } else if (QUnit.is( "Date", o)) {
- return "date";
-
- // consider: /./ instanceof Object;
- // /./ instanceof RegExp;
- // typeof /./ === "function"; // => false in IE and Opera,
- // true in FF and Safari
- } else if (QUnit.is( "RegExp", o)) {
- return "regexp";
-
- } else if (typeof o === "object") {
- return "object";
-
- } else if (QUnit.is( "Function", o)) {
- return "function";
- } else {
- return undefined;
- }
- }
-
- // Call the o related callback with the given arguments.
- function bindCallbacks(o, callbacks, args) {
- var prop = hoozit(o);
- if (prop) {
- if (hoozit(callbacks[prop]) === "function") {
- return callbacks[prop].apply(callbacks, args);
- } else {
- return callbacks[prop]; // or undefined
+ optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000;
}
}
- }
-
- var callbacks = function () {
-
- // for string, boolean, number and null
- function useStrictEquality(b, a) {
- if (b instanceof a.constructor || a instanceof b.constructor) {
- // to catch short annotaion VS 'new' annotation of a declaration
- // e.g. var i = 1;
- // var j = new Number(1);
- return a == b;
- } else {
- return a === b;
+ deadline = optDeadline;
+
+ // Check for null inputs.
+ if ( text1 === null || text2 === null ) {
+ throw new Error( "Null input. (DiffMain)" );
+ }
+
+ // Check for equality (speedup).
+ if ( text1 === text2 ) {
+ if ( text1 ) {
+ return [
+ [ DIFF_EQUAL, text1 ]
+ ];
+ }
+ return [];
+ }
+
+ if ( typeof optChecklines === "undefined" ) {
+ optChecklines = true;
+ }
+
+ checklines = optChecklines;
+
+ // Trim off common prefix (speedup).
+ commonlength = this.diffCommonPrefix( text1, text2 );
+ commonprefix = text1.substring( 0, commonlength );
+ text1 = text1.substring( commonlength );
+ text2 = text2.substring( commonlength );
+
+ // Trim off common suffix (speedup).
+ /////////
+ commonlength = this.diffCommonSuffix( text1, text2 );
+ commonsuffix = text1.substring( text1.length - commonlength );
+ text1 = text1.substring( 0, text1.length - commonlength );
+ text2 = text2.substring( 0, text2.length - commonlength );
+
+ // Compute the diff on the middle block.
+ diffs = this.diffCompute( text1, text2, checklines, deadline );
+
+ // Restore the prefix and suffix.
+ if ( commonprefix ) {
+ diffs.unshift( [ DIFF_EQUAL, commonprefix ] );
+ }
+ if ( commonsuffix ) {
+ diffs.push( [ DIFF_EQUAL, commonsuffix ] );
+ }
+ this.diffCleanupMerge( diffs );
+ return diffs;
+ };
+
+ /**
+ * Reduce the number of edits by eliminating operationally trivial equalities.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ */
+ DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) {
+ var changes, equalities, equalitiesLength, lastequality,
+ pointer, preIns, preDel, postIns, postDel;
+ changes = false;
+ equalities = []; // Stack of indices where equalities are found.
+ equalitiesLength = 0; // Keeping our own length var is faster in JS.
+ /** @type {?string} */
+ lastequality = null;
+ // Always equal to diffs[equalities[equalitiesLength - 1]][1]
+ pointer = 0; // Index of current position.
+ // Is there an insertion operation before the last equality.
+ preIns = false;
+ // Is there a deletion operation before the last equality.
+ preDel = false;
+ // Is there an insertion operation after the last equality.
+ postIns = false;
+ // Is there a deletion operation after the last equality.
+ postDel = false;
+ while ( pointer < diffs.length ) {
+ if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found.
+ if ( diffs[ pointer ][ 1 ].length < this.DiffEditCost && ( postIns || postDel ) ) {
+ // Candidate found.
+ equalities[ equalitiesLength++ ] = pointer;
+ preIns = postIns;
+ preDel = postDel;
+ lastequality = diffs[ pointer ][ 1 ];
+ } else {
+ // Not a candidate, and can never become one.
+ equalitiesLength = 0;
+ lastequality = null;
+ }
+ postIns = postDel = false;
+ } else { // An insertion or deletion.
+ if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) {
+ postDel = true;
+ } else {
+ postIns = true;
+ }
+ /*
+ * Five types to be split:
+ * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
+ * <ins>A</ins>X<ins>C</ins><del>D</del>
+ * <ins>A</ins><del>B</del>X<ins>C</ins>
+ * <ins>A</del>X<ins>C</ins><del>D</del>
+ * <ins>A</ins><del>B</del>X<del>C</del>
+ */
+ if ( lastequality && ( ( preIns && preDel && postIns && postDel ) ||
+ ( ( lastequality.length < this.DiffEditCost / 2 ) &&
+ ( preIns + preDel + postIns + postDel ) === 3 ) ) ) {
+ // Duplicate record.
+ diffs.splice( equalities[equalitiesLength - 1], 0, [ DIFF_DELETE, lastequality ] );
+ // Change second copy to insert.
+ diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT;
+ equalitiesLength--; // Throw away the equality we just deleted;
+ lastequality = null;
+ if (preIns && preDel) {
+ // No changes made which could affect previous entry, keep going.
+ postIns = postDel = true;
+ equalitiesLength = 0;
+ } else {
+ equalitiesLength--; // Throw away the previous equality.
+ pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1;
+ postIns = postDel = false;
+ }
+ changes = true;
+ }
+ }
+ pointer++;
+ }
+
+ if ( changes ) {
+ this.diffCleanupMerge( diffs );
+ }
+ };
+
+ /**
+ * Convert a diff array into a pretty HTML report.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ * @param {integer} string to be beautified.
+ * @return {string} HTML representation.
+ */
+ DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) {
+ var op, data, x, html = [];
+ for ( x = 0; x < diffs.length; x++ ) {
+ op = diffs[x][0]; // Operation (insert, delete, equal)
+ data = diffs[x][1]; // Text of change.
+ switch ( op ) {
+ case DIFF_INSERT:
+ html[x] = "<ins>" + data + "</ins>";
+ break;
+ case DIFF_DELETE:
+ html[x] = "<del>" + data + "</del>";
+ break;
+ case DIFF_EQUAL:
+ html[x] = "<span>" + data + "</span>";
+ break;
}
}
-
- return {
- "string": useStrictEquality,
- "boolean": useStrictEquality,
- "number": useStrictEquality,
- "null": useStrictEquality,
- "undefined": useStrictEquality,
-
- "nan": function (b) {
- return isNaN(b);
- },
-
- "date": function (b, a) {
- return hoozit(b) === "date" && a.valueOf() === b.valueOf();
- },
-
- "regexp": function (b, a) {
- return hoozit(b) === "regexp" &&
- a.source === b.source && // the regex itself
- a.global === b.global && // and its modifers (gmi) ...
- a.ignoreCase === b.ignoreCase &&
- a.multiline === b.multiline;
- },
-
- // - skip when the property is a method of an instance (OOP)
- // - abort otherwise,
- // initial === would have catch identical references anyway
- "function": function () {
- var caller = callers[callers.length - 1];
- return caller !== Object &&
- typeof caller !== "undefined";
- },
-
- "array": function (b, a) {
- var i, j, loop;
- var len;
-
- // b could be an object literal here
- if ( ! (hoozit(b) === "array")) {
- return false;
- }
-
- len = a.length;
- if (len !== b.length) { // safe and faster
- return false;
+ return html.join("");
+ };
+
+ /**
+ * Determine the common prefix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the start of each
+ * string.
+ */
+ DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) {
+ var pointermid, pointermax, pointermin, pointerstart;
+ // Quick check for common null cases.
+ if ( !text1 || !text2 || text1.charAt(0) !== text2.charAt(0) ) {
+ return 0;
+ }
+ // Binary search.
+ // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+ pointermin = 0;
+ pointermax = Math.min( text1.length, text2.length );
+ pointermid = pointermax;
+ pointerstart = 0;
+ while ( pointermin < pointermid ) {
+ if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) {
+ pointermin = pointermid;
+ pointerstart = pointermin;
+ } else {
+ pointermax = pointermid;
+ }
+ pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
+ }
+ return pointermid;
+ };
+
+ /**
+ * Determine the common suffix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the end of each string.
+ */
+ DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) {
+ var pointermid, pointermax, pointermin, pointerend;
+ // Quick check for common null cases.
+ if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) {
+ return 0;
+ }
+ // Binary search.
+ // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+ pointermin = 0;
+ pointermax = Math.min(text1.length, text2.length);
+ pointermid = pointermax;
+ pointerend = 0;
+ while ( pointermin < pointermid ) {
+ if (text1.substring( text1.length - pointermid, text1.length - pointerend ) ===
+ text2.substring( text2.length - pointermid, text2.length - pointerend ) ) {
+ pointermin = pointermid;
+ pointerend = pointermin;
+ } else {
+ pointermax = pointermid;
+ }
+ pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
+ }
+ return pointermid;
+ };
+
+ /**
+ * Find the differences between two texts. Assumes that the texts do not
+ * have any common prefix or suffix.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {boolean} checklines Speedup flag. If false, then don't run a
+ * line-level diff first to identify the changed areas.
+ * If true, then run a faster, slightly less optimal diff.
+ * @param {number} deadline Time when the diff should be complete by.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) {
+ var diffs, longtext, shorttext, i, hm,
+ text1A, text2A, text1B, text2B,
+ midCommon, diffsA, diffsB;
+
+ if ( !text1 ) {
+ // Just add some text (speedup).
+ return [
+ [ DIFF_INSERT, text2 ]
+ ];
+ }
+
+ if (!text2) {
+ // Just delete some text (speedup).
+ return [
+ [ DIFF_DELETE, text1 ]
+ ];
+ }
+
+ longtext = text1.length > text2.length ? text1 : text2;
+ shorttext = text1.length > text2.length ? text2 : text1;
+ i = longtext.indexOf( shorttext );
+ if ( i !== -1 ) {
+ // Shorter text is inside the longer text (speedup).
+ diffs = [
+ [ DIFF_INSERT, longtext.substring( 0, i ) ],
+ [ DIFF_EQUAL, shorttext ],
+ [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ]
+ ];
+ // Swap insertions for deletions if diff is reversed.
+ if ( text1.length > text2.length ) {
+ diffs[0][0] = diffs[2][0] = DIFF_DELETE;
+ }
+ return diffs;
+ }
+
+ if ( shorttext.length === 1 ) {
+ // Single character string.
+ // After the previous speedup, the character can't be an equality.
+ return [
+ [ DIFF_DELETE, text1 ],
+ [ DIFF_INSERT, text2 ]
+ ];
+ }
+
+ // Check to see if the problem can be split in two.
+ hm = this.diffHalfMatch(text1, text2);
+ if (hm) {
+ // A half-match was found, sort out the return data.
+ text1A = hm[0];
+ text1B = hm[1];
+ text2A = hm[2];
+ text2B = hm[3];
+ midCommon = hm[4];
+ // Send both pairs off for separate processing.
+ diffsA = this.DiffMain(text1A, text2A, checklines, deadline);
+ diffsB = this.DiffMain(text1B, text2B, checklines, deadline);
+ // Merge the results.
+ return diffsA.concat([
+ [ DIFF_EQUAL, midCommon ]
+ ], diffsB);
+ }
+
+ if (checklines && text1.length > 100 && text2.length > 100) {
+ return this.diffLineMode(text1, text2, deadline);
+ }
+
+ return this.diffBisect(text1, text2, deadline);
+ };
+
+ /**
+ * Do the two texts share a substring which is at least half the length of the
+ * longer text?
+ * This speedup can produce non-minimal diffs.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {Array.<string>} Five element Array, containing the prefix of
+ * text1, the suffix of text1, the prefix of text2, the suffix of
+ * text2 and the common middle. Or null if there was no match.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffHalfMatch = function(text1, text2) {
+ var longtext, shorttext, dmp,
+ text1A, text2B, text2A, text1B, midCommon,
+ hm1, hm2, hm;
+ if (this.DiffTimeout <= 0) {
+ // Don't risk returning a non-optimal diff if we have unlimited time.
+ return null;
+ }
+ longtext = text1.length > text2.length ? text1 : text2;
+ shorttext = text1.length > text2.length ? text2 : text1;
+ if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
+ return null; // Pointless.
+ }
+ dmp = this; // 'this' becomes 'window' in a closure.
+
+ /**
+ * Does a substring of shorttext exist within longtext such that the substring
+ * is at least half the length of longtext?
+ * Closure, but does not reference any external variables.
+ * @param {string} longtext Longer string.
+ * @param {string} shorttext Shorter string.
+ * @param {number} i Start index of quarter length substring within longtext.
+ * @return {Array.<string>} Five element Array, containing the prefix of
+ * longtext, the suffix of longtext, the prefix of shorttext, the suffix
+ * of shorttext and the common middle. Or null if there was no match.
+ * @private
+ */
+ function diffHalfMatchI(longtext, shorttext, i) {
+ var seed, j, bestCommon, prefixLength, suffixLength,
+ bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB;
+ // Start with a 1/4 length substring at position i as a seed.
+ seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
+ j = -1;
+ bestCommon = "";
+ while ((j = shorttext.indexOf(seed, j + 1)) !== -1) {
+ prefixLength = dmp.diffCommonPrefix(longtext.substring(i),
+ shorttext.substring(j));
+ suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i),
+ shorttext.substring(0, j));
+ if (bestCommon.length < suffixLength + prefixLength) {
+ bestCommon = shorttext.substring(j - suffixLength, j) +
+ shorttext.substring(j, j + prefixLength);
+ bestLongtextA = longtext.substring(0, i - suffixLength);
+ bestLongtextB = longtext.substring(i + prefixLength);
+ bestShorttextA = shorttext.substring(0, j - suffixLength);
+ bestShorttextB = shorttext.substring(j + prefixLength);
}
-
- //track reference to avoid circular references
- parents.push(a);
- for (i = 0; i < len; i++) {
- loop = false;
- for(j=0;j<parents.length;j++){
- if(parents[j] === a[i]){
- loop = true;//dont rewalk array
+ }
+ if (bestCommon.length * 2 >= longtext.length) {
+ return [ bestLongtextA, bestLongtextB,
+ bestShorttextA, bestShorttextB, bestCommon
+ ];
+ } else {
+ return null;
+ }
+ }
+
+ // First check if the second quarter is the seed for a half-match.
+ hm1 = diffHalfMatchI(longtext, shorttext,
+ Math.ceil(longtext.length / 4));
+ // Check again based on the third quarter.
+ hm2 = diffHalfMatchI(longtext, shorttext,
+ Math.ceil(longtext.length / 2));
+ if (!hm1 && !hm2) {
+ return null;
+ } else if (!hm2) {
+ hm = hm1;
+ } else if (!hm1) {
+ hm = hm2;
+ } else {
+ // Both matched. Select the longest.
+ hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
+ }
+
+ // A half-match was found, sort out the return data.
+ text1A, text1B, text2A, text2B;
+ if (text1.length > text2.length) {
+ text1A = hm[0];
+ text1B = hm[1];
+ text2A = hm[2];
+ text2B = hm[3];
+ } else {
+ text2A = hm[0];
+ text2B = hm[1];
+ text1A = hm[2];
+ text1B = hm[3];
+ }
+ midCommon = hm[4];
+ return [ text1A, text1B, text2A, text2B, midCommon ];
+ };
+
+ /**
+ * Do a quick line-level diff on both strings, then rediff the parts for
+ * greater accuracy.
+ * This speedup can produce non-minimal diffs.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} deadline Time when the diff should be complete by.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffLineMode = function(text1, text2, deadline) {
+ var a, diffs, linearray, pointer, countInsert,
+ countDelete, textInsert, textDelete, j;
+ // Scan the text on a line-by-line basis first.
+ a = this.diffLinesToChars(text1, text2);
+ text1 = a.chars1;
+ text2 = a.chars2;
+ linearray = a.lineArray;
+
+ diffs = this.DiffMain(text1, text2, false, deadline);
+
+ // Convert the diff back to original text.
+ this.diffCharsToLines(diffs, linearray);
+ // Eliminate freak matches (e.g. blank lines)
+ this.diffCleanupSemantic(diffs);
+
+ // Rediff any replacement blocks, this time character-by-character.
+ // Add a dummy entry at the end.
+ diffs.push( [ DIFF_EQUAL, "" ] );
+ pointer = 0;
+ countDelete = 0;
+ countInsert = 0;
+ textDelete = "";
+ textInsert = "";
+ while (pointer < diffs.length) {
+ switch ( diffs[pointer][0] ) {
+ case DIFF_INSERT:
+ countInsert++;
+ textInsert += diffs[pointer][1];
+ break;
+ case DIFF_DELETE:
+ countDelete++;
+ textDelete += diffs[pointer][1];
+ break;
+ case DIFF_EQUAL:
+ // Upon reaching an equality, check for prior redundancies.
+ if (countDelete >= 1 && countInsert >= 1) {
+ // Delete the offending records and add the merged ones.
+ diffs.splice(pointer - countDelete - countInsert,
+ countDelete + countInsert);
+ pointer = pointer - countDelete - countInsert;
+ a = this.DiffMain(textDelete, textInsert, false, deadline);
+ for (j = a.length - 1; j >= 0; j--) {
+ diffs.splice( pointer, 0, a[j] );
}
+ pointer = pointer + a.length;
}
- if (!loop && ! innerEquiv(a[i], b[i])) {
- parents.pop();
- return false;
- }
+ countInsert = 0;
+ countDelete = 0;
+ textDelete = "";
+ textInsert = "";
+ break;
+ }
+ pointer++;
+ }
+ diffs.pop(); // Remove the dummy entry at the end.
+
+ return diffs;
+ };
+
+ /**
+ * Find the 'middle snake' of a diff, split the problem in two
+ * and return the recursively constructed diff.
+ * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} deadline Time at which to bail if not yet complete.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffBisect = function(text1, text2, deadline) {
+ var text1Length, text2Length, maxD, vOffset, vLength,
+ v1, v2, x, delta, front, k1start, k1end, k2start,
+ k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2;
+ // Cache the text lengths to prevent multiple calls.
+ text1Length = text1.length;
+ text2Length = text2.length;
+ maxD = Math.ceil((text1Length + text2Length) / 2);
+ vOffset = maxD;
+ vLength = 2 * maxD;
+ v1 = new Array(vLength);
+ v2 = new Array(vLength);
+ // Setting all elements to -1 is faster in Chrome & Firefox than mixing
+ // integers and undefined.
+ for (x = 0; x < vLength; x++) {
+ v1[x] = -1;
+ v2[x] = -1;
+ }
+ v1[vOffset + 1] = 0;
+ v2[vOffset + 1] = 0;
+ delta = text1Length - text2Length;
+ // If the total number of characters is odd, then the front path will collide
+ // with the reverse path.
+ front = (delta % 2 !== 0);
+ // Offsets for start and end of k loop.
+ // Prevents mapping of space beyond the grid.
+ k1start = 0;
+ k1end = 0;
+ k2start = 0;
+ k2end = 0;
+ for (d = 0; d < maxD; d++) {
+ // Bail out if deadline is reached.
+ if ((new Date()).getTime() > deadline) {
+ break;
+ }
+
+ // Walk the front path one step.
+ for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
+ k1Offset = vOffset + k1;
+ if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) {
+ x1 = v1[k1Offset + 1];
+ } else {
+ x1 = v1[k1Offset - 1] + 1;
}
- parents.pop();
- return true;
- },
-
- "object": function (b, a) {
- var i, j, loop;
- var eq = true; // unless we can proove it
- var aProperties = [], bProperties = []; // collection of strings
-
- // comparing constructors is more strict than using instanceof
- if ( a.constructor !== b.constructor) {
- return false;
+ y1 = x1 - k1;
+ while (x1 < text1Length && y1 < text2Length &&
+ text1.charAt(x1) === text2.charAt(y1)) {
+ x1++;
+ y1++;
}
-
- // stack constructor before traversing properties
- callers.push(a.constructor);
- //track reference to avoid circular references
- parents.push(a);
-
- for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
- loop = false;
- for(j=0;j<parents.length;j++){
- if(parents[j] === a[i])
- loop = true; //don't go down the same path twice
- }
- aProperties.push(i); // collect a's properties
-
- if (!loop && ! innerEquiv(a[i], b[i])) {
- eq = false;
- break;
+ v1[k1Offset] = x1;
+ if (x1 > text1Length) {
+ // Ran off the right of the graph.
+ k1end += 2;
+ } else if (y1 > text2Length) {
+ // Ran off the bottom of the graph.
+ k1start += 2;
+ } else if (front) {
+ k2Offset = vOffset + delta - k1;
+ if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) {
+ // Mirror x2 onto top-left coordinate system.
+ x2 = text1Length - v2[k2Offset];
+ if (x1 >= x2) {
+ // Overlap detected.
+ return this.diffBisectSplit(text1, text2, x1, y1, deadline);
+ }
}
}
-
- callers.pop(); // unstack, we are done
- parents.pop();
-
- for (i in b) {
- bProperties.push(i); // collect b's properties
+ }
+
+ // Walk the reverse path one step.
+ for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
+ k2Offset = vOffset + k2;
+ if ( k2 === -d || (k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) {
+ x2 = v2[k2Offset + 1];
+ } else {
+ x2 = v2[k2Offset - 1] + 1;
+ }
+ y2 = x2 - k2;
+ while (x2 < text1Length && y2 < text2Length &&
+ text1.charAt(text1Length - x2 - 1) ===
+ text2.charAt(text2Length - y2 - 1)) {
+ x2++;
+ y2++;
+ }
+ v2[k2Offset] = x2;
+ if (x2 > text1Length) {
+ // Ran off the left of the graph.
+ k2end += 2;
+ } else if (y2 > text2Length) {
+ // Ran off the top of the graph.
+ k2start += 2;
+ } else if (!front) {
+ k1Offset = vOffset + delta - k2;
+ if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) {
+ x1 = v1[k1Offset];
+ y1 = vOffset + x1 - k1Offset;
+ // Mirror x2 onto top-left coordinate system.
+ x2 = text1Length - x2;
+ if (x1 >= x2) {
+ // Overlap detected.
+ return this.diffBisectSplit(text1, text2, x1, y1, deadline);
+ }
+ }
+ }
+ }
+ }
+ // Diff took too long and hit the deadline or
+ // number of diffs equals number of characters, no commonality at all.
+ return [
+ [ DIFF_DELETE, text1 ],
+ [ DIFF_INSERT, text2 ]
+ ];
+ };
+
+ /**
+ * Given the location of the 'middle snake', split the diff in two parts
+ * and recurse.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} x Index of split point in text1.
+ * @param {number} y Index of split point in text2.
+ * @param {number} deadline Time at which to bail if not yet complete.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) {
+ var text1a, text1b, text2a, text2b, diffs, diffsb;
+ text1a = text1.substring(0, x);
+ text2a = text2.substring(0, y);
+ text1b = text1.substring(x);
+ text2b = text2.substring(y);
+
+ // Compute both diffs serially.
+ diffs = this.DiffMain(text1a, text2a, false, deadline);
+ diffsb = this.DiffMain(text1b, text2b, false, deadline);
+
+ return diffs.concat(diffsb);
+ };
+
+ /**
+ * Reduce the number of edits by eliminating semantically trivial equalities.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ */
+ DiffMatchPatch.prototype.diffCleanupSemantic = function(diffs) {
+ var changes, equalities, equalitiesLength, lastequality,
+ pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1,
+ lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2;
+ changes = false;
+ equalities = []; // Stack of indices where equalities are found.
+ equalitiesLength = 0; // Keeping our own length var is faster in JS.
+ /** @type {?string} */
+ lastequality = null;
+ // Always equal to diffs[equalities[equalitiesLength - 1]][1]
+ pointer = 0; // Index of current position.
+ // Number of characters that changed prior to the equality.
+ lengthInsertions1 = 0;
+ lengthDeletions1 = 0;
+ // Number of characters that changed after the equality.
+ lengthInsertions2 = 0;
+ lengthDeletions2 = 0;
+ while (pointer < diffs.length) {
+ if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found.
+ equalities[equalitiesLength++] = pointer;
+ lengthInsertions1 = lengthInsertions2;
+ lengthDeletions1 = lengthDeletions2;
+ lengthInsertions2 = 0;
+ lengthDeletions2 = 0;
+ lastequality = diffs[pointer][1];
+ } else { // An insertion or deletion.
+ if (diffs[pointer][0] === DIFF_INSERT) {
+ lengthInsertions2 += diffs[pointer][1].length;
+ } else {
+ lengthDeletions2 += diffs[pointer][1].length;
+ }
+ // Eliminate an equality that is smaller or equal to the edits on both
+ // sides of it.
+ if (lastequality && (lastequality.length <=
+ Math.max(lengthInsertions1, lengthDeletions1)) &&
+ (lastequality.length <= Math.max(lengthInsertions2,
+ lengthDeletions2))) {
+ // Duplicate record.
+ diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] );
+ // Change second copy to insert.
+ diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
+ // Throw away the equality we just deleted.
+ equalitiesLength--;
+ // Throw away the previous equality (it needs to be reevaluated).
+ equalitiesLength--;
+ pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
+ lengthInsertions1 = 0; // Reset the counters.
+ lengthDeletions1 = 0;
+ lengthInsertions2 = 0;
+ lengthDeletions2 = 0;
+ lastequality = null;
+ changes = true;
}
-
- // Ensures identical properties name
- return eq && innerEquiv(aProperties.sort(), bProperties.sort());
+ }
+ pointer++;
+ }
+
+ // Normalize the diff.
+ if (changes) {
+ this.diffCleanupMerge(diffs);
+ }
+
+ // Find any overlaps between deletions and insertions.
+ // e.g: <del>abcxxx</del><ins>xxxdef</ins>
+ // -> <del>abc</del>xxx<ins>def</ins>
+ // e.g: <del>xxxabc</del><ins>defxxx</ins>
+ // -> <ins>def</ins>xxx<del>abc</del>
+ // Only extract an overlap if it is as big as the edit ahead or behind it.
+ pointer = 1;
+ while (pointer < diffs.length) {
+ if (diffs[pointer - 1][0] === DIFF_DELETE &&
+ diffs[pointer][0] === DIFF_INSERT) {
+ deletion = diffs[pointer - 1][1];
+ insertion = diffs[pointer][1];
+ overlapLength1 = this.diffCommonOverlap(deletion, insertion);
+ overlapLength2 = this.diffCommonOverlap(insertion, deletion);
+ if (overlapLength1 >= overlapLength2) {
+ if (overlapLength1 >= deletion.length / 2 ||
+ overlapLength1 >= insertion.length / 2) {
+ // Overlap found. Insert an equality and trim the surrounding edits.
+ diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] );
+ diffs[pointer - 1][1] =
+ deletion.substring(0, deletion.length - overlapLength1);
+ diffs[pointer + 1][1] = insertion.substring(overlapLength1);
+ pointer++;
+ }
+ } else {
+ if (overlapLength2 >= deletion.length / 2 ||
+ overlapLength2 >= insertion.length / 2) {
+ // Reverse overlap found.
+ // Insert an equality and swap and trim the surrounding edits.
+ diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] );
+ diffs[pointer - 1][0] = DIFF_INSERT;
+ diffs[pointer - 1][1] =
+ insertion.substring(0, insertion.length - overlapLength2);
+ diffs[pointer + 1][0] = DIFF_DELETE;
+ diffs[pointer + 1][1] =
+ deletion.substring(overlapLength2);
+ pointer++;
+ }
+ }
+ pointer++;
}
+ pointer++;
+ }
+ };
+
+ /**
+ * Determine if the suffix of one string is the prefix of another.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the end of the first
+ * string and the start of the second string.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffCommonOverlap = function(text1, text2) {
+ var text1Length, text2Length, textLength,
+ best, length, pattern, found;
+ // Cache the text lengths to prevent multiple calls.
+ text1Length = text1.length;
+ text2Length = text2.length;
+ // Eliminate the null case.
+ if (text1Length === 0 || text2Length === 0) {
+ return 0;
+ }
+ // Truncate the longer string.
+ if (text1Length > text2Length) {
+ text1 = text1.substring(text1Length - text2Length);
+ } else if (text1Length < text2Length) {
+ text2 = text2.substring(0, text1Length);
+ }
+ textLength = Math.min(text1Length, text2Length);
+ // Quick check for the worst case.
+ if (text1 === text2) {
+ return textLength;
+ }
+
+ // Start by looking for a single character match
+ // and increase length until no match is found.
+ // Performance analysis: http://neil.fraser.name/news/2010/11/04/
+ best = 0;
+ length = 1;
+ while (true) {
+ pattern = text1.substring(textLength - length);
+ found = text2.indexOf(pattern);
+ if (found === -1) {
+ return best;
+ }
+ length += found;
+ if (found === 0 || text1.substring(textLength - length) ===
+ text2.substring(0, length)) {
+ best = length;
+ length++;
+ }
+ }
+ };
+
+ /**
+ * Split two texts into an array of strings. Reduce the texts to a string of
+ * hashes where each Unicode character represents one line.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}}
+ * An object containing the encoded text1, the encoded text2 and
+ * the array of unique strings.
+ * The zeroth element of the array of unique strings is intentionally blank.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffLinesToChars = function(text1, text2) {
+ var lineArray, lineHash, chars1, chars2;
+ lineArray = []; // e.g. lineArray[4] === 'Hello\n'
+ lineHash = {}; // e.g. lineHash['Hello\n'] === 4
+
+ // '\x00' is a valid character, but various debuggers don't like it.
+ // So we'll insert a junk entry to avoid generating a null character.
+ lineArray[0] = "";
+
+ /**
+ * Split a text into an array of strings. Reduce the texts to a string of
+ * hashes where each Unicode character represents one line.
+ * Modifies linearray and linehash through being a closure.
+ * @param {string} text String to encode.
+ * @return {string} Encoded string.
+ * @private
+ */
+ function diffLinesToCharsMunge(text) {
+ var chars, lineStart, lineEnd, lineArrayLength, line;
+ chars = "";
+ // Walk the text, pulling out a substring for each line.
+ // text.split('\n') would would temporarily double our memory footprint.
+ // Modifying text would create many large strings to garbage collect.
+ lineStart = 0;
+ lineEnd = -1;
+ // Keeping our own length variable is faster than looking it up.
+ lineArrayLength = lineArray.length;
+ while (lineEnd < text.length - 1) {
+ lineEnd = text.indexOf("\n", lineStart);
+ if (lineEnd === -1) {
+ lineEnd = text.length - 1;
+ }
+ line = text.substring(lineStart, lineEnd + 1);
+ lineStart = lineEnd + 1;
+
+ if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
+ (lineHash[line] !== undefined)) {
+ chars += String.fromCharCode( lineHash[ line ] );
+ } else {
+ chars += String.fromCharCode(lineArrayLength);
+ lineHash[line] = lineArrayLength;
+ lineArray[lineArrayLength++] = line;
+ }
+ }
+ return chars;
+ }
+
+ chars1 = diffLinesToCharsMunge(text1);
+ chars2 = diffLinesToCharsMunge(text2);
+ return {
+ chars1: chars1,
+ chars2: chars2,
+ lineArray: lineArray
};
- }();
-
- innerEquiv = function () { // can take multiple arguments
- var args = Array.prototype.slice.apply(arguments);
- if (args.length < 2) {
- return true; // end transition
+ };
+
+ /**
+ * Rehydrate the text in a diff from a string of line hashes to real lines of
+ * text.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ * @param {!Array.<string>} lineArray Array of unique strings.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) {
+ var x, chars, text, y;
+ for ( x = 0; x < diffs.length; x++ ) {
+ chars = diffs[x][1];
+ text = [];
+ for ( y = 0; y < chars.length; y++ ) {
+ text[y] = lineArray[chars.charCodeAt(y)];
+ }
+ diffs[x][1] = text.join("");
+ }
+ };
+
+ /**
+ * Reorder and merge like edit sections. Merge equalities.
+ * Any edit section can move as long as it doesn't cross an equality.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ */
+ DiffMatchPatch.prototype.diffCleanupMerge = function(diffs) {
+ var pointer, countDelete, countInsert, textInsert, textDelete,
+ commonlength, changes;
+ diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end.
+ pointer = 0;
+ countDelete = 0;
+ countInsert = 0;
+ textDelete = "";
+ textInsert = "";
+ commonlength;
+ while (pointer < diffs.length) {
+ switch ( diffs[ pointer ][ 0 ] ) {
+ case DIFF_INSERT:
+ countInsert++;
+ textInsert += diffs[pointer][1];
+ pointer++;
+ break;
+ case DIFF_DELETE:
+ countDelete++;
+ textDelete += diffs[pointer][1];
+ pointer++;
+ break;
+ case DIFF_EQUAL:
+ // Upon reaching an equality, check for prior redundancies.
+ if (countDelete + countInsert > 1) {
+ if (countDelete !== 0 && countInsert !== 0) {
+ // Factor out any common prefixies.
+ commonlength = this.diffCommonPrefix(textInsert, textDelete);
+ if (commonlength !== 0) {
+ if ((pointer - countDelete - countInsert) > 0 &&
+ diffs[pointer - countDelete - countInsert - 1][0] ===
+ DIFF_EQUAL) {
+ diffs[pointer - countDelete - countInsert - 1][1] +=
+ textInsert.substring(0, commonlength);
+ } else {
+ diffs.splice( 0, 0, [ DIFF_EQUAL,
+ textInsert.substring( 0, commonlength )
+ ] );
+ pointer++;
+ }
+ textInsert = textInsert.substring(commonlength);
+ textDelete = textDelete.substring(commonlength);
+ }
+ // Factor out any common suffixies.
+ commonlength = this.diffCommonSuffix(textInsert, textDelete);
+ if (commonlength !== 0) {
+ diffs[pointer][1] = textInsert.substring(textInsert.length -
+ commonlength) + diffs[pointer][1];
+ textInsert = textInsert.substring(0, textInsert.length -
+ commonlength);
+ textDelete = textDelete.substring(0, textDelete.length -
+ commonlength);
+ }
+ }
+ // Delete the offending records and add the merged ones.
+ if (countDelete === 0) {
+ diffs.splice( pointer - countInsert,
+ countDelete + countInsert, [ DIFF_INSERT, textInsert ] );
+ } else if (countInsert === 0) {
+ diffs.splice( pointer - countDelete,
+ countDelete + countInsert, [ DIFF_DELETE, textDelete ] );
+ } else {
+ diffs.splice( pointer - countDelete - countInsert,
+ countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] );
+ }
+ pointer = pointer - countDelete - countInsert +
+ (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1;
+ } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {
+ // Merge this equality with the previous one.
+ diffs[pointer - 1][1] += diffs[pointer][1];
+ diffs.splice(pointer, 1);
+ } else {
+ pointer++;
+ }
+ countInsert = 0;
+ countDelete = 0;
+ textDelete = "";
+ textInsert = "";
+ break;
+ }
+ }
+ if (diffs[diffs.length - 1][1] === "") {
+ diffs.pop(); // Remove the dummy entry at the end.
+ }
+
+ // Second pass: look for single edits surrounded on both sides by equalities
+ // which can be shifted sideways to eliminate an equality.
+ // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
+ changes = false;
+ pointer = 1;
+ // Intentionally ignore the first and last element (don't need checking).
+ while (pointer < diffs.length - 1) {
+ if (diffs[pointer - 1][0] === DIFF_EQUAL &&
+ diffs[pointer + 1][0] === DIFF_EQUAL) {
+ // This is a single edit surrounded by equalities.
+ if ( diffs[ pointer ][ 1 ].substring( diffs[ pointer ][ 1 ].length -
+ diffs[ pointer - 1 ][ 1 ].length ) === diffs[ pointer - 1 ][ 1 ] ) {
+ // Shift the edit over the previous equality.
+ diffs[pointer][1] = diffs[pointer - 1][1] +
+ diffs[pointer][1].substring(0, diffs[pointer][1].length -
+ diffs[pointer - 1][1].length);
+ diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
+ diffs.splice(pointer - 1, 1);
+ changes = true;
+ } else if ( diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer + 1 ][ 1 ].length ) ===
+ diffs[ pointer + 1 ][ 1 ] ) {
+ // Shift the edit over the next equality.
+ diffs[pointer - 1][1] += diffs[pointer + 1][1];
+ diffs[pointer][1] =
+ diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
+ diffs[pointer + 1][1];
+ diffs.splice(pointer + 1, 1);
+ changes = true;
+ }
+ }
+ pointer++;
}
-
- return (function (a, b) {
- if (a === b) {
- return true; // catch the most you can
- } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) {
- return false; // don't lose time with error prone cases
- } else {
- return bindCallbacks(a, callbacks, [b, a]);
- }
-
- // apply transition with (1..n) arguments
- })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
+ // If shifts were made, the diff needs reordering and another shift sweep.
+ if (changes) {
+ this.diffCleanupMerge(diffs);
+ }
+ };
+
+ return function(o, n) {
+ var diff, output, text;
+ diff = new DiffMatchPatch();
+ output = diff.DiffMain(o, n);
+ //console.log(output);
+ diff.diffCleanupEfficiency(output);
+ text = diff.diffPrettyHtml(output);
+
+ return text;
};
-
- return innerEquiv;
-
-}();
+}());
+// jscs:enable
+
+(function() {
+
+// Deprecated QUnit.init - Ref #530
+// Re-initialize the configuration options
+QUnit.init = function() {
+ var tests, banner, result, qunit,
+ config = QUnit.config;
+
+ config.stats = { all: 0, bad: 0 };
+ config.moduleStats = { all: 0, bad: 0 };
+ config.started = 0;
+ config.updateRate = 1000;
+ config.blocking = false;
+ config.autostart = true;
+ config.autorun = false;
+ config.filter = "";
+ config.queue = [];
+
+ // Return on non-browser environments
+ // This is necessary to not break on node tests
+ if ( typeof window === "undefined" ) {
+ return;
+ }
+
+ qunit = id( "qunit" );
+ if ( qunit ) {
+ qunit.innerHTML =
+ "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
+ "<h2 id='qunit-banner'></h2>" +
+ "<div id='qunit-testrunner-toolbar'></div>" +
+ "<h2 id='qunit-userAgent'></h2>" +
+ "<ol id='qunit-tests'></ol>";
+ }
+
+ tests = id( "qunit-tests" );
+ banner = id( "qunit-banner" );
+ result = id( "qunit-testresult" );
+
+ if ( tests ) {
+ tests.innerHTML = "";
+ }
+
+ if ( banner ) {
+ banner.className = "";
+ }
+
+ if ( result ) {
+ result.parentNode.removeChild( result );
+ }
+
+ if ( tests ) {
+ result = document.createElement( "p" );
+ result.id = "qunit-testresult";
+ result.className = "result";
+ tests.parentNode.insertBefore( result, tests );
+ result.innerHTML = "Running...<br /> ";
+ }
+};
+
+// Don't load the HTML Reporter on non-Browser environments
+if ( typeof window === "undefined" ) {
+ return;
+}
+
+var config = QUnit.config,
+ hasOwn = Object.prototype.hasOwnProperty,
+ defined = {
+ document: window.document !== undefined,
+ sessionStorage: (function() {
+ var x = "qunit-test-string";
+ try {
+ sessionStorage.setItem( x, x );
+ sessionStorage.removeItem( x );
+ return true;
+ } catch ( e ) {
+ return false;
+ }
+ }())
+ },
+ modulesList = [];
+
+/**
+* Escape text for attribute or text content.
+*/
+function escapeText( s ) {
+ if ( !s ) {
+ return "";
+ }
+ s = s + "";
+
+ // Both single quotes and double quotes (for attributes)
+ return s.replace( /['"<>&]/g, function( s ) {
+ switch ( s ) {
+ case "'":
+ return "'";
+ case "\"":
+ return """;
+ case "<":
+ return "<";
+ case ">":
+ return ">";
+ case "&":
+ return "&";
+ }
+ });
+}
+
+/**
+ * @param {HTMLElement} elem
+ * @param {string} type
+ * @param {Function} fn
+ */
+function addEvent( elem, type, fn ) {
+ if ( elem.addEventListener ) {
+
+ // Standards-based browsers
+ elem.addEventListener( type, fn, false );
+ } else if ( elem.attachEvent ) {
+
+ // support: IE <9
+ elem.attachEvent( "on" + type, function() {
+ var event = window.event;
+ if ( !event.target ) {
+ event.target = event.srcElement || document;
+ }
+
+ fn.call( elem, event );
+ });
+ }
+}
/**
- * jsDump
- * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
- * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
- * Date: 5/15/2008
- * @projectDescription Advanced and extensible data dumping for Javascript.
- * @version 1.0.0
- * @author Ariel Flesler
- * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
+ * @param {Array|NodeList} elems
+ * @param {string} type
+ * @param {Function} fn
*/
-QUnit.jsDump = (function() {
- function quote( str ) {
- return '"' + str.toString().replace(/"/g, '\\"') + '"';
- };
- function literal( o ) {
- return o + '';
- };
- function join( pre, arr, post ) {
- var s = jsDump.separator(),
- base = jsDump.indent(),
- inner = jsDump.indent(1);
- if ( arr.join )
- arr = arr.join( ',' + s + inner );
- if ( !arr )
- return pre + post;
- return [ pre, inner + arr, base + post ].join(s);
- };
- function array( arr ) {
- var i = arr.length, ret = Array(i);
- this.up();
- while ( i-- )
- ret[i] = this.parse( arr[i] );
- this.down();
- return join( '[', ret, ']' );
- };
-
- var reName = /^function (\w+)/;
-
- var jsDump = {
- parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance
- var parser = this.parsers[ type || this.typeOf(obj) ];
- type = typeof parser;
-
- return type == 'function' ? parser.call( this, obj ) :
- type == 'string' ? parser :
- this.parsers.error;
- },
- typeOf:function( obj ) {
- var type;
- if ( obj === null ) {
- type = "null";
- } else if (typeof obj === "undefined") {
- type = "undefined";
- } else if (QUnit.is("RegExp", obj)) {
- type = "regexp";
- } else if (QUnit.is("Date", obj)) {
- type = "date";
- } else if (QUnit.is("Function", obj)) {
- type = "function";
- } else if (obj.setInterval && obj.document && !obj.nodeType) {
- type = "window";
- } else if (obj.nodeType === 9) {
- type = "document";
- } else if (obj.nodeType) {
- type = "node";
- } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) {
- type = "array";
+function addEvents( elems, type, fn ) {
+ var i = elems.length;
+ while ( i-- ) {
+ addEvent( elems[ i ], type, fn );
+ }
+}
+
+function hasClass( elem, name ) {
+ return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0;
+}
+
+function addClass( elem, name ) {
+ if ( !hasClass( elem, name ) ) {
+ elem.className += ( elem.className ? " " : "" ) + name;
+ }
+}
+
+function toggleClass( elem, name ) {
+ if ( hasClass( elem, name ) ) {
+ removeClass( elem, name );
+ } else {
+ addClass( elem, name );
+ }
+}
+
+function removeClass( elem, name ) {
+ var set = " " + elem.className + " ";
+
+ // Class name may appear multiple times
+ while ( set.indexOf( " " + name + " " ) >= 0 ) {
+ set = set.replace( " " + name + " ", " " );
+ }
+
+ // trim for prettiness
+ elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" );
+}
+
+function id( name ) {
+ return defined.document && document.getElementById && document.getElementById( name );
+}
+
+function getUrlConfigHtml() {
+ var i, j, val,
+ escaped, escapedTooltip,
+ selection = false,
+ len = config.urlConfig.length,
+ urlConfigHtml = "";
+
+ for ( i = 0; i < len; i++ ) {
+ val = config.urlConfig[ i ];
+ if ( typeof val === "string" ) {
+ val = {
+ id: val,
+ label: val
+ };
+ }
+
+ escaped = escapeText( val.id );
+ escapedTooltip = escapeText( val.tooltip );
+
+ if ( config[ val.id ] === undefined ) {
+ config[ val.id ] = QUnit.urlParams[ val.id ];
+ }
+
+ if ( !val.value || typeof val.value === "string" ) {
+ urlConfigHtml += "<input id='qunit-urlconfig-" + escaped +
+ "' name='" + escaped + "' type='checkbox'" +
+ ( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) +
+ ( config[ val.id ] ? " checked='checked'" : "" ) +
+ " title='" + escapedTooltip + "' /><label for='qunit-urlconfig-" + escaped +
+ "' title='" + escapedTooltip + "'>" + val.label + "</label>";
+ } else {
+ urlConfigHtml += "<label for='qunit-urlconfig-" + escaped +
+ "' title='" + escapedTooltip + "'>" + val.label +
+ ": </label><select id='qunit-urlconfig-" + escaped +
+ "' name='" + escaped + "' title='" + escapedTooltip + "'><option></option>";
+
+ if ( QUnit.is( "array", val.value ) ) {
+ for ( j = 0; j < val.value.length; j++ ) {
+ escaped = escapeText( val.value[ j ] );
+ urlConfigHtml += "<option value='" + escaped + "'" +
+ ( config[ val.id ] === val.value[ j ] ?
+ ( selection = true ) && " selected='selected'" : "" ) +
+ ">" + escaped + "</option>";
+ }
} else {
- type = typeof obj;
+ for ( j in val.value ) {
+ if ( hasOwn.call( val.value, j ) ) {
+ urlConfigHtml += "<option value='" + escapeText( j ) + "'" +
+ ( config[ val.id ] === j ?
+ ( selection = true ) && " selected='selected'" : "" ) +
+ ">" + escapeText( val.value[ j ] ) + "</option>";
+ }
+ }
+ }
+ if ( config[ val.id ] && !selection ) {
+ escaped = escapeText( config[ val.id ] );
+ urlConfigHtml += "<option value='" + escaped +
+ "' selected='selected' disabled='disabled'>" + escaped + "</option>";
+ }
+ urlConfigHtml += "</select>";
+ }
+ }
+
+ return urlConfigHtml;
+}
+
+// Handle "click" events on toolbar checkboxes and "change" for select menus.
+// Updates the URL with the new state of `config.urlConfig` values.
+function toolbarChanged() {
+ var updatedUrl, value,
+ field = this,
+ params = {};
+
+ // Detect if field is a select menu or a checkbox
+ if ( "selectedIndex" in field ) {
+ value = field.options[ field.selectedIndex ].value || undefined;
+ } else {
+ value = field.checked ? ( field.defaultValue || true ) : undefined;
+ }
+
+ params[ field.name ] = value;
+ updatedUrl = setUrl( params );
+
+ if ( "hidepassed" === field.name && "replaceState" in window.history ) {
+ config[ field.name ] = value || false;
+ if ( value ) {
+ addClass( id( "qunit-tests" ), "hidepass" );
+ } else {
+ removeClass( id( "qunit-tests" ), "hidepass" );
+ }
+
+ // It is not necessary to refresh the whole page
+ window.history.replaceState( null, "", updatedUrl );
+ } else {
+ window.location = updatedUrl;
+ }
+}
+
+function setUrl( params ) {
+ var key,
+ querystring = "?";
+
+ params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params );
+
+ for ( key in params ) {
+ if ( hasOwn.call( params, key ) ) {
+ if ( params[ key ] === undefined ) {
+ continue;
+ }
+ querystring += encodeURIComponent( key );
+ if ( params[ key ] !== true ) {
+ querystring += "=" + encodeURIComponent( params[ key ] );
}
- return type;
- },
- separator:function() {
- return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? ' ' : ' ';
- },
- indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
- if ( !this.multiline )
- return '';
- var chr = this.indentChar;
- if ( this.HTML )
- chr = chr.replace(/\t/g,' ').replace(/ /g,' ');
- return Array( this._depth_ + (extra||0) ).join(chr);
- },
- up:function( a ) {
- this._depth_ += a || 1;
- },
- down:function( a ) {
- this._depth_ -= a || 1;
- },
- setParser:function( name, parser ) {
- this.parsers[name] = parser;
- },
- // The next 3 are exposed so you can use them
- quote:quote,
- literal:literal,
- join:join,
- //
- _depth_: 1,
- // This is the list of parsers, to modify them, use jsDump.setParser
- parsers:{
- window: '[Window]',
- document: '[Document]',
- error:'[ERROR]', //when no parser is found, shouldn't happen
- unknown: '[Unknown]',
- 'null':'null',
- undefined:'undefined',
- 'function':function( fn ) {
- var ret = 'function',
- name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
- if ( name )
- ret += ' ' + name;
- ret += '(';
-
- ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join('');
- return join( ret, this.parse(fn,'functionCode'), '}' );
- },
- array: array,
- nodelist: array,
- arguments: array,
- object:function( map ) {
- var ret = [ ];
- this.up();
- for ( var key in map )
- ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) );
- this.down();
- return join( '{', ret, '}' );
- },
- node:function( node ) {
- var open = this.HTML ? '<' : '<',
- close = this.HTML ? '>' : '>';
-
- var tag = node.nodeName.toLowerCase(),
- ret = open + tag;
-
- for ( var a in this.DOMAttrs ) {
- var val = node[this.DOMAttrs[a]];
- if ( val )
- ret += ' ' + a + '=' + this.parse( val, 'attribute' );
- }
- return ret + close + open + '/' + tag + close;
- },
- functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
- var l = fn.length;
- if ( !l ) return '';
-
- var args = Array(l);
- while ( l-- )
- args[l] = String.fromCharCode(97+l);//97 is 'a'
- return ' ' + args.join(', ') + ' ';
- },
- key:quote, //object calls it internally, the key part of an item in a map
- functionCode:'[code]', //function calls it internally, it's the content of the function
- attribute:quote, //node calls it internally, it's an html attribute value
- string:quote,
- date:quote,
- regexp:literal, //regex
- number:literal,
- 'boolean':literal
- },
- DOMAttrs:{//attributes to dump from nodes, name=>realName
- id:'id',
- name:'name',
- 'class':'className'
- },
- HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
- indentChar:' ',//indentation unit
- multiline:false //if true, items in a collection, are separated by a \n, else just a space.
- };
-
- return jsDump;
+ querystring += "&";
+ }
+ }
+ return location.protocol + "//" + location.host +
+ location.pathname + querystring.slice( 0, -1 );
+}
+
+function applyUrlParams() {
+ var selectedModule,
+ modulesList = id( "qunit-modulefilter" ),
+ filter = id( "qunit-filter-input" ).value;
+
+ selectedModule = modulesList ?
+ decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) :
+ undefined;
+
+ window.location = setUrl({
+ module: ( selectedModule === "" ) ? undefined : selectedModule,
+ filter: ( filter === "" ) ? undefined : filter,
+
+ // Remove testId filter
+ testId: undefined
+ });
+}
+
+function toolbarUrlConfigContainer() {
+ var urlConfigContainer = document.createElement( "span" );
+
+ urlConfigContainer.innerHTML = getUrlConfigHtml();
+ addClass( urlConfigContainer, "qunit-url-config" );
+
+ // For oldIE support:
+ // * Add handlers to the individual elements instead of the container
+ // * Use "click" instead of "change" for checkboxes
+ addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged );
+ addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged );
+
+ return urlConfigContainer;
+}
+
+function toolbarLooseFilter() {
+ var filter = document.createElement( "form" ),
+ label = document.createElement( "label" ),
+ input = document.createElement( "input" ),
+ button = document.createElement( "button" );
+
+ addClass( filter, "qunit-filter" );
+
+ label.innerHTML = "Filter: ";
+
+ input.type = "text";
+ input.value = config.filter || "";
+ input.name = "filter";
+ input.id = "qunit-filter-input";
+
+ button.innerHTML = "Go";
+
+ label.appendChild( input );
+
+ filter.appendChild( label );
+ filter.appendChild( button );
+ addEvent( filter, "submit", function( ev ) {
+ applyUrlParams();
+
+ if ( ev && ev.preventDefault ) {
+ ev.preventDefault();
+ }
+
+ return false;
+ });
+
+ return filter;
+}
+
+function toolbarModuleFilterHtml() {
+ var i,
+ moduleFilterHtml = "";
+
+ if ( !modulesList.length ) {
+ return false;
+ }
+
+ modulesList.sort(function( a, b ) {
+ return a.localeCompare( b );
+ });
+
+ moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label>" +
+ "<select id='qunit-modulefilter' name='modulefilter'><option value='' " +
+ ( QUnit.urlParams.module === undefined ? "selected='selected'" : "" ) +
+ ">< All Modules ></option>";
+
+ for ( i = 0; i < modulesList.length; i++ ) {
+ moduleFilterHtml += "<option value='" +
+ escapeText( encodeURIComponent( modulesList[ i ] ) ) + "' " +
+ ( QUnit.urlParams.module === modulesList[ i ] ? "selected='selected'" : "" ) +
+ ">" + escapeText( modulesList[ i ] ) + "</option>";
+ }
+ moduleFilterHtml += "</select>";
+
+ return moduleFilterHtml;
+}
+
+function toolbarModuleFilter() {
+ var toolbar = id( "qunit-testrunner-toolbar" ),
+ moduleFilter = document.createElement( "span" ),
+ moduleFilterHtml = toolbarModuleFilterHtml();
+
+ if ( !toolbar || !moduleFilterHtml ) {
+ return false;
+ }
+
+ moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
+ moduleFilter.innerHTML = moduleFilterHtml;
+
+ addEvent( moduleFilter.lastChild, "change", applyUrlParams );
+
+ toolbar.appendChild( moduleFilter );
+}
+
+function appendToolbar() {
+ var toolbar = id( "qunit-testrunner-toolbar" );
+
+ if ( toolbar ) {
+ toolbar.appendChild( toolbarUrlConfigContainer() );
+ toolbar.appendChild( toolbarLooseFilter() );
+ }
+}
+
+function appendHeader() {
+ var header = id( "qunit-header" );
+
+ if ( header ) {
+ header.innerHTML = "<a href='" +
+ setUrl({ filter: undefined, module: undefined, testId: undefined }) +
+ "'>" + header.innerHTML + "</a> ";
+ }
+}
+
+function appendBanner() {
+ var banner = id( "qunit-banner" );
+
+ if ( banner ) {
+ banner.className = "";
+ }
+}
+
+function appendTestResults() {
+ var tests = id( "qunit-tests" ),
+ result = id( "qunit-testresult" );
+
+ if ( result ) {
+ result.parentNode.removeChild( result );
+ }
+
+ if ( tests ) {
+ tests.innerHTML = "";
+ result = document.createElement( "p" );
+ result.id = "qunit-testresult";
+ result.className = "result";
+ tests.parentNode.insertBefore( result, tests );
+ result.innerHTML = "Running...<br /> ";
+ }
+}
+
+function storeFixture() {
+ var fixture = id( "qunit-fixture" );
+ if ( fixture ) {
+ config.fixture = fixture.innerHTML;
+ }
+}
+
+function appendUserAgent() {
+ var userAgent = id( "qunit-userAgent" );
+
+ if ( userAgent ) {
+ userAgent.innerHTML = "";
+ userAgent.appendChild(
+ document.createTextNode(
+ "QUnit " + QUnit.version + "; " + navigator.userAgent
+ )
+ );
+ }
+}
+
+function appendTestsList( modules ) {
+ var i, l, x, z, test, moduleObj;
+
+ for ( i = 0, l = modules.length; i < l; i++ ) {
+ moduleObj = modules[ i ];
+
+ if ( moduleObj.name ) {
+ modulesList.push( moduleObj.name );
+ }
+
+ for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) {
+ test = moduleObj.tests[ x ];
+
+ appendTest( test.name, test.testId, moduleObj.name );
+ }
+ }
+}
+
+function appendTest( name, testId, moduleName ) {
+ var title, rerunTrigger, testBlock, assertList,
+ tests = id( "qunit-tests" );
+
+ if ( !tests ) {
+ return;
+ }
+
+ title = document.createElement( "strong" );
+ title.innerHTML = getNameHtml( name, moduleName );
+
+ rerunTrigger = document.createElement( "a" );
+ rerunTrigger.innerHTML = "Rerun";
+ rerunTrigger.href = setUrl({ testId: testId });
+
+ testBlock = document.createElement( "li" );
+ testBlock.appendChild( title );
+ testBlock.appendChild( rerunTrigger );
+ testBlock.id = "qunit-test-output-" + testId;
+
+ assertList = document.createElement( "ol" );
+ assertList.className = "qunit-assert-list";
+
+ testBlock.appendChild( assertList );
+
+ tests.appendChild( testBlock );
+}
+
+// HTML Reporter initialization and load
+QUnit.begin(function( details ) {
+ var qunit = id( "qunit" );
+
+ // Fixture is the only one necessary to run without the #qunit element
+ storeFixture();
+
+ if ( qunit ) {
+ qunit.innerHTML =
+ "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
+ "<h2 id='qunit-banner'></h2>" +
+ "<div id='qunit-testrunner-toolbar'></div>" +
+ "<h2 id='qunit-userAgent'></h2>" +
+ "<ol id='qunit-tests'></ol>";
+ }
+
+ appendHeader();
+ appendBanner();
+ appendTestResults();
+ appendUserAgent();
+ appendToolbar();
+ appendTestsList( details.modules );
+ toolbarModuleFilter();
+
+ if ( qunit && config.hidepassed ) {
+ addClass( qunit.lastChild, "hidepass" );
+ }
+});
+
+QUnit.done(function( details ) {
+ var i, key,
+ banner = id( "qunit-banner" ),
+ tests = id( "qunit-tests" ),
+ html = [
+ "Tests completed in ",
+ details.runtime,
+ " milliseconds.<br />",
+ "<span class='passed'>",
+ details.passed,
+ "</span> assertions of <span class='total'>",
+ details.total,
+ "</span> passed, <span class='failed'>",
+ details.failed,
+ "</span> failed."
+ ].join( "" );
+
+ if ( banner ) {
+ banner.className = details.failed ? "qunit-fail" : "qunit-pass";
+ }
+
+ if ( tests ) {
+ id( "qunit-testresult" ).innerHTML = html;
+ }
+
+ if ( config.altertitle && defined.document && document.title ) {
+
+ // show ✖ for good, ✔ for bad suite result in title
+ // use escape sequences in case file gets loaded with non-utf-8-charset
+ document.title = [
+ ( details.failed ? "\u2716" : "\u2714" ),
+ document.title.replace( /^[\u2714\u2716] /i, "" )
+ ].join( " " );
+ }
+
+ // clear own sessionStorage items if all tests passed
+ if ( config.reorder && defined.sessionStorage && details.failed === 0 ) {
+ for ( i = 0; i < sessionStorage.length; i++ ) {
+ key = sessionStorage.key( i++ );
+ if ( key.indexOf( "qunit-test-" ) === 0 ) {
+ sessionStorage.removeItem( key );
+ }
+ }
+ }
+
+ // scroll back to top to show results
+ if ( config.scrolltop && window.scrollTo ) {
+ window.scrollTo( 0, 0 );
+ }
+});
+
+function getNameHtml( name, module ) {
+ var nameHtml = "";
+
+ if ( module ) {
+ nameHtml = "<span class='module-name'>" + escapeText( module ) + "</span>: ";
+ }
+
+ nameHtml += "<span class='test-name'>" + escapeText( name ) + "</span>";
+
+ return nameHtml;
+}
+
+QUnit.testStart(function( details ) {
+ var running, testBlock, bad;
+
+ testBlock = id( "qunit-test-output-" + details.testId );
+ if ( testBlock ) {
+ testBlock.className = "running";
+ } else {
+
+ // Report later registered tests
+ appendTest( details.name, details.testId, details.module );
+ }
+
+ running = id( "qunit-testresult" );
+ if ( running ) {
+ bad = QUnit.config.reorder && defined.sessionStorage &&
+ +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name );
+
+ running.innerHTML = ( bad ?
+ "Rerunning previously failed test: <br />" :
+ "Running: <br />" ) +
+ getNameHtml( details.name, details.module );
+ }
+
+});
+
+QUnit.log(function( details ) {
+ var assertList, assertLi,
+ message, expected, actual,
+ testItem = id( "qunit-test-output-" + details.testId );
+
+ if ( !testItem ) {
+ return;
+ }
+
+ message = escapeText( details.message ) || ( details.result ? "okay" : "failed" );
+ message = "<span class='test-message'>" + message + "</span>";
+ message += "<span class='runtime'>@ " + details.runtime + " ms</span>";
+
+ // pushFailure doesn't provide details.expected
+ // when it calls, it's implicit to also not show expected and diff stuff
+ // Also, we need to check details.expected existence, as it can exist and be undefined
+ if ( !details.result && hasOwn.call( details, "expected" ) ) {
+ expected = escapeText( QUnit.dump.parse( details.expected ) );
+ actual = escapeText( QUnit.dump.parse( details.actual ) );
+ message += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" +
+ expected +
+ "</pre></td></tr>";
+
+ if ( actual !== expected ) {
+ message += "<tr class='test-actual'><th>Result: </th><td><pre>" +
+ actual + "</pre></td></tr>" +
+ "<tr class='test-diff'><th>Diff: </th><td><pre>" +
+ QUnit.diff( expected, actual ) + "</pre></td></tr>";
+ } else {
+ if ( expected.indexOf( "[object Array]" ) !== -1 ||
+ expected.indexOf( "[object Object]" ) !== -1 ) {
+ message += "<tr class='test-message'><th>Message: </th><td>" +
+ "Diff suppressed as the depth of object is more than current max depth (" +
+ QUnit.config.maxDepth + ").<p>Hint: Use <code>QUnit.dump.maxDepth</code> to " +
+ " run with a higher max depth or <a href='" + setUrl({ maxDepth: -1 }) + "'>" +
+ "Rerun</a> without max depth.</p></td></tr>";
+ }
+ }
+
+ if ( details.source ) {
+ message += "<tr class='test-source'><th>Source: </th><td><pre>" +
+ escapeText( details.source ) + "</pre></td></tr>";
+ }
+
+ message += "</table>";
+
+ // this occours when pushFailure is set and we have an extracted stack trace
+ } else if ( !details.result && details.source ) {
+ message += "<table>" +
+ "<tr class='test-source'><th>Source: </th><td><pre>" +
+ escapeText( details.source ) + "</pre></td></tr>" +
+ "</table>";
+ }
+
+ assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
+
+ assertLi = document.createElement( "li" );
+ assertLi.className = details.result ? "pass" : "fail";
+ assertLi.innerHTML = message;
+ assertList.appendChild( assertLi );
+});
+
+QUnit.testDone(function( details ) {
+ var testTitle, time, testItem, assertList,
+ good, bad, testCounts, skipped,
+ tests = id( "qunit-tests" );
+
+ if ( !tests ) {
+ return;
+ }
+
+ testItem = id( "qunit-test-output-" + details.testId );
+
+ assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
+
+ good = details.passed;
+ bad = details.failed;
+
+ // store result when possible
+ if ( config.reorder && defined.sessionStorage ) {
+ if ( bad ) {
+ sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad );
+ } else {
+ sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name );
+ }
+ }
+
+ if ( bad === 0 ) {
+ addClass( assertList, "qunit-collapsed" );
+ }
+
+ // testItem.firstChild is the test name
+ testTitle = testItem.firstChild;
+
+ testCounts = bad ?
+ "<b class='failed'>" + bad + "</b>, " + "<b class='passed'>" + good + "</b>, " :
+ "";
+
+ testTitle.innerHTML += " <b class='counts'>(" + testCounts +
+ details.assertions.length + ")</b>";
+
+ if ( details.skipped ) {
+ testItem.className = "skipped";
+ skipped = document.createElement( "em" );
+ skipped.className = "qunit-skipped-label";
+ skipped.innerHTML = "skipped";
+ testItem.insertBefore( skipped, testTitle );
+ } else {
+ addEvent( testTitle, "click", function() {
+ toggleClass( assertList, "qunit-collapsed" );
+ });
+
+ testItem.className = bad ? "fail" : "pass";
+
+ time = document.createElement( "span" );
+ time.className = "runtime";
+ time.innerHTML = details.runtime + " ms";
+ testItem.insertBefore( time, assertList );
+ }
+});
+
+if ( defined.document ) {
+ if ( document.readyState === "complete" ) {
+ QUnit.load();
+ } else {
+ addEvent( window, "load", QUnit.load );
+ }
+} else {
+ config.pageLoaded = true;
+ config.autorun = true;
+}
+
})();
-
-})(this);
--- a/devtools/devctl.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/devctl.py Tue Jul 19 16:13:12 2016 +0200
@@ -73,10 +73,12 @@
return None
def init_log(self):
pass
- def load_configuration(self):
+ def load_configuration(self, **kw):
pass
def default_log_file(self):
return None
+ def default_stats_file(self):
+ return None
def cleanup_sys_modules(config):
@@ -580,8 +582,8 @@
# 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 this program. If not, see <http://www.gnu.org/licenses/>.
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
''',
'GPL': '''\
@@ -592,7 +594,8 @@
#
# This program 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 General Public License for more details.
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
@@ -834,21 +837,11 @@
p.wait()
-class GenerateQUnitHTML(Command):
- """Generate a QUnit html file to see test in your browser"""
- name = "qunit-html"
- arguments = '<test file> [<dependancy js file>...]'
-
- def run(self, args):
- from cubicweb.devtools.qunit import make_qunit_html
- print make_qunit_html(args[0], args[1:])
-
for cmdcls in (UpdateCubicWebCatalogCommand,
UpdateCubeCatalogCommand,
#LiveServerCommand,
NewCubeCommand,
ExamineLogCommand,
GenerateSchema,
- GenerateQUnitHTML,
):
CWCTL.register(cmdcls)
--- a/devtools/fake.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/fake.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2015 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,8 @@
__docformat__ = "restructuredtext en"
+from contextlib import contextmanager
+
from logilab.database import get_db_helper
from cubicweb.req import RequestSessionBase
@@ -159,6 +161,10 @@
# for use with enabled_security context manager
read_security = write_security = True
+ @contextmanager
+ def running_hooks_ops(self):
+ yield
+
class FakeRepo(object):
querier = None
def __init__(self, schema, vreg=None, config=None):
@@ -173,7 +179,7 @@
def internal_session(self):
return FakeSession(self)
- def extid2eid(self, source, extid, etype, session, insert=True):
+ def extid2eid(self, source, extid, etype, cnx, insert=True):
try:
return self.extids[extid]
except KeyError:
@@ -181,10 +187,10 @@
return None
self._count += 1
eid = self._count
- entity = source.before_entity_insertion(session, extid, etype, eid)
+ entity = source.before_entity_insertion(cnx, extid, etype, eid)
self.extids[extid] = eid
self.eids[eid] = extid
- source.after_entity_insertion(session, extid, entity)
+ source.after_entity_insertion(cnx, extid, entity)
return eid
--- a/devtools/httptest.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/httptest.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -78,8 +78,6 @@
self.global_set_option('port', port) # force rewrite here
return 'http://127.0.0.1:%d/' % self['port']
- def pyro_enabled(self):
- return False
class CubicWebServerTC(CubicWebTC):
@@ -139,7 +137,6 @@
passwd = self.admpassword
if passwd is None:
passwd = user
- self.login(user)
response = self.web_get("login?__login=%s&__password=%s" %
(user, passwd))
assert response.status == httplib.SEE_OTHER, response.status
--- a/devtools/qunit.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/qunit.py Tue Jul 19 16:13:12 2016 +0200
@@ -29,7 +29,9 @@
from logilab.common.shellutils import getlogin
import cubicweb
+from cubicweb.view import View
from cubicweb.web.controller import Controller
+from cubicweb.web.views.staticcontrollers import StaticFileController, STATIC_CONTROLLERS
from cubicweb.devtools.httptest import CubicWebServerTC
@@ -66,7 +68,7 @@
self.firefox_cmd = ['firefox', '-no-remote']
if os.name == 'posix':
self.firefox_cmd = [osp.join(osp.dirname(__file__), 'data', 'xvfb-run.sh'),
- '-a', '-s', '-noreset -screen 0 640x480x8'] + self.firefox_cmd
+ '-a', '-s', '-noreset -screen 0 800x600x24'] + self.firefox_cmd
def start(self, url):
self.stop()
@@ -102,11 +104,14 @@
test_queue = self.test_queue
self._qunit_controller = MyQUnitResultController
self.vreg.register(MyQUnitResultController)
+ self.vreg.register(QUnitView)
+ self.vreg.register(CWSoftwareRootStaticController)
def tearDown(self):
super(QUnitTestCase, self).tearDown()
self.vreg.unregister(self._qunit_controller)
-
+ self.vreg.unregister(QUnitView)
+ self.vreg.unregister(CWSoftwareRootStaticController)
def abspath(self, path):
"""use self.__module__ to build absolute path if necessary"""
@@ -130,35 +135,21 @@
yield js_test
@with_tempdir
- def _test_qunit(self, test_file, depends=(), data_files=(), timeout=30):
+ def _test_qunit(self, test_file, depends=(), data_files=(), timeout=10):
assert osp.exists(test_file), test_file
for dep in depends:
assert osp.exists(dep), dep
for data in data_files:
assert osp.exists(data), data
- # generate html test file
- jquery_dir = 'file://' + self.config.locate_resource('jquery.js')[0]
- html_test_file = NamedTemporaryFile(suffix='.html', delete=False)
- html_test_file.write(make_qunit_html(test_file, depends,
- base_url=self.config['base-url'],
- web_data_path=jquery_dir))
- html_test_file.flush()
- # copying data file
- for data in data_files:
- copyfile(data, tempfile.tempdir)
+ QUnitView.test_file = test_file
+ QUnitView.depends = depends
while not self.test_queue.empty():
self.test_queue.get(False)
browser = FirefoxHelper()
- # start firefox once to let it init the profile (and run system-wide
- # add-ons post setup, blegh), and then kill it ...
- browser.start('about:blank')
- import time; time.sleep(5)
- browser.stop()
- # ... then actually run the test file
- browser.start(html_test_file.name)
+ browser.start(self.config['base-url'] + "?vid=qunit")
test_count = 0
error = False
def raise_exception(cls, *data):
@@ -220,100 +211,114 @@
def handle_log(self):
result = self._cw.form['result']
- message = self._cw.form['message']
- self._log_stack.append('%s: %s' % (result, message))
+ message = self._cw.form.get('message', '<no message>')
+ actual = self._cw.form.get('actual')
+ expected = self._cw.form.get('expected')
+ source = self._cw.form.get('source')
+ log = '%s: %s' % (result, message)
+ if result == 'false' and actual is not None and expected is not None:
+ log += ' (got: %s, expected: %s)' % (actual, expected)
+ if source is not None:
+ log += '\n' + source
+ self._log_stack.append(log)
-def cw_path(*paths):
- return file_path(osp.join(cubicweb.CW_SOFTWARE_ROOT, *paths))
-
-def file_path(path):
- return 'file://' + osp.abspath(path)
+class QUnitView(View):
+ __regid__ = 'qunit'
-def build_js_script(host):
- return """
- var host = '%s';
+ templatable = False
- QUnit.moduleStart = function (name) {
- jQuery.ajax({
- url: host+'/qunit_result',
- data: {"event": "module_start",
- "name": name},
- async: false});
- }
+ depends = None
+ test_file = None
- QUnit.testDone = function (name, failures, total) {
- jQuery.ajax({
- url: host+'/qunit_result',
- data: {"event": "test_done",
- "name": name,
- "failures": failures,
- "total":total},
- async: false});
- }
+ def call(self, **kwargs):
+ w = self.w
+ req = self._cw
+ data = {
+ 'jquery': req.data_url('jquery.js'),
+ 'web_test': req.build_url('cwsoftwareroot/devtools/data'),
+ }
+ w(u'''<!DOCTYPE html>
+ <html>
+ <head>
+ <meta http-equiv="content-type" content="application/html; charset=UTF-8"/>
+ <!-- JS lib used as testing framework -->
+ <link rel="stylesheet" type="text/css" media="all" href="%(web_test)s/qunit.css" />
+ <script src="%(jquery)s" type="text/javascript"></script>
+ <script src="%(web_test)s/cwmock.js" type="text/javascript"></script>
+ <script src="%(web_test)s/qunit.js" type="text/javascript"></script>'''
+ % data)
+ w(u'<!-- result report tools -->')
+ w(u'<script type="text/javascript">')
+ w(u"var BASE_URL = '%s';" % req.base_url())
+ w(u'''
+ QUnit.moduleStart(function (details) {
+ jQuery.ajax({
+ url: BASE_URL + 'qunit_result',
+ data: {"event": "module_start",
+ "name": details.name},
+ async: false});
+ });
- QUnit.done = function (failures, total) {
- jQuery.ajax({
- url: host+'/qunit_result',
- data: {"event": "done",
- "failures": failures,
- "total":total},
- async: false});
- window.close();
- }
+ QUnit.testDone(function (details) {
+ jQuery.ajax({
+ url: BASE_URL + 'qunit_result',
+ data: {"event": "test_done",
+ "name": details.name,
+ "failures": details.failed,
+ "total": details.total},
+ async: false});
+ });
- QUnit.log = function (result, message) {
- jQuery.ajax({
- url: host+'/qunit_result',
- data: {"event": "log",
- "result": result,
- "message": message},
- async: false});
- }
- """ % host
+ QUnit.done(function (details) {
+ jQuery.ajax({
+ url: BASE_URL + 'qunit_result',
+ data: {"event": "done",
+ "failures": details.failed,
+ "total": details.total},
+ async: false});
+ });
-def make_qunit_html(test_file, depends=(), base_url=None,
- web_data_path=cw_path('web', 'data')):
- """"""
- data = {
- 'web_data': web_data_path,
- 'web_test': cw_path('devtools', 'data'),
- }
+ QUnit.log(function (details) {
+ jQuery.ajax({
+ url: BASE_URL + 'qunit_result',
+ data: {"event": "log",
+ "result": details.result,
+ "actual": details.actual,
+ "expected": details.expected,
+ "source": details.source,
+ "message": details.message},
+ async: false});
+ });''')
+ w(u'</script>')
+ w(u'<!-- Test script dependencies (tested code for example) -->')
- html = ['''<!DOCTYPE html>
-<html>
- <head>
- <meta http-equiv="content-type" content="application/html; charset=UTF-8"/>
- <!-- JS lib used as testing framework -->
- <link rel="stylesheet" type="text/css" media="all" href="%(web_test)s/qunit.css" />
- <script src="%(web_data)s/jquery.js" type="text/javascript"></script>
- <script src="%(web_test)s/cwmock.js" type="text/javascript"></script>
- <script src="%(web_test)s/qunit.js" type="text/javascript"></script>'''
- % data]
- if base_url is not None:
- html.append('<!-- result report tools -->')
- html.append('<script type="text/javascript">')
- html.append(build_js_script(base_url))
- html.append('</script>')
- html.append('<!-- Test script dependencies (tested code for example) -->')
+ prefix = len(cubicweb.CW_SOFTWARE_ROOT) + 1
+ for dep in self.depends:
+ dep = req.build_url('cwsoftwareroot/') + dep[prefix:]
+ w(u' <script src="%s" type="text/javascript"></script>' % dep)
- for dep in depends:
- html.append(' <script src="%s" type="text/javascript"></script>' % file_path(dep))
+ w(u' <!-- Test script itself -->')
+ test_url = req.build_url('cwsoftwareroot/') + self.test_file[prefix:]
+ w(u' <script src="%s" type="text/javascript"></script>' % test_url)
+ w(u''' </head>
+ <body>
+ <div id="qunit-fixture"></div>
+ <div id="qunit"></div>
+ </body>
+ </html>''')
+
- html.append(' <!-- Test script itself -->')
- html.append(' <script src="%s" type="text/javascript"></script>'% (file_path(test_file),))
- html.append(''' </head>
- <body>
- <div id="main">
- </div>
- <h1 id="qunit-header">QUnit example</h1>
- <h2 id="qunit-banner"></h2>
- <h2 id="qunit-userAgent"></h2>
- <ol id="qunit-tests"></ol>
- </body>
-</html>''')
- return u'\n'.join(html)
+class CWSoftwareRootStaticController(StaticFileController):
+ __regid__ = 'cwsoftwareroot'
+ def publish(self, rset=None):
+ staticdir = cubicweb.CW_SOFTWARE_ROOT
+ relpath = self.relpath[len(self.__regid__) + 1:]
+ return self.static_file(osp.join(staticdir, relpath))
+
+
+STATIC_CONTROLLERS.append(CWSoftwareRootStaticController)
if __name__ == '__main__':
--- a/devtools/repotest.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/repotest.py Tue Jul 19 16:13:12 2016 +0200
@@ -259,12 +259,11 @@
def qexecute(self, rql, args=None, build_descr=True):
with self.session.new_cnx() as cnx:
- with cnx.ensure_cnx_set:
- try:
- return self.o.execute(cnx, rql, args, build_descr)
- finally:
- if rql.startswith(('INSERT', 'DELETE', 'SET')):
- cnx.commit()
+ try:
+ return self.o.execute(cnx, rql, args, build_descr)
+ finally:
+ if rql.startswith(('INSERT', 'DELETE', 'SET')):
+ cnx.commit()
class BasePlannerTC(BaseQuerierTC):
--- a/devtools/test/data/cubes/i18ntestcube/views.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/test/data/cubes/i18ntestcube/views.py Tue Jul 19 16:13:12 2016 +0200
@@ -26,9 +26,6 @@
_myafs = MyAFS()
-# XXX useless ASA logilab.common.registry is fixed
-_myafs.__module__ = "cubes.i18ntestcube.views"
-
_myafs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined')
afs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined')
--- a/devtools/test/data/js_examples/test_simple_failure.js Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/test/data/js_examples/test_simple_failure.js Tue Jul 19 16:13:12 2016 +0200
@@ -1,18 +1,18 @@
$(document).ready(function() {
- module("air");
+ QUnit.module("air");
- test("test 1", function() {
- equals(2, 4);
+ QUnit.test("test 1", function (assert) {
+ assert.equal(2, 4);
});
- test("test 2", function() {
- equals('', '45');
- equals('1024', '32');
+ QUnit.test("test 2", function (assert) {
+ assert.equal('', '45');
+ assert.equal('1024', '32');
});
- module("able");
- test("test 3", function() {
- same(1, 1);
+ QUnit.module("able");
+ QUnit.test("test 3", function (assert) {
+ assert.deepEqual(1, 1);
});
});
--- a/devtools/test/data/js_examples/test_simple_success.js Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/test/data/js_examples/test_simple_success.js Tue Jul 19 16:13:12 2016 +0200
@@ -1,17 +1,17 @@
$(document).ready(function() {
- module("air");
+ QUnit.module("air");
- test("test 1", function() {
- equals(2, 2);
+ QUnit.test("test 1", function (assert) {
+ assert.equal(2, 2);
});
- test("test 2", function() {
- equals('45', '45');
+ QUnit.test("test 2", function (assert) {
+ assert.equal('45', '45');
});
- module("able");
- test("test 3", function() {
- same(1, 1);
+ QUnit.module("able");
+ QUnit.test("test 3", function (assert) {
+ assert.deepEqual(1, 1);
});
});
--- a/devtools/test/data/js_examples/test_with_dep.js Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/test/data/js_examples/test_with_dep.js Tue Jul 19 16:13:12 2016 +0200
@@ -1,9 +1,9 @@
$(document).ready(function() {
- module("air");
+ QUnit.module("air");
- test("test 1", function() {
- equals(a, 4);
+ QUnit.test("test 1", function (assert) {
+ assert.equal(a, 4);
});
});
--- a/devtools/test/data/js_examples/test_with_ordered_deps.js Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/test/data/js_examples/test_with_ordered_deps.js Tue Jul 19 16:13:12 2016 +0200
@@ -1,9 +1,9 @@
$(document).ready(function() {
- module("air");
+ QUnit.module("air");
- test("test 1", function() {
- equals(b, 6);
+ QUnit.test("test 1", function (assert) {
+ assert.equal(b, 6);
});
});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,3 @@
+Twisted
+webtest
+cubicweb-person
--- a/devtools/test/unittest_i18n.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/test/unittest_i18n.py Tue Jul 19 16:13:12 2016 +0200
@@ -20,8 +20,9 @@
import os, os.path as osp
import sys
+import subprocess
-from logilab.common.testlib import TestCase, unittest_main
+from unittest import TestCase, main
from cubicweb.cwconfig import CubicWebNoAppConfiguration
@@ -52,28 +53,23 @@
class cubePotGeneratorTC(TestCase):
"""test case for i18n pot file generator"""
- def setUp(self):
- self._CUBES_PATH = CubicWebNoAppConfiguration.CUBES_PATH[:]
- CubicWebNoAppConfiguration.CUBES_PATH.append(osp.join(DATADIR, 'cubes'))
- CubicWebNoAppConfiguration.cls_adjust_sys_path()
-
- def tearDown(self):
- CubicWebNoAppConfiguration.CUBES_PATH[:] = self._CUBES_PATH
-
def test_i18ncube(self):
- # MUST import here to make, since the import statement fire
- # the cube paths setup (and then must occur after the setUp)
- from cubicweb.devtools.devctl import update_cube_catalogs
+ env = os.environ.copy()
+ env['CW_CUBES_PATH'] = osp.join(DATADIR, 'cubes')
+ if 'PYTHONPATH' in env:
+ env['PYTHONPATH'] += os.pathsep
+ else:
+ env['PYTHONPATH'] = ''
+ env['PYTHONPATH'] += DATADIR
+ cwctl = osp.abspath(osp.join(osp.dirname(__file__), '../../bin/cubicweb-ctl'))
+ with open(os.devnull, 'w') as devnull:
+ subprocess.check_call([sys.executable, cwctl, 'i18ncube', 'i18ntestcube'],
+ env=env, stdout=devnull)
cube = osp.join(DATADIR, 'cubes', 'i18ntestcube')
msgs = load_po(osp.join(cube, 'i18n', 'en.po.ref'))
- update_cube_catalogs(cube)
newmsgs = load_po(osp.join(cube, 'i18n', 'en.po'))
self.assertEqual(msgs, newmsgs)
+
if __name__ == '__main__':
- # XXX dirty hack to make this test runnable using python (works
- # fine with pytest, but not with python directly if this hack is
- # not present)
- # XXX to remove ASA logilab.common is fixed
- sys.path.append('')
- unittest_main()
+ main()
--- a/devtools/test/unittest_testlib.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/test/unittest_testlib.py Tue Jul 19 16:13:12 2016 +0200
@@ -42,7 +42,7 @@
'__maineid': 0,
'__type:0': 'Entity',
'_cw_entity_fields:0': '__type,field',
- '_cw_fields': 'file,encoding',
+ '_cw_fields': 'encoding,file',
'eid': [0],
'encoding': u'utf-8',
'field:0': 'value',
--- a/devtools/testlib.py Tue Jul 19 15:59:02 2016 +0200
+++ b/devtools/testlib.py Tue Jul 19 16:13:12 2016 +0200
@@ -156,30 +156,6 @@
cwconfig.SMTP = MockSMTP
-class TestCaseConnectionProxy(object):
- """thin wrapper around `cubicweb.repoapi.ClientConnection` context-manager
- used in CubicWebTC (cf. `cubicweb.devtools.testlib.CubicWebTC.login` method)
-
- It just proxies to the default connection context manager but
- restores the original connection on exit.
- """
- def __init__(self, testcase, cnx):
- self.testcase = testcase
- self.cnx = cnx
-
- def __getattr__(self, attrname):
- return getattr(self.cnx, attrname)
-
- def __enter__(self):
- # already open
- return self.cnx
-
- def __exit__(self, exctype, exc, tb):
- try:
- return self.cnx.__exit__(exctype, exc, tb)
- finally:
- self.testcase.restore_connection()
-
# Repoaccess utility ###############################################3###########
class RepoAccess(object):
@@ -189,8 +165,7 @@
A repo access can create three type of object:
- .. automethod:: cubicweb.testlib.RepoAccess.repo_cnx
- .. automethod:: cubicweb.testlib.RepoAccess.client_cnx
+ .. automethod:: cubicweb.testlib.RepoAccess.cnx
.. automethod:: cubicweb.testlib.RepoAccess.web_request
The RepoAccess need to be closed to destroy the associated Session.
@@ -225,16 +200,13 @@
return session
@contextmanager
- def repo_cnx(self):
+ def cnx(self):
"""Context manager returning a server side connection for the user"""
with self._session.new_cnx() as cnx:
yield cnx
- @contextmanager
- def client_cnx(self):
- """Context manager returning a client side connection for the user"""
- with repoapi.ClientConnection(self._session) as cnx:
- yield cnx
+ # aliases for bw compat
+ client_cnx = repo_cnx = cnx
@contextmanager
def web_request(self, url=None, headers={}, method='GET', **kwargs):
@@ -247,9 +219,10 @@
"""
req = self.requestcls(self._repo.vreg, url=url, headers=headers,
method=method, form=kwargs)
- clt_cnx = repoapi.ClientConnection(self._session)
- req.set_cnx(clt_cnx)
- with clt_cnx:
+ with self._session.new_cnx() as cnx:
+ if 'ecache' in cnx.transaction_data:
+ del cnx.transaction_data['ecache']
+ req.set_cnx(cnx)
yield req
def close(self):
@@ -261,7 +234,7 @@
@contextmanager
def shell(self):
from cubicweb.server.migractions import ServerMigrationHelper
- with repoapi.ClientConnection(self._session) as cnx:
+ with self._session.new_cnx() as cnx:
mih = ServerMigrationHelper(None, repo=self._repo, cnx=cnx,
interactive=False,
# hack so it don't try to load fs schema
@@ -294,17 +267,12 @@
requestcls = fake.FakeRequest
tags = TestCase.tags | Tags('cubicweb', 'cw_repo')
test_db_id = DEFAULT_EMPTY_DB_ID
- _cnxs = set() # establised connection
- # stay on connection for leak detection purpose
# anonymous is logged by default in cubicweb test cases
anonymous_allowed = True
def __init__(self, *args, **kwargs):
self._admin_session = None
- self._admin_clt_cnx = None
- self._current_session = None
- self._current_clt_cnx = None
self.repo = None
self._open_access = set()
super(CubicWebTC, self).__init__(*args, **kwargs)
@@ -315,6 +283,7 @@
"""provide a new RepoAccess object for a given user
The access is automatically closed at the end of the test."""
+ login = unicode(login)
access = RepoAccess(self.repo, login, self.requestcls)
self._open_access.add(access)
return access
@@ -326,92 +295,11 @@
except BadConnectionId:
continue # already closed
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def set_cnx(self, cnx):
- assert getattr(cnx, '_session', None) is not None
- if cnx is self._admin_clt_cnx:
- self._pop_custom_cnx()
- else:
- self._cnxs.add(cnx) # register the cnx to make sure it is removed
- self._current_session = cnx._session
- self._current_clt_cnx = cnx
-
@property
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def cnx(self):
- # XXX we want to deprecate this
- clt_cnx = self._current_clt_cnx
- if clt_cnx is None:
- clt_cnx = self._admin_clt_cnx
- return clt_cnx
-
- def _close_cnx(self):
- """ensure that all cnx used by a test have been closed"""
- for cnx in list(self._cnxs):
- if cnx._open and not cnx._session.closed:
- cnx.rollback()
- cnx.close()
- self._cnxs.remove(cnx)
-
- @property
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
def session(self):
- """return current server side session"""
- # XXX We want to use a srv_connection instead and deprecate this
- # property
- session = self._current_session
- if session is None:
- session = self._admin_session
- # bypassing all sanity to use the same repo cnx in the session
- #
- # we can't call set_cnx as the Connection is not managed by the
- # session.
- session._Session__threaddata.cnx = self._admin_clt_cnx._cnx
- else:
- session._Session__threaddata.cnx = self.cnx._cnx
- session.set_cnxset()
- return session
-
- @property
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def websession(self):
- return self.session
-
- @property
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def adminsession(self):
- """return current server side session (using default manager account)"""
+ """return admin session"""
return self._admin_session
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def login(self, login, **kwargs):
- """return a connection for the given login/password"""
- __ = kwargs.pop('autoclose', True) # not used anymore
- if login == self.admlogin:
- # undo any previous login, if we're not used as a context manager
- self.restore_connection()
- return self.cnx
- else:
- if not kwargs:
- kwargs['password'] = str(login)
- clt_cnx = repoapi.connect(self.repo, login, **kwargs)
- self.set_cnx(clt_cnx)
- clt_cnx.__enter__()
- return TestCaseConnectionProxy(self, clt_cnx)
-
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def restore_connection(self):
- self._pop_custom_cnx()
-
- def _pop_custom_cnx(self):
- if self._current_clt_cnx is not None:
- if self._current_clt_cnx._open:
- self._current_clt_cnx.close()
- if not self._current_session.closed:
- self.repo.close(self._current_session.sessionid)
- self._current_clt_cnx = None
- self._current_session = None
-
#XXX this doesn't need to a be classmethod anymore
def _init_repo(self):
"""init the repository and connection to it.
@@ -425,62 +313,6 @@
login = unicode(db_handler.config.default_admin_config['login'])
self.admin_access = self.new_access(login)
self._admin_session = self.admin_access._session
- self._admin_clt_cnx = repoapi.ClientConnection(self._admin_session)
- self._cnxs.add(self._admin_clt_cnx)
- self._admin_clt_cnx.__enter__()
-
- # db api ##################################################################
-
- @nocoverage
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def cursor(self, req=None):
- if req is not None:
- return req.cnx
- else:
- return self.cnx
-
- @nocoverage
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def execute(self, rql, args=None, req=None):
- """executes <rql>, builds a resultset, and returns a couple (rset, req)
- where req is a FakeRequest
- """
- req = req or self.request(rql=rql)
- return req.execute(unicode(rql), args)
-
- @nocoverage
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def commit(self):
- try:
- return self.cnx.commit()
- finally:
- self.session.set_cnxset() # ensure cnxset still set after commit
-
- @nocoverage
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def rollback(self):
- try:
- self.cnx.rollback()
- except ProgrammingError:
- pass # connection closed
- finally:
- self.session.set_cnxset() # ensure cnxset still set after commit
-
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def request(self, rollbackfirst=False, url=None, headers={}, **kwargs):
- """return a web ui request"""
- if rollbackfirst:
- self.cnx.rollback()
- req = self.requestcls(self.vreg, url=url, headers=headers, form=kwargs)
- req.set_cnx(self.cnx)
- return req
-
- # server side db api #######################################################
-
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def sexecute(self, rql, args=None):
- self.session.set_cnxset()
- return self.session.execute(rql, args)
# config management ########################################################
@@ -548,15 +380,6 @@
"""return the application schema"""
return self.vreg.schema
- @deprecated('[3.19] explicitly use RepoAccess object in test instead')
- def shell(self):
- """return a shell session object"""
- from cubicweb.server.migractions import ServerMigrationHelper
- return ServerMigrationHelper(None, repo=self.repo, cnx=self.cnx,
- interactive=False,
- # hack so it don't try to load fs schema
- schema=1)
-
def set_option(self, optname, value):
self.config.global_set_option(optname, value)
@@ -577,24 +400,17 @@
self.skipTest('repository is not initialised: %r' % previous_failure)
try:
self._init_repo()
- self.addCleanup(self._close_cnx)
except Exception as ex:
self.__class__._repo_init_failed = ex
raise
self.addCleanup(self._close_access)
self.setup_database()
- self._admin_clt_cnx.commit()
MAILBOX[:] = [] # reset mailbox
def tearDown(self):
# XXX hack until logilab.common.testlib is fixed
- if self._admin_clt_cnx is not None:
- if self._admin_clt_cnx._open:
- self._admin_clt_cnx.close()
- self._admin_clt_cnx = None
if self._admin_session is not None:
- if not self._admin_session.closed:
- self.repo.close(self._admin_session.sessionid)
+ self.repo.close(self._admin_session.sessionid)
self._admin_session = None
while self._cleanups:
cleanup, args, kwargs = self._cleanups.pop(-1)
@@ -634,20 +450,11 @@
def create_user(self, req, login=None, groups=('users',), password=None,
email=None, commit=True, **kwargs):
"""create and return a new user entity"""
- if isinstance(req, basestring):
- warn('[3.12] create_user arguments are now (req, login[, groups, password, commit, **kwargs])',
- DeprecationWarning, stacklevel=2)
- if not isinstance(groups, (tuple, list)):
- password = groups
- groups = login
- elif isinstance(login, tuple):
- groups = login
- login = req
- assert not isinstance(self, type)
- req = self._admin_clt_cnx
if password is None:
- password = login.encode('utf8')
- user = req.create_entity('CWUser', login=unicode(login),
+ password = login
+ if login is not None:
+ login = unicode(login)
+ user = req.create_entity('CWUser', login=login,
upassword=password, **kwargs)
req.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
% ','.join(repr(str(g)) for g in groups),
@@ -918,7 +725,7 @@
if entity_fields:
form[eid_param('_cw_entity_fields', entity.eid)] = ','.join(entity_fields)
if fields:
- form['_cw_fields'] = ','.join(fields)
+ form['_cw_fields'] = ','.join(sorted(fields))
return form
@deprecated('[3.19] use .admin_request_from_url instead')
@@ -1038,8 +845,8 @@
def assertAuthSuccess(self, req, origsession, nbsessions=1):
sh = self.app.session_handler
session = self.app.get_session(req)
- clt_cnx = repoapi.ClientConnection(session)
- req.set_cnx(clt_cnx)
+ cnx = repoapi.Connection(session)
+ req.set_cnx(cnx)
self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
self.assertEqual(session.login, origsession.login)
self.assertEqual(session.anonymous_session, False)
@@ -1212,7 +1019,8 @@
def assertDocTestFile(self, testfile):
# doctest returns tuple (failure_count, test_count)
- result = self.shell().process_script(testfile)
+ with self.admin_access.shell() as mih:
+ result = mih.process_script(testfile)
if result[0] and result[1]:
raise self.failureException("doctest file '%s' failed"
% testfile)
@@ -1325,7 +1133,7 @@
"""this method populates the database with `how_many` entities
of each possible type. It also inserts random relations between them
"""
- with self.admin_access.repo_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
with cnx.security_enabled(read=False, write=False):
self._auto_populate(cnx, how_many)
cnx.commit()
--- a/doc/3.14.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,164 +0,0 @@
-Whats new in CubicWeb 3.14
-==========================
-
-First notice CW 3.14 depends on yams 0.34 (which is incompatible with prior
-cubicweb releases regarding instance re-creation).
-
-API changes
------------
-
-* `Entity.fetch_rql` `restriction` argument has been deprecated and should be
- replaced with a call to the new `Entity.fetch_rqlst` method, get the returned
- value (a rql `Select` node) and use the RQL syntax tree API to include the
- above-mentionned restrictions.
-
- Backward compat is kept with proper warning.
-
-* `Entity.fetch_order` and `Entity.fetch_unrelated_order` class methods have been
- replaced by `Entity.cw_fetch_order` and `Entity.cw_fetch_unrelated_order` with
- a different prototype:
-
- - instead of taking (attr, var) as two string argument, they now take (select,
- attr, var) where select is the rql syntax tree beinx constructed and var the
- variable *node*.
-
- - instead of returning some string to be inserted in the ORDERBY clause, it has
- to modify the syntax tree
-
- Backward compat is kept with proper warning, BESIDE cases below:
-
- - custom order method return **something else the a variable name with or
- without the sorting order** (e.g. cases where you sort on the value of a
- registered procedure as it was done in the tracker for instance). In such
- case, an error is logged telling that this sorting is ignored until API
- upgrade.
-
- - client code use direct access to one of those methods on an entity (no code
- known to do that).
-
-* `Entity._rest_attr_info` class method has been renamed to
- `Entity.cw_rest_attr_info`
-
- No backward compat yet since this is a protected method an no code is known to
- use it outside cubicweb itself.
-
-* `AnyEntity.linked_to` has been removed as part of a refactoring of this
- functionality (link a entity to another one at creation step). It was replaced
- by a `EntityFieldsForm.linked_to` property.
-
- In the same refactoring, `cubicweb.web.formfield.relvoc_linkedto`,
- `cubicweb.web.formfield.relvoc_init` and
- `cubicweb.web.formfield.relvoc_unrelated` were removed and replaced by
- RelationField methods with the same names, that take a form as a parameter.
-
- **No backward compatibility yet**. It's still time to cry for it.
- Cubes known to be affected: tracker, vcsfile, vcreview.
-
-* `CWPermission` entity type and its associated require_permission relation type
- (abstract) and require_group relation definitions have been moved to a new
- `localperms` cube. With this have gone some functions from the
- `cubicweb.schemas` package as well as some views. This makes cubicweb itself
- smaller while you get all the local permissions stuff into a single,
- documented, place.
-
- Backward compat is kept for existing instances, **though you should have
- installed the localperms cubes**. A proper error should be displayed when
- trying to migrate to 3.14 an instance the use `CWPermission` without the new
- cube installed. For new instances / test, you should add a dependancy on the
- new cube in cubes using this feature, along with a dependancy on cubicweb >=
- 3.14.
-
-* jQuery has been updated to 1.6.4 and jquery-tablesorter to 2.0.5. No backward
- compat issue known.
-
-* Table views refactoring : new `RsetTableView` and `EntityTableView`, as well as
- rewritten an enhanced version of `PyValTableView` on the same bases, with logic
- moved to some column renderers and a layout. Those should be well documented
- and deprecates former `TableView`, `EntityAttributesTableView` and `CellView`,
- which are however kept for backward compat, with some warnings that may not be
- very clear unfortunatly (you may see your own table view subclass name here,
- which doesn't make the problem that clear). Notice that `_cw.view('table',
- rset, *kwargs)` will be routed to the new `RsetTableView` or to the old
- `TableView` depending on given extra arguments. See #1986413.
-
-* `display_name` don't call .lower() anymore. This may leads to changes in your
- user interface. Different msgid for upper/lower cases version of entity type
- names, as this is the only proper way to handle this with some languages.
-
-* `IEditControlAdapter` has been deprecated in favor of `EditController`
- overloading, which was made easier by adding dedicated selectors called
- `match_edited_type` and `match_form_id`.
-
-* Pre 3.6 API backward compat has been dropped, though *data* migration
- compatibility has been kept. You may have to fix errors due to old API usage
- for your instance before to be able to run migration, but then you should be
- able to upgrade even a pre 3.6 database.
-
-* Deprecated `cubicweb.web.views.iprogress` in favor of new `iprogress` cube.
-
-* Deprecated `cubicweb.web.views.flot` in favor of new `jqplot` cube.
-
-
-Unintrusive API changes
------------------------
-
-* Refactored properties forms (eg user preferences and site wide properties) as
- well as pagination components to ease overridding.
-
-* New `cubicweb.web.uihelper` module with high-level helpers for uicfg.
-
-* New `anonymized_request` decorator to temporary run stuff as an anonymous
- user, whatever the currently logged in user.
-
-* New 'verbatimattr' attribute view.
-
-* New facet and form widget for Integer used to store binary mask.
-
-* New `js_href` function to generated proper javascript href.
-
-* `match_kwargs` and `match_form_params` selectors both accept a new
- `once_is_enough` argument.
-
-* `printable_value` is now a method of request, and may be given dict of
- formatters to use.
-
-* `[Rset]TableView` allows to set None in 'headers', meaning the label should be
- fetched from the result set as done by default.
-
-* Field vocabulary computation on entity creation now takes `__linkto`
- information into accounet.
-
-* Started a `cubicweb.pylintext` pylint plugin to help pylint analyzing cubes.
-
-
-RQL
----
-
-* Support for HAVING in 'SET' and 'DELETE' queries.
-
-* new `AT_TZ` function to get back a timestamp at a given time-zone.
-
-* new `WEEKDAY` date extraction function
-
-
-User interface changes
-----------------------
-
-* Datafeed source now present an history of the latest import's log, including
- global status and debug/info/warning/error messages issued during
- imports. Import logs older than a configurable amount of time are automatically
- deleted.
-
-* Breadcrumbs component is properly kept when creating an entity with '__linkto'.
-
-* users and groups management now really lead to that (i.e. includes *groups*
- management).
-
-* New 'jsonp' controller with 'jsonexport' and 'ejsonexport' views.
-
-
-Configuration
-------------
-
-* Added option 'resources-concat' to make javascript/css files concatenation
- optional.
--- a/doc/3.15.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-What's new in CubicWeb 3.15?
-============================
-
-New functionnalities
---------------------
-
-* Add Zmq server, based on the cutting edge ZMQ (http://www.zeromq.org/) socket
- library. This allows to access distant instance, in a similar way as Pyro.
-
-* Publish/subscribe mechanism using ZMQ for communication among cubicweb
- instances. The new zmq-address-sub and zmq-address-pub configuration variables
- define where this communication occurs. As of this release this mechanism is
- used for entity cache invalidation.
-
-* Improved WSGI support. While there is still some caveats, most of the code
- which was twisted only is now generic and allows related functionalities to work
- with a WSGI front-end.
-
-* Full undo/transaction support : undo of modification has eventually been
- implemented, and the configuration simplified (basically you activate it or not
- on an instance basis).
-
-* Controlling HTTP status code used is not much more easier :
-
- - `WebRequest` now has a `status_out` attribut to control the response status ;
-
- - most web-side exceptions take an optional ``status`` argument.
-
-API changes
------------
-
-* The base registry implementation has been moved to a new
- `logilab.common.registry` module (see #1916014). This includes code from :
-
- * `cubicweb.vreg` (the whole things that was in there)
- * `cw.appobject` (base selectors and all).
-
- In the process, some renaming was done:
-
- * the top level registry is now `RegistryStore` (was `VRegistry`), but that
- should not impact cubicweb client code ;
-
- * former selectors functions are now known as "predicate", though you still use
- predicates to build an object'selector ;
-
- * for consistency, the `objectify_selector` decoraror has hence be renamed to
- `objectify_predicate` ;
-
- * on the CubicWeb side, the `selectors` module has been renamed to
- `predicates`.
-
- Debugging refactoring dropped the more need for the `lltrace` decorator. There
- should be full backward compat with proper deprecation warnings. Notice the
- `yes` predicate and `objectify_predicate` decorator, as well as the
- `traced_selection` function should now be imported from the
- `logilab.common.registry` module.
-
-* All login forms are now submitted to <app_root>/login. Redirection to requested
- page is now handled by the login controller (it was previously handle by the
- session manager).
-
-* `Publisher.publish` has been renamed to `Publisher.handle_request`. This
- method now contains generic version of logic previously handled by
- Twisted. `Controller.publish` is **not** affected.
-
-Unintrusive API changes
------------------------
-
-* New 'ldapfeed' source type, designed to replace 'ldapuser' source with
- data-feed (i.e. copy based) source ideas.
-
-* New 'zmqrql' source type, similar to 'pyrorql' but using ømq instead of Pyro.
-
-* A new registry called `services` has appeared, where you can register
- server-side `cubicweb.server.Service` child classes. Their `call` method can be
- invoked from a web-side AppObject instance using new `self._cw.call_service`
- method or a server-side one using `self.session.call_service`. This is a new
- way to call server-side methods, much cleaner than monkey patching the
- Repository class, which becomes a deprecated way to perform similar tasks.
-
-* a new `ajax-func` registry now hosts all remote functions (i.e. functions
- callable through the `asyncRemoteExec` JS api). A convenience `ajaxfunc`
- decorator will let you expose your python function easily without all the
- appobject standard boilerplate. Backward compatibility is preserved.
-
-* the 'json' controller is now deprecated in favor of the 'ajax' one.
-
-* `WebRequest.build_url` can now take a __secure__ argument. When True cubicweb
- try to generate an https url.
-
-
-User interface changes
-----------------------
-
-A new 'undohistory' view expose the undoable transactions and give access to undo
-some of them.
--- a/doc/3.16.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-What's new in CubicWeb 3.16?
-============================
-
-New functionalities
---------------------
-
-* Add a new dataimport store (`SQLGenObjectStore`). This store enables a fast
- import of data (entity creation, link creation) in CubicWeb, by directly
- flushing information in SQL. This may only be used with PostgreSQL, as it
- requires the 'COPY FROM' command.
-
-
-API changes
------------
-
-* Orm: `set_attributes` and `set_relations` are unified (and
- deprecated) in favor of `cw_set` that works in all cases.
-
-* db-api/configuration: all the external repository connection information is
- now in an URL (see `#2521848 <http://www.cubicweb.org/2521848>`_),
- allowing to drop specific options of pyro nameserver host, group, etc and fix
- broken `ZMQ <http://www.zeromq.org/>`_ source. Configuration related changes:
-
- * Dropped 'pyro-ns-host', 'pyro-instance-id', 'pyro-ns-group' from the client side
- configuration, in favor of 'repository-uri'. **NO MIGRATION IS DONE**,
- supposing there is no web-only configuration in the wild.
-
- * Stop discovering the connection method through `repo_method` class attribute
- of the configuration, varying according to the configuration class. This is
- a first step on the way to a simpler configuration handling.
-
- DB-API related changes:
-
- * Stop indicating the connection method using `ConnectionProperties`.
-
- * Drop `_cnxtype` attribute from `Connection` and `cnxtype` from
- `Session`. The former is replaced by a `is_repo_in_memory` property
- and the later is totaly useless.
-
- * Turn `repo_connect` into `_repo_connect` to mark it as a private function.
-
- * Deprecate `in_memory_cnx` which becomes useless, use `_repo_connect` instead
- if necessary.
-
-* the "tcp://" uri scheme used for `ZMQ <http://www.zeromq.org/>`_
- communications (in a way reminiscent of Pyro) is now named
- "zmqpickle-tcp://", so as to make room for future zmq-based lightweight
- communications (without python objects pickling).
-
-* Request.base_url gets a `secure=True` optional parameter that yields
- an https url if possible, allowing hook-generated content to send
- secure urls (e.g. when sending mail notifications)
-
-* Dataimport ucsvreader gets a new boolean `ignore_errors`
- parameter.
-
-
-Unintrusive API changes
------------------------
-
-* Drop of `cubicweb.web.uicfg.AutoformSectionRelationTags.bw_tag_map`,
- deprecated since 3.6.
-
-
-User interface changes
-----------------------
-
-* The RQL search bar has now some auto-completion support. It means
- relation types or entity types can be suggested while typing. It is
- an awesome improvement over the current behaviour !
-
-* The `action box` associated with `table` views (from `tableview.py`)
- has been transformed into a nice-looking series of small tabs; it
- means that the possible actions are immediately visible and need not
- be discovered by clicking on an almost invisible icon on the upper
- right.
-
-* The `uicfg` module has moved to web/views/ and ui configuration
- objects are now selectable. This will reduce the amount of
- subclassing and whole methods replacement usually needed to
- customize the ui behaviour in many cases.
-
-* Remove changelog view, as neither cubicweb nor known
- cubes/applications were properly feeding related files.
-
-
-Other changes
--------------
-
-* 'pyrorql' sources will be automatically updated to use an URL to locate the source
- rather than configuration option. 'zmqrql' sources were broken before this change,
- so no upgrade is needed...
-
-* Debugging filters for Hooks and Operations have been added.
-
-* Some cubicweb-ctl commands used to show the output of `msgcat` and
- `msgfmt`; they don't anymore.
--- a/doc/3.17.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-What's new in CubicWeb 3.17?
-============================
-
-New functionalities
---------------------
-
-* add a command to compare db schema and file system schema
- (see `#464991 <http://www.cubicweb.org/464991>`_)
-
-* Add CubicWebRequestBase.content with the content of the HTTP request (see #2742453)
- (see `#2742453 <http://www.cubicweb.org/2742453>`_)
-
-* Add directive bookmark to ReST rendering
- (see `#2545595 <http://www.cubicweb.org/ticket/2545595>`_)
-
-* Allow user defined final type
- (see `#124342 <https://www.logilab.org/ticket/124342>`_)
-
-
-API changes
------------
-
-* drop typed_eid() in favour of int() (see `#2742462 <http://www.cubicweb.org/2742462>`_)
-
-* The SIOC views and adapters have been removed from CubicWeb and moved to the
- `sioc` cube.
-
-* The web page embedding views and adapters have been removed from CubicWeb and
- moved to the `embed` cube.
-
-* The email sending views and controllers have been removed from CubicWeb and
- moved to the `massmailing` cube.
-
-* ``RenderAndSendNotificationView`` is deprecated in favor of
- ``ActualNotificationOp`` the new operation use the more efficient *data*
- idiom.
-
-* Looping task can now have a interval <= ``0``. Negative interval disable the
- looping task entirely.
-
-* We now serve html instead of xhtml.
- (see `#2065651 <http://www.cubicweb.org/ticket/2065651>`_)
-
-
-Deprecation
----------------------
-
-* ``ldapuser`` have been deprecated. It'll be fully dropped in the next
- version. If you are still using ldapuser switch to ``ldapfeed`` **NOW**!
-
-* ``hijack_user`` have been deprecated. It will be dropped soon.
-
-Deprecated Code Drops
-----------------------
-
-* The progress views and adapters have been removed from CubicWeb. These
- classes were deprecated since 3.14.0. They are still available in the
- `iprogress` cube.
-
-* API deprecated since 3.7 have been dropped.
--- a/doc/3.18.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-What's new in CubicWeb 3.18?
-============================
-
-The migration script does not handle sqlite nor mysql instances.
-
-
-New functionalities
---------------------
-
-* add a security debugging tool
- (see `#2920304 <http://www.cubicweb.org/2920304>`_)
-
-* introduce an `add` permission on attributes, to be interpreted at
- entity creation time only and allow the implementation of complex
- `update` rules that don't block entity creation (before that the
- `update` attribute permission was interpreted at entity creation and
- update time)
-
-* the primary view display controller (uicfg) now has a
- `set_fields_order` method similar to the one available for forms
-
-* new method `ResultSet.one(col=0)` to retrive a single entity and enforce the
- result has only one row (see `#3352314 https://www.cubicweb.org/ticket/3352314`_)
-
-* new method `RequestSessionBase.find` to look for entities
- (see `#3361290 https://www.cubicweb.org/ticket/3361290`_)
-
-* the embedded jQuery copy has been updated to version 1.10.2, and jQuery UI to
- version 1.10.3.
-
-* initial support for wsgi for the debug mode, available through the new
- ``wsgi`` cubicweb-ctl command, which can use either python's builtin
- wsgi server or the werkzeug module if present.
-
-* a ``rql-table`` directive is now available in ReST fields
-
-* cubicweb-ctl upgrade can now generate the static data resource directory
- directly, without a manual call to gen-static-datadir.
-
-API changes
------------
-
-* not really an API change, but the entity permission checks are now
- systematically deferred to an operation, instead of a) trying in a
- hook and b) if it failed, retrying later in an operation
-
-* The default value storage for attributes is no longer String, but
- Bytes. This opens the road to storing arbitrary python objects, e.g.
- numpy arrays, and fixes a bug where default values whose truth value
- was False were not properly migrated.
-
-* `symmetric` relations are no more handled by an rql rewrite but are
- now handled with hooks (from the `activeintegrity` category); this
- may have some consequences for applications that do low-level database
- manipulations or at times disable (some) hooks.
-
-* `unique together` constraints (multi-columns unicity constraints)
- get a `name` attribute that maps the CubicWeb contraint entities to
- corresponding backend index.
-
-* BreadCrumbEntityVComponent's open_breadcrumbs method now includes
- the first breadcrumbs separator
-
-* entities can be compared for equality and hashed
-
-* the ``on_fire_transition`` predicate accepts a sequence of possible
- transition names
-
-* the GROUP_CONCAT rql aggregate function no longer repeats duplicate
- values, on the sqlite and postgresql backends
-
-Deprecation
----------------------
-
-* ``pyrorql`` sources have been deprecated. Multisource will be fully dropped
- in the next version. If you are still using pyrorql, switch to ``datafeed``
- **NOW**!
-
-* the old multi-source system
-
-* `find_one_entity` and `find_entities` in favor of `find`
- (see `#3361290 https://www.cubicweb.org/ticket/3361290`_)
-
-* the `TmpFileViewMixin` and `TmpPngView` classes (see `#3400448
- https://www.cubicweb.org/ticket/3400448`_)
-
-Deprecated Code Drops
-----------------------
-
-* ``ldapuser`` have been dropped; use ``ldapfeed`` now
- (see `#2936496 <http://www.cubicweb.org/2936496>`_)
-
-* action ``GotRhythm`` was removed, make sure you do not
- import it in your cubes (even to unregister it)
- (see `#3093362 <http://www.cubicweb.org/3093362>`_)
-
-* all 3.8 backward compat is gone
-
-* all 3.9 backward compat (including the javascript side) is gone
-
-* the ``twisted`` (web-only) instance type has been removed
--- a/doc/3.19.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-What's new in CubicWeb 3.19?
-============================
-
-New functionalities
---------------------
-
-* implement Cross Origin Resource Sharing (CORS)
- (see `#2491768 <http://www.cubicweb.org/2491768>`_)
-
-* system_source.create_eid can get a range of IDs, to reduce overhead of batch
- entity creation
-
-Behaviour Changes
------------------
-
-* The anonymous property of Session and Connection are now computed from the
- related user login. If it matches the ``anonymous-user`` in the config the
- connection is anonymous. Beware that the ``anonymous-user`` config is web
- specific. Therefore, no session may be anonymous in a repository only setup.
-
-
-New Repository Access API
--------------------------
-
-Connection replaces Session
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A new explicit Connection object replaces Session as the main repository entry
-point. Connection holds all the necessary methods to be used server-side
-(``execute``, ``commit``, ``rollback``, ``call_service``, ``entity_from_eid``,
-etc...). One obtains a new Connection object using ``session.new_cnx()``.
-Connection objects need to have an explicit begin and end. Use them as a context
-manager to never miss an end::
-
- with session.new_cnx() as cnx:
- cnx.execute('INSERT Elephant E, E name "Babar"')
- cnx.commit()
- cnx.execute('INSERT Elephant E, E name "Celeste"')
- cnx.commit()
- # Once you get out of the "with" clause, the connection is closed.
-
-Using the same Connection object in multiple threads will give you access to the
-same Transaction. However, Connection objects are not thread safe (hence at your
-own risks).
-
-``repository.internal_session`` is deprecated in favor of
-``repository.internal_cnx``. Note that internal connections are now `safe` by default,
-i.e. the integrity hooks are enabled.
-
-Backward compatibility is preserved on Session.
-
-
-dbapi vs repoapi
-~~~~~~~~~~~~~~~~
-
-A new API has been introduced to replace the dbapi. It is called `repoapi`.
-
-There are three relevant functions for now:
-
-* ``repoapi.get_repository`` returns a Repository object either from an
- URI when used as ``repoapi.get_repository(uri)`` or from a config
- when used as ``repoapi.get_repository(config=config)``.
-
-* ``repoapi.connect(repo, login, **credentials)`` returns a ClientConnection
- associated with the user identified by the credentials. The
- ClientConnection is associated with its own Session that is closed
- when the ClientConnection is closed. A ClientConnection is a
- Connection-like object to be used client side.
-
-* ``repoapi.anonymous_cnx(repo)`` returns a ClientConnection associated
- with the anonymous user if described in the config.
-
-
-repoapi.ClientConnection replace dbapi.Connection and company
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-On the client/web side, the Request is now using a ``repoapi.ClientConnection``
-instead of a ``dbapi.connection``. The ``ClientConnection`` has multiple backward
-compatible methods to make it look like a ``dbapi.Cursor`` and ``dbapi.Connection``.
-
-Session used on the Web side are now the same than the one used Server side.
-Some backward compatibility methods have been installed on the server side Session
-to ease the transition.
-
-The authentication stack has been altered to use the ``repoapi`` instead of
-the ``dbapi``. Cubes adding new element to this stack are likely to break.
-
-Session data can be accessed using the cnx.data dictionary, while
-transaction data is available through cnx.transaction_data. These
-replace the [gs]et_shared_data methods with optional txid kwarg.
-
-New API in tests
-~~~~~~~~~~~~~~~~
-
-All current methods and attributes used to access the repo on ``CubicWebTC`` are
-deprecated. You may now use a ``RepoAccess`` object. A ``RepoAccess`` object is
-linked to a new ``Session`` for a specified user. It is able to create
-``Connection``, ``ClientConnection`` and web side requests linked to this
-session::
-
- access = self.new_access('babar') # create a new RepoAccess for user babar
- with access.repo_cnx() as cnx:
- # some work with server side cnx
- cnx.execute(...)
- cnx.commit()
- cnx.execute(...)
- cnx.commit()
-
- with access.client_cnx() as cnx:
- # some work with client side cnx
- cnx.execute(...)
- cnx.commit()
-
- with access.web_request(elephant='babar') as req:
- # some work with client side cnx
- elephant_name = req.form['elephant']
- req.execute(...)
- req.cnx.commit()
-
-By default ``testcase.admin_access`` contains a ``RepoAccess`` object for the
-default admin session.
-
-
-API changes
------------
-
-* ``RepositorySessionManager.postlogin`` is now called with two arguments,
- request and session. And this now happens before the session is linked to the
- request.
-
-* ``SessionManager`` and ``AuthenticationManager`` now take a repo object at
- initialization time instead of a vreg.
-
-* The ``async`` argument of ``_cw.call_service`` has been dropped. All calls are
- now synchronous. The zmq notification bus looks like a good replacement for
- most async use cases.
-
-* ``repo.stats()`` is now deprecated. The same information is available through
- a service (``_cw.call_service('repo_stats')``).
-
-* ``repo.gc_stats()`` is now deprecated. The same information is available through
- a service (``_cw.call_service('repo_gc_stats')``).
-
-* ``repo.register_user()`` is now deprecated. The functionality is now
- available through a service (``_cw.call_service('register_user')``).
-
-* ``request.set_session`` no longer takes an optional ``user`` argument.
-
-* CubicwebTC does not have repo and cnx as class attributes anymore. They are
- standard instance attributes. ``set_cnx`` and ``_init_repo`` class methods
- become instance methods.
-
-* ``set_cnxset`` and ``free_cnxset`` are deprecated. cnxset are now
- automatically managed.
-
-* The implementation of cascading deletion when deleting `composite`
- entities has changed. There comes a semantic change: merely deleting
- a composite relation does not entail any more the deletion of the
- component side of the relation.
-
-* ``_cw.user_callback`` and ``_cw.user_rql_callback`` are deprecated. Users
- are encouraged to write an actual controller (e.g. using ``ajaxfunc``)
- instead of storing a closure in the session data.
-
-* A new ``entity.cw_linkable_rql`` method provides the rql to fetch all entities
- that are already or may be related to the current entity using the given
- relation.
-
-
-Deprecated Code Drops
-----------------------
-
-* session.hijack_user mechanism has been dropped.
-
-* EtypeRestrictionComponent has been removed, its functionality has been
- replaced by facets a while ago.
-
-* the old multi-source support has been removed. Only copy-based sources
- remain, such as datafeed or ldapfeed.
-
--- a/doc/3.20.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-What's new in CubicWeb 3.20
-===========================
-
-New features
-------------
-
-* virtual relations: a new ComputedRelation class can be used in
- schema.py; its `rule` attribute is an RQL snippet that defines the new
- relation.
-
-* computed attributes: an attribute can now be defined with a `formula`
- argument (also an RQL snippet); it will be read-only, and updated
- automatically.
-
- Both of these features are described in `CWEP-002`_, and the updated
- "Data model" chapter of the CubicWeb book.
-
-* cubicweb-ctl plugins can use the ``cubicweb.utils.admincnx`` function
- to get a Connection object from an instance name.
-
-* new 'tornado' wsgi backend
-
-* session cookies have the HttpOnly flag, so they're no longer exposed to
- javascript
-
-* rich text fields can be formatted as markdown
-
-* the edit controller detects concurrent editions, and raises a ValidationError
- if an entity was modified between form generation and submission
-
-* cubicweb can use a postgresql "schema" (namespace) for its tables
-
-* "cubicweb-ctl configure" can be used to set values of the admin user
- credentials in the sources configuration file
-
-* in debug mode, setting the _cwtracehtml parameter on a request allows tracing
- where each bit of output is produced
-
-.. _CWEP-002: http://hg.logilab.org/review/cwep/file/tip/CWEP-002.rst
-
-
-API Changes
------------
-
-* ``ucsvreader()`` and ``ucsvreader_pb()`` from the ``dataimport`` module have
- 2 new keyword arguments ``delimiter`` and ``quotechar`` to replace the
- ``separator`` and ``quote`` arguments respectively. This makes the API match
- that of Python's ``csv.reader()``. The old arguments are still supported
- though deprecated.
-
-* the migration environment's ``remove_cube`` function is now called ``drop_cube``.
-
-* cubicweb.old.css is now cubicweb.css. The previous "new"
- cubicweb.css, along with its cubicweb.reset.css companion, have been
- removed.
-
-* the jquery-treeview plugin was updated to its latest version
-
-
-Deprecated Code Drops
-----------------------
-
-* most of 3.10 and 3.11 backward compat is gone; this includes:
- - CtxComponent.box_action() and CtxComponent.build_link()
- - cubicweb.devtools.htmlparser.XMLDemotingValidator
- - various methods and properties on Entities, replaced by cw_edited and cw_attr_cache
- - 'commit_event' method on hooks, replaced by 'postcommit_event'
- - server.hook.set_operation(), replaced by Operation.get_instance(...).add_data()
- - View.div_id(), View.div_class() and View.create_url()
- - `*VComponent` classes
- - in forms, Field.value() and Field.help() must take the form and the field itself as arguments
- - form.render() must get `w` as a named argument, and renderer.render() must take `w` as first argument
- - in breadcrumbs, the optional `recurs` argument must be a set, not False
- - cubicweb.web.views.idownloadable.{download_box,IDownloadableLineView}
- - primary views no longer have `render_entity_summary` and `summary` methods
- - WFHistoryVComponent's `cell_call` method is replaced by `render_body`
- - cubicweb.dataimport.ObjectStore.add(), replaced by create_entity
- - ManageView.{folders,display_folders}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/Makefile Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,88 @@
+SRC=.
+
+# You can set these sphinx variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+#BUILDDIR = build
+BUILDDIR = _build
+CWDIR = ..
+JSDIR = ${CWDIR}/web/data
+JSTORST = tools/pyjsrest.py
+BUILDJS = js_api
+
+# Internal variables for sphinx
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d ${BUILDDIR}/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+
+
+.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " all to make standalone HTML files, developer manual and API doc"
+ @echo " html to make standalone HTML files"
+ @echo "--- "
+ @echo " pickle to make pickle files (usable by e.g. sphinx-web)"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " changes to make an overview over all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+
+clean:
+ rm -f *.html
+ -rm -rf ${BUILDDIR}/html ${BUILDDIR}/doctrees
+ -rm -rf ${BUILDJS}
+
+all: html
+
+# run sphinx ###
+html: js
+ mkdir -p ${BUILDDIR}/html ${BUILDDIR}/doctrees
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) ${BUILDDIR}/html
+ @echo
+ @echo "Build finished. The HTML pages are in ${BUILDDIR}/html."
+
+js:
+ mkdir -p ${BUILDJS}
+ $(JSTORST) -p ${JSDIR} -o ${BUILDJS}
+
+pickle:
+ mkdir -p ${BUILDDIR}/pickle ${BUILDDIR}/doctrees
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) ${BUILDDIR}/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files or run"
+ @echo " sphinx-web ${BUILDDIR}/pickle"
+ @echo "to start the sphinx-web server."
+
+web: pickle
+
+htmlhelp:
+ mkdir -p ${BUILDDIR}/htmlhelp ${BUILDDIR}/doctrees
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) ${BUILDDIR}/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in ${BUILDDIR}/htmlhelp."
+
+latex:
+ mkdir -p ${BUILDDIR}/latex ${BUILDDIR}/doctrees
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) ${BUILDDIR}/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in ${BUILDDIR}/latex."
+ @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+ "run these through (pdf)latex."
+
+changes:
+ mkdir -p ${BUILDDIR}/changes ${BUILDDIR}/doctrees
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) ${BUILDDIR}/changes
+ @echo
+ @echo "The overview file is in ${BUILDDIR}/changes."
+
+linkcheck:
+ mkdir -p ${BUILDDIR}/linkcheck ${BUILDDIR}/doctrees
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) ${BUILDDIR}/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in ${BUILDDIR}/linkcheck/output.txt."
Binary file doc/_static/cubicweb.png has changed
Binary file doc/_static/logilab.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_static/sphinx-default.css Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,861 @@
+/**
+ * Sphinx Doc Design
+ */
+
+html, body {
+ background: white;
+}
+
+body {
+ font-family: Verdana, sans-serif;
+ font-size: 100%;
+ background-color: white;
+ color: black;
+ margin: 0;
+ padding: 0;
+}
+
+/* :::: LAYOUT :::: */
+
+div.logilablogo {
+ padding: 10px 10px 10px 10px;
+ height:75;
+}
+
+
+div.document {
+ background-color: white;
+}
+
+div.documentwrapper {
+ float: left;
+ width: 100%;
+}
+
+div.bodywrapper {
+ margin: 0 0 0 230px;
+}
+
+div.body {
+ background-color: white;
+ padding: 0 20px 30px 20px;
+ border-left:solid;
+ border-left-color:#e2e2e2;
+ border-left-width:thin;
+}
+
+div.sphinxsidebarwrapper {
+ padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+ float: left;
+ width: 230px;
+ margin-left: -100%;
+ font-size: 90%;
+}
+
+div.clearer {
+ clear: both;
+}
+
+div.footer {
+ color: #ff4500;
+ width: 100%;
+ padding: 9px 0 9px 0;
+ text-align: center;
+ font-size: 75%;
+}
+
+div.footer a {
+ color: #ff4500;
+ text-decoration: underline;
+}
+
+div.related {
+ background-color: #ff7700;
+ color: white;
+ width: 100%;
+ height: 30px;
+ line-height: 30px;
+ font-size: 90%;
+}
+
+div.related h3 {
+ display: none;
+}
+
+div.related ul {
+ margin: 0;
+ padding: 0 0 0 10px;
+ list-style: none;
+}
+
+div.related li {
+ display: inline;
+}
+
+div.related li.right {
+ float: right;
+ margin-right: 5px;
+}
+
+div.related a {
+ color: white;
+ font-weight:bold;
+}
+
+/* ::: TOC :::: */
+
+div.sphinxsidebar {
+ border-style:solid;
+ border-color: white;
+/* background-color:#e2e2e2;*/
+ padding-bottom:5px;
+}
+
+div.sphinxsidebar h3 {
+ font-family: Verdana, sans-serif;
+ color: black;
+ font-size: 1.2em;
+ font-weight: normal;
+ margin: 0;
+ padding: 0;
+ font-weight:bold;
+ font-style:italic;
+}
+
+div.sphinxsidebar h4 {
+ font-family: Verdana, sans-serif;
+ color: black;
+ font-size: 1.1em;
+ font-weight: normal;
+ margin: 5px 0 0 0;
+ padding: 0;
+ font-weight:bold;
+ font-style:italic;
+}
+
+div.sphinxsidebar p {
+ color: black;
+}
+
+div.sphinxsidebar p.topless {
+ margin: 5px 10px 10px 10px;
+}
+
+div.sphinxsidebar ul {
+ margin: 10px;
+ padding: 0;
+ list-style: none;
+ color: black;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+ margin-left: 20px;
+ list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+div.sphinxsidebar a {
+ color: black;
+ text-decoration: none;
+}
+
+div.sphinxsidebar form {
+ margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+ border: 1px solid #e2e2e2;
+ font-family: sans-serif;
+ font-size: 1em;
+ padding-bottom: 5px;
+}
+
+/* :::: MODULE CLOUD :::: */
+div.modulecloud {
+ margin: -5px 10px 5px 10px;
+ padding: 10px;
+ line-height: 160%;
+ border: 1px solid #cbe7e5;
+ background-color: #f2fbfd;
+}
+
+div.modulecloud a {
+ padding: 0 5px 0 5px;
+}
+
+/* :::: SEARCH :::: */
+ul.search {
+ margin: 10px 0 0 20px;
+ padding: 0;
+}
+
+ul.search li {
+ padding: 5px 0 5px 20px;
+ background-image: url(file.png);
+ background-repeat: no-repeat;
+ background-position: 0 7px;
+}
+
+ul.search li a {
+ font-weight: bold;
+}
+
+ul.search li div.context {
+ color: #888;
+ margin: 2px 0 0 30px;
+ text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+ font-weight: bold;
+}
+
+/* :::: COMMON FORM STYLES :::: */
+
+div.actions {
+ padding: 5px 10px 5px 10px;
+ border-top: 1px solid #cbe7e5;
+ border-bottom: 1px solid #cbe7e5;
+ background-color: #e0f6f4;
+}
+
+form dl {
+ color: #333;
+}
+
+form dt {
+ clear: both;
+ float: left;
+ min-width: 110px;
+ margin-right: 10px;
+ padding-top: 2px;
+}
+
+input#homepage {
+ display: none;
+}
+
+div.error {
+ margin: 5px 20px 0 0;
+ padding: 5px;
+ border: 1px solid #d00;
+ font-weight: bold;
+}
+
+/* :::: INLINE COMMENTS :::: */
+
+div.inlinecomments {
+ position: absolute;
+ right: 20px;
+}
+
+div.inlinecomments a.bubble {
+ display: block;
+ float: right;
+ background-image: url(style/comment.png);
+ background-repeat: no-repeat;
+ width: 25px;
+ height: 25px;
+ text-align: center;
+ padding-top: 3px;
+ font-size: 0.9em;
+ line-height: 14px;
+ font-weight: bold;
+ color: black;
+}
+
+div.inlinecomments a.bubble span {
+ display: none;
+}
+
+div.inlinecomments a.emptybubble {
+ background-image: url(style/nocomment.png);
+}
+
+div.inlinecomments a.bubble:hover {
+ background-image: url(style/hovercomment.png);
+ text-decoration: none;
+ color: #3ca0a4;
+}
+
+div.inlinecomments div.comments {
+ float: right;
+ margin: 25px 5px 0 0;
+ max-width: 50em;
+ min-width: 30em;
+ border: 1px solid #2eabb0;
+ background-color: #f2fbfd;
+ z-index: 150;
+}
+
+div#comments {
+ border: 1px solid #2eabb0;
+ margin-top: 20px;
+}
+
+div#comments div.nocomments {
+ padding: 10px;
+ font-weight: bold;
+}
+
+div.inlinecomments div.comments h3,
+div#comments h3 {
+ margin: 0;
+ padding: 0;
+ background-color: #2eabb0;
+ color: white;
+ border: none;
+ padding: 3px;
+}
+
+div.inlinecomments div.comments div.actions {
+ padding: 4px;
+ margin: 0;
+ border-top: none;
+}
+
+div#comments div.comment {
+ margin: 10px;
+ border: 1px solid #2eabb0;
+}
+
+div.inlinecomments div.comment h4,
+div.commentwindow div.comment h4,
+div#comments div.comment h4 {
+ margin: 10px 0 0 0;
+ background-color: #2eabb0;
+ color: white;
+ border: none;
+ padding: 1px 4px 1px 4px;
+}
+
+div#comments div.comment h4 {
+ margin: 0;
+}
+
+div#comments div.comment h4 a {
+ color: #d5f4f4;
+}
+
+div.inlinecomments div.comment div.text,
+div.commentwindow div.comment div.text,
+div#comments div.comment div.text {
+ margin: -5px 0 -5px 0;
+ padding: 0 10px 0 10px;
+}
+
+div.inlinecomments div.comment div.meta,
+div.commentwindow div.comment div.meta,
+div#comments div.comment div.meta {
+ text-align: right;
+ padding: 2px 10px 2px 0;
+ font-size: 95%;
+ color: #538893;
+ border-top: 1px solid #cbe7e5;
+ background-color: #e0f6f4;
+}
+
+div.commentwindow {
+ position: absolute;
+ width: 500px;
+ border: 1px solid #cbe7e5;
+ background-color: #f2fbfd;
+ display: none;
+ z-index: 130;
+}
+
+div.commentwindow h3 {
+ margin: 0;
+ background-color: #2eabb0;
+ color: white;
+ border: none;
+ padding: 5px;
+ font-size: 1.5em;
+ cursor: pointer;
+}
+
+div.commentwindow div.actions {
+ margin: 10px -10px 0 -10px;
+ padding: 4px 10px 4px 10px;
+ color: #538893;
+}
+
+div.commentwindow div.actions input {
+ border: 1px solid #2eabb0;
+ background-color: white;
+ color: #135355;
+ cursor: pointer;
+}
+
+div.commentwindow div.form {
+ padding: 0 10px 0 10px;
+}
+
+div.commentwindow div.form input,
+div.commentwindow div.form textarea {
+ border: 1px solid #3c9ea2;
+ background-color: white;
+ color: black;
+}
+
+div.commentwindow div.error {
+ margin: 10px 5px 10px 5px;
+ background-color: #fbe5dc;
+ display: none;
+}
+
+div.commentwindow div.form textarea {
+ width: 99%;
+}
+
+div.commentwindow div.preview {
+ margin: 10px 0 10px 0;
+ background-color: #70d0d4;
+ padding: 0 1px 1px 25px;
+}
+
+div.commentwindow div.preview h4 {
+ margin: 0 0 -5px -20px;
+ padding: 4px 0 0 4px;
+ color: white;
+ font-size: 1.3em;
+}
+
+div.commentwindow div.preview div.comment {
+ background-color: #f2fbfd;
+}
+
+div.commentwindow div.preview div.comment h4 {
+ margin: 10px 0 0 0!important;
+ padding: 1px 4px 1px 4px!important;
+ font-size: 1.2em;
+}
+
+/* :::: SUGGEST CHANGES :::: */
+div#suggest-changes-box input, div#suggest-changes-box textarea {
+ border: 1px solid #ccc;
+ background-color: white;
+ color: black;
+}
+
+div#suggest-changes-box textarea {
+ width: 99%;
+ height: 400px;
+}
+
+
+/* :::: PREVIEW :::: */
+div.preview {
+ background-image: url(style/preview.png);
+ padding: 0 20px 20px 20px;
+ margin-bottom: 30px;
+}
+
+
+/* :::: INDEX PAGE :::: */
+
+table.contentstable {
+ width: 90%;
+}
+
+table.contentstable p.biglink {
+ line-height: 150%;
+}
+
+a.biglink {
+ font-size: 1.3em;
+}
+
+span.linkdescr {
+ font-style: italic;
+ padding-top: 5px;
+ font-size: 90%;
+}
+
+/* :::: INDEX STYLES :::: */
+
+table.indextable td {
+ text-align: left;
+ vertical-align: top;
+}
+
+table.indextable dl, table.indextable dd {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+table.indextable tr.pcap {
+ height: 10px;
+}
+
+table.indextable tr.cap {
+ margin-top: 10px;
+ background-color: #f2f2f2;
+}
+
+img.toggler {
+ margin-right: 3px;
+ margin-top: 3px;
+ cursor: pointer;
+}
+
+form.pfform {
+ margin: 10px 0 20px 0;
+}
+
+/* :::: GLOBAL STYLES :::: */
+
+.docwarning {
+ background-color: #ffe4e4;
+ padding: 10px;
+ margin: 0 -20px 0 -20px;
+ border-bottom: 1px solid #f66;
+}
+
+p.subhead {
+ font-weight: bold;
+ margin-top: 20px;
+}
+
+a {
+ color: orangered;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+ font-family: 'Verdana', sans-serif;
+ background-color: white;
+ font-weight: bold;
+ color: black;
+ border-bottom: 1px solid #ccc;
+ margin: 20px -20px 10px -20px;
+ padding: 3px 0 3px 10px;
+}
+
+div.body h1 { margin-top: 10pt; font-size: 150%; }
+div.body h2 { font-size: 120%; }
+div.body h3 { font-size: 100%; }
+div.body h4 { font-size: 80%; }
+div.body h5 { font-size: 600%; }
+div.body h6 { font-size: 40%; }
+
+a.headerlink {
+ color: #c60f0f;
+ font-size: 0.8em;
+ padding: 0 4px 0 4px;
+ text-decoration: none;
+ visibility: hidden;
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink {
+ visibility: visible;
+}
+
+a.headerlink:hover {
+ background-color: #c60f0f;
+ color: white;
+}
+
+div.body p, div.body dd, div.body li {
+ text-align: justify;
+ line-height: 130%;
+}
+
+div.body p.caption {
+ text-align: inherit;
+}
+
+div.body td {
+ text-align: left;
+}
+
+ul.fakelist {
+ list-style: none;
+ margin: 10px 0 10px 20px;
+ padding: 0;
+}
+
+.field-list ul {
+ padding-left: 1em;
+}
+
+.first {
+ margin-top: 0 !important;
+}
+
+/* "Footnotes" heading */
+p.rubric {
+ margin-top: 30px;
+ font-weight: bold;
+}
+
+/* "Topics" */
+
+div.topic {
+ background-color: #eee;
+ border: 1px solid #ccc;
+ padding: 0 7px 0 7px;
+ margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+ font-size: 1.1em;
+ font-weight: bold;
+ margin-top: 10px;
+}
+
+/* Admonitions */
+
+div.admonition {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ padding: 7px;
+}
+
+div.admonition dt {
+ font-weight: bold;
+}
+
+div.admonition dl {
+ margin-bottom: 0;
+}
+
+div.admonition p {
+ display: inline;
+}
+
+div.seealso {
+ background-color: #ffc;
+ border: 1px solid #ff6;
+}
+
+div.warning {
+ background-color: #ffe4e4;
+ border: 1px solid #f66;
+}
+
+div.note {
+ background-color: #eee;
+ border: 1px solid #ccc;
+}
+
+p.admonition-title {
+ margin: 0px 10px 5px 0px;
+ font-weight: bold;
+ display: inline;
+}
+
+p.admonition-title:after {
+ content: ":";
+}
+
+div.body p.centered {
+ text-align: center;
+ margin-top: 25px;
+}
+
+table.docutils {
+ border: 0;
+}
+
+table.docutils td, table.docutils th {
+ padding: 1px 8px 1px 0;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 1px solid #aaa;
+}
+
+table.field-list td, table.field-list th {
+ border: 0 !important;
+}
+
+table.footnote td, table.footnote th {
+ border: 0 !important;
+}
+
+.field-list ul {
+ margin: 0;
+ padding-left: 1em;
+}
+
+.field-list p {
+ margin: 0;
+}
+
+dl {
+ margin-bottom: 15px;
+ clear: both;
+}
+
+dd p {
+ margin-top: 0px;
+}
+
+dd ul, dd table {
+ margin-bottom: 10px;
+}
+
+dd {
+ margin-top: 3px;
+ margin-bottom: 10px;
+ margin-left: 30px;
+}
+
+.refcount {
+ color: #060;
+}
+
+dt:target,
+.highlight {
+ background-color: #fbe54e;
+}
+
+dl.glossary dt {
+ font-weight: bold;
+ font-size: 1.1em;
+}
+
+th {
+ text-align: left;
+ padding-right: 5px;
+}
+
+pre {
+ padding: 5px;
+ background-color: #efc;
+ color: #333;
+ border: 1px solid #ac9;
+ border-left: none;
+ border-right: none;
+ overflow: auto;
+}
+
+td.linenos pre {
+ padding: 5px 0px;
+ border: 0;
+ background-color: transparent;
+ color: #aaa;
+}
+
+table.highlighttable {
+ margin-left: 0.5em;
+}
+
+table.highlighttable td {
+ padding: 0 0.5em 0 0.5em;
+}
+
+tt {
+ background-color: #ecf0f3;
+ padding: 0 1px 0 1px;
+ font-size: 0.95em;
+}
+
+tt.descname {
+ background-color: transparent;
+ font-weight: bold;
+ font-size: 1.2em;
+}
+
+tt.descclassname {
+ background-color: transparent;
+}
+
+tt.xref, a tt {
+ background-color: transparent;
+ font-weight: bold;
+}
+
+.footnote:target { background-color: #ffa }
+
+h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
+ background-color: transparent;
+}
+
+.optional {
+ font-size: 1.3em;
+}
+
+.versionmodified {
+ font-style: italic;
+}
+
+form.comment {
+ margin: 0;
+ padding: 10px 30px 10px 30px;
+ background-color: #eee;
+}
+
+form.comment h3 {
+ background-color: #326591;
+ color: white;
+ margin: -10px -30px 10px -30px;
+ padding: 5px;
+ font-size: 1.4em;
+}
+
+form.comment input,
+form.comment textarea {
+ border: 1px solid #ccc;
+ padding: 2px;
+ font-family: sans-serif;
+ font-size: 100%;
+}
+
+form.comment input[type="text"] {
+ width: 240px;
+}
+
+form.comment textarea {
+ width: 100%;
+ height: 200px;
+ margin-bottom: 10px;
+}
+
+.system-message {
+ background-color: #fda;
+ padding: 5px;
+ border: 3px solid red;
+}
+
+/* :::: PRINT :::: */
+@media print {
+ div.document,
+ div.documentwrapper,
+ div.bodywrapper {
+ margin: 0;
+ width : 100%;
+ }
+
+ div.sphinxsidebar,
+ div.related,
+ div.footer,
+ div#comments div.new-comment-box,
+ #top-link {
+ display: none;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_templates/layout.html Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,196 @@
+{%- block doctype -%}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+{%- endblock %}
+{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %}
+{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
+{%- macro relbar() %}
+ <div class="related">
+ <h3>Navigation</h3>
+ <ul>
+ {%- for rellink in rellinks %}
+ <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
+ <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags }}"
+ accesskey="{{ rellink[2] }}">{{ rellink[3] }}</a>
+ {%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
+ {%- endfor %}
+ {%- block rootrellink %}
+ <li><a href="{{ pathto('index') }}">{{ shorttitle }}</a>{{ reldelim1 }}</li>
+ {%- endblock %}
+ {%- for parent in parents %}
+ <li><a href="{{ parent.link|e }}" accesskey="U">{{ parent.title }}</a>{{ reldelim1 }}</li>
+ {%- endfor %}
+ {%- block relbaritems %}{% endblock %}
+ </ul>
+ </div>
+{%- endmacro %}
+{%- macro sidebar() %}
+ {%- if builder != 'htmlhelp' %}
+ <div class="sphinxsidebar">
+ <div class="sphinxsidebarwrapper">
+ {%- block sidebarlogo %}
+ {%- if logo %}
+ <p class="logo"><img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/></p>
+ {%- endif %}
+ {%- endblock %}
+ {%- block sidebartoc %}
+ {%- if display_toc %}
+ <h3>Table Of Contents</h3>
+ {{ toc }}
+ {%- endif %}
+ {%- endblock %}
+ {%- block sidebarrel %}
+ {%- if prev %}
+ <h4>Previous topic</h4>
+ <p class="topless"><a href="{{ prev.link|e }}" title="previous chapter">{{ prev.title }}</a></p>
+ {%- endif %}
+ {%- if next %}
+ <h4>Next topic</h4>
+ <p class="topless"><a href="{{ next.link|e }}" title="next chapter">{{ next.title }}</a></p>
+ {%- endif %}
+ {%- endblock %}
+ {%- if sourcename %}
+ <!--<h3>This Page</h3>
+ <ul class="this-page-menu">
+ {%- if builder == 'web' %}
+ <li><a href="#comments">Comments ({{ comments|length }} so far)</a></li>
+ <li><a href="{{ pathto('@edit/' + sourcename)|e }}">Suggest Change</a></li>
+ <li><a href="{{ pathto('@source/' + sourcename)|e }}">Show Source</a></li>
+ {%- elif builder == 'html' %}
+ <li><a href="{{ pathto('_sources/' + sourcename, true)|e }}">Show Source</a></li>
+ {%- endif %}
+ </ul>-->
+ {%- endif %}
+ {%- if customsidebar %}
+ {{ rendertemplate(customsidebar) }}
+ {%- endif %}
+ {%- block sidebarsearch %}
+ {%- if pagename != "search" %}
+ <h3>{{ builder == 'web' and 'Keyword' or 'Quick' }} search</h3>
+ <form class="search" action="{{ pathto('search') }}" method="get">
+ <input type="text" name="q" size="18" /> <input type="submit" value="Go" />
+ <input type="hidden" name="check_keywords" value="yes" />
+ <input type="hidden" name="area" value="default" />
+ </form>
+ {%- if builder == 'web' %}
+ <p style="font-size: 90%">Enter a module, class or function name.</p>
+ {%- endif %}
+ {%- endif %}
+ {%- endblock %}
+ </div>
+ </div>
+ {%- endif %}
+{%- endmacro -%}
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ {%- if builder != 'htmlhelp' %}
+ {%- set titlesuffix = " — " + docstitle %}
+ {%- endif %}
+ <title>{{ title|striptags }}{{ titlesuffix }}</title>
+ {%- if builder == 'web' %}
+ <link rel="stylesheet" href="{{ pathto('index') }}?do=stylesheet{%
+ if in_admin_panel %}&admin=yes{% endif %}" type="text/css" />
+ {%- for link, type, title in page_links %}
+ <link rel="alternate" type="{{ type|e(true) }}" title="{{ title|e(true) }}" href="{{ link|e(true) }}" />
+ {%- endfor %}
+ {%- else %}
+ <link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
+ <link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
+ {%- endif %}
+ {%- if builder != 'htmlhelp' %}
+ <script type="text/javascript">
+ var DOCUMENTATION_OPTIONS = {
+ URL_ROOT: '{{ pathto("", 1) }}',
+ VERSION: '{{ release }}',
+ COLLAPSE_MODINDEX: false,
+ FILE_SUFFIX: '{{ file_suffix }}'
+ };
+ </script>
+ <script type="text/javascript" src="{{ pathto('_static/jquery.js', 1) }}"></script>
+ <script type="text/javascript" src="{{ pathto('_static/interface.js', 1) }}"></script>
+ <script type="text/javascript" src="{{ pathto('_static/doctools.js', 1) }}"></script>
+ <script type="text/javascript" src="{{ pathto('_static/searchtools.js', 1) }}"></script>
+ {%- if use_opensearch %}
+ <link rel="search" type="application/opensearchdescription+xml"
+ title="Search within {{ docstitle }}"
+ href="{{ pathto('_static/opensearch.xml', 1) }}"/>
+ {%- endif %}
+ {%- if favicon %}
+ <link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
+ {%- endif %}
+ {%- endif %}
+{%- block rellinks %}
+ {%- if hasdoc('about') %}
+ <link rel="author" title="About these documents" href="{{ pathto('about') }}" />
+ {%- endif %}
+ <link rel="contents" title="Global table of contents" href="{{ pathto('contents') }}" />
+ <link rel="index" title="Global index" href="{{ pathto('genindex') }}" />
+ <link rel="search" title="Search" href="{{ pathto('search') }}" />
+ {%- if hasdoc('copyright') %}
+ <link rel="copyright" title="Copyright" href="{{ pathto('copyright') }}" />
+ {%- endif %}
+ <link rel="top" title="{{ docstitle }}" href="{{ pathto('index') }}" />
+ {%- if parents %}
+ <link rel="up" title="{{ parents[-1].title|striptags }}" href="{{ parents[-1].link|e }}" />
+ {%- endif %}
+ {%- if next %}
+ <link rel="next" title="{{ next.title|striptags }}" href="{{ next.link|e }}" />
+ {%- endif %}
+ {%- if prev %}
+ <link rel="prev" title="{{ prev.title|striptags }}" href="{{ prev.link|e }}" />
+ {%- endif %}
+{%- endblock %}
+{%- block extrahead %}{% endblock %}
+ </head>
+ <body>
+
+{% block logilablogo %}
+<div class="logilablogo">
+ <a class="logogo" href="http://www.cubicweb.org"><img border="0" src="{{ pathto('_static/cubicweb.png', 1) }}"/></a>
+ </div>
+{% endblock %}
+
+{%- block relbar1 %}{{ relbar() }}{% endblock %}
+
+{%- block sidebar1 %}{# possible location for sidebar #}{% endblock %}
+
+{%- block document %}
+ <div class="document">
+ <div class="documentwrapper">
+ {%- if builder != 'htmlhelp' %}
+ <div class="bodywrapper">
+ {%- endif %}
+ <div class="body">
+ {% block body %}{% endblock %}
+ </div>
+ {%- if builder != 'htmlhelp' %}
+ </div>
+ {%- endif %}
+ </div>
+{%- endblock %}
+
+{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
+ <div class="clearer"></div>
+ </div>
+
+{%- block relbar2 %}{{ relbar() }}{% endblock %}
+
+{%- block footer %}
+ <div class="footer">
+ {%- if hasdoc('copyright') %}
+ © <a href="{{ pathto('copyright') }}">Copyright</a> {{ copyright }}.
+ {%- else %}
+ © Copyright {{ copyright }}.
+ {%- endif %}
+ {%- if last_updated %}
+ Last updated on {{ last_updated }}.
+ {%- endif %}
+ {%- if show_sphinx %}
+ Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
+ {%- endif %}
+ </div>
+{%- endblock %}
+ </body>
+</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/layout.html Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,61 @@
+{% extends "basic/layout.html" %}
+
+{%- block extrahead %}
+<!--[if lte IE 6]>
+<link rel="stylesheet" href="{{ pathto('_static/ie6.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
+<![endif]-->
+{%- if theme_favicon %}
+<link rel="shortcut icon" href="{{ pathto('_static/'+theme_favicon, 1) }}"/>
+{%- endif %}
+
+{%- if theme_canonical_url %}
+<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
+{%- endif %}
+{% endblock %}
+
+{% block header %}
+
+{% if theme_in_progress|tobool %}
+ <img style="position: fixed; display: block; width: 165px; height: 165px; bottom: 60px; right: 0; border: 0;" src="{{ pathto('_static/in_progress.png', 1) }}" alt="Documentation in progress" />
+{% endif %}
+
+{% if theme_outdated|tobool %}
+ <div style="bottom: 60px; right: 20px;position: fixed;"><a href="{{ latest_url }}" class="btn btn-large btn-danger"><strong>></strong> Read the latest version of this page</a></div>
+{% endif %}
+
+<div class="header-small">
+ {%- if theme_logo %}
+ {% set img, ext = theme_logo.split('.', -1) %}
+ <div class="logo-small">
+ <a href="{{ pathto(master_doc) }}">
+ <img class="logo" src="{{ pathto('_static/%s-small.%s' % (img, ext), 1)}}" alt="Logo"/>
+ </a>
+ </div>
+ {%- endif %}
+</div>
+{% endblock %}
+
+{%- macro relbar() %}
+<div class="related">
+ <h3>{{ _('Navigation') }}</h3>
+ <ul>
+ {%- for rellink in rellinks %}
+ <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
+ <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags|e }}"
+ {{ accesskey(rellink[2]) }}>{{ rellink[3] }}</a>
+ {%- if not loop.first %}{{ reldelim2 }}{% endif %}
+ </li>
+ {%- endfor %}
+ {%- block rootrellink %}
+ <li><a href="{{ pathto(master_doc) }}">{{ docstitle|e }}</a>{{ reldelim1 }}</li>
+ {%- endblock %}
+ {%- for parent in parents %}
+ <li><a href="{{ parent.link|e }}" {% if loop.last %}{{ accesskey("U") }}{% endif %}>{{ parent.title }}</a>{{ reldelim1 }}</li>
+ {%- endfor %}
+ {%- block relbaritems %} {% endblock %}
+ </ul>
+</div>
+{%- endmacro %}
+
+{%- block sidebarlogo %}{%- endblock %}
+{%- block sidebarsourcelink %}{%- endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/static/cubicweb.css_t Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,33 @@
+/*
+ * cubicweb.css_t
+ * ~~~~~~~~~~~~~~
+ *
+ * Sphinx stylesheet -- cubicweb theme.
+ *
+ * :copyright: Copyright 2014 by the Cubicweb team, see AUTHORS.
+ * :license: LGPL, see LICENSE for details.
+ *
+ */
+
+@import url("pyramid.css");
+
+div.header-small {
+ background-image: linear-gradient(white, #e2e2e2);
+ border-bottom: 1px solid #bbb;
+}
+
+div.logo-small {
+ padding: 10px;
+}
+
+img.logo {
+ width: 150px;
+}
+
+div.related a {
+ color: #e6820e;
+}
+
+a, a .pre {
+ color: #e6820e;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/static/cubicweb.ico Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+../../../../web/data/favicon.ico
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/static/logo-cubicweb-small.svg Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+logo-cubicweb.svg
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/static/logo-cubicweb.svg Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+../../../../web/data/logo-cubicweb.svg
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/_themes/cubicweb/theme.conf Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,12 @@
+[theme]
+inherit = pyramid
+pygments_style = sphinx.pygments_styles.PyramidStyle
+stylesheet = cubicweb.css
+
+
+[options]
+logo = logo-cubicweb.svg
+favicon = cubicweb.ico
+in_progress = false
+outdated = false
+canonical_url =
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/__init__.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,115 @@
+.. _index_module:
+
+:mod:`cubicweb`
+===============
+
+.. automodule:: cubicweb
+
+ Exceptions
+ ----------
+
+ Base exceptions
+ ~~~~~~~~~~~~~~~
+
+ .. autoexception:: ProgrammingError
+ :show-inheritance:
+
+ .. autoexception:: CubicWebException
+ :show-inheritance:
+
+ .. autoexception:: InternalError
+ :show-inheritance:
+
+ .. autoexception:: SecurityError
+ :show-inheritance:
+
+ .. autoexception:: RepositoryError
+ :show-inheritance:
+
+ .. autoexception:: SourceException
+ :show-inheritance:
+
+ .. autoexception:: CubicWebRuntimeError
+ :show-inheritance:
+
+ Repository exceptions
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ .. autoexception:: ConnectionError
+ :show-inheritance:
+
+ .. autoexception:: AuthenticationError
+ :show-inheritance:
+
+ .. autoexception:: BadConnectionId
+ :show-inheritance:
+
+ .. autoexception:: UnknownEid
+ :show-inheritance:
+
+ .. autoexception:: UniqueTogetherError
+ :show-inheritance:
+
+ Security Exceptions
+ ~~~~~~~~~~~~~~~~~~~
+
+ .. autoexception:: Unauthorized
+ :show-inheritance:
+
+ .. autoexception:: Forbidden
+ :show-inheritance:
+
+ Source exceptions
+ ~~~~~~~~~~~~~~~~~
+
+ .. autoexception:: EidNotInSource
+ :show-inheritance:
+
+ Registry exceptions
+ ~~~~~~~~~~~~~~~~~~~
+
+ .. autoexception:: UnknownProperty
+ :show-inheritance:
+
+ Query exceptions
+ ~~~~~~~~~~~~~~~~
+
+ .. autoexception:: QueryError
+ :show-inheritance:
+
+ .. autoexception:: NotAnEntity
+ :show-inheritance:
+
+ .. autoexception:: MultipleResultsError
+ :show-inheritance:
+
+ .. autoexception:: NoResultError
+ :show-inheritance:
+
+ .. autoexception:: UndoTransactionException
+ :show-inheritance:
+
+
+ Misc
+ ~~~~
+
+ .. autoexception:: ConfigurationError
+ :show-inheritance:
+
+ .. autoexception:: ExecutionError
+ :show-inheritance:
+
+ .. autoexception:: BadCommandUsage
+ :show-inheritance:
+
+ .. autoexception:: ValidationError
+ :show-inheritance:
+
+
+ Utilities
+ ---------
+
+ .. autoclass:: Binary
+ .. autoclass:: CubicWebEventManager
+ .. autofunction:: onevent
+ .. autofunction:: validation_error
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/appobject.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,10 @@
+.. _appobject_module:
+
+:mod:`cubicweb.appobject`
+=========================
+
+.. automodule:: cubicweb.appobject
+
+ .. autoclass:: AppObject
+ :show-inheritance:
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/cwvreg.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,45 @@
+.. _cwvreg_module:
+
+:mod:`cubicweb.cwvreg`
+======================
+
+.. automodule:: cubicweb.cwvreg
+
+ .. autoclass:: CWRegistryStore
+ :show-inheritance:
+ :members:
+ :undoc-members:
+
+ .. autoclass:: CWRegistry
+ :show-inheritance:
+ :members: schema, poss_visible_objects, select
+
+ .. autoclass:: InstancesRegistry
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: ETypeRegistry
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: ViewsRegistry
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: ActionsRegistry
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: CtxComponentsRegistry
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: BwCompatCWRegistry
+ :show-inheritance:
+ :members:
+
+
+:mod:`logilab.common.registry`
+==============================
+
+.. automodule:: logilab.common.registry
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/dataimport.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,63 @@
+.. _dataimport_module:
+
+:mod:`cubicweb.dataimport`
+==========================
+
+.. automodule:: cubicweb.dataimport
+
+ Utilities
+ ---------
+
+ .. autofunction:: count_lines
+
+ .. autofunction:: ucsvreader_pb
+
+ .. autofunction:: ucsvreader
+
+ .. autofunction:: callfunc_every
+
+ .. autofunction:: lazytable
+
+ .. autofunction:: lazydbtable
+
+ .. autofunction:: mk_entity
+
+ Sanitizing/coercing functions
+ -----------------------------
+
+ .. autofunction:: optional
+ .. autofunction:: required
+ .. autofunction:: todatetime
+ .. autofunction:: call_transform_method
+ .. autofunction:: call_check_method
+
+ Integrity functions
+ -------------------
+
+ .. autofunction:: check_doubles
+ .. autofunction:: check_doubles_not_none
+
+ Object Stores
+ -------------
+
+ .. autoclass:: ObjectStore
+ :members:
+
+ .. autoclass:: RQLObjectStore
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: NoHookRQLObjectStore
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: SQLGenObjectStore
+ :show-inheritance:
+ :members:
+
+ Import Controller
+ -----------------
+
+ .. autoclass:: CWImportController
+ :show-inheritance:
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/predicates.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,57 @@
+.. _predicates_module:
+
+:mod:`cubicweb.predicates`
+==========================
+
+.. automodule:: cubicweb.predicates
+
+ .. autoclass:: cubicweb.appobject.yes
+ .. autoclass:: cubicweb.predicates.match_kwargs
+ .. autoclass:: cubicweb.predicates.appobject_selectable
+ .. autoclass:: cubicweb.predicates.adaptable
+ .. autoclass:: cubicweb.predicates.configuration_values
+
+ .. autoclass:: cubicweb.predicates.none_rset
+ .. autoclass:: cubicweb.predicates.any_rset
+ .. autoclass:: cubicweb.predicates.nonempty_rset
+ .. autoclass:: cubicweb.predicates.empty_rset
+ .. autoclass:: cubicweb.predicates.one_line_rset
+ .. autoclass:: cubicweb.predicates.multi_lines_rset
+ .. autoclass:: cubicweb.predicates.multi_columns_rset
+ .. autoclass:: cubicweb.predicates.paginated_rset
+ .. autoclass:: cubicweb.predicates.sorted_rset
+ .. autoclass:: cubicweb.predicates.one_etype_rset
+ .. autoclass:: cubicweb.predicates.multi_etypes_rset
+
+ .. autoclass:: cubicweb.predicates.non_final_entity
+ .. autoclass:: cubicweb.predicates.is_instance
+ .. autoclass:: cubicweb.predicates.score_entity
+ .. autoclass:: cubicweb.predicates.rql_condition
+ .. autoclass:: cubicweb.predicates.relation_possible
+ .. autoclass:: cubicweb.predicates.partial_relation_possible
+ .. autoclass:: cubicweb.predicates.has_related_entities
+ .. autoclass:: cubicweb.predicates.partial_has_related_entities
+ .. autoclass:: cubicweb.predicates.has_permission
+ .. autoclass:: cubicweb.predicates.has_add_permission
+ .. autoclass:: cubicweb.predicates.has_mimetype
+ .. autoclass:: cubicweb.predicates.is_in_state
+ .. autofunction:: cubicweb.predicates.on_fire_transition
+
+ .. autoclass:: cubicweb.predicates.match_user_groups
+
+ .. autoclass:: cubicweb.predicates.no_cnx
+ .. autoclass:: cubicweb.predicates.anonymous_user
+ .. autoclass:: cubicweb.predicates.authenticated_user
+ .. autoclass:: cubicweb.predicates.match_form_params
+ .. autoclass:: cubicweb.predicates.match_search_state
+ .. autoclass:: cubicweb.predicates.match_context_prop
+ .. autoclass:: cubicweb.predicates.match_context
+ .. autoclass:: cubicweb.predicates.match_view
+ .. autoclass:: cubicweb.predicates.primary_view
+ .. autoclass:: cubicweb.predicates.contextual
+ .. autoclass:: cubicweb.predicates.specified_etype_implements
+ .. autoclass:: cubicweb.predicates.attribute_edited
+ .. autoclass:: cubicweb.predicates.match_transition
+
+ .. autoclass:: cubicweb.predicates.match_exception
+ .. autoclass:: cubicweb.predicates.debug_mode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/req.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,11 @@
+.. _req_module:
+
+:mod:`cubicweb.req`
+===================
+
+.. automodule:: cubicweb.req
+
+ .. autoexception:: FindEntityError
+
+ .. autoclass:: RequestSessionBase
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/rset.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,10 @@
+.. _rset_module:
+
+:mod:`cubicweb.rset`
+====================
+
+.. automodule:: cubicweb.rset
+
+ .. autoclass:: ResultSet
+ :members:
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/urlpublishing.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,36 @@
+.. _urlpublishing_module:
+
+:mod:`cubicweb.web.views.urlpublishing`
+=======================================
+
+.. automodule:: cubicweb.web.views.urlpublishing
+
+ .. autoexception:: PathDontMatch
+
+ .. autoclass:: URLPublisherComponent
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: URLPathEvaluator
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: RawPathEvaluator
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: EidPathEvaluator
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: RestPathEvaluator
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: URLRewriteEvaluator
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: ActionPathEvaluator
+ :show-inheritance:
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/urlrewrite.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,18 @@
+.. _urlrewrite_module:
+
+:mod:`cubicweb.web.views.urlrewrite`
+=======================================
+
+.. automodule:: cubicweb.web.views.urlrewrite
+
+ .. autoclass:: URLRewriter
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: SimpleReqRewriter
+ :show-inheritance:
+ :members:
+
+ .. autoclass:: SchemaBasedRewriter
+ :show-inheritance:
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/web.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,21 @@
+.. _web_module:
+
+:mod:`cubicweb.web`
+===================
+
+.. automodule:: cubicweb.web
+
+ Exceptions
+ ----------
+
+ .. autoexception:: DirectResponse
+ .. autoexception:: InvalidSession
+ .. autoexception:: PublishException
+ .. autoexception:: LogOut
+ .. autoexception:: Redirect
+ .. autoexception:: StatusResponse
+ .. autoexception:: RequestError
+ .. autoexception:: NothingToEdit
+ .. autoexception:: ProcessFormError
+ .. autoexception:: NotFound
+ .. autoexception:: RemoteCallFailed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/MERGE_ME-tut-create-app.en.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,386 @@
+.. -*- coding: utf-8 -*-
+
+
+Tutoriel : créer votre première application web pour Google AppEngine
+=====================================================================
+
+[TRANSLATE ME TO FRENCH]
+
+This tutorial will guide you step by step to build a blog application
+and discover the unique features of `LAX`. It assumes that you followed
+the :ref:`installation` guidelines and that both the `AppEngine SDK` and the
+`LAX` framework are setup on your computer.
+
+Creating a new application
+--------------------------
+
+We choosed in this tutorial to develop a blog as an example of web application
+and will go through each required steps/actions to have it running with `LAX`.
+When you installed `LAX`, you saw a directory named ``skel``. Make a copy of
+this directory and call it ``BlogDemo``.
+
+The location of this directory does not matter. But once decided, make sure your ``PYTHONPATH`` is properly set (:ref:`installation`).
+
+
+Defining a schema
+-----------------
+
+With `LAX`, the schema/datamodel is the core of the application. This is where
+you will define the type of content you have to hanlde in your application.
+
+Let us start with something simple and improve on it iteratively.
+
+In schema.py, we define two entities: ``Blog`` and ``BlogEntry``.
+
+::
+
+ class Blog(EntityType):
+ title = String(maxsize=50, required=True)
+ description = String()
+
+ class BlogEntry(EntityType):
+ title = String(maxsize=100, required=True)
+ publish_date = Date(default='TODAY')
+ text = String(fulltextindexed=True)
+ category = String(vocabulary=('important','business'))
+ entry_of = SubjectRelation('Blog', cardinality='?*')
+
+A Blog has a title and a description. The title is a string that is
+required by the class EntityType and must be less than 50 characters.
+The description is a string that is not constrained.
+
+A BlogEntry has a title, a publish_date and a text. The title is a
+string that is required and must be less than 100 characters. The
+publish_date is a Date with a default value of TODAY, meaning that
+when a BlogEntry is created, its publish_date will be the current day
+unless it is modified. The text is a string that will be indexed in
+the full-text index and has no constraint.
+
+A BlogEntry also has a relationship ``entry_of`` that link it to a
+Blog. The cardinality ``?*`` means that a BlogEntry can be part of
+zero or one Blog (``?`` means `zero or one`) and that a Blog can
+have any number of BlogEntry (``*`` means `any number including
+zero`). For completeness, remember that ``+`` means `one or more`.
+
+Running the application
+-----------------------
+
+Defining this simple schema is enough to get us started. Make sure you
+followed the setup steps described in detail in the installation
+chapter (especially visiting http://localhost:8080/_load as an
+administrator), then launch the application with the command::
+
+ python dev_appserver.py BlogDemo
+
+and point your browser at http://localhost:8080/ (if it is easier for
+you, use the on-line demo at http://lax.appspot.com/).
+
+.. image:: images/lax-book.00-login.en.png
+ :alt: login screen
+
+After you log in, you will see the home page of your application. It
+lists the entity types: Blog and BlogEntry. If these links read
+``blog_plural`` and ``blogentry_plural`` it is because
+internationalization (i18n) is not working for you yet. Please ignore
+this for now.
+
+.. image:: images/lax-book.01-start.en.png
+ :alt: home page
+
+Creating system entities
+------------------------
+You can only create new users if you decided not to use google authentication.
+
+
+[WRITE ME : create users manages permissions etc]
+
+
+
+Creating application entites
+----------------------------
+
+Create a Blog
+~~~~~~~~~~~~~
+
+Let us create a few of these entities. Click on the [+] at the right
+of the link Blog. Call this new Blog ``Tech-blog`` and type in
+``everything about technology`` as the description, then validate the
+form by clicking on ``Validate``.
+
+.. image:: images/lax-book.02-create-blog.en.png
+ :alt: from to create blog
+
+Click on the logo at top left to get back to the home page, then
+follow the Blog link that will list for you all the existing Blog.
+You should be seeing a list with a single item ``Tech-blog`` you
+just created.
+
+.. image:: images/lax-book.03-list-one-blog.en.png
+ :alt: displaying a list of a single blog
+
+Clicking on this item will get you to its detailed description except
+that in this case, there is not much to display besides the name and
+the phrase ``everything about technology``.
+
+.. image:: images/lax-book.04-detail-one-blog.en.png
+ :alt: displaying the detailed view of a blog
+
+Now get back to the home page by clicking on the top-left logo, then
+create a new Blog called ``MyLife`` and get back to the home page
+again to follow the Blog link for the second time. The list now
+has two items.
+
+.. image:: images/lax-book.05-list-two-blog.en.png
+ :alt: displaying a list of two blogs
+
+
+Create a BlogEntry
+~~~~~~~~~~~~~~~~~~
+
+Get back to the home page and click on [+] at the right of the link
+BlogEntry. Call this new entry ``Hello World`` and type in some text
+before clicking on ``Validate``. You added a new blog entry without
+saying to what blog it belongs. There is a box on the left entitled
+``actions``, click on the menu item ``modify``. You are back to the form
+to edit the blog entry you just created, except that the form now has
+another section with a combobox titled ``add relation``. Chose
+``entry_of`` in this menu and a second combobox appears where you pick
+``MyLife``.
+
+You could also have, at the time you started to fill the form for a
+new entity BlogEntry, hit ``Apply`` instead of ``Validate`` and the
+combobox titled ``add relation`` would have showed up.
+
+.. image:: images/lax-book.06-add-relation-entryof.en.png
+ :alt: editing a blog entry to add a relation to a blog
+
+Validate the changes by clicking ``Validate``. The entity BlogEntry
+that is displayed now includes a link to the entity Blog named
+``MyLife``.
+
+.. image:: images/lax-book.07-detail-one-blogentry.en.png
+ :alt: displaying the detailed view of a blogentry
+
+Remember that all of this was handled by the framework and that the
+only input that was provided so far is the schema. To get a graphical
+view of the schema, run the ``laxctl genschema BlogDemo`` command as
+explained in the installation section and point your browser to the
+URL http://localhost:8080/schema
+
+.. image:: images/lax-book.08-schema.en.png
+ :alt: graphical view of the schema (aka data-model)
+
+Site configuration
+------------------
+
+.. image:: images/lax-book.03-site-config-panel.en.png
+
+This panel allows you to configure the appearance of your application site.
+Six menus are available and we will go through each of them to explain how
+to use them.
+
+Navigation
+~~~~~~~~~~
+This menu provides you a way to adjust some navigation options depending on
+your needs, such as the number of entities to display by page of results.
+Follows the detailled list of available options:
+
+* navigation.combobox-limit: maximum number of entities to display in related
+ combo box (sample format: 23)
+* navigation.page-size: maximum number of objects displayed by page of results
+ (sample format: 23)
+* navigation.related-limit: maximum number of related entities to display in
+ the primary view (sample format: 23)
+* navigation.short-line-size: maximum number of characters in short description
+ (sample format: 23)
+
+UI
+~~
+This menu provides you a way to customize the user interface settings such as
+date format or encoding in the produced html.
+Follows the detailled list of available options:
+
+* ui.date-format : how to format date in the ui ("man strftime" for format description)
+* ui.datetime-format : how to format date and time in the ui ("man strftime" for format
+ description)
+* ui.default-text-format : default text format for rich text fields.
+* ui.encoding : user interface encoding
+* ui.fckeditor : should html fields being edited using fckeditor (a HTML WYSIWYG editor).
+ You should also select text/html as default text format to actually get fckeditor.
+* ui.float-format : how to format float numbers in the ui
+* ui.language : language of the user interface
+* ui.main-template : id of main template used to render pages
+* ui.site-title : site title, which is displayed right next to the logo in the header
+* ui.time-format : how to format time in the ui ("man strftime" for format description)
+
+
+Actions
+~~~~~~~
+This menu provides a way to configure the context in which you expect the actions
+to be displayed to the user and if you want the action to be visible or not.
+You must have notice that when you view a list of entities, an action box is
+available on the left column which display some actions as well as a drop-down
+menu for more actions.
+
+The context available are:
+
+* mainactions : actions listed in the left box
+* moreactions : actions listed in the `more` menu of the left box
+* addrelated : add actions listed in the left box
+* useractions : actions listed in the first section of drop-down menu
+ accessible from the right corner user login link
+* siteactions : actions listed in the second section of drop-down menu
+ accessible from the right corner user login link
+* hidden : select this to hide the specific action
+
+Boxes
+~~~~~
+The application has already a pre-defined set of boxes you can use right away.
+This configuration section allows you to place those boxes where you want in the
+application interface to customize it.
+
+The available boxes are:
+
+* actions box : box listing the applicable actions on the displayed data
+
+* boxes_blog_archives_box : box listing the blog archives
+
+* possible views box : box listing the possible views for the displayed data
+
+* rss box : RSS icon to get displayed data as a RSS thread
+
+* search box : search box
+
+* startup views box : box listing the configuration options available for
+ the application site, such as `Preferences` and `Site Configuration`
+
+Components
+~~~~~~~~~~
+[WRITE ME]
+
+Contextual components
+~~~~~~~~~~~~~~~~~~~~~
+[WRITE ME]
+
+Set-up a workflow
+-----------------
+
+Before starting, make sure you refresh your mind by reading [link to
+definition_workflow chapter].
+
+We want to create a workflow to control the quality of the BlogEntry
+submitted on your application. When a BlogEntry is created by a user
+its state should be `submitted`. To be visible to all, it needs to
+be in the state `published`. To move from `submitted` to `published`
+we need a transition that we can name `approve_blogentry`.
+
+We do not want every user to be allowed to change the state of a
+BlogEntry. We need to define a group of user, `moderators`, and
+this group will have appropriate permissions to approve BlogEntry
+to be published and visible to all.
+
+There are two ways to create a workflow, form the user interface,
+and also by defining it in ``migration/postcreate.py``. This script
+is executed each time a new ``./bin/laxctl db-init`` is done.
+If you create the states and transitions through the user interface
+this means that next time you will need to initialize the database
+you will have to re-create all the entities.
+We strongly recommand you create the workflow in ``migration\postcreate.py``
+and we will now show you how.
+The user interface would only be a reference for you to view the states
+and transitions but is not the appropriate interface to define your
+application workflow.
+
+Update the schema
+~~~~~~~~~~~~~~~~~
+To enable a BlogEntry to have a State, we have to define a relation
+``in_state`` in the schema of BlogEntry. Please do as follows, add
+the line ``in_state (...)``::
+
+ class BlogEntry(EntityType):
+ title = String(maxsize=100, required=True)
+ publish_date = Date(default='TODAY')
+ text_format = String(meta=True, internationalizable=True, maxsize=50,
+ default='text/rest', constraints=[format_constraint])
+ text = String(fulltextindexed=True)
+ category = String(vocabulary=('important','business'))
+ entry_of = SubjectRelation('Blog', cardinality='?*')
+ in_state = SubjectRelation('State', cardinality='1*')
+
+As you updated the schema, you will have re-execute ``./bin/laxctl db-init``
+to initialize the database and migrate your existing entities.
+[WRITE ABOUT MIGRATION]
+
+Create states, transitions and group permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+At the time the ``postcreate.py`` script is executed, several methods
+can be used. They are all defined in the ``class ServerMigrationHelper``.
+We will only discuss the method we use to create a wrokflow here.
+
+To define our workflow for BlogDemo, please add the following lines
+to ``migration/postcreate.py``::
+
+ _ = unicode
+
+ moderators = add_entity('CWGroup', name=u"moderators")
+
+ submitted = add_state(_('submitted'), 'BlogEntry', initial=True)
+ published = add_state(_('published'), 'BlogEntry')
+
+ add_transition(_('approve_blogentry'), 'BlogEntry', (submitted,), published, ('moderators', 'managers'),)
+
+ checkpoint()
+
+``add_entity`` is used here to define the new group of users that we
+need to define the transitions, `moderators`.
+If this group required by the transition is not defined before the
+transition is created, it will not create the relation `transition
+require the group moderator`.
+
+``add_state`` expects as the first argument the name of the state you are
+willing to create, then the entity type on which the state can be applied,
+and an optionnal argument to set if the state is the initial state
+of the entity type or not.
+
+``add_transition`` expects as the first argument the name of the
+transition, then the entity type on which we can apply the transition,
+then the list of possible initial states from which the transition
+can be applied, the target state of the transition, and the permissions
+(e.g. list of the groups of users who can apply the transition).
+
+.. image:: images/lax-book.03-transitions-view.en.png
+
+You can now notice that in the actions box of a BlogEntry, the state
+is now listed as well as the possible transitions from this state
+defined by the workflow. This transition, as defined in the workflow,
+will only being displayed for the users belonging to the group
+moderators of managers.
+
+Change view permission
+~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+Conclusion
+----------
+
+Exercise
+~~~~~~~~
+
+Create new blog entries in ``Tech-blog``.
+
+What we learned
+~~~~~~~~~~~~~~~
+
+Creating a simple schema was enough to set up a new application that
+can store blogs and blog entries.
+
+What is next ?
+~~~~~~~~~~~~~~
+
+Although the application is fully functionnal, its look is very
+basic. In the following section we will learn to create views to
+customize how data is displayed.
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/MERGE_ME-tut-create-gae-app.en.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,218 @@
+.. -*- coding: utf-8 -*-
+
+.. _tutorielGAE:
+
+Tutoriel : créer votre première application web pour Google AppEngine
+=====================================================================
+
+Ce tutoriel va vous guider pas à pas a construire une apllication web
+de gestion de Blog afin de vous faire découvrir les fonctionnalités de
+*CubicWeb*.
+
+Nous supposons que vous avec déjà suivi le guide :ref:`installationGAE`.
+
+
+Créez une nouvelle application
+------------------------------
+
+Nous choisissons dans ce tutoriel de développer un blog comme un exemple
+d'application web et nous allons expliciter toutes les étapes nécessaires
+à sa réalisation.
+
+::
+
+ cubicweb-ctl newgapp blogdemo
+
+`newgapp` est la commande permettant de créer une instance *CubicWeb* pour
+le datastore.
+
+Assurez-vous que votre variable d'environnement ``PYTHONPATH`` est correctement
+initialisée (:ref:`installationGAE`)
+
+Définissez un schéma
+--------------------
+
+Le modèle de données ou schéma est au coeur d'une application *CubicWeb*.
+C'est là où vous allez devoir définir le type de contenu que votre application
+devra gérer.
+
+Commençons par un schéma simple que nous améliorerons progressivemment.
+
+Une fois votre instance ``blogdemo`` crée, vous trouverez un fichier ``schema.py``
+contenant la définition des entités suivantes : ``Blog`` and ``BlogEntry``.
+
+::
+
+ class Blog(EntityType):
+ title = String(maxsize=50, required=True)
+ description = String()
+
+ class BlogEntry(EntityType):
+ title = String(maxsize=100, required=True)
+ publish_date = Date(default='TODAY')
+ text = String(fulltextindexed=True)
+ category = String(vocabulary=('important','business'))
+ entry_of = SubjectRelation('Blog', cardinality='?*')
+
+
+Un ``Blog`` a un titre et une description. Le titre est une chaîne
+de caractères requise par la classe parente EntityType and ne doit
+pas excéder 50 caractères. La description est une chaîne de
+caractères sans contraintes.
+
+Une ``BlogEntry`` a un titre, une date de publication et du texte
+étant son contenu. Le titre est une chaîne de caractères qui ne
+doit pas excéder 100 caractères. La date de publication est de type Date et a
+pour valeur par défaut TODAY, ce qui signifie que lorsqu'une
+``BlogEntry`` sera créée, sa date de publication sera la date
+courante a moins de modifier ce champ. Le texte est une chaîne de
+caractères qui sera indexée en plein texte et sans contraintes.
+
+Une ``BlogEntry`` a aussi une relation nommée ``entry_of`` qui la
+relie à un ``Blog``. La cardinalité ``?*`` signifie que BlogEntry
+peut faire partie de zero a un Blog (``?`` signifie `zero ou un`) et
+qu'un Blog peut avoir une infinité de BlogEntry (``*`` signifie
+`n'importe quel nombre incluant zero`).
+Par soucis de complétude, nous rappellerons que ``+`` signifie
+`un ou plus`.
+
+Lancez l'application
+--------------------
+
+Définir ce simple schéma est suffisant pour commencer. Assurez-vous
+que vous avez suivi les étapes décrites dans la section installation
+(en particulier visitez http://localhost:8080/_load en tant qu'administrateur
+afin d'initialiser le datastore), puis lancez votre application avec la commande ::
+
+ python dev_appserver.py BlogDemo
+
+puis dirigez vous vers http://localhost:8080/ (ou si c'est plus facile
+vous pouvez utiliser la démo en ligne http://lax.appspot.com/).
+[FIXME] -- changer la demo en ligne en quelque chose qui marche (!)
+
+.. image:: images/lax-book.00-login.en.png
+ :alt: login screen
+
+Après vous être authentifié, vous arrivez sur la page d'accueil de votre
+application. Cette page liste les types d'entités accessibles dans votre
+application, en l'occurrence : Blog et Articles. Si vous lisez ``blog_plural``
+et ``blogentry_plural`` cela signifie que l'internationalisation (i18n)
+n'a pas encore fonctionné. Ignorez cela pour le moment.
+
+.. image:: images/lax-book.01-start.en.png
+ :alt: home page
+
+Créez des entités système
+-------------------------
+
+Vous ne pourrez créer de nouveaux utilisateurs que dans le cas où vous
+avez choisi de ne pas utiliser l'authentification Google.
+
+
+[WRITE ME : create users manages permissions etc]
+
+
+
+Créez des entités applicatives
+------------------------------
+
+Créez un Blog
+~~~~~~~~~~~~~
+
+Créons à présent quelques entités. Cliquez sur `[+]` sur la
+droite du lien Blog. Appelez cette nouvelle entité Blog ``Tech-Blog``
+et tapez pour la description ``everything about technology``,
+puis validez le formulaire d'édition en cliquant sur le bouton
+``Validate``.
+
+
+.. image:: images/lax-book.02-create-blog.en.png
+ :alt: from to create blog
+
+En cliquant sur le logo situé dans le coin gauche de la fenêtre,
+vous allez être redirigé vers la page d'accueil. Ensuite, si vous allez
+sur le lien Blog, vous devriez voir la liste des entités Blog, en particulier
+celui que vous venez juste de créer ``Tech-Blog``.
+
+.. image:: images/lax-book.03-list-one-blog.en.png
+ :alt: displaying a list of a single blog
+
+Si vous cliquez sur ``Tech-Blog`` vous devriez obtenir une description
+détaillée, ce qui dans notre cas, n'est rien de plus que le titre
+et la phrase ``everything about technology``
+
+
+.. image:: images/lax-book.04-detail-one-blog.en.png
+ :alt: displaying the detailed view of a blog
+
+Maintenant retournons sur la page d'accueil et créons un nouveau
+Blog ``MyLife`` et retournons sur la page d'accueil, puis suivons
+le lien Blog et nous constatons qu'à présent deux blogs sont listés.
+
+.. image:: images/lax-book.05-list-two-blog.en.png
+ :alt: displaying a list of two blogs
+
+Créons un article
+~~~~~~~~~~~~~~~~~
+
+Revenons sur la page d'accueil et cliquons sur `[+]` Ã droite du lien
+`articles`. Appellons cette nouvelle entité ``Hello World`` et introduisons
+un peut de texte avant de ``Valider``. Vous venez d'ajouter un article
+sans avoir précisé à quel Blog il appartenait. Dans la colonne de gauche
+se trouve une boite intitulé ``actions``, cliquez sur le menu ``modifier``.
+Vous êtes de retour sur le formulaire d'édition de l'article que vous
+venez de créer, à ceci près que ce formulaire a maintenant une nouvelle
+section intitulée ``ajouter relation``. Choisissez ``entry_of`` dans ce menu,
+cela va faire apparaitre une deuxième menu déroulant dans lequel vous
+allez pouvoir séléctionner le Blog ``MyLife``.
+
+Vous auriez pu aussi, au moment où vous avez crée votre article, sélectionner
+``appliquer`` au lieu de ``valider`` et le menu ``ajouter relation`` serait apparu.
+
+.. image:: images/lax-book.06-add-relation-entryof.en.png
+ :alt: editing a blog entry to add a relation to a blog
+
+Validez vos modifications en cliquant sur ``Valider``. L'entité article
+qui est listée contient maintenant un lien vers le Blog auquel il
+appartient, ``MyLife``.
+
+.. image:: images/lax-book.07-detail-one-blogentry.en.png
+ :alt: displaying the detailed view of a blogentry
+
+Rappelez-vous que pour le moment, tout a été géré par la plate-forme
+*CubicWeb* et que la seule chose qui a été fournie est le schéma de
+données. D'ailleurs pour obtenir une vue graphique du schéma, exécutez
+la commande ``laxctl genschema blogdemo`` et vous pourrez visualiser
+votre schéma a l'URL suivante : http://localhost:8080/schema
+
+.. image:: images/lax-book.08-schema.en.png
+ :alt: graphical view of the schema (aka data-model)
+
+
+Change view permission
+~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+Conclusion
+----------
+
+Exercise
+~~~~~~~~
+
+Create new blog entries in ``Tech-blog``.
+
+What we learned
+~~~~~~~~~~~~~~~
+
+Creating a simple schema was enough to set up a new application that
+can store blogs and blog entries.
+
+What is next ?
+~~~~~~~~~~~~~~
+
+Although the application is fully functionnal, its look is very
+basic. In the following section we will learn to create views to
+customize how data is displayed.
+
+
--- a/doc/book/README Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-====
-Book
-====
-
-----
-Part
-----
-
-Chapter
-=======
-
-.. _Level1AnchorForLaterReference:
-
-Level 1 section
----------------
-
-Level 2 section
-~~~~~~~~~~~~~~~
-
-Level 3 section
-```````````````
-
-
-
-*CubicWeb*
-
-
-inline directives:
- :file:`directory/file`
- :envvar:`AN_ENV_VARIABLE`
- :command:`command --option arguments`
-
- :ref:, :mod:
-
-
-.. sourcecode:: python
-
- class SomePythonCode:
- ...
-
-.. XXX a comment, wont be rendered
-
-
-a [foot note]_
-
-.. [foot note] the foot note content
-
-
-Boxes
-=====
-
-- warning box:
- .. warning::
-
- Warning content
-- note box:
- .. note::
-
- Note content
-
-
-
-Cross references
-================
-
-To arbitrary section
---------------------
-
-:ref:`identifier` ou :ref:`label <identifier>`
-
-Label required of referencing node which as no title, else the node's title will be used.
-
-
-To API objects
---------------
-See the autodoc sphinx extension documentation. Quick overview:
-
-* ref to a class: :class:`cubicweb.devtools.testlib.AutomaticWebTest`
-
-* if you can to see only the class name in the generated documentation, add a ~:
- :class:`~cubicweb.devtools.testlib.AutomaticWebTest`
-
-* you can also use :mod: (module), :exc: (exception), :func: (function), :meth: (method)...
-
-* syntax explained above to specify label explicitly may also be used
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/additionnal_services/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,14 @@
+Additional services
+===================
+
+In this chapter, we introduce services crossing the *web -
+repository - administration* organisation of the first parts of the
+CubicWeb book. Those services can be either proper services (like the
+undo functionality) or mere *topical cross-sections* across CubicWeb.
+
+.. toctree::
+ :maxdepth: 2
+
+ undo
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/additionnal_services/undo.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,337 @@
+Undoing changes in CubicWeb
+---------------------------
+
+Many desktop applications offer the possibility for the user to
+undo its last changes : this *undo feature* has now been
+integrated into the CubicWeb framework. This document will
+introduce you to the *undo feature* both from the end-user and the
+application developer point of view.
+
+But because a semantic web application and a common desktop
+application are not the same thing at all, especially as far as
+undoing is concerned, we will first introduce *what* is the *undo
+feature* for now.
+
+What's *undoing* in a CubicWeb application
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+What is an *undo feature* is quite intuitive in the context of a
+desktop application. But it is a bit subtler in the context of a
+Semantic Web application. This section introduces some of the main
+differences between a classical desktop and a Semantic Web
+applications to keep in mind in order to state precisely *what we
+want*.
+
+The notion transactions
+```````````````````````
+
+A CubicWeb application acts upon an *Entity-Relationship* model,
+described by a schema. This allows to ensure some data integrity
+properties. It also implies that changes are made by all-or-none
+groups called *transactions*, such that the data integrity is
+preserved whether the transaction is completely applied *or* none
+of it is applied.
+
+A transaction can thus include more actions than just those
+directly required by the main purpose of the user. For example,
+when a user *just* writes a new blog entry, the underlying
+*transaction* holds several *actions* as illustrated below :
+
+* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
+
+ #. Created Blog entry : Torototo
+ #. Added relation : Torototo owned by admin
+ #. Added relation : Torototo blog entry of Undo Blog
+ #. Added relation : Torototo in state draft (draft)
+ #. Added relation : Torototo created by admin
+
+Because of the very nature (all-or-none) of the transactions, the
+"undoable stuff" are the transactions and not the actions !
+
+Public and private actions within a transaction
+```````````````````````````````````````````````
+
+Actually, within the *transaction* "Created Blog entry :
+Torototo", two of those *actions* are said to be *public* and
+the others are said to be *private*. *Public* here means that the
+public actions (1 and 3) were directly requested by the end user ;
+whereas *private* means that the other actions (2, 4, 5) were
+triggered "under the hood" to fulfill various requirements for the
+user operation (ensuring integrity, security, ... ).
+
+And because quite a lot of actions can be triggered by a "simple"
+end-user request, most of which the end-user is not (and does not
+need or wish to be) aware, only the so-called public actions will
+appear [1]_ in the description of the an undoable transaction.
+
+* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
+
+ #. Created Blog entry : Torototo
+ #. Added relation : Torototo blog entry of Undo Blog
+
+But note that both public and private actions will be undone
+together when the transaction is undone.
+
+(In)dependent transactions : the simple case
+````````````````````````````````````````````
+
+A CubicWeb application can be used *simultaneously* by different users
+(whereas a single user works on an given office document at a
+given time), so that there is not always a single history
+time-line in the CubicWeb case. Moreover CubicWeb provides
+security through the mechanism of *permissions* granted to each
+user. This can lead to some transactions *not* being undoable in
+some contexts.
+
+In the simple case two (unprivileged) users Alice and Bob make
+relatively independent changes : then both Alice and Bob can undo
+their changes. But in some case there is a clean dependency
+between Alice's and Bob's actions or between actions of one of
+them. For example let's suppose that :
+
+- Alice has created a blog,
+- then has published a first post inside,
+- then Bob has published a second post in the same blog,
+- and finally Alice has updated its post contents.
+
+Then it is clear that Alice can undo her contents changes and Bob
+can undo his post creation independently. But Alice can not undo
+her post creation while she has not first undone her changes.
+It is also clear that Bob should *not* have the
+permissions to undo any of Alice's transactions.
+
+
+More complex dependencies between transactions
+``````````````````````````````````````````````
+
+But more surprising things can quickly happen. Going back to the
+previous example, Alice *can* undo the creation of the blog after
+Bob has published its post in it ! But this is possible only
+because the schema does not *require* for a post to be in a
+blog. Would the *blog entry of* relation have been mandatory, then
+Alice could not have undone the blog creation because it would
+have broken integrity constraint for Bob's post.
+
+When a user attempts to undo a transaction the system will check
+whether a later transaction has explicit dependency on the
+would-be-undone transaction. In this case the system will not even
+attempt the undo operation and inform the user.
+
+If no such dependency is detected the system will attempt the undo
+operation but it can fail, typically because of integrity
+constraint violations. In such a case the undo operation is
+completely [3]_ rollbacked.
+
+
+The *undo feature* for CubicWeb end-users
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The exposition of the undo feature to the end-user through a Web
+interface is still quite basic and will be improved toward a
+greater usability. But it is already fully functional. For now
+there are two ways to access the *undo feature* as long as the it
+has been activated in the instance configuration file with the
+option *undo-support=yes*.
+
+Immediately after having done the change to be canceled through
+the **undo** link in the message. This allows to undo an
+hastily action immediately. For example, just after having
+validated the creation of the blog entry *A second blog entry* we
+get the following message, allowing to undo the creation.
+
+.. image:: /images/undo_mesage_w600.png
+ :width: 600px
+ :alt: Screenshot of the undo link in the message
+ :align: center
+
+At any time we can access the **undo-history view** accessible from the
+start-up page.
+
+.. image:: /images/undo_startup-link_w600.png
+ :width: 600px
+ :alt: Screenshot of the startup menu with access to the history view
+ :align: center
+
+This view will provide inspection of the transaction and their (public)
+actions. Each transaction provides its own **undo** link. Only the
+transactions the user has permissions to see and undo will be shown.
+
+.. image:: /images/undo_history-view_w600.png
+ :width: 600px
+ :alt: Screenshot of the undo history main view
+ :align: center
+
+If the user attempts to undo a transaction which can't be undone or
+whose undoing fails, then a message will explain the situation and
+no partial undoing will be left behind.
+
+This is all for the end-user side of the undo mechanism : this is
+quite simple indeed ! Now, in the following section, we are going
+to introduce the developer side of the undo mechanism.
+
+The *undo feature* for CubicWeb application developers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A word of warning : this section is intended for developers,
+already having some knowledge of what's under CubicWeb's hood. If
+it is not *yet* the case, please refer to CubicWeb documentation
+http://docs.cubicweb.org/ .
+
+Overview
+````````
+
+The core of the undo mechanisms is at work in the *native source*,
+beyond the RQL. This does mean that *transactions* and *actions*
+are *no entities*. Instead they are represented at the SQL level
+and exposed through the *DB-API* supported by the repository
+*Connection* objects.
+
+Once the *undo feature* has been activated in the instance
+configuration file with the option *undo-support=yes*, each
+mutating operation (cf. [2]_) will be recorded in some special SQL
+table along with its associated transaction. Transaction are
+identified by a *txuuid* through which the functions of the
+*DB-API* handle them.
+
+On the web side the last commited transaction *txuuid* is
+remembered in the request's data to allow for imediate undoing
+whereas the *undo-history view* relies upon the *DB-API* to list
+the accessible transactions. The actual undoing is performed by
+the *UndoController* accessible at URL of the form
+`www.my.host/my/instance/undo?txuuid=...`
+
+The repository side
+```````````````````
+
+Please refer to the file `cubicweb/server/sources/native.py` and
+`cubicweb/transaction.py` for the details.
+
+The undoing information is mainly stored in three SQL tables:
+
+`transactions`
+ Stores the txuuid, the user eid and the date-and-time of
+ the transaction. This table is referenced by the two others.
+
+`tx_entity_actions`
+ Stores the undo information for actions on entities.
+
+`tx_relation_actions`
+ Stores the undo information for the actions on relations.
+
+When the undo support is activated, entries are added to those
+tables for each mutating operation on the data repository, and are
+deleted on each transaction undoing.
+
+Those table are accessible through the following methods of the
+repository `Connection` object :
+
+`undoable_transactions`
+ Returns a list of `Transaction` objects accessible to the user
+ and according to the specified filter(s) if any.
+
+`tx_info`
+ Returns a `Transaction` object from a `txuuid`
+
+`undo_transaction`
+ Returns the list of `Action` object for the given `txuuid`.
+
+ NB: By default it only return *public* actions.
+
+The web side
+````````````
+
+The exposure of the *undo feature* to the end-user through the Web
+interface relies on the *DB-API* introduced above. This implies
+that the *transactions* and *actions* are not *entities* linked by
+*relations* on which the usual views can be applied directly.
+
+That's why the file `cubicweb/web/views/undohistory.py` defines
+some dedicated views to access the undo information :
+
+`UndoHistoryView`
+ This is a *StartupView*, the one accessible from the home
+ page of the instance which list all transactions.
+
+`UndoableTransactionView`
+ This view handles the display of a single `Transaction` object.
+
+`UndoableActionBaseView`
+ This (abstract) base class provides private methods to build
+ the display of actions whatever their nature.
+
+`Undoable[Add|Remove|Create|Delete|Update]ActionView`
+ Those views all inherit from `UndoableActionBaseView` and
+ each handles a specific kind of action.
+
+`UndoableActionPredicate`
+ This predicate is used as a *selector* to pick the appropriate
+ view for actions.
+
+Apart from this main *undo-history view* a `txuuid` is stored in
+the request's data `last_undoable_transaction` in order to allow
+immediate undoing of a hastily validated operation. This is
+handled in `cubicweb/web/application.py` in the `main_publish` and
+`add_undo_link_to_msg` methods for the storing and displaying
+respectively.
+
+Once the undo information is accessible, typically through a
+`txuuid` in an *undo* URL, the actual undo operation can be
+performed by the `UndoController` defined in
+`cubicweb/web/views/basecontrollers.py`. This controller basically
+extracts the `txuuid` and performs a call to `undo_transaction` and
+in case of an undo-specific error, lets the top level publisher
+handle it as a validation error.
+
+
+Conclusion
+~~~~~~~~~~
+
+The undo mechanism relies upon a low level recording of the
+mutating operation on the repository. Those records are accessible
+through some method added to the *DB-API* and exposed to the
+end-user either through a whole history view of through an
+immediate undoing link in the message box.
+
+The undo feature is functional but the interface and configuration
+options are still quite reduced. One major improvement would be to
+be able to filter with a finer grain which transactions or actions
+one wants to see in the *undo-history view*. Another critical
+improvement would be to enable the undo feature on a part only of
+the entity-relationship schema to avoid storing too much useless
+data and reduce the underlying overhead.
+
+But both functionality are related to the strong design choice not
+to represent transactions and actions as entities and
+relations. This has huge benefits in terms of safety and conceptual
+simplicity but prevents from using lots of convenient CubicWeb
+features such as *facets* to access undo information.
+
+Before developing further the undo feature or eventually revising
+this design choice, it appears that some return of experience is
+strongly needed. So don't hesitate to try the undo feature in your
+application and send us some feedback.
+
+
+Notes
+~~~~~
+
+.. [1] The end-user Web interface could be improved to enable
+ user to choose whether he wishes to see private actions.
+
+.. [2] There is only five kind of elementary actions (beyond
+ merely accessing data for reading):
+
+ * **C** : creating an entity
+ * **D** : deleting an entity
+ * **U** : updating an entity attributes
+ * **A** : adding a relation
+ * **R** : removing a relation
+
+.. [3] Meaning none of the actions in the transaction is
+ undone. Depending upon the application, it might make sense
+ to enable *partial* undo. That is to say undo in which some
+ actions could not be undo without preventing to undo the
+ others actions in the transaction (as long as it does not
+ break schema integrity). This is not forbidden by the
+ back-end but is deliberately not supported by the front-end
+ (for now at least).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/additional-tips.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,64 @@
+
+.. _Additional Tips:
+
+Backups (mostly with postgresql)
+--------------------------------
+
+It is always a good idea to backup. If your system does not do that,
+you should set it up. Note that whenever you do an upgrade,
+`cubicweb-ctl` offers you to backup your database. There are a number
+of ways for doing backups.
+
+Using postgresql (and only that)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before you
+go ahead, make sure the following permissions are correct ::
+
+ # chgrp postgres /var/lib/cubicweb/backup
+ # chmod g+ws /var/lib/cubicweb/backup
+ # chgrp postgres /etc/cubicweb.d/*<instance>*/sources
+ # chmod g+r /etc/cubicweb.d/*<instance>*/sources
+
+Simply use the pg_dump in a cron installed for `postgres` user on the database server::
+
+ # m h dom mon dow command
+ 0 2 * * * pg_dump -Fc --username=cubicweb --no-owner <instance> > /var/backups/<instance>-$(date '+%Y-%m-%d_%H:%M:%S').dump
+
+Using :command:`cubicweb-ctl db-dump`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The CubicWeb way is to use the :command:`db-dump` command. For that,
+you have to put your passwords in a user-only-readable file at the
+home directory of root user. The file is `.pgpass` (`chmod 0600`), in
+this case for a socket run connection to PostgreSQL ::
+
+ /var/run/postgresql:5432:<instance>:<database user>:<database password>
+
+The postgres documentation for the `.pgpass` format can be found `here`_
+
+Then add the following command to the crontab of the user (`crontab -e`)::
+
+ # m h dom mon dow command
+ 0 2 * * * cubicweb-ctl db-dump <instance>
+
+
+Backup ninja
+~~~~~~~~~~~~
+
+You can use a combination `backup-ninja`_ (which has a postgres script in the
+example directory), `backuppc`)_ (for versionning).
+
+Please note that in the *CubicWeb way* it adds a second location for your
+password which is error-prone.
+
+.. _`here` : http://www.postgresql.org/docs/current/static/libpq-pgpass.html
+.. _`backup-ninja` : https://labs.riseup.net/code/projects/show/backupninja/
+.. _`backuppc` : http://backuppc.sourceforge.net/
+
+.. warning::
+
+ Remember that these indications will fail you whenever you use
+ another database backend than postgres. Also it does properly handle
+ externally managed data such as files (using the Bytes File System
+ Storage).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/config.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,229 @@
+.. -*- coding: utf-8 -*-
+
+.. _ConfigEnv:
+
+Set-up of a *CubicWeb* environment
+==================================
+
+You can `configure the database`_ system of your choice:
+
+ - `PostgreSQL configuration`_
+ - `MySql configuration`_
+ - `SQLServer configuration`_
+ - `SQLite configuration`_
+
+For advanced features, have a look to:
+
+ - `Cubicweb resources configuration`_
+
+.. _`configure the database`: DatabaseInstallation_
+.. _`PostgreSQL configuration`: PostgresqlConfiguration_
+.. _`MySql configuration`: MySqlConfiguration_
+.. _`SQLServer configuration`: SQLServerConfiguration_
+.. _`SQLite configuration`: SQLiteConfiguration_
+.. _`Cubicweb resources configuration`: RessourcesConfiguration_
+
+
+
+.. _RessourcesConfiguration:
+
+Cubicweb resources configuration
+--------------------------------
+
+.. autodocstring:: cubicweb.cwconfig
+
+
+.. _DatabaseInstallation:
+
+Databases configuration
+-----------------------
+
+Each instance can be configured with its own database connection information,
+that will be stored in the instance's :file:`sources` file. The database to use
+will be chosen when creating the instance. CubicWeb is known to run with
+Postgresql (recommended), SQLServer and SQLite, and may run with MySQL.
+
+Other possible sources of data include CubicWeb, Subversion, LDAP and Mercurial,
+but at least one relational database is required for CubicWeb to work. You do
+not need to install a backend that you do not intend to use for one of your
+instances. SQLite is not fit for production use, but it works well for testing
+and ships with Python, which saves installation time when you want to get
+started quickly.
+
+.. _PostgresqlConfiguration:
+
+PostgreSQL
+~~~~~~~~~~
+
+Many Linux distributions ship with the appropriate PostgreSQL packages.
+Basically, you need to install the following packages:
+
+* `postgresql` and `postgresql-client`, which will pull the respective
+ versioned packages (e.g. `postgresql-9.1` and `postgresql-client-9.1`) and,
+ optionally,
+* a `postgresql-plpython-X.Y` package with a version corresponding to that of
+ the aforementioned packages (e.g. `postgresql-plpython-9.1`).
+
+If you run postgres version prior to 8.3, you'll also need the
+`postgresql-contrib-8.X` package for full-text search extension.
+
+If you run postgres on another host than the |cubicweb| repository, you should
+install the `postgresql-client` package on the |cubicweb| host, and others on the
+database host.
+
+For extra details concerning installation, please refer to the `PostgreSQL
+project online documentation`_.
+
+.. _`PostgreSQL project online documentation`: http://www.postgresql.org/docs
+
+
+Database cluster
+++++++++++++++++
+
+If you already have an existing cluster and PostgreSQL server running, you do
+not need to execute the initilization step of your PostgreSQL database unless
+you want a specific cluster for |cubicweb| databases or if your existing
+cluster doesn't use the UTF8 encoding (see note below).
+
+To initialize a PostgreSQL cluster, use the command ``initdb``::
+
+ $ initdb -E UTF8 -D /path/to/pgsql
+
+Notice the encoding specification. This is necessary since |cubicweb| usually
+want UTF8 encoded database. If you use a cluster with the wrong encoding, you'll
+get error like::
+
+ new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)
+ HINT: Use the same encoding as in the template database, or use template0 as template.
+
+Once initialized, start the database server PostgreSQL with the command::
+
+ $ postgres -D /path/to/psql
+
+If you cannot execute this command due to permission issues, please make sure
+that your username has write access on the database. ::
+
+ $ chown username /path/to/pgsql
+
+Database authentication
++++++++++++++++++++++++
+
+The database authentication is configured in `pg_hba.conf`. It can be either set
+to `ident sameuser` or `md5`. If set to `md5`, make sure to use an existing
+user of your database. If set to `ident sameuser`, make sure that your client's
+operating system user name has a matching user in the database. If not, please
+do as follow to create a user::
+
+ $ su
+ $ su - postgres
+ $ createuser -s -P username
+
+The option `-P` (for password prompt), will encrypt the password with the
+method set in the configuration file :file:`pg_hba.conf`. If you do not use this
+option `-P`, then the default value will be null and you will need to set it
+with::
+
+ $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
+
+The above login/password will be requested when you will create an instance with
+`cubicweb-ctl create` to initialize the database of your instance.
+
+Notice that the `cubicweb-ctl db-create` does database initialization that
+may requires a postgres superuser. That's why a login/password is explicitly asked
+at this step, so you can use there a superuser without using this user when running
+the instance. Things that require special privileges at this step:
+
+* database creation, require the 'create database' permission
+* install the plpython extension language (require superuser)
+* install the tsearch extension for postgres version prior to 8.3 (require superuser)
+
+To avoid using a super user each time you create an install, a nice trick is to
+install plpython (and tsearch when needed) on the special `template1` database,
+so they will be installed automatically when cubicweb databases are created
+without even with needs for special access rights. To do so, run ::
+
+ # Installation of plpythonu language by default ::
+ $ createlang -U pgadmin plpythonu template1
+ $ psql -U pgadmin template1
+ template1=# update pg_language set lanpltrusted=TRUE where lanname='plpythonu';
+
+Where `pgadmin` is a postgres superuser. The last command is necessary since by
+default plpython is an 'untrusted' language and as such can't be used by non
+superuser. This update fix that problem by making it trusted.
+
+To install the tsearch plain-text index extension on postgres prior to 8.3, run::
+
+ cat /usr/share/postgresql/8.X/contrib/tsearch2.sql | psql -U username template1
+
+
+.. _MySqlConfiguration:
+
+MySql
+~~~~~
+.. warning::
+ CubicWeb's MySQL support is not commonly used, so things may or may not work properly.
+
+You must add the following lines in ``/etc/mysql/my.cnf`` file::
+
+ transaction-isolation=READ-COMMITTED
+ default-storage-engine=INNODB
+ default-character-set=utf8
+ max_allowed_packet = 128M
+
+.. Note::
+ It is unclear whether mysql supports indexed string of arbitrary length or
+ not.
+
+
+.. _SQLServerConfiguration:
+
+SQLServer
+~~~~~~~~~
+
+As of this writing, support for SQLServer 2005 is functional but incomplete. You
+should be able to connect, create a database and go quite far, but some of the
+SQL generated from RQL queries is still currently not accepted by the
+backend. Porting to SQLServer 2008 is also an item on the backlog.
+
+The `source` configuration file may look like this (specific parts only are
+shown)::
+
+ [system]
+ db-driver=sqlserver2005
+ db-user=someuser
+ # database password not needed
+ #db-password=toto123
+ #db-create/init may ask for a pwd: just say anything
+ db-extra-arguments=Trusted_Connection
+ db-encoding=utf8
+
+
+You need to change the default settings on the database by running::
+
+ ALTER DATABASE <databasename> SET READ_COMMITTED_SNAPSHOT ON;
+
+The ALTER DATABASE command above requires some permissions that your
+user may not have. In that case you will have to ask your local DBA to
+run the query for you.
+
+You can check that the setting is correct by running the following
+query which must return '1'::
+
+ SELECT is_read_committed_snapshot_on
+ FROM sys.databases WHERE name='<databasename>';
+
+
+
+.. _SQLiteConfiguration:
+
+SQLite
+~~~~~~
+
+SQLite has the great advantage of requiring almost no configuration. Simply
+use 'sqlite' as db-driver, and set path to the dabase as db-name. Don't specify
+anything for db-user and db-password, they will be ignore anyway.
+
+.. Note::
+ SQLite is great for testing and to play with cubicweb but is not suited for
+ production environments.
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/create-instance.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,100 @@
+.. -*- coding: utf-8 -*-
+
+Creation of your first instance
+===============================
+
+Instance creation
+-----------------
+
+Now that we created a cube, we can create an instance and access it via a web
+browser. We will use a `all-in-one` configuration to simplify things ::
+
+ cubicweb-ctl create -c all-in-one mycube myinstance
+
+.. note::
+ Please note that we created a new cube for a demo purposes but
+ you could have used an existing cube available in our standard library
+ such as blog or person for example.
+
+A series of questions will be prompted to you, the default answer is usually
+sufficient. You can anyway modify the configuration later on by editing
+configuration files. When a login/password are requested to access the database
+please use the credentials you created at the time you configured the database
+(:ref:`PostgresqlConfiguration`).
+
+It is important to distinguish here the user used to access the database and the
+user used to login to the cubicweb instance. When an instance starts, it uses
+the login/password for the database to get the schema and handle low level
+transaction. But, when :command:`cubicweb-ctl create` asks for a manager
+login/psswd of *CubicWeb*, it refers to the user you will use during the
+development to administrate your web instance. It will be possible, later on,
+to use this user to create other users for your final web instance.
+
+
+Instance administration
+-----------------------
+
+start / stop
+~~~~~~~~~~~~
+
+When this command is completed, the definition of your instance is
+located in :file:`~/etc/cubicweb.d/myinstance/*`. To launch it, you
+just type ::
+
+ cubicweb-ctl start -D myinstance
+
+The option `-D` specifies the *debug mode* : the instance is not
+running in server mode and does not disconnect from the terminal,
+which simplifies debugging in case the instance is not properly
+launched. You can see how it looks by visiting the URL
+`http://localhost:8080` (the port number depends of your
+configuration). To login, please use the cubicweb administrator
+login/password you defined when you created the instance.
+
+To shutdown the instance, Crtl-C in the terminal window is enough.
+If you did not use the option `-D`, then type ::
+
+ cubicweb-ctl stop myinstance
+
+This is it! All is settled down to start developping your data model...
+
+.. note::
+
+ The output of `cubicweb-ctl start -D myinstance` can be
+ overwhelming. It is possible to reduce the log level with the
+ `--loglevel` parameter as in `cubicweb-ctl start -D myinstance -l
+ info` to filter out all logs under `info` gravity.
+
+upgrade
+~~~~~~~
+
+A manual upgrade step is necessary whenever a new version of CubicWeb or
+a cube is installed, in order to synchronise the instance's
+configuration and schema with the new code. The command is::
+
+ cubicweb-ctl upgrade myinstance
+
+A series of questions will be asked. It always starts with a proposal
+to make a backup of your sources (where it applies). Unless you know
+exactly what you are doing (i.e. typically fiddling in debug mode, but
+definitely NOT migrating a production instance), you should answer YES
+to that.
+
+The remaining questions concern the migration steps of |cubicweb|,
+then of the cubes that form the whole application, in reverse
+dependency order.
+
+In principle, if the migration scripts have been properly written and
+tested, you should answer YES to all questions.
+
+Somtimes, typically while debugging a migration script, something goes
+wrong and the migration fails. Unfortunately the databse may be in an
+incoherent state. You have two options here:
+
+* fix the bug, restore the database and restart the migration process
+ from scratch (quite recommended in a production environement)
+
+* try to replay the migration up to the last successful commit, that
+ is answering NO to all questions up to the step that failed, and
+ finish by answering YES to the remaining questions.
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/cubicweb-ctl.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,111 @@
+.. -*- coding: utf-8 -*-
+
+.. _cubicweb-ctl:
+
+``cubicweb-ctl`` tool
+=====================
+
+`cubicweb-ctl` is the swiss knife to manage *CubicWeb* instances.
+The general syntax is ::
+
+ cubicweb-ctl <command> [options command] <arguments commands>
+
+To view available commands ::
+
+ cubicweb-ctl
+ cubicweb-ctl --help
+
+Please note that the commands available depends on the *CubicWeb* packages
+and cubes that have been installed.
+
+To view the help menu on specific command ::
+
+ cubicweb-ctl <command> --help
+
+Listing available cubes and instance
+-------------------------------------
+
+* ``list``, provides a list of the available configuration, cubes
+ and instances.
+
+
+Creation of a new cube
+-----------------------
+
+Create your new cube cube ::
+
+ cubicweb-ctl newcube
+
+This will create a new cube in
+``/path/to/grshell-cubicweb/cubes/<mycube>`` for a Mercurial
+installation, or in ``/usr/share/cubicweb/cubes`` for a debian
+packages installation.
+
+Create an instance
+-------------------
+
+You must ensure `~/etc/cubicweb.d/` exists prior to this. On windows, the
+'~' part will probably expand to 'Documents and Settings/user'.
+
+To create an instance from an existing cube, execute the following
+command ::
+
+ cubicweb-ctl create <cube_name> <instance_name>
+
+This command will create the configuration files of an instance in
+``~/etc/cubicweb.d/<instance_name>``.
+
+The tool ``cubicweb-ctl`` executes the command ``db-create`` and
+``db-init`` when you run ``create`` so that you can complete an
+instance creation in a single command. But of course it is possible
+to issue these separate commands separately, at a later stage.
+
+Command to create/initialize an instance database
+-------------------------------------------------
+
+* ``db-create``, creates the system database of an instance (tables and
+ extensions only)
+* ``db-init``, initializes the system database of an instance
+ (schema, groups, users, workflows...)
+
+Commands to control instances
+-----------------------------
+
+* ``start``, starts one or more or all instances
+
+of special interest::
+
+ start -D
+
+will start in debug mode (under windows, starting without -D will not
+work; you need instead to setup your instance as a service).
+
+* ``stop``, stops one or more or all instances
+* ``restart``, restarts one or more or all instances
+* ``status``, returns the status of the instance(s)
+
+Commands to maintain instances
+------------------------------
+
+* ``upgrade``, launches the existing instances migration when a new version
+ of *CubicWeb* or the cubes installed is available
+* ``shell``, opens a (Python based) migration shell for manual maintenance of the instance
+* ``db-dump``, creates a dump of the system database
+* ``db-restore``, restores a dump of the system database
+* ``db-check``, checks data integrity of an instance. If the automatic correction
+ is activated, it is recommanded to create a dump before this operation.
+* ``schema-sync``, synchronizes the persistent schema of an instance with
+ the instance schema. It is recommanded to create a dump before this operation.
+
+Commands to maintain i18n catalogs
+----------------------------------
+* ``i18ncubicweb``, regenerates messages catalogs of the *CubicWeb* library
+* ``i18ncube``, regenerates the messages catalogs of a cube
+* ``i18ninstance``, recompiles the messages catalogs of an instance.
+ This is automatically done while upgrading.
+
+See also chapter :ref:`internationalization`.
+
+Other commands
+--------------
+* ``delete``, deletes an instance (configuration files and database)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,28 @@
+.. -*- coding: utf-8 -*-
+
+.. _Part3:
+
+--------------
+Administration
+--------------
+
+This part is for installation and administration of the *CubicWeb* framework and
+instances based on that framework.
+
+.. toctree::
+ :maxdepth: 1
+ :numbered:
+
+ setup
+ setup-windows
+ config
+ cubicweb-ctl
+ create-instance
+ instance-config
+ site-config
+ multisources
+ ldap
+ migration
+ additional-tips
+ rql-logs
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/instance-config.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,200 @@
+.. -*- coding: utf-8 -*-
+
+
+Configure an instance
+=====================
+
+While creating an instance, a configuration file is generated in::
+
+ $ (CW_INSTANCES_DIR) / <instance> / <configuration name>.conf
+
+For example::
+
+ /etc/cubicweb.d/myblog/all-in-one.conf
+
+It is a simple text file in the INI format
+(http://en.wikipedia.org/wiki/INI_file). In the following description,
+each option name is prefixed with its own section and followed by its
+default value if necessary, e.g. "`<section>.<option>` [value]."
+
+.. _`WebServerConfig`:
+
+Configuring the Web server
+--------------------------
+:`web.auth-model` [cookie]:
+ authentication mode, cookie or http
+:`web.realm`:
+ realm of the instance in http authentication mode
+:`web.http-session-time` [0]:
+ period of inactivity of an HTTP session before it closes automatically.
+ Duration in seconds, 0 meaning no expiration (or more exactly at the
+ closing of the browser client)
+
+:`main.anonymous-user`, `main.anonymous-password`:
+ login and password to use to connect to the RQL server with
+ HTTP anonymous connection. CWUser account should exist.
+
+:`main.base-url`:
+ url base site to be used to generate the urls of web pages
+
+Https configuration
+```````````````````
+It is possible to make a site accessible for anonymous http connections
+and https for authenticated users. This requires to
+use apache (for example) for redirection and the variable `main.https-url`
+of configuration file.
+
+For this to work you have to activate the following apache modules :
+
+* rewrite
+* proxy
+* http_proxy
+
+The command on Debian based systems for that is ::
+
+ a2enmod rewrite http_proxy proxy
+ /etc/init.d/apache2 restart
+
+:Example:
+
+ For an apache redirection of a site accessible via `http://localhost/demo`
+ and `https://localhost/demo` and actually running on port 8080, it
+ takes to the http:::
+
+ ProxyPreserveHost On
+ RewriteEngine On
+ RewriteCond %{REQUEST_URI} ^/demo
+ RewriteRule ^/demo$ /demo/
+ RewriteRule ^/demo/(.*) http://127.0.0.1:8080/$1 [L,P]
+
+ and for the https:::
+
+ ProxyPreserveHost On
+ RewriteEngine On
+ RewriteCond %{REQUEST_URI} ^/ demo
+ RewriteRule ^/demo$/demo/
+ RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]
+
+
+ and we will file in the all-in-one.conf of the instance:::
+
+ base-url = http://localhost/demo
+ https-url = https://localhost/demo
+
+Notice that if you simply want a site accessible through https, not *both* http
+and https, simply set `base-url` to the https url and the first section into your
+apache configuration (as you would have to do for an http configuration with an
+apache front-end).
+
+Setting up the web client
+-------------------------
+:`web.embed-allowed`:
+ regular expression matching sites which could be "embedded" in
+ the site (controllers 'embed')
+:`web.submit-url`:
+ url where the bugs encountered in the instance can be mailed to
+
+
+RQL server configuration
+------------------------
+:`main.host`:
+ host name if it can not be detected correctly
+:`main.pid-file`:
+ file where will be written the server pid
+:`main.uid`:
+ user account to use for launching the server when it is
+ root launched by init
+:`main.session-time [30*60]`:
+ timeout of a RQL session
+:`main.query-log-file`:
+ file where all requests RQL executed by the server are written
+
+
+Configuring e-mail
+------------------
+RQL and web server side:
+
+:`email.mangle-mails [no]`:
+ indicates whether the email addresses must be displayed as is or
+ transformed
+
+RQL server side:
+
+:`email.smtp-host [mail]`:
+ hostname hosting the SMTP server to use for outgoing mail
+:`email.smtp-port [25]`:
+ SMTP server port to use for outgoing mail
+:`email.sender-name`:
+ name to use for outgoing mail of the instance
+:`email.sender-addr`:
+ address for outgoing mail of the instance
+:`email.default dest-addrs`:
+ destination addresses by default, if used by the configuration of the
+ dissemination of the model (separated by commas)
+:`email.supervising-addrs`:
+ destination addresses of e-mails of supervision (separated by
+ commas)
+
+
+Configuring logging
+-------------------
+:`main.log-threshold`:
+ level of filtering messages (DEBUG, INFO, WARNING, ERROR)
+:`main.log-file`:
+ file to write messages
+
+
+.. _PersistentProperties:
+
+Configuring persistent properties
+---------------------------------
+Other configuration settings are in the form of entities `CWProperty`
+in the database. It must be edited via the web interface or by
+RQL queries.
+
+:`ui.encoding`:
+ Character encoding to use for the web
+:`navigation.short-line-size`:
+ number of characters for "short" display
+:`navigation.page-size`:
+ maximum number of entities to show per results page
+:`navigation.related-limit`:
+ number of related entities to show up on primary entity view
+:`navigation.combobox-limit`:
+ number of entities unrelated to show up on the drop-down lists of
+ the sight on an editing entity view
+
+Cross-Origin Resource Sharing
+-----------------------------
+
+CubicWeb provides some support for the CORS_ protocol. For now, the
+provided implementation only deals with access to a CubicWeb instance
+as a whole. Support for a finer granularity may be considered in the
+future.
+
+Specificities of the provided implementation:
+
+- ``Access-Control-Allow-Credentials`` is always true
+- ``Access-Control-Allow-Origin`` header in response will never be
+ ``*``
+- ``Access-Control-Expose-Headers`` can be configured globally (see below)
+- ``Access-Control-Max-Age`` can be configured globally (see below)
+- ``Access-Control-Allow-Methods`` can be configured globally (see below)
+- ``Access-Control-Allow-Headers`` can be configured globally (see below)
+
+
+A few parameters can be set to configure the CORS_ capabilities of CubicWeb.
+
+.. _CORS: http://www.w3.org/TR/cors/
+
+:`access-control-allow-origin`:
+ comma-separated list of allowed origin domains or "*" for any domain
+:`access-control-allow-methods`:
+ comma-separated list of allowed HTTP methods
+:`access-control-max-age`:
+ maximum age of cross-origin resource sharing (in seconds)
+:`access-control-allow-headers`:
+ comma-separated list of allowed HTTP custom headers (used in simple requests)
+:`access-control-expose-headers`:
+ comma-separated list of allowed HTTP custom headers (used in preflight requests)
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/ldap.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,134 @@
+.. _LDAP:
+
+LDAP integration
+================
+
+Overview
+--------
+
+Using LDAP as a source for user credentials and information is quite
+easy. The most difficult part lies in building an LDAP schema or
+using an existing one.
+
+At cube creation time, one is asked if more sources are wanted. LDAP
+is one possible option at this time. Of course, it is always possible
+to set it up later using the `CWSource` entity type, which we discuss
+there.
+
+It is possible to add as many LDAP sources as wanted, which translates
+in as many `CWSource` entities as needed.
+
+The general principle of the LDAP source is, given a proper
+configuration, to create local users matching the users available in
+the directory and deriving local user attributes from directory users
+attributes. Then a periodic task ensures local user information
+synchronization with the directory.
+
+Users handled by such a source should not be edited directly from
+within the application instance itself. Rather, updates should happen
+at the LDAP server level.
+
+Credential checks are _always_ done against the LDAP server.
+
+.. Note::
+
+ There are currently two ldap source types: the older `ldapuser` and
+ the newer `ldapfeed`. The older will be deprecated anytime soon, as
+ the newer has now gained all the features of the old and does not
+ suffer from some of its illnesses.
+
+ The ldapfeed creates real `CWUser` entities, and then
+ activate/deactivate them depending on their presence/absence in the
+ corresponding LDAP source. Their attribute and state
+ (activated/deactivated) are hence managed by the source mechanism;
+ they should not be altered by other means (as such alterations may
+ be overridden in some subsequent source synchronisation).
+
+
+Configuration of an LDAPfeed source
+-----------------------------------
+
+Additional sources are created at cube creation time or later through the
+user interface.
+
+Configure an `ldapfeed` source from the user interface under `Manage` then
+`data sources`:
+
+* At this point `type` has been set to `ldapfeed`.
+
+* The `parser` attribute shall be set to `ldapfeed`.
+
+* The `url` attribute shall be set to an URL such as ldap://ldapserver.domain/.
+
+* The `configuration` attribute contains many options. They are described in
+ detail in the next paragraph.
+
+
+Options of an LDAPfeed source
+-----------------------------
+
+Let us enumerate the options by categories (LDAP server connection,
+LDAP schema mapping information).
+
+LDAP server connection options:
+
+* `auth-mode`, (choices are simple, cram_md5, digest_md5, gssapi, support
+ for the later being partial as of now)
+
+* `auth-realm`, realm to use when using gssapi/kerberos authentication
+
+* `data-cnx-dn`, user dn to use to open data connection to the ldap (eg
+ used to respond to rql queries)
+
+* `data-cnx-password`, password to use to open data connection to the
+ ldap (eg used to respond to rql queries)
+
+If the LDAP server accepts anonymous binds, then it is possible to
+leave data-cnx-dn and data-cnx-password empty. This is, however, quite
+unlikely in practice. Beware that the LDAP server might hide attributes
+such as "userPassword" while the rest of the attributes remain visible
+through an anonymous binding.
+
+LDAP schema mapping options:
+
+* `user-base-dn`, base DN to lookup for users
+
+* `user-scope`, user search scope (valid values: "BASE", "ONELEVEL",
+ "SUBTREE")
+
+* `user-classes`, classes of user (with Active Directory, you want to
+ say "user" here)
+
+* `user-filter`, additional filters to be set in the ldap query to
+ find valid users
+
+* `user-login-attr`, attribute used as login on authentication (with
+ Active Directory, you want to use "sAMAccountName" here)
+
+* `user-default-group`, name of a group in which ldap users will be by
+ default. You can set multiple groups by separating them by a comma
+
+* `user-attrs-map`, map from ldap user attributes to cubicweb
+ attributes (with Active Directory, you want to use
+ sAMAccountName:login,mail:email,givenName:firstname,sn:surname)
+
+
+Other notes
+-----------
+
+* Cubicweb is able to start if ldap cannot be reached, even on
+ cubicweb-ctl start ... If some source ldap server cannot be used
+ while an instance is running, the corresponding users won't be
+ authenticated but their status will not change (e.g. they will not
+ be deactivated)
+
+* The user-base-dn is a key that helps cubicweb map CWUsers to LDAP
+ users: beware updating it
+
+* When a user is removed from an LDAP source, it is deactivated in the
+ CubicWeb instance; when a deactivated user comes back in the LDAP
+ source, it (automatically) is activated again
+
+* You can use the :class:`CWSourceHostConfig` to have variants for a source
+ configuration according to the host the instance is running on. To do so
+ go on the source's view from the sources management view.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/migration.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,38 @@
+.. -*- coding: utf-8 -*-
+
+Migrating cubicweb instances - benefits from a distributed architecture
+=======================================================================
+
+Migrate apache & cubicweb
+-------------------------
+
+**Aim** : do the migration for N cubicweb instances hosted on a server to another with no downtime.
+
+**Prerequisites** : have an explicit definition of the database host (not default or localhost). In our case, the database is hosted on another host.
+
+**Steps** :
+
+1. *on new machine* : install your environment (*pseudocode*) ::
+
+ apt-get install cubicweb cubicweb-applications apache2
+
+2. *on old machine* : copy your cubicweb and apache configuration to the new machine ::
+
+ scp /etc/cubicweb.d/ newmachine:/etc/cubicweb.d/
+ scp /etc/apache2/sites-available/ newmachine:/etc/apache2/sites-available/
+
+3. *on new machine* : start your instances ::
+
+ cubicweb start
+
+4. *on new machine* : enable sites and modules for apache and start it, test it using by modifying your /etc/host file.
+
+5. change dns entry from your oldmachine to newmachine
+
+6. shutdown your *old machine* (if it doesn't host other services or your database)
+
+7. That's it.
+
+**Possible enhancements** : use right from the start a pound server behind your apache, that way you can add backends and smoothily migrate by shuting down backends that pound will take into account.
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/multisources.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,6 @@
+Multiple sources of data
+========================
+
+Data sources include SQL, LDAP, RQL, mercurial and subversion.
+
+.. XXX feed me
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/rql-logs.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,14 @@
+.. -*- coding: utf-8 -*-
+
+RQL logs
+========
+
+You can configure the *CubicWeb* instance to keep a log
+of the queries executed against your database. To do so,
+edit the configuration file of your instance
+``.../etc/cubicweb.d/myapp/all-in-one.conf`` and uncomment the
+variable ``query-log-file``::
+
+ # web instance query log file
+ query-log-file=/tmp/rql-myapp.log
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/setup-windows.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,146 @@
+.. -*- coding: utf-8 -*-
+
+.. _SetUpWindowsEnv:
+
+Installing a development environement on Windows
+================================================
+
+Setting up a Windows development environment is not too complicated
+but it requires a series of small steps.
+
+We propose an example of a typical |cubicweb| installation on Windows
+from sources. We assume everything goes into ``C:\\`` and for any
+package, without version specification, "the latest is
+the greatest".
+
+Mind that adjusting the installation drive should be straightforward.
+
+
+
+Install the required elements
+-----------------------------
+
+|cubicweb| requires some base elements that must be installed to run
+correctly. So, first of all, you must install them :
+
+* python >= 2.6 and < 3
+ (`Download Python <http://www.python.org/download/>`_).
+ You can also consider the Python(x,y) distribution
+ (`Download Python(x,y) <http://code.google.com/p/pythonxy/wiki/Downloads>`_)
+ as it makes things easier for Windows user by wrapping in a single installer
+ python 2.7 plus numerous useful third-party modules and
+ applications (including Eclipse + pydev, which is an arguably good
+ IDE for Python under Windows).
+
+* `Twisted <http://twistedmatrix.com/trac/>`_ is an event-driven
+ networking engine
+ (`Download Twisted <http://twistedmatrix.com/trac/>`_)
+
+* `lxml <http://codespeak.net/lxml/>`_ library
+ (version >=2.2.1) allows working with XML and HTML
+ (`Download lxml <http://pypi.python.org/pypi/lxml/2.2.1>`_)
+
+* `Postgresql <http://www.postgresql.org/>`_,
+ an object-relational database system
+ (`Download Postgresql <http://www.enterprisedb.com/products/pgdownload.do#windows>`_)
+ and its python drivers
+ (`Download psycopg <http://www.stickpeople.com/projects/python/win-psycopg/#Version2>`_)
+
+* A recent version of `gettext`
+ (`Download gettext <http://download.logilab.org/pub/gettext/gettext-0.17-win32-setup.exe>`_).
+
+* `rql <http://www.logilab.org/project/rql>`_,
+ the recent version of the Relationship Query Language parser.
+
+Install optional elements
+-------------------------
+
+We recommend you to install the following elements. They are not
+mandatory but they activate very interesting features in |cubicweb|:
+
+* `python-ldap <http://pypi.python.org/pypi/python-ldap>`_
+ provides access to LDAP/Active directory directories
+ (`Download python-ldap <http://www.osuch.org/python-ldap>`_).
+
+* `graphviz <http://www.graphviz.org/>`_
+ which allow schema drawings.
+ (`Download graphviz <http://www.graphviz.org/Download_windows.php>`_).
+ It is quite recommended (albeit not mandatory).
+
+Other elements will activate more features once installed. Take a look
+at :ref:`InstallDependencies`.
+
+Useful tools
+------------
+
+Some additional tools could be useful to develop :ref:`cubes <AvailableCubes>`
+with the framework.
+
+* `mercurial <http://mercurial.selenic.com/>`_ and its standard windows GUI
+ (`TortoiseHG <http://tortoisehg.bitbucket.org/>`_) allow you to get the source
+ code of |cubicweb| from control version repositories. So you will be able to
+ get the latest development version and pre-release bugfixes in an easy way
+ (`Download mercurial <http://bitbucket.org/tortoisehg/stable/wiki/download>`_).
+
+* You can also consider the ssh client `Putty` in order to peruse
+ mercurial over ssh (`Download <http://www.putty.org/>`_).
+
+* If you are an Eclipse user, mercurial can be integrated using the
+ `MercurialEclipse` plugin
+ (`Home page <http://www.vectrace.com/mercurialeclipse/>`_).
+
+Getting the sources
+-------------------
+
+There are two ways to get the sources of |cubicweb| and its
+:ref:`cubes <AvailableCubes>`:
+
+* download the latest release (:ref:`SourceInstallation`)
+* get the development version using Mercurial
+ (:ref:`MercurialInstallation`)
+
+Environment variables
+---------------------
+
+You will need some convenience environment variables once all is set up. These
+variables are settable through the GUI by getting at the `System properties`
+window (by righ-clicking on `My Computer` -> `properties`).
+
+In the `advanced` tab, there is an `Environment variables` button. Click on
+it. That opens a small window allowing edition of user-related and system-wide
+variables.
+
+We will consider only user variables. First, the ``PATH`` variable. Assuming
+you are logged as user *Jane*, add the following paths, separated by
+semi-colons::
+
+ C:\Documents and Settings\Jane\My Documents\Python\cubicweb\cubicweb\bin
+ C:\Program Files\Graphviz2.24\bin
+
+The ``PYTHONPATH`` variable should also contain::
+
+ C:\Documents and Settings\Jane\My Documents\Python\cubicweb\
+
+From now, on a fresh `cmd` shell, you should be able to type::
+
+ cubicweb-ctl list
+
+... and get a meaningful output.
+
+Running an instance as a service
+--------------------------------
+
+This currently assumes that the instances configurations is located at
+``C:\\etc\\cubicweb.d``. For a cube 'my_instance', you will find
+``C:\\etc\\cubicweb.d\\my_instance\\win32svc.py``.
+
+Now, register your instance as a windows service with::
+
+ win32svc install
+
+Then start the service with::
+
+ net start cubicweb-my_instance
+
+In case this does not work, you should be able to see error reports in
+the application log, using the windows event log viewer.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/setup.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,239 @@
+.. -*- coding: utf-8 -*-
+
+.. _SetUpEnv:
+
+Installation of a *CubicWeb* environment
+========================================
+
+Official releases are available from the `CubicWeb.org forge`_ and from
+`PyPI`_. Since CubicWeb is developed using `Agile software development
+<http://en.wikipedia.org/wiki/Agile_software_development>`_ techniques, releases
+happen frequently. In a version numbered X.Y.Z, X changes after a few years when
+the API breaks, Y changes after a few weeks when features are added and Z
+changes after a few days when bugs are fixed.
+
+Depending on your needs, you will chose a different way to install CubicWeb on
+your system:
+
+- `Installation on Debian/Ubuntu`_
+- `Installation on Windows`_
+- `Installation in a virtualenv`_
+- `Installation with pip`_
+- `Installation with easy_install`_
+- `Installation from tarball`_
+
+If you are a power-user and need the very latest features, you will
+
+- `Install from version control`_
+
+Once the software is installed, move on to :ref:`ConfigEnv` for better control
+and advanced features of |cubicweb|.
+
+.. _`Installation on Debian/Ubuntu`: DebianInstallation_
+.. _`Installation on Windows`: WindowsInstallation_
+.. _`Installation in a virtualenv`: VirtualenvInstallation_
+.. _`Installation with pip`: PipInstallation_
+.. _`Installation with easy_install`: EasyInstallInstallation_
+.. _`Installation from tarball`: TarballInstallation_
+.. _`Install from version control`: MercurialInstallation_
+
+
+.. _DebianInstallation:
+
+Debian/Ubuntu install
+---------------------
+
+|cubicweb| is packaged for Debian/Ubuntu (and derived
+distributions). Their integrated package-management system make
+installation and upgrade much easier for users since
+dependencies (like databases) are automatically installed.
+
+Depending on the distribution you are using, add the appropriate line to your
+`list of sources` (for example by editing ``/etc/apt/sources.list``), replacing
+``<release>`` with e.g. ``wheezy`` or ``trusty``::
+
+ deb http://download.logilab.org/production/ <release>/
+
+The repositories are signed with `Logilab's gnupg key`_. You can download
+and register the key to avoid warnings::
+
+ wget -O/etc/apt/trusted.gpg.d/logilab.gpg https://www.logilab.fr/logilab-debian-keyring.gpg
+
+Update your list of packages and perform the installation::
+
+ apt-get update
+ apt-get install cubicweb cubicweb-dev
+
+``cubicweb`` installs the framework itself, allowing you to create new
+instances. ``cubicweb-dev`` installs the development environment
+allowing you to develop new cubes.
+
+There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
+list of available cubes using ``apt-cache search cubicweb`` or at the
+`CubicWeb.org forge`_.
+
+.. note::
+
+ `cubicweb-dev` will install basic sqlite support. You can easily setup
+ :ref:`cubicweb with other database <DatabaseInstallation>` using the following
+ virtual packages :
+
+ * `cubicweb-postgresql-support` contains the necessary dependencies for
+ using :ref:`cubicweb with postgresql datatabase <PostgresqlConfiguration>`
+
+ * `cubicweb-mysql-support` contains the necessary dependencies for using
+ :ref:`cubicweb with mysql database <MySqlConfiguration>`.
+
+.. _`list of sources`: http://wiki.debian.org/SourcesList
+.. _`Logilab's gnupg key`: https://www.logilab.fr/logilab-debian-keyring.gpg
+.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
+
+.. _WindowsInstallation:
+
+Windows Install
+---------------
+
+You need to have `python`_ version >= 2.5 and < 3 installed.
+
+If you want an automated install, your best option is probably the
+:ref:`EasyInstallInstallation`. EasyInstall is a tool that helps users to
+install python packages along with their dependencies, searching for suitable
+pre-compiled binaries on the `The Python Package Index`_.
+
+If you want better control over the process as well as a suitable development
+environment or if you are having problems with `easy_install`, read on to
+:ref:`SetUpWindowsEnv`.
+
+.. _python: http://www.python.org/
+.. _`The Python Package Index`: http://pypi.python.org
+
+.. _VirtualenvInstallation:
+
+`Virtualenv` install
+--------------------
+
+|cubicweb| can be safely installed, used and contained inside a
+`virtualenv`_. You can use either :ref:`pip <PipInstallation>` or
+:ref:`easy_install <EasyInstallInstallation>` to install |cubicweb|
+inside an activated virtual environment.
+
+.. _PipInstallation:
+
+`pip` install
+-------------
+
+`pip <http://pip.openplans.org/>`_ is a python tool that helps downloading,
+building, installing, and managing Python packages and their dependencies. It
+is fully compatible with `virtualenv`_ and installs the packages from sources
+published on the `The Python Package Index`_.
+
+.. _`virtualenv`: http://virtualenv.openplans.org/
+
+A working compilation chain is needed to build the modules that include C
+extensions. If you really do not want to compile anything, installing `lxml <http://lxml.de/>`_,
+`Twisted Web <http://twistedmatrix.com/trac/wiki/Downloads/>`_ and `libgecode
+<http://www.gecode.org/>`_ will help.
+
+For Debian, these minimal dependencies can be obtained by doing::
+
+ apt-get install gcc python-pip python-dev python-lxml
+
+or, if you prefer to get as much as possible from pip::
+
+ apt-get install gcc python-pip python-dev libxslt1-dev libxml2-dev
+
+For Windows, you can install pre-built packages (possible `source
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/>`_). For a minimal setup, install:
+
+- pip http://www.lfd.uci.edu/~gohlke/pythonlibs/#pip
+- setuptools http://www.lfd.uci.edu/~gohlke/pythonlibs/#setuptools
+- libxml-python http://www.lfd.uci.edu/~gohlke/pythonlibs/#libxml-python>
+- lxml http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml and
+- twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
+
+Make sure to choose the correct architecture and version of Python.
+
+Finally, install |cubicweb| and its dependencies, by running::
+
+ pip install cubicweb
+
+Many other :ref:`cubes <AvailableCubes>` are available. A list is available at
+`PyPI <http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
+or at the `CubicWeb.org forge`_.
+
+For example, installing the *blog cube* is achieved by::
+
+ pip install cubicweb-blog
+
+.. _EasyInstallInstallation:
+
+`easy_install` install
+----------------------
+
+.. note::
+
+ If you are not a Windows user and you have a compilation environment, we
+ recommend you to use the PipInstallation_.
+
+`easy_install`_ is a python utility that helps downloading, installing, and
+managing python packages and their dependencies.
+
+Install |cubicweb| and its dependencies, run::
+
+ easy_install cubicweb
+
+There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
+list of available cubes on `PyPI
+<http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
+or at the `CubicWeb.org Forge`_.
+
+For example, installing the *blog cube* is achieved by::
+
+ easy_install cubicweb-blog
+
+.. note::
+
+ If you encounter problem with :ref:`cubes <AvailableCubes>` installation,
+ consider using :ref:`PipInstallation` which is more stable
+ but can not installed pre-compiled binaries.
+
+.. _`easy_install`: http://packages.python.org/distribute/easy_install.html
+
+
+.. _SourceInstallation:
+
+Install from source
+-------------------
+
+.. _TarballInstallation:
+
+You can download the archive containing the sources from
+`http://download.logilab.org/pub/cubicweb/ <http://download.logilab.org/pub/cubicweb/>`_.
+
+Make sure you also have all the :ref:`InstallDependencies`.
+
+Once uncompressed, you can install the framework from inside the uncompressed
+folder with::
+
+ python setup.py install
+
+Or you can run |cubicweb| directly from the source directory by
+setting the :ref:`resource mode <RessourcesConfiguration>` to `user`. This will
+ease the development with the framework.
+
+There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
+list of availble cubes at the `CubicWeb.org Forge`_.
+
+
+.. _MercurialInstallation:
+
+Install from version control system
+-----------------------------------
+
+To keep-up with on-going development, clone the :ref:`Mercurial
+<MercurialPresentation>` repository::
+
+ hg clone -u 'last(tag())' http://hg.logilab.org/cubicweb # stable version
+ hg clone http://hg.logilab.org/cubicweb # development branch
+
+Make sure you also have all the :ref:`InstallDependencies`.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/admin/site-config.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,94 @@
+.. -*- coding: utf-8 -*-
+
+User interface for web site configuration
+=========================================
+
+.. image:: ../../images/lax-book_03-site-config-panel_en.png
+
+This panel allows you to configure the appearance of your instance site.
+Six menus are available and we will go through each of them to explain how
+to use them.
+
+Navigation
+~~~~~~~~~~
+This menu provides you a way to adjust some navigation options depending on
+your needs, such as the number of entities to display by page of results.
+Follows the detailled list of available options :
+
+* navigation.combobox-limit : maximum number of entities to display in related
+ combo box (sample format: 23)
+* navigation.page-size : maximum number of objects displayed by page of results
+ (sample format: 23)
+* navigation.related-limit : maximum number of related entities to display in
+ the primary view (sample format: 23)
+* navigation.short-line-size : maximum number of characters in short description
+ (sample format: 23)
+
+UI
+~~
+This menu provides you a way to customize the user interface settings such as
+date format or encoding in the produced html.
+Follows the detailled list of available options :
+
+* ui.date-format : how to format date in the ui ("man strftime" for format description)
+* ui.datetime-format : how to format date and time in the ui ("man strftime" for format
+ description)
+* ui.default-text-format : default text format for rich text fields.
+* ui.encoding : user interface encoding
+* ui.fckeditor :should html fields being edited using fckeditor (a HTML WYSIWYG editor).
+ You should also select text/html as default text format to actually get fckeditor.
+* ui.float-format : how to format float numbers in the ui
+* ui.language : language of the user interface
+* ui.main-template : id of main template used to render pages
+* ui.site-title : site title, which is displayed right next to the logo in the header
+* ui.time-format : how to format time in the ui ("man strftime" for format description)
+
+
+Actions
+~~~~~~~
+This menu provides a way to configure the context in which you expect the actions
+to be displayed to the user and if you want the action to be visible or not.
+You must have notice that when you view a list of entities, an action box is
+available on the left column which display some actions as well as a drop-down
+menu for more actions.
+
+The context available are :
+
+* mainactions : actions listed in the left box
+* moreactions : actions listed in the `more` menu of the left box
+* addrelated : add actions listed in the left box
+* useractions : actions listed in the first section of drop-down menu
+ accessible from the right corner user login link
+* siteactions : actions listed in the second section of drop-down menu
+ accessible from the right corner user login link
+* hidden : select this to hide the specific action
+
+Boxes
+~~~~~
+The instance has already a pre-defined set of boxes you can use right away.
+This configuration section allows you to place those boxes where you want in the
+instance interface to customize it.
+
+The available boxes are :
+
+* actions box : box listing the applicable actions on the displayed data
+
+* boxes_blog_archives_box : box listing the blog archives
+
+* possible views box : box listing the possible views for the displayed data
+
+* rss box : RSS icon to get displayed data as a RSS thread
+
+* search box : search box
+
+* startup views box : box listing the configuration options available for
+ the instance site, such as `Preferences` and `Site Configuration`
+
+Components
+~~~~~~~~~~
+[WRITE ME]
+
+Contextual components
+~~~~~~~~~~~~~~~~~~~~~
+[WRITE ME]
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/depends.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,58 @@
+.. -*- coding: utf-8 -*-
+
+.. _InstallDependencies:
+
+Installation dependencies
+=========================
+
+When you run CubicWeb from source, either by downloading the tarball or
+cloning the mercurial tree, here is the list of tools and libraries you need
+to have installed in order for CubicWeb to work:
+
+* yapps - http://theory.stanford.edu/~amitp/yapps/ -
+ http://pypi.python.org/pypi/Yapps2
+
+* pygraphviz - http://networkx.lanl.gov/pygraphviz/ -
+ http://pypi.python.org/pypi/pygraphviz
+
+* docutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
+
+* lxml - http://codespeak.net/lxml - http://pypi.python.org/pypi/lxml
+
+* twisted - http://twistedmatrix.com/ - http://pypi.python.org/pypi/Twisted
+
+* logilab-common - http://www.logilab.org/project/logilab-common -
+ http://pypi.python.org/pypi/logilab-common/
+
+* logilab-database - http://www.logilab.org/project/logilab-database -
+ http://pypi.python.org/pypi/logilab-database/
+
+* logilab-constraint - http://www.logilab.org/project/logilab-constraint -
+ http://pypi.python.org/pypi/constraint/
+
+* logilab-mtconverter - http://www.logilab.org/project/logilab-mtconverter -
+ http://pypi.python.org/pypi/logilab-mtconverter
+
+* rql - http://www.logilab.org/project/rql - http://pypi.python.org/pypi/rql
+
+* yams - http://www.logilab.org/project/yams - http://pypi.python.org/pypi/yams
+
+* indexer - http://www.logilab.org/project/indexer -
+ http://pypi.python.org/pypi/indexer
+
+* passlib - https://code.google.com/p/passlib/ -
+ http://pypi.python.org/pypi/passlib
+
+If you're using a Postgresql database (recommended):
+
+* psycopg2 - http://initd.org/projects/psycopg2 - http://pypi.python.org/pypi/psycopg2
+* plpythonu extension
+
+Other optional packages:
+
+* fyzz - http://www.logilab.org/project/fyzz -
+ http://pypi.python.org/pypi/fyzz *to activate Sparql querying*
+
+
+Any help with the packaging of CubicWeb for more than Debian/Ubuntu (including
+eggs, buildouts, etc) will be greatly appreciated.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/docstrings-conventions.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,106 @@
+Javascript docstrings
+=====================
+
+Whereas in Python source code we only need to include a module docstrings
+using the directive `.. automodule:: mypythonmodule`, we will have to
+explicitely define Javascript modules and functions in the doctrings since
+there is no native directive to include Javascript files.
+
+Rest generation
+---------------
+
+`pyjsrest` is a small utility parsing Javascript doctrings and generating the
+corresponding Restructured file used by Sphinx to generate HTML documentation.
+This script will have the following structure::
+
+ ===========
+ filename.js
+ ===========
+ .. module:: filename.js
+
+We use the `.. module::` directive to register a javascript library
+as a Python module for Sphinx. This provides an entry in the module index.
+
+The contents of the docstring found in the javascript file will be added as is
+following the module declaration. No treatment will be done on the doctring.
+All the documentation structure will be in the docstrings and will comply
+with the following rules.
+
+Docstring structure
+-------------------
+
+Basically we document javascript with RestructuredText docstring
+following the same convention as documenting Python code.
+
+The doctring in Javascript files must be contained in standard
+Javascript comment signs, starting with `/**` and ending with `*/`,
+such as::
+
+ /**
+ * My comment starts here.
+ * This is the second line prefixed with a `*`.
+ * ...
+ * ...
+ * All the follwing line will be prefixed with a `*` followed by a space.
+ * ...
+ * ...
+ */
+
+
+Comments line prefixed by `//` will be ignored. They are reserved for source
+code comments dedicated to developers.
+
+
+Javscript functions docstring
+-----------------------------
+
+By default, the `function` directive describes a module-level function.
+
+`function` directive
+~~~~~~~~~~~~~~~~~~~~
+
+Its purpose is to define the function prototype such as::
+
+ .. function:: loadxhtml(url, data, reqtype, mode)
+
+If any namespace is used, we should add it in the prototype for now,
+until we define an appropriate directive::
+
+ .. function:: jQuery.fn.loadxhtml(url, data, reqtype, mode)
+
+Function parameters
+~~~~~~~~~~~~~~~~~~~
+
+We will define function parameters as a bulleted list, where the
+parameter name will be backquoted and followed by its description.
+
+Example of a javascript function docstring::
+
+ .. function:: loadxhtml(url, data, reqtype, mode)
+
+ cubicweb loadxhtml plugin to make jquery handle xhtml response
+
+ fetches `url` and replaces this's content with the result
+
+ Its arguments are:
+
+ * `url`
+
+ * `mode`, how the replacement should be done (default is 'replace')
+ Possible values are :
+ - 'replace' to replace the node's content with the generated HTML
+ - 'swap' to replace the node itself with the generated HTML
+ - 'append' to append the generated HTML to the node's content
+
+
+Optional parameter specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Javascript functions handle arguments not listed in the function signature.
+In the javascript code, they will be flagged using `/* ... */`. In the docstring,
+we flag those optional arguments the same way we would define it in
+Python::
+
+ .. function:: asyncRemoteExec(fname, arg1=None, arg2=None)
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/faq.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,433 @@
+.. -*- coding: utf-8 -*-
+
+Frequently Asked Questions (FAQ)
+================================
+
+
+Generalities
+````````````
+
+Why do you use the LGPL license to prevent me from doing X ?
+------------------------------------------------------------
+
+LGPL means that *if* you redistribute your application, you need to
+redistribute the changes you made to CubicWeb under the LGPL licence.
+
+Publishing a web site has nothing to do with redistributing source
+code according to the terms of the LGPL. A fair amount of companies
+use modified LGPL code for internal use. And someone could publish a
+*CubicWeb* component under a BSD licence for others to plug into a
+LGPL framework without any problem. The only thing we are trying to
+prevent here is someone taking the framework and packaging it as
+closed source to his own clients.
+
+Why does not CubicWeb have a template language ?
+------------------------------------------------
+
+There are enough template languages out there. You can use your
+preferred template language if you want. [explain how to use a
+template language]
+
+*CubicWeb* does not define its own templating language as this was
+not our goal. Based on our experience, we realized that
+we could gain productivity by letting designers use design tools
+and developpers develop without the use of the templating language
+as an intermediary that could not be anyway efficient for both parties.
+Python is the templating language that we use in *CubicWeb*, but again,
+it does not prevent you from using a templating language.
+
+Moreover, CubicWeb currently supports `simpletal`_ out of the box and
+it is also possible to use the `cwtags`_ library to build html trees
+using the `with statement`_ with more comfort than raw strings.
+
+.. _`simpletal`: http://www.owlfish.com/software/simpleTAL/
+.. _`cwtags`: http://www.cubicweb.org/project/cwtags
+.. _`with statement`: http://www.python.org/dev/peps/pep-0343/
+
+Why do you think using pure python is better than using a template language ?
+-----------------------------------------------------------------------------
+
+Python is an Object Oriented Programming language and as such it
+already provides a consistent and strong architecture and syntax
+a templating language would not reach.
+
+Using Python instead of a template langage for describing the user interface
+makes it to maintain with real functions/classes/contexts without the need of
+learning a new dialect. By using Python, we use standard OOP techniques and
+this is a key factor in a robust application.
+
+CubicWeb looks pretty recent. Is it stable ?
+--------------------------------------------
+
+It is constantly evolving, piece by piece. The framework has evolved since
+2001 and data has been migrated from one schema to the other ever since. There
+is a well-defined way to handle data and schema migration.
+
+You can see the roadmap there:
+http://www.cubicweb.org/project/cubicweb?tab=projectroadmap_tab.
+
+
+Why is the RQL query language looking similar to X ?
+----------------------------------------------------
+
+It may remind you of SQL but it is higher level than SQL, more like
+SPARQL. Except that SPARQL did not exist when we started the project.
+With version 3.4, CubicWeb has support for SPARQL.
+
+The RQL language is what is going to make a difference with django-
+like frameworks for several reasons.
+
+1. accessing data is *much* easier with it. One can write complex
+ queries with RQL that would be tedious to define and hard to maintain
+ using an object/filter suite of method calls.
+
+2. it offers an abstraction layer allowing your applications to run
+ on multiple back-ends. That means not only various SQL backends
+ (postgresql, sqlite, sqlserver, mysql), but also non-SQL data stores like
+ LDAP directories and subversion/mercurial repositories (see the `vcsfile`
+ component).
+
+Which ajax library is CubicWeb using ?
+--------------------------------------
+
+CubicWeb uses jQuery_ and provides a few helpers on top of that. Additionally,
+some jQuery plugins are provided (some are provided in specific cubes).
+
+.. _jQuery: http://jquery.com
+
+
+Development
+```````````
+
+How to change the instance logo ?
+---------------------------------
+
+The logo is managed by css. You must provide a custom css that will contain
+the code below:
+
+::
+
+ #logo {
+ background-image: url("logo.jpg");
+ }
+
+
+``logo.jpg`` is in ``mycube/data`` directory.
+
+How to create an anonymous user ?
+---------------------------------
+
+This allows to browse the site without being authenticated. In the
+``all-in-one.conf`` file of your instance, define the anonymous user
+as follows ::
+
+ # login of the CubicWeb user account to use for anonymous user (if you want to
+ # allow anonymous)
+ anonymous-user=anon
+
+ # password of the CubicWeb user account matching login
+ anonymous-password=anon
+
+You also must ensure that this `anon` user is a registered user of
+the DB backend. If not, you can create through the administation
+interface of your instance by adding a user with in the group `guests`.
+
+.. note::
+ While creating a new instance, you can decide to allow access
+ to anonymous user, which will automatically execute what is
+ decribed above.
+
+
+How to format an entity date attribute ?
+----------------------------------------
+
+If your schema has an attribute of type `Date` or `Datetime`, you usually want to
+format it when displaying it. First, you should define your preferred format
+using the site configuration panel
+``http://appurl/view?vid=systempropertiesform`` and then set ``ui.date`` and/or
+``ui.datetime``. Then in the view code, use:
+
+.. sourcecode:: python
+
+ entity.printable_value(date_attribute)
+
+which will always return a string whatever the attribute's type (so it's
+recommended also for other attribute types). By default it expects to generate
+HTML, so it deals with rich text formating, xml escaping...
+
+How to update a database after a schema modification ?
+------------------------------------------------------
+
+It depends on what has been modified in the schema.
+
+* update the permissions and properties of an entity or a relation:
+ ``sync_schema_props_perms('MyEntityOrRelation')``.
+
+* add an attribute: ``add_attribute('MyEntityType', 'myattr')``.
+
+* add a relation: ``add_relation_definition('SubjRelation', 'MyRelation', 'ObjRelation')``.
+
+I get `NoSelectableObject` exceptions, how do I debug selectors ?
+-----------------------------------------------------------------
+
+You just need to put the appropriate context manager around view/component
+selection. One standard place for components is in cubicweb/vregistry.py:
+
+.. sourcecode:: python
+
+ def possible_objects(self, *args, **kwargs):
+ """return an iterator on possible objects in this registry for the given
+ context
+ """
+ from logilab.common.registry import traced_selection
+ with traced_selection():
+ for appobjects in self.itervalues():
+ try:
+ yield self._select_best(appobjects, *args, **kwargs)
+ except NoSelectableObject:
+ continue
+
+This will yield additional WARNINGs, like this::
+
+ 2009-01-09 16:43:52 - (cubicweb.selectors) WARNING: selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
+
+For views, you can put this context in `cubicweb/web/views/basecontrollers.py` in
+the `ViewController`:
+
+.. sourcecode:: python
+
+ def _select_view_and_rset(self, rset):
+ ...
+ try:
+ from logilab.common.registry import traced_selection
+ with traced_selection():
+ view = self._cw.vreg['views'].select(vid, req, rset=rset)
+ except ObjectNotFound:
+ self.warning("the view %s could not be found", vid)
+ req.set_message(req._("The view %s could not be found") % vid)
+ vid = vid_from_rset(req, rset, self._cw.vreg.schema)
+ view = self._cw.vreg['views'].select(vid, req, rset=rset)
+ ...
+
+I get "database is locked" when executing tests
+-----------------------------------------------
+
+If you have "database is locked" as error when you are executing security tests,
+it is usually because commit or rollback are missing before login() calls.
+
+You can also use a context manager, to avoid such errors, as described
+here: :ref:`securitytest`.
+
+
+What are hooks used for ?
+-------------------------
+
+Hooks are executed around (actually before or after) events. The most common
+events are data creation, update and deletion. They permit additional constraint
+checking (those not expressible at the schema level), pre and post computations
+depending on data movements.
+
+As such, they are a vital part of the framework.
+
+Other kinds of hooks, called Operations, are available
+for execution just before commit.
+
+For more information, read :ref:`hooks` section.
+
+
+Configuration
+`````````````
+
+How to configure a LDAP source ?
+--------------------------------
+
+See :ref:`LDAP`.
+
+How to import LDAP users in |cubicweb| ?
+----------------------------------------
+
+ Here is a useful script which enables you to import LDAP users
+ into your *CubicWeb* instance by running the following:
+
+.. sourcecode:: python
+
+ import os
+ import pwd
+ import sys
+
+ from logilab.database import get_connection
+
+ def getlogin():
+ """avoid using os.getlogin() because of strange tty/stdin problems
+ (man 3 getlogin)
+ Another solution would be to use $LOGNAME, $USER or $USERNAME
+ """
+ return pwd.getpwuid(os.getuid())[0]
+
+
+ try:
+ database = sys.argv[1]
+ except IndexError:
+ print 'USAGE: python ldap2system.py <database>'
+ sys.exit(1)
+
+ if raw_input('update %s db ? [y/n]: ' % database).strip().lower().startswith('y'):
+ cnx = get_connection(user=getlogin(), database=database)
+ cursor = cnx.cursor()
+
+ insert = ('INSERT INTO euser (creation_date, eid, modification_date, login, '
+ ' firstname, surname, last_login_time, upassword) '
+ "VALUES (%(mtime)s, %(eid)s, %(mtime)s, %(login)s, %(firstname)s, "
+ "%(surname)s, %(mtime)s, './fqEz5LeZnT6');")
+ update = "UPDATE entities SET source='system' WHERE eid=%(eid)s;"
+ cursor.execute("SELECT eid,type,source,extid,mtime FROM entities WHERE source!='system'")
+ for eid, type, source, extid, mtime in cursor.fetchall():
+ if type != 'CWUser':
+ print "don't know what to do with entity type", type
+ continue
+ if source != 'ldapuser':
+ print "don't know what to do with source type", source
+ continue
+ ldapinfos = dict(x.strip().split('=') for x in extid.split(','))
+ login = ldapinfos['uid']
+ firstname = ldapinfos['uid'][0].upper()
+ surname = ldapinfos['uid'][1:].capitalize()
+ if login != 'jcuissinat':
+ args = dict(eid=eid, type=type, source=source, login=login,
+ firstname=firstname, surname=surname, mtime=mtime)
+ print args
+ cursor.execute(insert, args)
+ cursor.execute(update, args)
+
+ cnx.commit()
+ cnx.close()
+
+
+Security
+````````
+
+How to reset the password for user joe ?
+----------------------------------------
+
+If you want to reset the admin password for ``myinstance``, do::
+
+ $ cubicweb-ctl reset-admin-pwd myinstance
+
+You need to generate a new encrypted password::
+
+ $ python
+ >>> from cubicweb.server.utils import crypt_password
+ >>> crypt_password('joepass')
+ 'qHO8282QN5Utg'
+ >>>
+
+and paste it in the database::
+
+ $ psql mydb
+ mydb=> update cw_cwuser set cw_upassword='qHO8282QN5Utg' where cw_login='joe';
+ UPDATE 1
+
+if you're running over SQL Server, you need to use the CONVERT
+function to convert the string to varbinary(255). The SQL query is
+therefore::
+
+ update cw_cwuser set cw_upassword=CONVERT(varbinary(255), 'qHO8282QN5Utg') where cw_login='joe';
+
+Be careful, the encryption algorithm is different on Windows and on
+Unix. You cannot therefore use a hash generated on Unix to fill in a
+Windows database, nor the other way round.
+
+
+You can prefer use a migration script similar to this shell invocation instead::
+
+ $ cubicweb-ctl shell <instance>
+ >>> from cubicweb import Binary
+ >>> from cubicweb.server.utils import crypt_password
+ >>> crypted = crypt_password('joepass')
+ >>> rset = rql('Any U WHERE U is CWUser, U login "joe"')
+ >>> joe = rset.get_entity(0,0)
+ >>> joe.cw_set(upassword=Binary(crypted))
+
+Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file.
+
+The more experimented people would use RQL request directly::
+
+ >>> rql('SET X upassword %(a)s WHERE X is CWUser, X login "joe"',
+ ... {'a': crypted})
+
+I've just created a user in a group and it doesn't work !
+---------------------------------------------------------
+
+You are probably getting errors such as ::
+
+ remove {'PR': 'Project', 'C': 'CWUser'} from solutions since your_user has no read access to cost
+
+This is because you have to put your user in the "users" group. The user has to
+be in both groups.
+
+How is security implemented ?
+------------------------------
+
+The basis for security is a mapping from operations to groups or
+arbitrary RQL expressions. These mappings are scoped to entities and
+relations.
+
+This is an example for an Entity Type definition:
+
+.. sourcecode:: python
+
+ class Version(EntityType):
+ """a version is defining the content of a particular project's
+ release"""
+ # definition of attributes is voluntarily missing
+ __permissions__ = {'read': ('managers', 'users', 'guests',),
+ 'update': ('managers', 'logilab', 'owners'),
+ 'delete': ('managers',),
+ 'add': ('managers', 'logilab',
+ ERQLExpression('X version_of PROJ, U in_group G, '
+ 'PROJ require_permission P, '
+ 'P name "add_version", P require_group G'),)}
+
+The above means that permission to read a Version is granted to any
+user that is part of one of the groups 'managers', 'users', 'guests'.
+The 'add' permission is granted to users in group 'managers' or
+'logilab' or to users in group G, if G is linked by a permission
+entity named "add_version" to the version's project.
+
+An example for a Relation Definition (RelationType both defines a
+relation type and implicitly one relation definition, on which the
+permissions actually apply):
+
+.. sourcecode:: python
+
+ class version_of(RelationType):
+ """link a version to its project. A version is necessarily linked
+ to one and only one project. """
+ # some lines voluntarily missing
+ __permissions__ = {'read': ('managers', 'users', 'guests',),
+ 'delete': ('managers', ),
+ 'add': ('managers', 'logilab',
+ RRQLExpression('O require_permission P, P name "add_version", '
+ 'U in_group G, P require_group G'),) }
+
+The main difference lies in the basic available operations (there is
+no 'update' operation) and the usage of an RRQLExpression (rql
+expression for a relation) instead of an ERQLExpression (rql
+expression for an entity).
+
+You can find additional information in the section :ref:`securitymodel`.
+
+Is it possible to bypass security from the UI (web front) part ?
+----------------------------------------------------------------
+
+No. Only Hooks/Operations can do that.
+
+Can PostgreSQL and CubicWeb authentication work with kerberos ?
+----------------------------------------------------------------
+
+If you have PostgreSQL set up to accept kerberos authentication, you can set
+the db-host, db-name and db-user parameters in the `sources` configuration
+file while leaving the password blank. It should be enough for your
+instance to connect to postgresql with a kerberos ticket.
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,19 @@
+.. -*- coding: utf-8 -*-
+
+.. _Part4:
+
+----------
+Appendixes
+----------
+
+The following chapters are reference material.
+
+.. toctree::
+ :maxdepth: 1
+ :numbered:
+
+ faq
+ rql/index
+ mercurial
+ depends
+ docstrings-conventions
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/mercurial.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,131 @@
+.. -*- coding: utf-8 -*-
+
+.. _MercurialPresentation:
+
+Introducing Mercurial
+=====================
+
+Introduction
+````````````
+Mercurial_ manages a distributed repository containing revisions
+trees (each revision indicates the changes required to obtain the
+next, and so on). Locally, we have a repository containing revisions
+tree, and a working directory. It is possible
+to put in its working directory, one of the versions of its local repository,
+modify and then push it in its repository.
+It is also possible to get revisions from another repository or to export
+its own revisions from the local repository to another repository.
+
+.. _Mercurial: http://www.selenic.com/mercurial/
+
+In contrast to CVS/Subversion, we usually create a repository per
+project to manage.
+
+In a collaborative development, we usually create a central repository
+accessible to all developers of the project. These central repository is used
+as a reference. According to their needs, everyone can have a local repository,
+that they will have to synchronize with the central repository from time to time.
+
+
+Major commands
+``````````````
+* Create a local repository::
+
+ hg clone ssh://myhost//home/src/repo
+
+* See the contents of the local repository (graphical tool in Qt)::
+
+ hgview
+
+* Add a sub-directory or file in the current directory::
+
+ hg add subdir
+
+* Move to the working directory a specific revision (or last
+ revision) from the local repository::
+
+ hg update [identifier-revision]
+ hg up [identifier-revision]
+
+* Get in its local repository, the tree of revisions contained in a
+ remote repository (this does not change the local directory)::
+
+ hg pull ssh://myhost//home/src/repo
+ hg pull -u ssh://myhost//home/src/repo # equivalent to pull + update
+
+* See what are the heads of branches of the local repository if a `pull`
+ returned a new branch::
+
+ hg heads
+
+* Submit the working directory in the local repository (and create a new
+ revision)::
+
+ hg commit
+ hg ci
+
+* Merge with the mother revision of local directory, another revision from
+ the local respository (the new revision will be then two mothers
+ revisions)::
+
+ hg merge identifier-revision
+
+* Export to a remote repository, the tree of revisions in its content
+ local respository (this does not change the local directory)::
+
+ hg push ssh://myhost//home/src/repo
+
+* See what local revisions are not in another repository::
+
+ hg outgoing ssh://myhost//home/src/repo
+
+* See what are the revisions of a repository not found locally::
+
+ hg incoming ssh://myhost//home/src/repo
+
+* See what is the revision of the local repository which has been taken out
+ from the working directory and amended::
+
+ hg parent
+
+* See the differences between the working directory and the mother revision
+ of the local repository, possibly to submit them in the local repository::
+
+ hg diff
+ hg commit-tool
+ hg ct
+
+
+Best Practices
+``````````````
+* Remember to `hg pull -u` regularly, and particularly before
+ a `hg commit`.
+
+* Remember to `hg push` when your repository contains a version
+ relatively stable of your changes.
+
+* If a `hg pull -u` created a new branch head:
+
+ 1. find its identifier with `hg head`
+ 2. merge with `hg merge`
+ 3. `hg ci`
+ 4. `hg push`
+
+Installation of the guestrepo extension
+```````````````````````````````````````
+
+Set up the guestrepo extension by getting a copy of the sources
+from https://bitbucket.org/selinc/guestrepo and adding the following
+lines to your ``~/.hgrc``: ::
+
+ [extensions]
+ guestrepo=/path/to/guestrepo/guestrepo
+
+
+More information
+````````````````
+
+For more information about Mercurial, please refer to the Mercurial project online documentation_.
+
+.. _documentation: http://www.selenic.com/mercurial/wiki/
+
Binary file doc/book/annexes/rql/Graph-ex.gif has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/debugging.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,55 @@
+.. -*- coding: utf-8 -*-
+
+.. _DEBUGGING:
+
+Debugging RQL
+-------------
+
+Available levels
+~~~~~~~~~~~~~~~~
+
+Server debugging flags. They may be combined using binary operators.
+
+.. autodata:: cubicweb.server.DBG_NONE
+.. autodata:: cubicweb.server.DBG_RQL
+.. autodata:: cubicweb.server.DBG_SQL
+.. autodata:: cubicweb.server.DBG_REPO
+.. autodata:: cubicweb.server.DBG_MS
+.. autodata:: cubicweb.server.DBG_HOOKS
+.. autodata:: cubicweb.server.DBG_OPS
+.. autodata:: cubicweb.server.DBG_MORE
+.. autodata:: cubicweb.server.DBG_ALL
+
+
+Enable verbose output
+~~~~~~~~~~~~~~~~~~~~~
+
+To debug your RQL statements, it can be useful to enable a verbose output:
+
+.. sourcecode:: python
+
+ from cubicweb import server
+ server.set_debug(server.DBG_RQL|server.DBG_SQL|server.DBG_ALL)
+
+.. autofunction:: cubicweb.server.set_debug
+
+Another example showing how to debug hooks at a specific code site:
+
+.. sourcecode:: python
+
+ from cubicweb.server import debugged, DBG_HOOKS
+ with debugged(DBG_HOOKS):
+ person.cw_set(works_for=company)
+
+
+Detect largest RQL queries
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+See `Profiling and performance` chapter (see :ref:`PROFILING`).
+
+
+API
+~~~
+
+.. autoclass:: cubicweb.server.debugged
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/implementation.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,159 @@
+
+
+Implementation
+--------------
+
+BNF grammar
+~~~~~~~~~~~
+
+The terminal elements are in capital letters, non-terminal in lowercase.
+The value of the terminal elements (between quotes) is a Python regular
+expression.
+::
+
+ statement ::= (select | delete | insert | update) ';'
+
+
+ # select specific rules
+ select ::= 'DISTINCT'? E_TYPE selected_terms restriction? group? sort?
+
+ selected_terms ::= expression ( ',' expression)*
+
+ group ::= 'GROUPBY' VARIABLE ( ',' VARIABLE)*
+
+ sort ::= 'ORDERBY' sort_term ( ',' sort_term)*
+
+ sort_term ::= VARIABLE sort_method =?
+
+ sort_method ::= 'ASC' | 'DESC'
+
+
+ # delete specific rules
+ delete ::= 'DELETE' (variables_declaration | relations_declaration) restriction?
+
+
+ # insert specific rules
+ insert ::= 'INSERT' variables_declaration ( ':' relations_declaration)? restriction?
+
+
+ # update specific rules
+ update ::= 'SET' relations_declaration restriction
+
+
+ # common rules
+ variables_declaration ::= E_TYPE VARIABLE (',' E_TYPE VARIABLE)*
+
+ relations_declaration ::= simple_relation (',' simple_relation)*
+
+ simple_relation ::= VARIABLE R_TYPE expression
+
+ restriction ::= 'WHERE' relations
+
+ relations ::= relation (LOGIC_OP relation)*
+ | '(' relations')'
+
+ relation ::= 'NOT'? VARIABLE R_TYPE COMP_OP? expression
+ | 'NOT'? R_TYPE VARIABLE 'IN' '(' expression (',' expression)* ')'
+
+ expression ::= var_or_func_or_const (MATH_OP var_or_func_or_const) *
+ | '(' expression ')'
+
+ var_or_func_or_const ::= VARIABLE | function | constant
+
+ function ::= FUNCTION '(' expression ( ',' expression) * ')'
+
+ constant ::= KEYWORD | STRING | FLOAT | INT
+
+ # tokens
+ LOGIC_OP ::= ',' | 'OR' | 'AND'
+ MATH_OP ::= '+' | '-' | '/' | '*'
+ COMP_OP ::= '>' | '>=' | '=' | '<=' | '<' | '~=' | 'LIKE'
+
+ FUNCTION ::= 'MIN' | 'MAX' | 'SUM' | 'AVG' | 'COUNT' | 'UPPER' | 'LOWER'
+
+ VARIABLE ::= '[A-Z][A-Z0-9]*'
+ E_TYPE ::= '[A-Z]\w*'
+ R_TYPE ::= '[a-z_]+'
+
+ KEYWORD ::= 'TRUE' | 'FALSE' | 'NULL' | 'TODAY' | 'NOW'
+ STRING ::= "'([^'\]|\\.)*'" |'"([^\"]|\\.)*\"'
+ FLOAT ::= '\d+\.\d*'
+ INT ::= '\d+'
+
+
+Internal representation (syntactic tree)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The tree research does not contain the selected variables
+(e.g. there is only what follows "WHERE").
+
+The insertion tree does not contain the variables inserted or relations
+defined on these variables (e.g. there is only what follows "WHERE").
+
+The removal tree does not contain the deleted variables and relations
+(e.g. there is only what follows the "WHERE").
+
+The update tree does not contain the variables and relations updated
+(e.g. there is only what follows the "WHERE").
+
+::
+
+ Select ((Relationship | And | Or)?, Group?, Sort?)
+ Insert (Relations | And | Or)?
+ Delete (Relationship | And | Or)?
+ Update (Relations | And | Or)?
+
+ And ((Relationship | And | Or), (Relationship | And | Or))
+ Or ((Relationship | And | Or), (Relationship | And | Or))
+
+ Relationship ((VariableRef, Comparison))
+
+ Comparison ((Function | MathExpression | Keyword | Constant | VariableRef) +)
+
+ Function (())
+ MathExpression ((MathExpression | Keyword | Constant | VariableRef), (MathExpression | Keyword | Constant | VariableRef))
+
+ Group (VariableRef +)
+ Sort (SortTerm +)
+ SortTerm (VariableRef +)
+
+ VariableRef ()
+ Variable ()
+ Keyword ()
+ Constant ()
+
+
+Known limitations
+~~~~~~~~~~~~~~~~~
+
+- The current implementation does not support linking two relations of type 'is'
+ with an OR. I do not think that the negation is supported on this type of
+ relation (XXX to be confirmed).
+
+- missing COALESCE and certainly other things...
+
+- writing an rql query requires knowledge of the used schema (with real relation
+ names and entities, not those viewed in the user interface). On the other
+ hand, we cannot really bypass that, and it is the job of a user interface to
+ hide the RQL.
+
+
+Topics
+~~~~~~
+
+It would be convenient to express the schema matching
+relations (non-recursive rules)::
+
+ Document class Type <-> Document occurence_of Fiche class Type
+ Sheet class Type <-> Form collection Collection class Type
+
+Therefore 1. becomes::
+
+ Document X where
+ X class C, C name 'Cartoon'
+ X owned_by U, U login 'syt'
+ X available true
+
+I'm not sure that we should handle this at RQL level ...
+
+There should also be a special relation 'anonymous'.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,14 @@
+.. _RQLChapter:
+
+Relation Query Language (RQL)
+=============================
+
+This chapter describes the Relation Query Language syntax and its implementation in CubicWeb.
+
+.. toctree::
+ :maxdepth: 2
+
+ intro
+ language
+ debugging
+ implementation
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/intro.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,162 @@
+
+.. _rql_intro:
+
+Introduction
+------------
+
+Goals of RQL
+~~~~~~~~~~~~
+
+The goal is to have a semantic language in order to:
+
+- query relations in a clear syntax
+- empowers access to data repository manipulation
+- making attributes/relations browsing easy
+
+As such, attributes will be regarded as cases of special relations (in
+terms of usage, the user should see no syntactic difference between an
+attribute and a relation).
+
+Comparison with existing languages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+SQL
+```
+
+RQL may remind of SQL but works at a higher abstraction level (the *CubicWeb*
+framework generates SQL from RQL to fetch data from relation databases). RQL is
+focused on browsing relations. The user needs only to know about the *CubicWeb*
+data model he is querying, but not about the underlying SQL model.
+
+Sparql
+``````
+
+The query language most similar to RQL is SPARQL_, defined by the W3C to serve
+for the semantic web.
+
+Versa
+`````
+
+We should look in more detail, but here are already some ideas for the moment
+... Versa_ is the language most similar to what we wanted to do, but the model
+underlying data being RDF, there are some things such as namespaces or
+handling of the RDF types which does not interest us. On the functionality
+level, Versa_ is very comprehensive including through many functions of
+conversion and basic types manipulation, which we may want to look at one time
+or another. Finally, the syntax is a little esoteric.
+
+Datalog
+```````
+
+Datalog_ is a prolog derived query langage which applies to relational
+databases. It is more expressive than RQL in that it accepts either
+extensional_ and intensional_ predicates (or relations). As of now,
+RQL only deals with intensional relations.
+
+The different types of queries
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Search (`Any`)
+ Extract entities and attributes of entities.
+
+Insert entities (`INSERT`)
+ Insert new entities or relations in the database.
+ It can also directly create relationships for the newly created entities.
+
+Update entities, create relations (`SET`)
+ Update existing entities in the database,
+ or create relations between existing entities.
+
+Delete entities or relationship (`DELETE`)
+ Remove entities or relations existing in the database.
+
+
+RQL relation expressions
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+RQL expressions apply to a live database defined by a
+:ref:`datamodel_definition`. Apart from the main type, or head, of the
+expression (search, insert, etc.) the most common constituent of an
+RQL expression is a (set of) relation expression(s).
+
+An RQL relation expression contains three components:
+
+* the subject, which is an entity type
+* the predicate, which is a relation definition (an arc of the schema)
+* the object, which is either an attribute or a relation to another entity
+
+.. image:: Graph-ex.gif
+ :alt: <subject> <predicate> <object>
+ :align: center
+
+.. warning::
+
+ A relation is always expressed in the order: ``subject``,
+ ``predicate``, ``object``.
+
+ It is important to determine if the entity type is subject or object
+ to construct a valid expression. Inverting the subject/object is an
+ error since the relation cannot be found in the schema.
+
+ If one does not have access to the code, one can find the order by
+ looking at the schema image in manager views (the subject is located
+ at the beginning of the arrow).
+
+An example of two related relation expressions::
+
+ P works_for C, P name N
+
+RQL variables represent typed entities. The type of entities is
+either automatically inferred (by looking at the possible relation
+definitions, see :ref:`RelationDefinition`) or explicitely constrained
+using the ``is`` meta relation.
+
+In the example above, we barely need to look at the schema. If
+variable names (in the RQL expression) and relation type names (in the
+schema) are expresssively designed, the human reader can infer as much
+as the |cubicweb| querier.
+
+The ``P`` variable is used twice but it always represent the same set
+of entities. Hence ``P works_for C`` and ``P name N`` must be
+compatible in the sense that all the Ps (which *can* refer to
+different entity types) must accept the ``works_for`` and ``name``
+relation types. This does restrict the set of possible values of P.
+
+Adding another relation expression::
+
+ P works_for C, P name N, C name "logilab"
+
+This further restricts the possible values of P through an indirect
+constraint on the possible values of ``C``. The RQL-level unification_
+happening there is translated to one (or several) joins_ at the
+database level.
+
+.. note::
+
+ In |cubicweb|, the term `relation` is often found without ambiguity
+ instead of `predicate`. This predicate is also known as the
+ `property` of the triple in `RDF concepts`_
+
+
+RQL Operators
+~~~~~~~~~~~~~
+
+An RQL expression's head can be completed using various operators such
+as ``ORDERBY``, ``GROUPBY``, ``HAVING``, ``LIMIT`` etc.
+
+RQL relation expressions can be grouped with ``UNION`` or
+``WITH``. Predicate oriented keywords such as ``EXISTS``, ``OR``,
+``NOT`` are available.
+
+The complete zoo of RQL operators is described extensively in the
+following chapter (:ref:`RQL`).
+
+.. _RDF concepts: http://www.w3.org/TR/rdf-concepts/
+.. _Versa: http://wiki.xml3k.org/Versa
+.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/
+.. _unification: http://en.wikipedia.org/wiki/Unification_(computing)
+.. _joins: http://en.wikipedia.org/wiki/Join_(SQL)
+.. _Datalog: http://en.wikipedia.org/wiki/Datalog
+.. _intensional: http://en.wikipedia.org/wiki/Intensional_definition
+.. _extensional: http://en.wikipedia.org/wiki/Extension_(predicate_logic)
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/annexes/rql/language.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,804 @@
+.. -*- coding: utf-8 -*-
+
+.. _RQL:
+
+RQL syntax
+----------
+
+.. _RQLKeywords:
+
+Reserved keywords
+~~~~~~~~~~~~~~~~~
+
+::
+
+ AND, ASC, BEING, DELETE, DESC, DISTINCT, EXISTS, FALSE, GROUPBY,
+ HAVING, ILIKE, INSERT, LIKE, LIMIT, NOT, NOW, NULL, OFFSET,
+ OR, ORDERBY, SET, TODAY, TRUE, UNION, WHERE, WITH
+
+The keywords are not case sensitive. You should not use them when defining your
+schema, or as RQL variable names.
+
+
+.. _RQLCase:
+
+Case
+~~~~
+
+* Variables should be all upper-cased.
+
+* Relation should be all lower-cased and match exactly names of relations defined
+ in the schema.
+
+* Entity types should start with an upper cased letter and be followed by at least
+ a lower cased latter.
+
+
+.. _RQLVariables:
+
+Variables and typing
+~~~~~~~~~~~~~~~~~~~~
+
+Entities and values to browse and/or select are represented in the query by
+*variables* that must be written in capital letters.
+
+With RQL, we do not distinguish between entities and attributes. The value of an
+attribute is considered as an entity of a particular type (see below), linked to
+one (real) entity by a relation called the name of the attribute, where the
+entity is the subject and the attribute the object.
+
+The possible type(s) for each variable is derived from the schema according to
+the constraints expressed above and thanks to the relations between each
+variable.
+
+We can restrict the possible types for a variable using the special relation
+**is** in the restrictions.
+
+
+.. _VirtualRelations:
+
+Virtual relations
+~~~~~~~~~~~~~~~~~
+
+Those relations may only be used in RQL query but are not actual attributes of
+your entities.
+
+* `has_text`: relation to use to query the full text index (only for entities
+ having fulltextindexed attributes).
+
+* `identity`: relation to use to tell that a RQL variable is the same as another
+ when you've to use two different variables for querying purpose. On the
+ opposite it's also useful together with the ``NOT`` operator to tell that two
+ variables should not identify the same entity
+
+
+.. _RQLLiterals:
+
+Literal expressions
+~~~~~~~~~~~~~~~~~~~
+
+Bases types supported by RQL are those supported by yams schema. Literal values
+are expressed as explained below:
+
+* string should be between double or single quotes. If the value contains a
+ quote, it should be preceded by a backslash '\\'
+
+* floats separator is dot '.'
+
+* boolean values are ``TRUE`` and ``FALSE`` keywords
+
+* date and time should be expressed as a string with ISO notation : YYYY/MM/DD
+ [hh:mm], or using keywords ``TODAY`` and ``NOW``
+
+You may also use the ``NULL`` keyword, meaning 'unspecified'.
+
+
+.. _RQLOperators:
+
+Operators
+~~~~~~~~~
+
+.. _RQLLogicalOperators:
+
+Logical operators
+`````````````````
+::
+
+ AND, OR, NOT, ','
+
+',' is equivalent to 'AND' but with the smallest among the priority of logical
+operators (see :ref:`RQLOperatorsPriority`).
+
+.. _RQLMathematicalOperators:
+
+Mathematical operators
+``````````````````````
+
++----------+---------------------+-----------+--------+
+| Operator | Description | Example | Result |
++==========+=====================+===========+========+
+| `+` | addition | 2 + 3 | 5 |
++----------+---------------------+-----------+--------+
+| `-` | subtraction | 2 - 3 | -1 |
++----------+---------------------+-----------+--------+
+| `*` | multiplication | 2 * 3 | 6 |
++----------+---------------------+-----------+--------+
+| / | division | 4 / 2 | 2 |
++----------+---------------------+-----------+--------+
+| % | modulo (remainder) | 5 % 4 | 1 |
++----------+---------------------+-----------+--------+
+| ^ | exponentiation | 2.0 ^ 3.0 | 8 |
++----------+---------------------+-----------+--------+
+| & | bitwise AND | 91 & 15 | 11 |
++----------+---------------------+-----------+--------+
+| `|` | bitwise OR | 32 | 3 | 35 |
++----------+---------------------+-----------+--------+
+| # | bitwise XOR | 17 # 5 | 20 |
++----------+---------------------+-----------+--------+
+| ~ | bitwise NOT | ~1 | -2 |
++----------+---------------------+-----------+--------+
+| << | bitwise shift left | 1 << 4 | 16 |
++----------+---------------------+-----------+--------+
+| >> | bitwise shift right | 8 >> 2 | 2 |
++----------+---------------------+-----------+--------+
+
+
+Notice integer division truncates results depending on the backend behaviour. For
+instance, postgresql does.
+
+
+.. _RQLComparisonOperators:
+
+Comparison operators
+````````````````````
+ ::
+
+ =, !=, <, <=, >=, >, IN
+
+
+The syntax to use comparison operators is:
+
+ `VARIABLE attribute <operator> VALUE`
+
+The `=` operator is the default operator and can be omitted, i.e. :
+
+ `VARIABLE attribute = VALUE`
+
+is equivalent to
+
+ `VARIABLE attribute VALUE`
+
+
+The operator `IN` provides a list of possible values:
+
+.. sourcecode:: sql
+
+ Any X WHERE X name IN ('chauvat', 'fayolle', 'di mascio', 'thenault')
+
+
+.. _RQLStringOperators:
+
+String operators
+````````````````
+::
+
+ LIKE, ILIKE, ~=, REGEXP
+
+The ``LIKE`` string operator can be used with the special character `%` in
+a string as wild-card:
+
+.. sourcecode:: sql
+
+ -- match every entity whose name starts with 'Th'
+ Any X WHERE X name ~= 'Th%'
+ -- match every entity whose name endswith 'lt'
+ Any X WHERE X name LIKE '%lt'
+ -- match every entity whose name contains a 'l' and a 't'
+ Any X WHERE X name LIKE '%l%t%'
+
+``ILIKE`` is the case insensitive version of ``LIKE``. It's not
+available on all backend (e.g. sqlite doesn't support it). If not available for
+your backend, ``ILIKE`` will behave like ``LIKE``.
+
+`~=` is a shortcut version of ``ILIKE``, or of ``LIKE`` when the
+former is not available on the back-end.
+
+
+The ``REGEXP`` is an alternative to ``LIKE`` that supports POSIX
+regular expressions:
+
+.. sourcecode:: sql
+
+ -- match entities whose title starts with a digit
+ Any X WHERE X title REGEXP "^[0-9].*"
+
+
+The underlying SQL operator used is back-end-dependent :
+
+- the ``~`` operator is used for postgresql,
+- the ``REGEXP`` operator for mysql and sqlite.
+
+Other back-ends are not supported yet.
+
+
+.. _RQLOperatorsPriority:
+
+Operators priority
+``````````````````
+
+#. `(`, `)`
+#. `^`, `<<`, `>>`
+#. `*`, `/`, `%`, `&`
+#. `+`, `-`, `|`, `#`
+#. `NOT`
+#. `AND`
+#. `OR`
+#. `,`
+
+
+.. _RQLSearchQuery:
+
+Search Query
+~~~~~~~~~~~~
+
+Simplified grammar of search query: ::
+
+ [ `DISTINCT`] `Any` V1 (, V2) \*
+ [ `GROUPBY` V1 (, V2) \*] [ `ORDERBY` <orderterms>]
+ [ `LIMIT` <value>] [ `OFFSET` <value>]
+ [ `WHERE` <triplet restrictions>]
+ [ `WITH` V1 (, V2)\* BEING (<query>)]
+ [ `HAVING` <other restrictions>]
+ [ `UNION` <query>]
+
+Selection
+`````````
+
+The fist occuring clause is the selection of terms that should be in the result
+set. Terms may be variable, literals, function calls, arithmetic, etc. and each
+term is separated by a comma.
+
+There will be as much column in the result set as term in this clause, respecting
+order.
+
+Syntax for function call is somewhat intuitive, for instance:
+
+.. sourcecode:: sql
+
+ Any UPPER(N) WHERE P firstname N
+
+
+Grouping and aggregating
+````````````````````````
+
+The ``GROUPBY`` keyword is followed by a list of terms on which results
+should be grouped. They are usually used with aggregate functions, responsible to
+aggregate values for each group (see :ref:`RQLAggregateFunctions`).
+
+For grouped queries, all selected variables must be either aggregated (i.e. used
+by an aggregate function) or grouped (i.e. listed in the ``GROUPBY``
+clause).
+
+
+Sorting
+```````
+
+The ``ORDERBY`` keyword if followed by the definition of the selection
+order: variable or column number followed by sorting method (``ASC``,
+``DESC``), ``ASC`` being the default. If the sorting method is not
+specified, then the sorting is ascendant (`ASC`).
+
+
+Pagination
+``````````
+
+The ``LIMIT`` and ``OFFSET`` keywords may be respectively used to
+limit the number of results and to tell from which result line to start (for
+instance, use `LIMIT 20` to get the first 20 results, then `LIMIT 20 OFFSET 20`
+to get the next 20.
+
+
+Restrictions
+````````````
+
+The ``WHERE`` keyword introduce one of the "main" part of the query, where
+you "define" variables and add some restrictions telling what you're interested
+in.
+
+It's a list of triplets "subject relation object", e.g. `V1 relation
+(V2 | <static value>)`. Triplets are separated using :ref:`RQLLogicalOperators`.
+
+.. note::
+
+ About the negation operator (``NOT``):
+
+ * ``NOT X relation Y`` is equivalent to ``NOT EXISTS(X relation Y)``
+
+ * ``Any X WHERE NOT X owned_by U`` means "entities that have no relation
+ ``owned_by``".
+
+ * ``Any X WHERE NOT X owned_by U, U login "syt"`` means "the entity have no
+ relation ``owned_by`` with the user syt". They may have a relation "owned_by"
+ with another user.
+
+In this clause, you can also use ``EXISTS`` when you want to know if some
+expression is true and do not need the complete set of elements that make it
+true. Testing for existence is much faster than fetching the complete set of
+results, especially when you think about using ``OR`` against several expressions. For instance
+if you want to retrieve versions which are in state "ready" or tagged by
+"priority", you should write :
+
+.. sourcecode:: sql
+
+ Any X ORDERBY PN,N
+ WHERE X num N, X version_of P, P name PN,
+ EXISTS(X in_state S, S name "ready")
+ OR EXISTS(T tags X, T name "priority")
+
+not
+
+.. sourcecode:: sql
+
+ Any X ORDERBY PN,N
+ WHERE X num N, X version_of P, P name PN,
+ (X in_state S, S name "ready")
+ OR (T tags X, T name "priority")
+
+Both queries aren't at all equivalent :
+
+* the former will retrieve all versions, then check for each one which are in the
+ matching state of or tagged by the expected tag,
+
+* the later will retrieve all versions, state and tags (cartesian product!),
+ compute join and then exclude each row which are in the matching state or
+ tagged by the expected tag. This implies that you won't get any result if the
+ in_state or tag tables are empty (ie there is no such relation in the
+ application). This is usually NOT what you want.
+
+Another common case where you may want to use ``EXISTS`` is when you
+find yourself using ``DISTINCT`` at the beginning of your query to
+remove duplicate results. The typical case is when you have a
+multivalued relation such as Version version_of Project and you want
+to retrieve projects which have a version:
+
+.. sourcecode:: sql
+
+ Any P WHERE V version_of P
+
+will return each project number of versions times. So you may be
+tempted to use:
+
+.. sourcecode:: sql
+
+ DISTINCT Any P WHERE V version_of P
+
+This will work, but is not efficient, as it will use the ``SELECT
+DISTINCT`` SQL predicate, which needs to retrieve all projects, then
+sort them and discard duplicates, which can have a very high cost for
+large result sets. So the best way to write this is:
+
+.. sourcecode:: sql
+
+ Any P WHERE EXISTS(V version_of P)
+
+
+You can also use the question mark (`?`) to mark optional relations. This allows
+you to select entities related **or not** to another. It is a similar concept
+to `Left outer join`_:
+
+ the result of a left outer join (or simply left join) for table A and B
+ always contains all records of the "left" table (A), even if the
+ join-condition does not find any matching record in the "right" table (B).
+
+You must use the `?` behind a variable to specify that the relation to
+that variable is optional. For instance:
+
+- Bugs of a project attached or not to a version
+
+ .. sourcecode:: sql
+
+ Any X, V WHERE X concerns P, P eid 42, X corrected_in V?
+
+ You will get a result set containing all the project's tickets, with either the
+ version in which it's fixed or None for tickets not related to a version.
+
+
+- All cards and the project they document if any
+
+ .. sourcecode:: sql
+
+ Any C, P WHERE C is Card, P? documented_by C
+
+Notice you may also use outer join:
+
+- on the RHS of attribute relation, e.g.
+
+ .. sourcecode:: sql
+
+ Any X WHERE X ref XR, Y name XR?
+
+ so that Y is outer joined on X by ref/name attributes comparison
+
+
+- on any side of an ``HAVING`` expression, e.g.
+
+ .. sourcecode:: sql
+
+ Any X WHERE X creation_date XC, Y creation_date YC
+ HAVING YEAR(XC)=YEAR(YC)?
+
+ so that Y is outer joined on X by comparison of the year extracted from their
+ creation date.
+
+ .. sourcecode:: sql
+
+ Any X WHERE X creation_date XC, Y creation_date YC
+ HAVING YEAR(XC)?=YEAR(YC)
+
+ would outer join X on Y instead.
+
+
+Having restrictions
+```````````````````
+
+The ``HAVING`` clause, as in SQL, may be used to restrict a query
+according to value returned by an aggregate function, e.g.
+
+.. sourcecode:: sql
+
+ Any X GROUPBY X WHERE X relation Y HAVING COUNT(Y) > 10
+
+It may however be used for something else: In the ``WHERE`` clause, we are
+limited to triplet expressions, so some things may not be expressed there. Let's
+take an example : if you want to get people whose upper-cased first name equals to
+another person upper-cased first name. There is no proper way to express this
+using triplet, so you should use something like:
+
+.. sourcecode:: sql
+
+ Any X WHERE X firstname XFN, Y firstname YFN, NOT X identity Y HAVING UPPER(XFN) = UPPER(YFN)
+
+Another example: imagine you want person born in 2000:
+
+.. sourcecode:: sql
+
+ Any X WHERE X birthday XB HAVING YEAR(XB) = 2000
+
+Notice that while we would like this to work without the HAVING clause, this
+can't be currently be done because it introduces an ambiguity in RQL's grammar
+that can't be handled by Yapps_, the parser's generator we're using.
+
+
+Sub-queries
+```````````
+
+The ``WITH`` keyword introduce sub-queries clause. Each sub-query has the
+form:
+
+ V1(,V2) BEING (rql query)
+
+Variables at the left of the ``BEING`` keyword defines into which
+variables results from the sub-query will be mapped to into the outer query.
+Sub-queries are separated from each other using a comma.
+
+Let's say we want to retrieve for each project its number of versions and its
+number of tickets. Due to the nature of relational algebra behind the scene, this
+can't be achieved using a single query. You have to write something along the
+line of:
+
+.. sourcecode:: sql
+
+ Any X, VC, TC WHERE X identity XX
+ WITH X, VC BEING (Any X, COUNT(V) GROUPBY X WHERE V version_of X),
+ XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
+
+Notice that we can't reuse a same variable name as alias for two different
+sub-queries, hence the usage of 'X' and 'XX' in this example, which are then
+unified using the special `identity` relation (see :ref:`VirtualRelations`).
+
+.. warning::
+
+ Sub-queries define a new variable scope, so even if a variable has the same name
+ in the outer query and in the sub-query, they technically **aren't** the same
+ variable. So:
+
+ .. sourcecode:: sql
+
+ Any W, REF WITH W, REF BEING
+ (Any W, REF WHERE W is Workcase, W ref REF,
+ W concerned_by D, D name "Logilab")
+
+ could be written:
+
+ .. sourcecode:: sql
+
+ Any W, REF WITH W, REF BEING
+ (Any W1, REF1 WHERE W1 is Workcase, W1 ref REF1,
+ W1 concerned_by D, D name "Logilab")
+
+ Also, when a variable is coming from a sub-query, you currently can't reference
+ its attribute or inlined relations in the outer query, you've to fetch them in
+ the sub-query. For instance, let's say we want to sort by project name in our
+ first example, we would have to write:
+
+ .. sourcecode:: sql
+
+
+ Any X, VC, TC ORDERBY XN WHERE X identity XX
+ WITH X, XN, VC BEING (Any X, COUNT(V) GROUPBY X,XN WHERE V version_of X, X name XN),
+ XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
+
+ instead of:
+
+ .. sourcecode:: sql
+
+ Any X, VC, TC ORDERBY XN WHERE X identity XX, X name XN,
+ WITH X, XN, VC BEING (Any X, COUNT(V) GROUPBY X WHERE V version_of X),
+ XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
+
+ which would result in a SQL execution error.
+
+
+Union
+`````
+
+You may get a result set containing the concatenation of several queries using
+the ``UNION``. The selection of each query should have the same number of
+columns.
+
+.. sourcecode:: sql
+
+ (Any X, XN WHERE X is Person, X surname XN) UNION (Any X,XN WHERE X is Company, X name XN)
+
+
+.. _RQLFunctions:
+
+Available functions
+~~~~~~~~~~~~~~~~~~~
+
+Below is the list of aggregate and transformation functions that are supported
+nativly by the framework. Notice that cubes may define additional functions.
+
+.. _RQLAggregateFunctions:
+
+Aggregate functions
+```````````````````
++------------------------+----------------------------------------------------------+
+| ``COUNT(Any)`` | return the number of rows |
++------------------------+----------------------------------------------------------+
+| ``MIN(Any)`` | return the minimum value |
++------------------------+----------------------------------------------------------+
+| ``MAX(Any)`` | return the maximum value |
++------------------------+----------------------------------------------------------+
+| ``AVG(Any)`` | return the average value |
++------------------------+----------------------------------------------------------+
+| ``SUM(Any)`` | return the sum of values |
++------------------------+----------------------------------------------------------+
+| ``COMMA_JOIN(String)`` | return each value separated by a comma (for string only) |
++------------------------+----------------------------------------------------------+
+
+All aggregate functions above take a single argument. Take care some aggregate
+functions (e.g. ``MAX``, ``MIN``) may return `None` if there is no
+result row.
+
+.. _RQLStringFunctions:
+
+String transformation functions
+```````````````````````````````
+
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``UPPER(String)`` | upper case the string |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``LOWER(String)`` | lower case the string |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``LENGTH(String)`` | return the length of the string |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``SUBSTRING(String, start, length)`` | extract from the string a string starting at given index and of |
+| | given length |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``LIMIT_SIZE(String, max size)`` | if the length of the string is greater than given max size, |
+| | strip it and add ellipsis ("..."). The resulting string will |
+| | hence have max size + 3 characters |
++-----------------------------------------------+-----------------------------------------------------------------+
+| ``TEXT_LIMIT_SIZE(String, format, max size)`` | similar to the above, but allow to specify the MIME type of the |
+| | text contained by the string. Supported formats are text/html, |
+| | text/xhtml and text/xml. All others will be considered as plain |
+| | text. For non plain text format, sgml tags will be first removed|
+| | before limiting the string. |
++-----------------------------------------------+-----------------------------------------------------------------+
+
+.. _RQLDateFunctions:
+
+Date extraction functions
+`````````````````````````
+
++----------------------+----------------------------------------+
+| ``YEAR(Date)`` | return the year of a date or datetime |
++----------------------+----------------------------------------+
+| ``MONTH(Date)`` | return the month of a date or datetime |
++----------------------+----------------------------------------+
+| ``DAY(Date)`` | return the day of a date or datetime |
++----------------------+----------------------------------------+
+| ``HOUR(Datetime)`` | return the hours of a datetime |
++----------------------+----------------------------------------+
+| ``MINUTE(Datetime)`` | return the minutes of a datetime |
++----------------------+----------------------------------------+
+| ``SECOND(Datetime)`` | return the seconds of a datetime |
++----------------------+----------------------------------------+
+| ``WEEKDAY(Date)`` | return the day of week of a date or |
+| | datetime. Sunday == 1, Saturday == 7. |
++----------------------+----------------------------------------+
+
+.. _RQLOtherFunctions:
+
+Other functions
+```````````````
++-------------------+--------------------------------------------------------------------+
+| ``ABS(num)`` | return the absolute value of a number |
++-------------------+--------------------------------------------------------------------+
+| ``RANDOM()`` | return a pseudo-random value from 0.0 to 1.0 |
++-------------------+--------------------------------------------------------------------+
+| ``FSPATH(X)`` | expect X to be an attribute whose value is stored in a |
+| | :class:`BFSStorage` and return its path on the file system |
++-------------------+--------------------------------------------------------------------+
+| ``FTIRANK(X)`` | expect X to be an entity used in a has_text relation, and return a |
+| | number corresponding to the rank order of each resulting entity |
++-------------------+--------------------------------------------------------------------+
+| ``CAST(Type, X)`` | expect X to be an attribute and return it casted into the given |
+| | final type |
++-------------------+--------------------------------------------------------------------+
+
+
+.. _RQLExamples:
+
+Examples
+~~~~~~~~
+
+- *Search for the object of identifier 53*
+
+ .. sourcecode:: sql
+
+ Any X WHERE X eid 53
+
+- *Search material such as comics, owned by syt and available*
+
+ .. sourcecode:: sql
+
+ Any X WHERE X is Document,
+ X occurence_of F, F class C, C name 'Comics',
+ X owned_by U, U login 'syt',
+ X available TRUE
+
+- *Looking for people working for eurocopter interested in training*
+
+ .. sourcecode:: sql
+
+ Any P WHERE P is Person, P work_for S, S name 'Eurocopter',
+ P interested_by T, T name 'training'
+
+- *Search note less than 10 days old written by jphc or ocy*
+
+ .. sourcecode:: sql
+
+ Any N WHERE N is Note, N written_on D, D day> (today -10),
+ N written_by P, P name 'jphc' or P name 'ocy'
+
+- *Looking for people interested in training or living in Paris*
+
+ .. sourcecode:: sql
+
+ Any P WHERE P is Person, EXISTS(P interested_by T, T name 'training') OR
+ (P city 'Paris')
+
+- *The surname and firstname of all people*
+
+ .. sourcecode:: sql
+
+ Any N, P WHERE X is Person, X name N, X firstname P
+
+ Note that the selection of several entities generally force
+ the use of "Any" because the type specification applies otherwise
+ to all the selected variables. We could write here
+
+ .. sourcecode:: sql
+
+ String N, P WHERE X is Person, X name N, X first_name P
+
+
+ Note: You can not specify several types with * ... where X is FirstType or X is SecondType*.
+ To specify several types explicitly, you have to do
+
+
+ .. sourcecode:: sql
+
+ Any X WHERE X is IN (FirstType, SecondType)
+
+
+.. _RQLInsertQuery:
+
+Insertion query
+~~~~~~~~~~~~~~~
+
+ `INSERT` <entity type> V1 (, <entity type> V2) \ * `:` <assignments>
+ [ `WHERE` <restriction>]
+
+:assignments:
+ list of relations to assign in the form `V1 relationship V2 | <static value>`
+
+The restriction can define variables used in assignments.
+
+Caution, if a restriction is specified, the insertion is done for
+*each line result returned by the restriction*.
+
+- *Insert a new person named 'foo'*
+
+ .. sourcecode:: sql
+
+ INSERT Person X: X name 'foo'
+
+- *Insert a new person named 'foo', another called 'nice' and a 'friend' relation
+ between them*
+
+ .. sourcecode:: sql
+
+ INSERT Person X, Person Y: X name 'foo', Y name 'nice', X friend Y
+
+- *Insert a new person named 'foo' and a 'friend' relation with an existing
+ person called 'nice'*
+
+ .. sourcecode:: sql
+
+ INSERT Person X: X name 'foo', X friend Y WHERE Y name 'nice'
+
+.. _RQLSetQuery:
+
+Update and relation creation queries
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ `SET` <assignements>
+ [ `WHERE` <restriction>]
+
+Caution, if a restriction is specified, the update is done *for
+each result line returned by the restriction*.
+
+- *Renaming of the person named 'foo' to 'bar' with the first name changed*
+
+ .. sourcecode:: sql
+
+ SET X name 'bar', X firstname 'original' WHERE X is Person, X name 'foo'
+
+- *Insert a relation of type 'know' between objects linked by
+ the relation of type 'friend'*
+
+ .. sourcecode:: sql
+
+ SET X know Y WHERE X friend Y
+
+
+.. _RQLDeleteQuery:
+
+Deletion query
+~~~~~~~~~~~~~~
+
+ `DELETE` (<entity type> V) | (V1 relation v2 ),...
+ [ `WHERE` <restriction>]
+
+Caution, if a restriction is specified, the deletion is made *for
+each line result returned by the restriction*.
+
+- *Deletion of the person named 'foo'*
+
+ .. sourcecode:: sql
+
+ DELETE Person X WHERE X name 'foo'
+
+- *Removal of all relations of type 'friend' from the person named 'foo'*
+
+ .. sourcecode:: sql
+
+ DELETE X friend Y WHERE X is Person, X name 'foo'
+
+
+.. _Yapps: http://theory.stanford.edu/~amitp/yapps/
+.. _Left outer join: http://en.wikipedia.org/wiki/Join_(SQL)#Left_outer_join
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/cubes/available-cubes.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,66 @@
+.. _AvailableCubes:
+
+Available cubes
+---------------
+
+An instance is made of several basic cubes. In the set of available
+basic cubes we can find for example:
+
+Base entity types
+~~~~~~~~~~~~~~~~~
+* addressbook_: PhoneNumber and PostalAddress
+* card_: Card, generic documenting card
+* event_: Event (define events, display them in calendars)
+* file_: File (to allow users to upload and store binary or text files)
+* link_: Link (to collect links to web resources)
+* mailinglist_: MailingList (to reference a mailing-list and the URLs
+ for its archives and its admin interface)
+* person_: Person (easily mixed with addressbook)
+* task_: Task (something to be done between start and stop date)
+* zone_: Zone (to define places within larger places, for example a
+ city in a state in a country)
+
+
+Classification
+~~~~~~~~~~~~~~
+* folder_: Folder (to organize things by grouping them in folders)
+* keyword_: Keyword (to define classification schemes)
+* tag_: Tag (to tag anything)
+
+Other features
+~~~~~~~~~~~~~~
+* basket_: Basket (like a shopping cart)
+* blog_: a blogging system using Blog and BlogEntry entity types
+* comment_: system to attach comment threads to entities)
+* email_: archiving management for emails (`Email`, `Emailpart`,
+ `Emailthread`), trigger action in cubicweb through email
+
+
+
+
+
+.. _addressbook: http://www.cubicweb.org/project/cubicweb-addressbook
+.. _basket: http://www.cubicweb.org/project/cubicweb-basket
+.. _card: http://www.cubicweb.org/project/cubicweb-card
+.. _blog: http://www.cubicweb.org/project/cubicweb-blog
+.. _comment: http://www.cubicweb.org/project/cubicweb-comment
+.. _email: http://www.cubicweb.org/project/cubicweb-email
+.. _event: http://www.cubicweb.org/project/cubicweb-event
+.. _file: http://www.cubicweb.org/project/cubicweb-file
+.. _folder: http://www.cubicweb.org/project/cubicweb-folder
+.. _keyword: http://www.cubicweb.org/project/cubicweb-keyword
+.. _link: http://www.cubicweb.org/project/cubicweb-link
+.. _mailinglist: http://www.cubicweb.org/project/cubicweb-mailinglist
+.. _person: http://www.cubicweb.org/project/cubicweb-person
+.. _tag: http://www.cubicweb.org/project/cubicweb-tag
+.. _task: http://www.cubicweb.org/project/cubicweb-task
+.. _zone: http://www.cubicweb.org/project/cubicweb-zone
+
+To declare the use of a cube, once installed, add the name of the cube
+and its dependency relation in the `__depends_cubes__` dictionary
+defined in the file `__pkginfo__.py` of your own component.
+
+.. note::
+ The listed cubes above are available as debian-packages on `CubicWeb's forge`_.
+
+.. _`CubicWeb's forge`: http://www.cubicweb.org/project?vtitle=All%20cubicweb%20projects
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/cubes/cc-newcube.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,57 @@
+Creating a new cube from scratch
+--------------------------------
+
+Let's start by creating the cube environment in which we will develop ::
+
+ cd ~/cubes
+ # use cubicweb-ctl to generate a template for the cube
+ # will ask some questions, most with nice default
+ cubicweb-ctl newcube mycube
+ # makes the cube source code managed by mercurial
+ cd mycube
+ hg init
+ hg add .
+ hg ci
+
+If all went well, you should see the cube you just created in the list
+returned by ``cubicweb-ctl list`` in the *Available cubes* section.
+If not, please refer to :ref:`ConfigurationEnv`.
+
+To reuse an existing cube, add it to the list named
+``__depends_cubes__`` which is defined in :file:`__pkginfo__.py`.
+This variable is used for the instance packaging (dependencies handled
+by system utility tools such as APT) and to find used cubes when the
+database for the instance is created.
+
+On a Unix system, the available cubes are usually stored in the
+directory :file:`/usr/share/cubicweb/cubes`. If you are using the
+cubicweb mercurial repository (:ref:`SourceInstallation`), the cubes
+are searched in the directory
+:file:`/path/to/cubicweb_toplevel/cubes`. In this configuration
+cubicweb itself ought to be located at
+:file:`/path/to/cubicweb_toplevel/cubicweb`.
+
+.. note::
+
+ Please note that if you do not wish to use default directory for your cubes
+ library, you should set the :envvar:`CW_CUBES_PATH` environment variable to
+ add extra directories where cubes will be search, and you'll then have to use
+ the option `--directory` to specify where you would like to place the source
+ code of your cube:
+
+ ``cubicweb-ctl newcube --directory=/path/to/cubes/library mycube``
+
+
+.. XXX resurrect once live-server is back
+.. Usage of :command:`cubicweb-ctl liveserver`
+.. -------------------------------------------
+
+.. To quickly test a new cube, you can also use the `liveserver` command for cubicweb-ctl
+.. which allows to create an instance in memory (using an SQLite database by
+.. default) and make it accessible through a web server ::
+
+.. cubicweb-ctl live-server mycube
+
+.. or by using an existing database (SQLite or Postgres)::
+
+.. cubicweb-ctl live-server -s myfile_sources mycube
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/cubes/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,11 @@
+Cubes
+=====
+
+This chapter describes how to define your own cubes and reuse already available cubes.
+
+.. toctree::
+ :maxdepth: 1
+
+ layout.rst
+ cc-newcube.rst
+ available-cubes.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/cubes/layout.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,153 @@
+
+.. _foundationsCube:
+
+.. _cubelayout:
+
+Standard structure for a cube
+-----------------------------
+
+A cube is structured as follows:
+
+::
+
+ mycube/
+ |
+ |-- data/
+ | |-- cubes.mycube.css
+ | |-- cubes.mycube.js
+ | `-- external_resources
+ |
+ |-- debian/
+ | |-- changelog
+ | |-- compat
+ | |-- control
+ | |-- copyright
+ | |-- cubicweb-mycube.prerm
+ | `-- rules
+ |
+ |-- entities.py
+ |
+ |-- i18n/
+ | |-- en.po
+ | |-- es.po
+ | `-- fr.po
+ |
+ |-- __init__.py
+ |
+ |-- MANIFEST.in
+ |
+ |-- migration/
+ | |-- postcreate.py
+ | `-- precreate.py
+ |
+ |-- __pkginfo__.py
+ |
+ |-- schema.py
+ |
+ |-- setup.py
+ |
+ |-- site_cubicweb.py
+ |
+ |-- hooks.py
+ |
+ |-- test/
+ | |-- data/
+ | | `-- bootstrap_cubes
+ | |-- pytestconf.py
+ | |-- realdb_test_mycube.py
+ | `-- test_mycube.py
+ |
+ `-- views.py
+
+
+We can use subpackages instead of python modules for ``views.py``, ``entities.py``,
+``schema.py`` or ``hooks.py``. For example, we could have:
+
+::
+
+ mycube/
+ |
+ |-- entities.py
+ |-- hooks.py
+ `-- views/
+ |-- __init__.py
+ |-- forms.py
+ |-- primary.py
+ `-- widgets.py
+
+
+where :
+
+* ``schema`` contains the schema definition (server side only)
+* ``entities`` contains the entity definitions (server side and web interface)
+* ``hooks`` contains hooks and/or views notifications (server side only)
+* ``views`` contains the web interface components (web interface only)
+* ``test`` contains tests related to the cube (not installed)
+* ``i18n`` contains message catalogs for supported languages (server side and
+ web interface)
+* ``data`` contains data files for static content (images, css,
+ javascript code)...(web interface only)
+* ``migration`` contains initialization files for new instances (``postcreate.py``)
+ and a file containing dependencies of the component depending on the version
+ (``depends.map``)
+* ``debian`` contains all the files managing debian packaging (you will find
+ the usual files ``control``, ``rules``, ``changelog``... not installed)
+* file ``__pkginfo__.py`` provides component meta-data, especially the distribution
+ and the current version (server side and web interface) or sub-cubes used by
+ the cube.
+
+
+At least you should have the file ``__pkginfo__.py``.
+
+
+The :file:`__init__.py` and :file:`site_cubicweb.py` files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX WRITEME
+
+The :file:`__pkginfo__.py` file
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It contains metadata describing your cube, mostly useful for packaging.
+
+Two important attributes of this module are __depends__ and __recommends__
+dictionaries that indicates what should be installed (and each version if
+necessary) for the cube to work.
+
+Dependency on other cubes are expected to be of the form 'cubicweb-<cubename>'.
+
+When an instance is created, dependencies are automatically installed, while
+recommends are not.
+
+Recommends may be seen as a kind of 'weak dependency'. Eg, the most important
+effect of recommending a cube is that, if cube A recommends cube B, the cube B
+will be loaded before the cube A (same thing happend when A depends on B).
+
+Having this behaviour is sometime desired: on schema creation, you may rely on
+something defined in the other's schema; on database creation, on something
+created by the other's postcreate, and so on.
+
+
+:file:`migration/precreate.py` and :file:`migration/postcreate.py`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX detail steps of instance creation
+
+
+External resources such as image, javascript and css files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX naming convention external_resources file
+
+
+Out-of the box testing
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX MANIFEST.in, __pkginfo__.include_dirs, debian
+
+
+Packaging and distribution
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX MANIFEST.in, __pkginfo__.include_dirs, debian
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/dataimport.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,85 @@
+.. -*- coding: utf-8 -*-
+
+.. _dataimport:
+
+Dataimport
+==========
+
+*CubicWeb* is designed to manipulate huge of amount of data, and provides utilities to do so.
+
+The main entry point is :mod:`cubicweb.dataimport.importer` which defines an
+:class:`ExtEntitiesImporter` class responsible for importing data from an external source in the
+form :class:`ExtEntity` objects. An :class:`ExtEntity` is a transitional representation of an
+entity to be imported in the CubicWeb instance; building this representation is usually
+domain-specific -- e.g. dependent of the kind of data source (RDF, CSV, etc.) -- and is thus the
+responsibility of the end-user.
+
+Along with the importer, a *store* must be selected, which is responsible for insertion of data into
+the database. There exists different kind of stores_, allowing to insert data within different
+levels of the *CubicWeb* API and with different speed/security tradeoffs. Those keeping all the
+*CubicWeb* hooks and security will be slower but the possible errors in insertion (bad data types,
+integrity error, ...) will be handled.
+
+
+Example
+-------
+
+Consider the following schema snippet.
+
+.. code-block:: python
+
+ class Person(EntityType):
+ name = String(required=True)
+
+ class knows(RelationDefinition):
+ subject = 'Person'
+ object = 'Person'
+
+along with some data in a ``people.csv`` file::
+
+ # uri,name,knows
+ http://www.example.org/alice,Alice,
+ http://www.example.org/bob,Bob,http://www.example.org/alice
+
+The following code (using a shell context) defines a function `extentities_from_csv` to read
+`Person` external entities coming from a CSV file and calls the :class:`ExtEntitiesImporter` to
+insert corresponding entities and relations into the CubicWeb instance.
+
+.. code-block:: python
+
+ from cubicweb.dataimport import ucsvreader, RQLObjectStore
+ from cubicweb.dataimport.importer import ExtEntity, ExtEntitiesImporter
+
+ def extentities_from_csv(fpath):
+ """Yield Person ExtEntities read from `fpath` CSV file."""
+ with open(fpath) as f:
+ for uri, name, knows in ucsvreader(f, skipfirst=True, skip_empty=False):
+ yield ExtEntity('Personne', uri,
+ {'nom': set([name]), 'connait': set([knows])})
+
+ extenties = extentities_from_csv('people.csv')
+ store = RQLObjectStore(cnx)
+ importer = ExtEntitiesImporter(schema, store)
+ importer.import_entities(extenties)
+ commit()
+ rset = cnx.execute('String N WHERE X nom N, X connait Y, Y nom "Alice"')
+ assert rset[0][0] == u'Bob', rset
+
+Importer API
+------------
+
+.. automodule:: cubicweb.dataimport.importer
+
+
+Stores
+~~~~~~
+
+.. automodule:: cubicweb.dataimport.stores
+
+
+SQLGenObjectStore
+-----------------
+
+This store relies on *COPY FROM*/execute many sql commands to directly push data using SQL commands
+rather than using the whole *CubicWeb* API. For now, **it only works with PostgresSQL** as it requires
+the *COPY FROM* command.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/baseschema.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,42 @@
+.. _pre_defined_entity_types:
+
+Pre-defined entities in the library
+-----------------------------------
+
+The library defines a set of entity schemas that are required by the system
+or commonly used in *CubicWeb* instances.
+
+
+Entity types used to store the schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* _`CWEType`, entity type
+* _`CWRType`, relation type
+* _`CWRelation`, relation definition
+* _`CWAttribute`, attribute relation definition
+* _`CWConstraint`, `CWConstraintType`, `RQLExpression`
+
+Entity types used to manage users and permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* _`CWUser`, system users
+* _`CWGroup`, users groups
+
+Entity types used to manage workflows
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* :ref:`Workflow <Workflow>`, workflow entity, linked to some entity types which may use this workflow
+* _`State`, workflow state
+* _`Transition`, workflow transition
+* _`TrInfo`, record of a transition trafic for an entity
+
+Other entity types
+~~~~~~~~~~~~~~~~~~
+* _`CWCache`, cache entities used to improve performances
+* _`CWProperty`, used to configure the instance
+
+* _`EmailAddress`, email address, used by the system to send notifications
+ to the users and also used by others optionnals schemas
+
+* _`Bookmark`, an entity type used to allow a user to customize his links within
+ the instance
+
+* _`ExternalUri`, used for semantic web site to indicate that an entity is the
+ same as another from an external site
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/define-workflows.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,160 @@
+.. -*- coding: utf-8 -*-
+
+.. _Workflow:
+
+Defining a Workflow
+===================
+
+General
+-------
+
+A workflow describes how certain entities have to evolve between different
+states. Hence we have a set of states, and a "transition graph", i.e. a set of
+possible transitions from one state to another state.
+
+We will define a simple workflow for a blog, with only the following two states:
+`submitted` and `published`. You may want to take a look at :ref:`TutosBase` if
+you want to quickly setup an instance running a blog.
+
+Setting up a workflow
+---------------------
+
+We want to create a workflow to control the quality of the BlogEntry
+submitted on the instance. When a BlogEntry is created by a user
+its state should be `submitted`. To be visible to all, it has to
+be in the state `published`. To move it from `submitted` to `published`,
+we need a transition that we can call `approve_blogentry`.
+
+A BlogEntry state should not be modifiable by every user.
+So we have to define a group of users, `moderators`, and
+this group will have appropriate permissions to publish a BlogEntry.
+
+There are two ways to create a workflow: from the user interface, or
+by defining it in ``migration/postcreate.py``. This script is executed
+each time a new ``cubicweb-ctl db-init`` is done. We strongly
+recommend to create the workflow in ``migration/postcreate.py`` and we
+will now show you how. Read `Two bits of warning`_ to understand why.
+
+The state of an entity is managed by the `in_state` attribute which
+can be added to your entity schema by inheriting from
+`cubicweb.schema.WorkflowableEntityType`.
+
+
+About our example of BlogEntry, we must have:
+
+.. sourcecode:: python
+
+ from cubicweb.schema import WorkflowableEntityType
+
+ class BlogEntry(WorkflowableEntityType):
+ ...
+
+
+Creating states, transitions and group permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :mod:`postcreate` script is executed in a special environment,
+adding several |cubicweb| primitives that can be used.
+
+They are all defined in the :class:`ServerMigrationHelper` class.
+We will only discuss the methods we use to create a workflow in this example.
+
+A workflow is a collection of entities of type ``State`` and of type
+``Transition`` which are standard *CubicWeb* entity types.
+
+To define a workflow for BlogDemo, please add the following lines
+to ``migration/postcreate.py``:
+
+.. sourcecode:: python
+
+ _ = unicode
+
+ moderators = add_entity('CWGroup', name=u"moderators")
+
+This adds the `moderators` user group.
+
+.. sourcecode:: python
+
+ wf = add_workflow(u'blog publication workflow', 'BlogEntry')
+
+At first, instanciate a new workflow object with a gentle description
+and the concerned entity types (this one can be a tuple for multiple
+value).
+
+.. sourcecode:: python
+
+ submitted = wf.add_state(_('submitted'), initial=True)
+ published = wf.add_state(_('published'))
+
+This will create two entities of type ``State``, one with name
+'submitted', and the other with name 'published'.
+
+``add_state`` expects as first argument the name of the state you want
+to create and an optional argument to say if it is supposed to be the
+initial state of the entity type.
+
+.. sourcecode:: python
+
+ wf.add_transition(_('approve_blogentry'), (submitted,), published, ('moderators', 'managers'),)
+
+This will create an entity of type ``Transition`` with name
+`approve_blogentry` which will be linked to the ``State`` entities
+created before.
+
+``add_transition`` expects
+
+ * as the first argument: the name of the transition
+ * then the list of states on which the transition can be triggered,
+ * the target state of the transition,
+ * and the permissions
+ (e.g. a list of user groups who can apply the transition; the user
+ has to belong to at least one of the listed group to perform the action).
+
+.. sourcecode:: python
+
+ checkpoint()
+
+.. note::
+ Do not forget to add the `_()` in front of all states and
+ transitions names while creating a workflow so that they will be
+ identified by the i18n catalog scripts.
+
+In addition to the user groups (one of which the user needs to belong
+to), we could have added a RQL condition. In this case, the user can
+only perform the action if the two conditions are satisfied.
+
+If we use an RQL condition on a transition, we can use the following variables:
+
+* `X`, the entity on which we may pass the transition
+* `U`, the user executing that may pass the transition
+
+
+.. image:: ../../../images/03-transitions-view_en.png
+
+You can notice that in the action box of a BlogEntry, the state is now
+listed as well as the possible transitions for the current state
+defined by the workflow.
+
+The transitions will only be displayed for users having the right permissions.
+In our example, the transition `approve_blogentry` will only be displayed
+for the users belonging to the group `moderators` or `managers`.
+
+
+Two bits of warning
+~~~~~~~~~~~~~~~~~~~
+
+We could perfectly use the administration interface to do these
+operations. It is a convenient thing to do at times (when doing
+development, to quick-check things). But it is not recommended beyond
+that because it is a bit complicated to do it right and it will be
+only local to your instance (or, said a bit differently, such a
+workflow only exists in an instance database). Furthermore, you cannot
+write unit tests against deployed instances, and experience shows it
+is mandatory to have tests for any mildly complicated workflow
+setup.
+
+Indeed, if you create the states and transitions through the user
+interface, next time you initialize the database you will have to
+re-create all the workflow entities. The user interface should only be
+a reference for you to view the states and transitions, but is not the
+appropriate interface to define your application workflow.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/definition.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,912 @@
+.. -*- coding: utf-8 -*-
+
+.. _datamodel_definition:
+
+Yams *schema*
+-------------
+
+The **schema** is the core piece of a *CubicWeb* instance as it
+defines and handles the data model. It is based on entity types that
+are either already defined in `Yams`_ and the *CubicWeb* standard
+library; or more specific types defined in cubes. The schema for a
+cube is defined in a `schema` python module or package.
+
+.. _`Yams`: http://www.logilab.org/project/yams
+
+.. _datamodel_overview:
+
+Overview
+~~~~~~~~
+
+The core idea of the yams schema is not far from the classical
+`Entity-relationship`_ model. But while an E/R model (or `logical
+model`) traditionally has to be manually translated to a lower-level
+data description language (such as the SQL `create table`
+sublanguage), also often described as the `physical model`, no such
+step is required with |yams| and |cubicweb|.
+
+.. _`Entity-relationship`: http://en.wikipedia.org/wiki/Entity-relationship_model
+
+This is because in addition to high-level, logical |yams| models, one
+uses the |rql| data manipulation language to query, insert, update and
+delete data. |rql| abstracts as much of the underlying SQL database as
+a |yams| schema abstracts from the physical layout. The vagaries of
+SQL are avoided.
+
+As a bonus point, such abstraction make it quite comfortable to build
+or use different backends to which |rql| queries apply.
+
+So, as in the E/R formalism, the building blocks are ``entities``
+(:ref:`EntityType`), ``relationships`` (:ref:`RelationType`,
+:ref:`RelationDefinition`) and ``attributes`` (handled like relation
+with |yams|).
+
+Let us detail a little the divergences between E/R and |yams|:
+
+* all relationship are binary which means that to represent a
+ non-binary relationship, one has to use an entity,
+* relationships do not support attributes (yet, see:
+ http://www.cubicweb.org/ticket/341318), hence the need to reify it
+ as an entity if need arises,
+* all entities have an `eid` attribute (an integer) that is its
+ primary key (but it is possible to declare uniqueness on other
+ attributes)
+
+Also |yams| supports the notions of:
+
+* entity inheritance (quite experimental yet, and completely
+ undocumented),
+* relation type: that is, relationships can be established over a set
+ of couple of entity types (henre the distinction made between
+ `RelationType` and `RelationDefinition` below)
+
+Finally |yams| has a few concepts of its own:
+
+* relationships being oriented and binary, we call the left hand
+ entity type the `subject` and the right hand entity type the
+ `object`
+
+.. note::
+
+ The |yams| schema is available at run time through the .schema
+ attribute of the `vregistry`. It's an instance of
+ :class:`cubicweb.schema.Schema`, which extends
+ :class:`yams.schema.Schema`.
+
+.. _EntityType:
+
+Entity type
+~~~~~~~~~~~
+
+An entity type is an instance of :class:`yams.schema.EntitySchema`. Each entity type has
+a set of attributes and relations, and some permissions which define who can add, read,
+update or delete entities of this type.
+
+The following built-in types are available: ``String``,
+``Int``, ``BigInt``, ``Float``, ``Decimal``, ``Boolean``,
+``Date``, ``Datetime``, ``Time``, ``Interval``, ``Byte`` and
+``Password``. They can only be used as attributes of an other entity
+type.
+
+There is also a `RichString` kindof type:
+
+.. autofunction:: yams.buildobjs.RichString
+
+The ``__unique_together__`` class attribute is a list of tuples of names of
+attributes or inlined relations. For each tuple, CubicWeb ensures the unicity
+of the combination. For example:
+
+.. sourcecode:: python
+
+ class State(EntityType):
+ __unique_together__ = [('name', 'state_of')]
+
+ name = String(required=True)
+ state_of = SubjectRelation('Workflow', cardinality='1*',
+ composite='object', inlined=True)
+
+
+You can find more base entity types in
+:ref:`pre_defined_entity_types`.
+
+.. XXX yams inheritance
+
+.. _RelationType:
+
+Relation type
+~~~~~~~~~~~~~
+
+A relation type is an instance of
+:class:`yams.schema.RelationSchema`. A relation type is simply a
+semantic definition of a kind of relationship that may occur in an
+application.
+
+It may be referenced by zero, one or more relation definitions.
+
+It is important to choose a good name, at least to avoid conflicts
+with some semantically different relation defined in other cubes
+(since there's only a shared name space for these names).
+
+A relation type holds the following properties (which are hence shared
+between all relation definitions of that type):
+
+* `inlined`: boolean handling the physical optimization for archiving
+ the relation in the subject entity table, instead of creating a specific
+ table for the relation. This applies to relations where cardinality
+ of subject->relation->object is 0..1 (`?`) or 1..1 (`1`) for *all* its relation
+ definitions.
+
+* `symmetric`: boolean indicating that the relation is symmetrical, which
+ means that `X relation Y` implies `Y relation X`.
+
+.. _RelationDefinition:
+
+Relation definition
+~~~~~~~~~~~~~~~~~~~
+
+A relation definition is an instance of
+:class:`yams.schema.RelationDefinition`. It is a complete triplet
+"<subject entity type> <relation type> <object entity type>".
+
+When creating a new instance of that class, the corresponding
+:class:`RelationType` instance is created on the fly if necessary.
+
+Properties
+``````````
+
+The available properties for relation definitions are enumerated
+here. There are several kind of properties, as some relation
+definitions are actually attribute definitions, and other are not.
+
+Some properties may be completely optional, other may have a default
+value.
+
+Common properties for attributes and relations:
+
+* `description`: a unicode string describing an attribute or a
+ relation. By default this string will be used in the editing form of
+ the entity, which means that it is supposed to help the end-user and
+ should be flagged by the function `_` to be properly
+ internationalized.
+
+* `constraints`: a list of conditions/constraints that the relation has to
+ satisfy (c.f. `Constraints`_)
+
+* `cardinality`: a two character string specifying the cardinality of
+ the relation. The first character defines the cardinality of the
+ relation on the subject, and the second on the object. When a
+ relation can have multiple subjects or objects, the cardinality
+ applies to all, not on a one-to-one basis (so it must be
+ consistent...). Default value is '**'. The possible values are
+ inspired from regular expression syntax:
+
+ * `1`: 1..1
+ * `?`: 0..1
+ * `+`: 1..n
+ * `*`: 0..n
+
+Attributes properties:
+
+* `unique`: boolean indicating if the value of the attribute has to be
+ unique or not within all entities of the same type (false by
+ default)
+
+* `indexed`: boolean indicating if an index needs to be created for
+ this attribute in the database (false by default). This is useful
+ only if you know that you will have to run numerous searches on the
+ value of this attribute.
+
+* `default`: default value of the attribute. In case of date types, the values
+ which could be used correspond to the RQL keywords `TODAY` and `NOW`.
+
+* `metadata`: Is also accepted as an argument of the attribute contructor. It is
+ not really an attribute property. see `Metadata`_ for details.
+
+Properties for `String` attributes:
+
+* `fulltextindexed`: boolean indicating if the attribute is part of
+ the full text index (false by default) (*applicable on the type
+ `Byte` as well*)
+
+* `internationalizable`: boolean indicating if the value of the
+ attribute is internationalizable (false by default)
+
+Relation properties:
+
+* `composite`: string indicating that the subject (composite ==
+ 'subject') is composed of the objects of the relations. For the
+ opposite case (when the object is composed of the subjects of the
+ relation), we just set 'object' as value. The composition implies
+ that when the relation is deleted (so when the composite is deleted,
+ at least), the composed are also deleted.
+
+* `fulltext_container`: string indicating if the value if the full
+ text indexation of the entity on one end of the relation should be
+ used to find the entity on the other end. The possible values are
+ 'subject' or 'object'. For instance the use_email relation has that
+ property set to 'subject', since when performing a full text search
+ people want to find the entity using an email address, and not the
+ entity representing the email address.
+
+Constraints
+```````````
+
+By default, the available constraint types are:
+
+General Constraints
+......................
+
+* `SizeConstraint`: allows to specify a minimum and/or maximum size on
+ string (generic case of `maxsize`)
+
+* `BoundaryConstraint`: allows to specify a minimum and/or maximum value
+ on numeric types and date
+
+.. sourcecode:: python
+
+ from yams.constraints import BoundaryConstraint, TODAY, NOW, Attribute
+
+ class DatedEntity(EntityType):
+ start = Date(constraints=[BoundaryConstraint('>=', TODAY())])
+ end = Date(constraints=[BoundaryConstraint('>=', Attribute('start'))])
+
+ class Before(EntityType);
+ last_time = DateTime(constraints=[BoundaryConstraint('<=', NOW())])
+
+* `IntervalBoundConstraint`: allows to specify an interval with
+ included values
+
+.. sourcecode:: python
+
+ class Node(EntityType):
+ latitude = Float(constraints=[IntervalBoundConstraint(-90, +90)])
+
+* `UniqueConstraint`: identical to "unique=True"
+
+* `StaticVocabularyConstraint`: identical to "vocabulary=(...)"
+
+Constraints can be dependent on a fixed value (90, Date(2015,3,23)) or a variable.
+In this second case, yams can handle :
+
+* `Attribute`: compare to the value of another attribute.
+* `TODAY`: compare to the current Date.
+* `NOW`: compare to the current Datetime.
+
+RQL Based Constraints
+......................
+
+RQL based constraints may take three arguments. The first one is the ``WHERE``
+clause of a RQL query used by the constraint. The second argument ``mainvars``
+is the ``Any`` clause of the query. By default this include `S` reserved for the
+subject of the relation and `O` for the object. Additional variables could be
+specified using ``mainvars``. The argument expects a single string with all
+variable's name separated by spaces. The last one, ``msg``, is the error message
+displayed when the constraint fails. As RQLVocabularyConstraint never fails the
+third argument is not available.
+
+* `RQLConstraint`: allows to specify a RQL query that has to be satisfied
+ by the subject and/or the object of relation. In this query the variables
+ `S` and `O` are reserved for the relation subject and object entities.
+
+* `RQLVocabularyConstraint`: similar to the previous type of constraint except
+ that it does not express a "strong" constraint, which means it is only used to
+ restrict the values listed in the drop-down menu of editing form, but it does
+ not prevent another entity to be selected.
+
+* `RQLUniqueConstraint`: allows to the specify a RQL query that ensure that an
+ attribute is unique in a specific context. The Query must **never** return more
+ than a single result to be satisfied. In this query the variables `S` is
+ reserved for the relation subject entity. The other variables should be
+ specified with the second constructor argument (mainvars). This constraint type
+ should be used when __unique_together__ doesn't fit.
+
+.. XXX note about how to add new constraint
+
+.. _securitymodel:
+
+The security model
+~~~~~~~~~~~~~~~~~~
+
+The security model of `CubicWeb` is based on `Access Control List`.
+The main principles are:
+
+* users and groups of users
+* a user belongs to at least one group of user
+* permissions (`read`, `update`, `create`, `delete`)
+* permissions are assigned to groups (and not to users)
+
+For *CubicWeb* in particular:
+
+* we associate rights at the entities/relations schema level
+
+* the default groups are: `managers`, `users` and `guests`
+
+* users belong to the `users` group
+
+* there is a virtual group called `owners` to which we can associate only
+ `delete` and `update` permissions
+
+ * we can not add users to the `owners` group, they are implicitly added to it
+ according to the context of the objects they own
+
+ * the permissions of this group are only checked on `update`/`delete` actions
+ if all the other groups the user belongs to do not provide those permissions
+
+Setting permissions is done with the class attribute `__permissions__`
+of entity types and relation definitions. The value of this attribute
+is a dictionary where the keys are the access types (action), and the
+values are the authorized groups or rql expressions.
+
+For an entity type, the possible actions are `read`, `add`, `update` and
+`delete`.
+
+For a relation, the possible actions are `read`, `add`, and `delete`.
+
+For an attribute, the possible actions are `read`, `add` and `update`,
+and they are a refinement of an entity type permission.
+
+.. note::
+
+ By default, the permissions of an entity type attributes are
+ equivalent to the permissions of the entity type itself.
+
+ It is possible to provide custom attribute permissions which are
+ stronger than, or are more lenient than the entity type
+ permissions.
+
+ In a situation where all attributes were given custom permissions,
+ the entity type permissions would not be checked, except for the
+ `delete` action.
+
+For each access type, a tuple indicates the name of the authorized groups and/or
+one or multiple RQL expressions to satisfy to grant access. The access is
+provided if the user is in one of the listed groups or if one of the RQL condition
+is satisfied.
+
+Default permissions
+```````````````````
+
+The default permissions for ``EntityType`` are:
+
+.. sourcecode:: python
+
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests',),
+ 'update': ('managers', 'owners',),
+ 'delete': ('managers', 'owners'),
+ 'add': ('managers', 'users',)
+ }
+
+The default permissions for relations are:
+
+.. sourcecode:: python
+
+ __permissions__ = {'read': ('managers', 'users', 'guests',),
+ 'delete': ('managers', 'users'),
+ 'add': ('managers', 'users',)}
+
+The default permissions for attributes are:
+
+.. sourcecode:: python
+
+ __permissions__ = {'read': ('managers', 'users', 'guests',),
+ 'add': ('managers', ERQLExpression('U has_add_permission X'),
+ 'update': ('managers', ERQLExpression('U has_update_permission X')),}
+
+.. note::
+
+ The default permissions for attributes are not syntactically
+ equivalent to the default permissions of the entity types, but the
+ rql expressions work by delegating to the entity type permissions.
+
+
+The standard user groups
+````````````````````````
+
+* `guests`
+
+* `users`
+
+* `managers`
+
+* `owners`: virtual group corresponding to the entity's owner.
+ This can only be used for the actions `update` and `delete` of an entity
+ type.
+
+It is also possible to use specific groups if they are defined in the precreate
+script of the cube (``migration/precreate.py``). Defining groups in postcreate
+script or later makes them unavailable for security purposes (in this case, an
+`sync_schema_props_perms` command has to be issued in a CubicWeb shell).
+
+
+Use of RQL expression for write permissions
+```````````````````````````````````````````
+
+It is possible to define RQL expression to provide update permission (`add`,
+`delete` and `update`) on entity type / relation definitions. An rql expression
+is a piece of query (corresponds to the WHERE statement of an RQL query), and the
+expression will be considered as satisfied if it returns some results. They can
+not be used in `read` permission.
+
+To use RQL expression in entity type permission:
+
+* you have to use the class :class:`~cubicweb.schema.ERQLExpression`
+
+* in this expression, the variables `X` and `U` are pre-defined references
+ respectively on the current entity (on which the action is verified) and on the
+ user who send the request
+
+For RQL expressions on a relation type, the principles are the same except for
+the following:
+
+* you have to use the class :class:`~cubicweb.schema.RRQLExpression` instead of
+ :class:`~cubicweb.schema.ERQLExpression`
+
+* in the expression, the variables `S`, `O` and `U` are pre-defined references to
+ respectively the subject and the object of the current relation (on which the
+ action is being verified) and the user who executed the query
+
+To define security for attributes of an entity (non-final relation), you have to
+use the class :class:`~cubicweb.schema.ERQLExpression` in which `X` represents
+the entity the attribute belongs to.
+
+It is possible to use in those expression a special relation
+`has_<ACTION>_permission` where the subject is the user (eg 'U') and the object
+is any variable representing an entity (usually 'X' in
+:class:`~cubicweb.schema.ERQLExpression`, 'S' or 'O' in
+:class:`~cubicweb.schema.RRQLExpression`), meaning that the user needs to have
+permission to execute the action <ACTION> on the entities represented by this
+variable. It's recommanded to use this feature whenever possible since it
+simplify greatly complex security definition and upgrade.
+
+
+.. sourcecode:: python
+
+ class my_relation(RelationDefinition):
+ __permissions__ = {'read': ('managers', 'users'),
+ 'add': ('managers', RRQLExpression('U has_update_permission S')),
+ 'delete': ('managers', RRQLExpression('U has_update_permission S'))
+ }
+
+In the above example, user will be allowed to add/delete `my_relation` if he has
+the `update` permission on the subject of the relation.
+
+.. note::
+
+ Potentially, the `use of an RQL expression to add an entity or a relation` can
+ cause problems for the user interface, because if the expression uses the
+ entity or the relation to create, we are not able to verify the permissions
+ before we actually added the entity (please note that this is not a problem for
+ the RQL server at all, because the permissions checks are done after the
+ creation). In such case, the permission check methods
+ (CubicWebEntitySchema.check_perm and has_perm) can indicate that the user is
+ not allowed to create this entity while it would obtain the permission. To
+ compensate this problem, it is usually necessary in such case to use an action
+ that reflects the schema permissions but which check properly the permissions
+ so that it would show up only if possible.
+
+
+Use of RQL expression for reading rights
+````````````````````````````````````````
+
+The principles are the same but with the following restrictions:
+
+* you can not use rql expression for the `read` permission of relations and
+ attributes,
+
+* you can not use special `has_<ACTION>_permission` relation in the rql
+ expression.
+
+
+Important notes about write permissions checking
+````````````````````````````````````````````````
+
+Write permissions (e.g. 'add', 'update', 'delete') are checked in core hooks.
+
+When a permission is checked slightly vary according to if it's an entity or
+relation, and if the relation is an attribute relation or not). It's important to
+understand that since according to when a permission is checked, values returned
+by rql expressions may changes, hence the permission being granted or not.
+
+Here are the current rules:
+
+1. permission to add/update entity and its attributes are checked on
+ commit
+
+2. permission to delete an entity is checked in 'before_delete_entity' hook
+
+3. permission to add a relation is checked either:
+
+ - in 'before_add_relation' hook if the relation type is in the
+ `BEFORE_ADD_RELATIONS` set
+
+ - else at commit time if the relation type is in the `ON_COMMIT_ADD_RELATIONS`
+ set
+
+ - else in 'after_add_relation' hook (the default)
+
+4. permission to delete a relation is checked in 'before_delete_relation' hook
+
+Last but not least, remember queries issued from hooks and operation are by
+default 'unsafe', eg there are no read or write security checks.
+
+See :mod:`cubicweb.hooks.security` for more details.
+
+
+.. _yams_example:
+
+
+Derived attributes and relations
+--------------------------------
+
+.. note:: **TODO** Check organisation of the whole chapter of the documentation
+
+Cubicweb offers the possibility to *query* data using so called
+*computed* relations and attributes. Those are *seen* by RQL requests
+as normal attributes and relations but are actually derived from other
+attributes and relations. In a first section we'll informally review
+two typical use cases. Then we see how to use computed attributes and
+relations in your schema. Last we will consider various significant
+aspects of their implementation and the impact on their usage.
+
+Motivating use cases
+~~~~~~~~~~~~~~~~~~~~
+
+Computed (or reified) relations
+```````````````````````````````
+
+It often arises that one must represent a ternary relation, or a
+family of relations. For example, in the context of an exhibition
+catalog you might want to link all *contributors* to the *work* they
+contributed to, but this contribution can be as *illustrator*,
+*author*, *performer*, ...
+
+The classical way to describe this kind of information within an
+entity-relationship schema is to *reify* the relation, that is turn
+the relation into a entity. In our example the schema will have a
+*Contribution* entity type used to represent the family of the
+contribution relations.
+
+
+.. sourcecode:: python
+
+ class ArtWork(EntityType):
+ name = String()
+ ...
+
+ class Person(EntityType):
+ name = String()
+ ...
+
+ class Contribution(EntityType):
+ contributor = SubjectRelation('Person', cardinality='1*', inlined=True)
+ manifestation = SubjectRelation('ArtWork')
+ role = SubjectRelation('Role')
+
+ class Role(EntityType):
+ name = String()
+
+But then, in order to query the illustrator(s) ``I`` of a work ``W``,
+one has to write::
+
+ Any I, W WHERE C is Contribution, C contributor I, C manifestation W,
+ C role R, R name 'illustrator'
+
+whereas we would like to be able to simply write::
+
+ Any I, W WHERE I illustrator_of W
+
+This is precisely what the computed relations allow.
+
+
+Computed (or synthesized) attribute
+```````````````````````````````````
+
+Assuming a trivial schema for describing employees in companies, one
+can be interested in the total of salaries payed by a company for
+all its employees. One has to write::
+
+ Any C, SUM(SA) GROUPBY S WHERE E works_for C, E salary SA
+
+whereas it would be most convenient to simply write::
+
+ Any C, TS WHERE C total_salary TS
+
+And this is again what computed attributes provide.
+
+
+Using computed attributes and relations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Computed (or reified) relations
+```````````````````````````````
+
+In the above case we would define the *computed relation*
+``illustrator_of`` in the schema by:
+
+.. sourcecode:: python
+
+ class illustrator_of(ComputedRelation):
+ rule = ('C is Contribution, C contributor S, C manifestation O,'
+ 'C role R, R name "illustrator"')
+
+You will note that:
+
+* the ``S`` and ``O`` RQL variables implicitly identify the subject and
+ object of the defined computed relation, akin to what happens in
+ RRQLExpression
+* the possible subject and object entity types are inferred from the rule;
+* computed relation definitions always have empty *add* and *delete* permissions
+* *read* permissions can be defined, permissions from the relations used in the
+ rewrite rule **are not considered** ;
+* nothing else may be defined on the `ComputedRelation` subclass beside
+ description, permissions and rule (e.g. no cardinality, composite, etc.,).
+ `BadSchemaDefinition` is raised on attempt to specify other attributes;
+* computed relations can not be used in 'SET' and 'DELETE' rql queries
+ (`BadQuery` exception raised).
+
+
+NB: The fact that the *add* and *delete* permissions are *empty* even
+for managers is expected to make the automatic UI not attempt to edit
+them.
+
+Computed (or synthesized) attributes
+````````````````````````````````````
+
+In the above case we would define the *computed attribute*
+``total_salary`` on the ``Company`` entity type in the schema by:
+
+.. sourcecode:: python
+
+ class Company(EntityType):
+ name = String()
+ total_salary = Int(formula='Any SUM(SA) GROUPBY E WHERE P works_for X, E salary SA')
+
+* the ``X`` RQL variable implicitly identifies the entity holding the
+ computed attribute, akin to what happens in ERQLExpression;
+* the type inferred from the formula is checked against the declared type, and
+ `BadSchemaDefinition` is raised if they don't match;
+* the computed attributes always have empty *update* permissions
+* `BadSchemaDefinition` is raised on attempt to set 'update' permissions;
+* 'read' permissions can be defined, permissions regarding the formula
+ **are not considered**;
+* other attribute's property (inlined, ...) can be defined as for normal attributes;
+* Similarly to computed relation, computed attribute can't be used in 'SET' and
+ 'DELETE' rql queries (`BadQuery` exception raised).
+
+
+API and implementation
+~~~~~~~~~~~~~~~~~~~~~~
+
+Representation in the data backend
+``````````````````````````````````
+
+Computed relations have no direct representation at the SQL table
+level. Instead, each time a query is issued the query is rewritten to
+replace the computed relation by its equivalent definition and the
+resulting rewritten query is performed in the usual way.
+
+On the contrary, computed attributes are represented as a column in the
+table for their host entity type, just like normal attributes. Their
+value is kept up-to-date with respect to their defintion by a system
+of hooks (also called triggers in most RDBMS) which recomputes them
+when the relations and attributes they depend on are modified.
+
+Yams API
+````````
+
+When accessing the schema through the *yams API* (not when defining a
+schema in a ``schema.py`` file) the computed attributes and relations
+are represented as follows:
+
+relations
+ The ``yams.RelationSchema`` class has a new ``rule`` attribute
+ holding the rule as a string. If this attribute is set all others
+ must not be set.
+attributes
+ A new property ``formula`` is added on class
+ ``yams.RelationDefinitionSchema`` alomng with a new keyword
+ argument ``formula`` on the initializer.
+
+Migration
+`````````
+
+The migrations are to be handled as summarized in the array below.
+
++------------+---------------------------------------------------+---------------------------------------+
+| | Computed rtype | Computed attribute |
++============+===================================================+=======================================+
+| add | * add_relation_type | * add_attribute |
+| | * add_relation_definition should trigger an error | * add_relation_definition |
++------------+---------------------------------------------------+---------------------------------------+
+| modify | * sync_schema_prop_perms: | * sync_schema_prop_perms: |
+| | checks the rule is | |
+| (rule or | synchronized with the database | - empty the cache, |
+| formula) | | - check formula, |
+| | | - make sure all the values get |
+| | | updated |
++------------+---------------------------------------------------+---------------------------------------+
+| del | * drop_relation_type | * drop_attribute |
+| | * drop_relation_definition should trigger an error| * drop_relation_definition |
++------------+---------------------------------------------------+---------------------------------------+
+
+
+Defining your schema using yams
+-------------------------------
+
+Entity type definition
+~~~~~~~~~~~~~~~~~~~~~~
+
+An entity type is defined by a Python class which inherits from
+:class:`yams.buildobjs.EntityType`. The class definition contains the
+description of attributes and relations for the defined entity type.
+The class name corresponds to the entity type name. It is expected to
+be defined in the module ``mycube.schema``.
+
+:Note on schema definition:
+
+ The code in ``mycube.schema`` is not meant to be executed. The class
+ EntityType mentioned above is different from the EntitySchema class
+ described in the previous chapter. EntityType is a helper class to
+ make Entity definition easier. Yams will process EntityType classes
+ and create EntitySchema instances from these class definitions. Similar
+ manipulation happen for relations.
+
+When defining a schema using python files, you may use the following shortcuts:
+
+- `required`: boolean indicating if the attribute is required, ed subject cardinality is '1'
+
+- `vocabulary`: specify static possible values of an attribute
+
+- `maxsize`: integer providing the maximum size of a string (no limit by default)
+
+For example:
+
+.. sourcecode:: python
+
+ class Person(EntityType):
+ """A person with the properties and the relations necessary for my
+ application"""
+
+ last_name = String(required=True, fulltextindexed=True)
+ first_name = String(required=True, fulltextindexed=True)
+ title = String(vocabulary=('Mr', 'Mrs', 'Miss'))
+ date_of_birth = Date()
+ works_for = SubjectRelation('Company', cardinality='?*')
+
+
+The entity described above defines three attributes of type String,
+last_name, first_name and title, an attribute of type Date for the date of
+birth and a relation that connects a `Person` to another entity of type
+`Company` through the semantic `works_for`.
+
+
+
+:Naming convention:
+
+ Entity class names must start with an uppercase letter. The common
+ usage is to use ``CamelCase`` names.
+
+ Attribute and relation names must start with a lowercase letter. The
+ common usage is to use ``underscore_separated_words``. Attribute and
+ relation names starting with a single underscore are permitted, to
+ denote a somewhat "protected" or "private" attribute.
+
+ In any case, identifiers starting with "CW" or "cw" are reserved for
+ internal use by the framework.
+
+ .. _Metadata:
+
+ Some attribute using the name of another attribute as prefix are considered
+ metadata. For example, if an EntityType have both a ``data`` and
+ ``data_format`` attribute, ``data_format`` is view as the ``format`` metadata
+ of ``data``. Later the :meth:`cw_attr_metadata` method will allow you to fetch
+ metadata related to an attribute. There are only three valid metadata names:
+ ``format``, ``encoding`` and ``name``.
+
+
+The name of the Python attribute corresponds to the name of the attribute
+or the relation in *CubicWeb* application.
+
+An attribute is defined in the schema as follows::
+
+ attr_name = AttrType(*properties, metadata={})
+
+where
+
+* `AttrType`: is one of the type listed in EntityType_,
+
+* `properties`: is a list of the attribute needs to satisfy (see `Properties`_
+ for more details),
+
+* `metadata`: is a dictionary of meta attributes related to ``attr_name``.
+ Dictionary keys are the name of the meta attribute. Dictionary values
+ attributes objects (like the content of ``AttrType``). For each entry of the
+ metadata dictionary a ``<attr_name>_<key> = <value>`` attribute is
+ automaticaly added to the EntityType. see `Metadata`_ section for details
+ about valid key.
+
+
+ ---
+
+While building your schema
+
+* it is possible to use the attribute `meta` to flag an entity type as a `meta`
+ (e.g. used to describe/categorize other entities)
+
+.. XXX the paragraph below needs clarification and / or moving out in
+.. another place
+
+*Note*: if you end up with an `if` in the definition of your entity, this probably
+means that you need two separate entities that implement the `ITree` interface and
+get the result from `.children()` which ever entity is concerned.
+
+.. Inheritance
+.. ```````````
+.. XXX feed me
+
+
+Definition of relations
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. XXX add note about defining relation type / definition
+
+A relation is defined by a Python class heriting `RelationType`. The name
+of the class corresponds to the name of the type. The class then contains
+a description of the properties of this type of relation, and could as well
+contain a string for the subject and a string for the object. This allows to create
+new definition of associated relations, (so that the class can have the
+definition properties from the relation) for example ::
+
+ class locked_by(RelationType):
+ """relation on all entities indicating that they are locked"""
+ inlined = True
+ cardinality = '?*'
+ subject = '*'
+ object = 'CWUser'
+
+If provided, the `subject` and `object` attributes denote the subject
+and object of the various relation definitions related to the relation
+type. Allowed values for these attributes are:
+
+* a string corresponding to an entity type
+* a tuple of string corresponding to multiple entity types
+* the '*' special string, meaning all types of entities
+
+When a relation is not inlined and not symmetrical, and it does not require
+specific permissions, it can be defined using a `SubjectRelation`
+attribute in the EntityType class. The first argument of `SubjectRelation` gives
+the entity type for the object of the relation.
+
+:Naming convention:
+
+ Although this way of defining relations uses a Python class, the
+ naming convention defined earlier prevails over the PEP8 conventions
+ used in the framework: relation type class names use
+ ``underscore_separated_words``.
+
+:Historical note:
+
+ It has been historically possible to use `ObjectRelation` which
+ defines a relation in the opposite direction. This feature is
+ deprecated and therefore should not be used in newly written code.
+
+:Future deprecation note:
+
+ In an even more remote future, it is quite possible that the
+ SubjectRelation shortcut will become deprecated, in favor of the
+ RelationType declaration which offers some advantages in the context
+ of reusable cubes.
+
+
+
+
+Handling schema changes
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Also, it should be clear that to properly handle data migration, an
+instance's schema is stored in the database, so the python schema file
+used to defined it is only read when the instance is created or
+upgraded.
+
+.. XXX complete me
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,12 @@
+Data model
+==========
+
+This chapter describes how you define a schema and how to make it evolves as the time goes.
+
+.. toctree::
+ :maxdepth: 1
+
+ definition
+ metadata
+ baseschema
+ define-workflows
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/datamodel/metadata.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,37 @@
+
+Metadata
+--------
+
+.. index::
+ schema: meta-data;
+ schema: eid; creation_date; modification_data; cwuri
+ schema: created_by; owned_by; is; is_instance;
+
+Each entity type in |cubicweb| has at least the following meta-data attributes and relations:
+
+`eid`
+ entity's identifier which is unique in an instance. We usually call this identifier `eid` for historical reason.
+
+`creation_date`
+ Date and time of the creation of the entity.
+
+`modification_date`
+ Date and time of the latest modification of an entity.
+
+`cwuri`
+ Reference URL of the entity, which is not expected to change.
+
+`created_by`
+ Relation to the :ref:`users <CWUser>` who has created the entity
+
+`owned_by`
+ Relation to :ref:`users <CWUser>` whom the entity belongs; usually the creator but not
+ necessary, and it could have multiple owners notably for permission control
+
+`is`
+ Relation to the :ref:`entity type <CWEType>` of which type the entity is.
+
+`is_instance`
+ Relation to the :ref:`entity types <CWEType>` of which type the
+ entity is an instance of.
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/devcore/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,8 @@
+Core APIs
+=========
+
+.. toctree::
+ :maxdepth: 1
+
+ reqbase.rst
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/devcore/reqbase.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,40 @@
+Request and ResultSet methods
+-----------------------------
+
+Those are methods you'll find on both request objects and on
+repository session.
+
+Request methods
+~~~~~~~~~~~~~~~
+
+`URL handling`:
+
+* `build_url(*args, **kwargs)`, returns an absolute URL based on the
+ given arguments. The *controller* supposed to handle the response,
+ can be specified through the first positional parameter (the
+ connection is theoretically done automatically :).
+
+`Data formatting`:
+
+* `format_date(date, date_format=None, time=False)` returns a string for a
+ date time according to instance's configuration
+
+* `format_time(time)` returns a string for a date time according to
+ instance's configuration
+
+`And more...`:
+
+* `tal_render(template, variables)`, renders a precompiled page template with
+ variables in the given dictionary as context
+
+
+Result set methods
+~~~~~~~~~~~~~~~~~~
+
+* `get_entity(row, col)`, returns the entity corresponding to the data position
+ in the *result set*
+
+* `complete_entity(row, col, skip_bytes=True)`, is equivalent to `get_entity` but
+ also call the method `complete()` on the entity before returning it
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/adapters.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,167 @@
+.. _adapters:
+
+Interfaces and Adapters
+-----------------------
+
+Interfaces are the same thing as object-oriented programming `interfaces`_.
+Adapter refers to a well-known `adapter`_ design pattern that helps separating
+concerns in object oriented applications.
+
+.. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
+.. _`adapter`: http://en.wikipedia.org/wiki/Adapter_pattern
+
+In |cubicweb| adapters provide logical functionalities to entity types.
+
+Definition of an adapter is quite trivial. An excerpt from cubicweb
+itself (found in :mod:`cubicweb.entities.adapters`):
+
+.. sourcecode:: python
+
+
+ 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 this default implementation
+ """
+ __regid__ = 'ITree'
+
+ child_role = 'subject'
+ parent_role = 'object'
+
+ def children_rql(self):
+ """returns RQL to get children """
+ return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
+
+The adapter object has ``self.entity`` attribute which represents the
+entity being adapted.
+
+.. Note::
+
+ Adapters came with the notion of service identified by the registry identifier
+ of an adapters, hence dropping the need for explicit interface and the
+ :class:`cubicweb.predicates.implements` selector. You should instead use
+ :class:`cubicweb.predicates.is_instance` when you want to select on an entity
+ type, or :class:`cubicweb.predicates.adaptable` when you want to select on a
+ service.
+
+
+Specializing and binding an adapter
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. sourcecode:: python
+
+ from cubicweb.entities.adapters import ITreeAdapter
+
+ class MyEntityITreeAdapter(ITreeAdapter):
+ __select__ = is_instance('MyEntity')
+ tree_relation = 'filed_under'
+
+The ITreeAdapter here provides a default implementation. The
+tree_relation class attribute is actually used by this implementation
+to help implement correct behaviour.
+
+Here we provide a specific implementation which will be bound for
+``MyEntity`` entity type (the `adaptee`).
+
+
+.. _interfaces_to_adapters:
+
+Converting code from Interfaces/Mixins to Adapters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here we go with a small example. Before:
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import implements
+ from cubicweb.interfaces import ITree
+ from cubicweb.mixins import ITreeMixIn
+
+ class MyEntity(ITreeMixIn, AnyEntity):
+ __implements__ = AnyEntity.__implements__ + (ITree,)
+
+
+ class ITreeView(EntityView):
+ __select__ = implements('ITree')
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(row, col)
+ children = entity.children()
+
+After:
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import adaptable, is_instance
+ from cubicweb.entities.adapters import ITreeAdapter
+
+ class MyEntityITreeAdapter(ITreeAdapter):
+ __select__ = is_instance('MyEntity')
+
+ class ITreeView(EntityView):
+ __select__ = adaptable('ITree')
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(row, col)
+ itree = entity.cw_adapt_to('ITree')
+ children = itree.children()
+
+As we can see, the interface/mixin duality disappears and the entity
+class itself is completely freed from these concerns. When you want
+to use the ITree interface of an entity, call its `cw_adapt_to` method
+to get an adapter for this interface, then access to members of the
+interface on the adapter
+
+Let's look at an example where we defined everything ourselves. We
+start from:
+
+.. sourcecode:: python
+
+ class IFoo(Interface):
+ def bar(self, *args):
+ raise NotImplementedError
+
+ class MyEntity(AnyEntity):
+ __regid__ = 'MyEntity'
+ __implements__ = AnyEntity.__implements__ + (IFoo,)
+
+ def bar(self, *args):
+ return sum(captain.age for captain in self.captains)
+
+ class FooView(EntityView):
+ __regid__ = 'mycube.fooview'
+ __select__ = implements('IFoo')
+
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(row, col)
+ self.w('bar: %s' % entity.bar())
+
+Converting to:
+
+.. sourcecode:: python
+
+ class IFooAdapter(EntityAdapter):
+ __regid__ = 'IFoo'
+ __select__ = is_instance('MyEntity')
+
+ def bar(self, *args):
+ return sum(captain.age for captain in self.entity.captains)
+
+ class FooView(EntityView):
+ __regid__ = 'mycube.fooview'
+ __select__ = adaptable('IFoo')
+
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(row, col)
+ self.w('bar: %s' % entity.cw_adapt_to('IFoo').bar())
+
+.. note::
+
+ When migrating an entity method to an adapter, the code can be moved as is
+ except for the `self` of the entity class, which in the adapter must become `self.entity`.
+
+Adapters defined in the library
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: cubicweb.entities.adapters
+ :members:
+
+More are defined in web/views.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/application-logic.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,177 @@
+How to use entities objects and adapters
+----------------------------------------
+
+The previous chapters detailed the classes and methods available to
+the developer at the so-called `ORM`_ level. However they say little
+about the common patterns of usage of these objects.
+
+.. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping
+
+Entities objects (and their adapters) are used in the repository and
+web sides of CubicWeb. On the repository side of things, one should
+manipulate them in Hooks and Operations.
+
+Hooks and Operations provide support for the implementation of rules
+such as computed attributes, coherency invariants, etc (they play the
+same role as database triggers, but in a way that is independent of
+the actual data sources).
+
+So a lot of an application's business rules will be written in Hooks
+(or Operations).
+
+On the web side, views also typically operate using entity
+objects. Obvious entity methods for use in views are the Dublin Core
+methods like ``dc_title``. For separation of concerns reasons, one
+should ensure no ui logic pervades the entities level, and also no
+business logic should creep into the views.
+
+In the duration of a transaction, entities objects can be instantiated
+many times, in views and hooks, even for the same database entity. For
+instance, in a classic CubicWeb deployment setup, the repository and
+the web front-end are separated process communicating over the
+wire. There is no way state can be shared between these processes
+(there is a specific API for that). Hence, it is not possible to use
+entity objects as messengers between these components of an
+application. It means that an attribute set as in ``obj.x = 42``,
+whether or not x is actually an entity schema attribute, has a short
+life span, limited to the hook, operation or view within which the
+object was built.
+
+Setting an attribute or relation value can be done in the context of a
+Hook/Operation, using the ``obj.cw_set(x=42)`` notation or a plain
+RQL ``SET`` expression.
+
+In views, it would be preferable to encapsulate the necessary logic in
+a method of an adapter for the concerned entity class(es). But of
+course, this advice is also reasonable for Hooks/Operations, though
+the separation of concerns here is less stringent than in the case of
+views.
+
+This leads to the practical role of objects adapters: it's where an
+important part of the application logic lies (the other part being
+located in the Hook/Operations).
+
+Anatomy of an entity class
+--------------------------
+
+We can look now at a real life example coming from the `tracker`_
+cube. Let us begin to study the ``entities/project.py`` content.
+
+.. sourcecode:: python
+
+ from cubicweb.entities.adapters import ITreeAdapter
+
+ class ProjectAdapter(ITreeAdapter):
+ __select__ = is_instance('Project')
+ tree_relation = 'subproject_of'
+
+ class Project(AnyEntity):
+ __regid__ = 'Project'
+ fetch_attrs, cw_fetch_order = fetch_config(('name', 'description',
+ 'description_format', 'summary'))
+
+ TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
+
+ def dc_title(self):
+ return self.name
+
+The fact that the `Project` entity type implements an ``ITree``
+interface is materialized by the ``ProjectAdapter`` class (inheriting
+the pre-defined ``ITreeAdapter`` whose ``__regid__`` is of course
+``ITree``), which will be selected on `Project` entity types because
+of its selector. On this adapter, we redefine the ``tree_relation``
+attribute of the ``ITreeAdapter`` class.
+
+This is typically used in views concerned with the representation of
+tree-like structures (CubicWeb provides several such views).
+
+It is important that the views themselves try not to implement this
+logic, not only because such views would be hardly applyable to other
+tree-like relations, but also because it is perfectly fine and useful
+to use such an interface in Hooks.
+
+In fact, Tree nature is a property of the data model that cannot be
+fully and portably expressed at the level of database entities (think
+about the transitive closure of the child relation). This is a further
+argument to implement it at entity class level.
+
+``fetch_attrs`` configures which attributes should be pre-fetched when using ORM
+methods retrieving entity of this type. In a same manner, the ``cw_fetch_order`` is
+a class method allowing to control sort order. More on this in :ref:`FetchAttrs`.
+
+We can observe the big ``TICKET_DEFAULT_STATE_RESTR`` is a pure
+application domain piece of data. There is, of course, no limitation
+to the amount of class attributes of this kind.
+
+The ``dc_title`` method provides a (unicode string) value likely to be
+consumed by views, but note that here we do not care about output
+encodings. We care about providing data in the most universal format
+possible, because the data could be used by a web view (which would be
+responsible of ensuring XHTML compliance), or a console or file
+oriented output (which would have the necessary context about the
+needed byte stream encoding).
+
+.. note::
+
+ The Dublin Core `dc_xxx` methods are not moved to an adapter as they
+ are extremely prevalent in CubicWeb and assorted cubes and should be
+ available for all entity types.
+
+Let us now dig into more substantial pieces of code, continuing the
+Project class.
+
+.. sourcecode:: python
+
+ def latest_version(self, states=('published',), reverse=None):
+ """returns the latest version(s) for the project in one of the given
+ states.
+
+ when no states specified, returns the latest published version.
+ """
+ order = 'DESC'
+ if reverse is not None:
+ warn('reverse argument is deprecated',
+ DeprecationWarning, stacklevel=1)
+ if reverse:
+ order = 'ASC'
+ rset = self.versions_in_state(states, order, True)
+ if rset:
+ return rset.get_entity(0, 0)
+ return None
+
+ def versions_in_state(self, states, order='ASC', limit=False):
+ """returns version(s) for the project in one of the given states, sorted
+ by version number.
+
+ If limit is true, limit result to one version.
+ If reverse, versions are returned from the smallest to the greatest.
+ """
+ if limit:
+ order += ' LIMIT 1'
+ rql = 'Any V,N ORDERBY version_sort_value(N) %s ' \
+ 'WHERE V num N, V in_state S, S name IN (%s), ' \
+ 'V version_of P, P eid %%(p)s' % (order, ','.join(repr(s) for s in states))
+ return self._cw.execute(rql, {'p': self.eid})
+
+.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker/
+
+These few lines exhibit the important properties we want to outline:
+
+* entity code is concerned with the application domain
+
+* it is NOT concerned with database consistency (this is the realm of
+ Hooks/Operations); in other words, it assumes a consistent world
+
+* it is NOT (directly) concerned with end-user interfaces
+
+* however it can be used in both contexts
+
+* it does not create or manipulate the internal object's state
+
+* it plays freely with RQL expression as needed
+
+* it is not concerned with internationalization
+
+* it does not raise exceptions
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/data-as-objects.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,142 @@
+Access to persistent data
+--------------------------
+
+Python-level access to persistent data is provided by the
+:class:`Entity <cubicweb.entity>` class.
+
+.. XXX this part is not clear. refactor it.
+
+An entity class is bound to a schema entity type. Descriptors are added when
+classes are registered in order to initialize the class according to its schema:
+
+* the attributes defined in the schema appear as attributes of these classes
+
+* the relations defined in the schema appear as attributes of these classes,
+ but are lists of instances
+
+`Formatting and output generation`:
+
+* :meth:`view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
+ (and returns a unicode string)
+
+* :meth:`absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
+
+* :meth:`rest_path()`, returns a relative REST URL to get the entity
+
+* :meth:`printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
+ returns a string enabling the display of an attribute value in a given format
+ (the value is automatically recovered if necessary)
+
+`Data handling`:
+
+* :meth:`as_rset()`, converts the entity into an equivalent result set simulating the
+ request `Any X WHERE X eid _eid_`
+
+* :meth:`complete(skip_bytes=True)`, executes a request that recovers at
+ once all the missing attributes of an entity
+
+* :meth:`get_value(name)`, returns the value associated to the attribute name given
+ in parameter
+
+* :meth:`related(rtype, role='subject', limit=None, entities=False)`,
+ returns a list of entities related to the current entity by the
+ relation given in parameter
+
+* :meth:`unrelated(rtype, targettype, role='subject', limit=None)`,
+ returns a result set corresponding to the entities not (yet)
+ related to the current entity by the relation given in parameter
+ and satisfying its constraints
+
+* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the
+ corresponding values given named parameters. To set a relation where this
+ entity is the object of the relation, use `reverse_<relation>` as argument
+ name. Values may be an entity, a list of entities, or None (meaning that all
+ relations of the given type from or to this object should be deleted).
+
+* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid
+ given in the parameters on the current entity
+
+* :meth:`cw_delete()` allows to delete the entity
+
+
+The :class:`AnyEntity` class
+----------------------------
+
+To provide a specific behavior for each entity, we can define a class
+inheriting from `cubicweb.entities.AnyEntity`. In general, we define this class
+in `mycube.entities` module (or in a submodule if we want to split code among
+multiple files) so that it will be available on both server and client side.
+
+The class `AnyEntity` is a sub-class of Entity that add methods to it,
+and helps specializing (by further subclassing) the handling of a
+given entity type.
+
+Most methods defined for `AnyEntity`, in addition to `Entity`, add
+support for the `Dublin Core`_ metadata.
+
+.. _`Dublin Core`: http://dublincore.org/
+
+`Standard meta-data (Dublin Core)`:
+
+* :meth:`dc_title()`, returns a unicode string corresponding to the
+ meta-data `Title` (used by default is the first non-meta attribute
+ of the entity schema)
+
+* :meth:`dc_long_title()`, same as dc_title but can return a more
+ detailed title
+
+* :meth:`dc_description(format='text/plain')`, returns a unicode string
+ corresponding to the meta-data `Description` (looks for a
+ description attribute by default)
+
+* :meth:`dc_authors()`, returns a unicode string corresponding to the meta-data
+ `Authors` (owners by default)
+
+* :meth:`dc_creator()`, returns a unicode string corresponding to the
+ creator of the entity
+
+* :meth:`dc_date(date_format=None)`, returns a unicode string corresponding to
+ the meta-data `Date` (update date by default)
+
+* :meth:`dc_type(form='')`, returns a string to display the entity type by
+ specifying the preferred form (`plural` for a plural form)
+
+* :meth:`dc_language()`, returns the language used by the entity
+
+Inheritance
+-----------
+
+When describing a data model, entities can inherit from other entities as is
+common in object-oriented programming.
+
+You have the possibility to redefine whatever pleases you, as follow:
+
+.. sourcecode:: python
+
+ from cubes.OTHER_CUBE import entities
+
+ class EntityExample(entities.EntityExample):
+
+ def dc_long_title(self):
+ return '%s (%s)' % (self.name, self.description)
+
+The most specific entity definition will always the one used by the
+ORM. For instance, the new EntityExample above in mycube replaces the
+one in OTHER_CUBE. These types are stored in the `etype` section of
+the `vregistry`.
+
+Notice this is different than yams schema inheritance, which is an
+experimental undocumented feature.
+
+
+Application logic
+-----------------
+
+While a lot of custom behaviour and application logic can be
+implemented using entity classes, the programmer must be aware that
+adding new attributes and method on an entity class adds may shadow
+schema-level attribute or relation definitions.
+
+To keep entities clean (mostly data structures plus a few universal
+methods such as listed above), one should use `adapters` (see
+:ref:`adapters`).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,13 @@
+Data as objects
+===============
+
+In this chapter, we will introduce the objects that are used to handle
+the logic associated to the data stored in the database.
+
+.. toctree::
+ :maxdepth: 1
+
+ data-as-objects
+ load-sort
+ adapters
+ application-logic
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/entityclasses/load-sort.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,40 @@
+
+.. _FetchAttrs:
+
+Loaded attributes and default sorting management
+````````````````````````````````````````````````
+
+* The class attribute `fetch_attrs` allows to define in an entity class a list of
+ names of attributes that should be automatically loaded when entities of this
+ type are fetched from the database using ORM methods retrieving entity of this
+ type (such as :meth:`related` and :meth:`unrelated`). You can also put relation
+ names in there, but we are limited to *subject relations of cardinality `?` or
+ `1`*.
+
+* The :meth:`cw_fetch_order` and :meth:`cw_fetch_unrelated_order` class methods
+ are respectively responsible to control how entities will be sorted when:
+
+ - retrieving all entities of a given type, or entities related to another
+
+ - retrieving a list of entities for use in drop-down lists enabling relations
+ creation in the editing view of an entity
+
+By default entities will be listed on their modification date descending,
+i.e. you'll get entities recently modified first. While this is usually a good
+default in drop-down list, you'll probably want to change `cw_fetch_order`.
+
+This may easily be done using the :func:`~cubicweb.entities.fetch_config`
+function, which simplifies the definition of attributes to load and sorting by
+returning a list of attributes to pre-load (considering automatically the
+attributes of `AnyEntity`) and a sorting function as described below:
+
+.. autofunction:: cubicweb.entities.fetch_config
+
+In you want something else (such as sorting on the result of a registered
+procedure), here is the prototype of those methods:
+
+
+.. automethod:: cubicweb.entity.Entity.cw_fetch_order
+
+.. automethod:: cubicweb.entity.Entity.cw_fetch_unrelated_order
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/fti.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,159 @@
+.. _fti:
+
+Full Text Indexing in CubicWeb
+------------------------------
+
+When an attribute is tagged as *fulltext-indexable* in the datamodel,
+CubicWeb will automatically trigger hooks to update the internal
+fulltext index (i.e the ``appears`` SQL table) each time this attribute
+is modified.
+
+CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
+fulltext on demand:
+
+.. sourcecode:: bash
+
+ cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
+
+You can also rebuild the fulltext index for a given set of entity types:
+
+.. sourcecode:: bash
+
+ cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
+
+In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
+will be rebuilt.
+
+
+Standard FTI process
+~~~~~~~~~~~~~~~~~~~~
+
+Considering an entity type ``ET``, the default *fti* process is to :
+
+1. fetch all entities of type ``ET``
+
+2. for each entity, adapt it to ``IFTIndexable`` (see
+ :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
+
+3. call
+ :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
+ the adapter which is supposed to return a dictionary *weight* ->
+ *list of words* as expected by
+ :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
+ tokenization of each attribute value is done by
+ :meth:`~logilab.database.fti.tokenize`.
+
+
+See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
+
+
+Yams and ``fulltext_container``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible in the datamodel to indicate that fulltext-indexed
+attributes defined for an entity type will be used to index not the
+entity itself but a related entity. This is especially useful for
+composite entities. Let's take a look at (a simplified version of)
+the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
+
+.. sourcecode:: python
+
+ class CWUser(WorkflowableEntityType):
+ login = String(required=True, unique=True, maxsize=64)
+ upassword = Password(required=True)
+
+ class EmailAddress(EntityType):
+ address = String(required=True, fulltextindexed=True,
+ indexed=True, unique=True, maxsize=128)
+
+
+ class use_email_relation(RelationDefinition):
+ name = 'use_email'
+ subject = 'CWUser'
+ object = 'EmailAddress'
+ cardinality = '*?'
+ composite = 'subject'
+
+
+The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
+and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
+in your application, if you use fulltext search to look for an email address, CubicWeb
+will return the ``EmailAddress`` itself. But the objects we'd like to index
+are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
+
+The simplest way to achieve that is to tag the ``use_email`` relation in
+the datamodel:
+
+.. sourcecode:: python
+
+ class use_email(RelationType):
+ fulltext_container = 'subject'
+
+
+Customizing how entities are fetched during ``db-rebuild-fti``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``db-rebuild-fti`` will call the
+:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
+method on your entity type.
+
+.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
+
+Now, suppose you've got a _huge_ table to index, you probably don't want to
+get all entities at once. So here's a simple customized example that will
+process block of 10000 entities:
+
+.. sourcecode:: python
+
+
+ class MyEntityClass(AnyEntity):
+ __regid__ = 'MyEntityClass'
+
+ @classmethod
+ def cw_fti_index_rql_queries(cls, req):
+ # get the default RQL method and insert LIMIT / OFFSET instructions
+ base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
+ selected, restrictions = base_rql.split(' WHERE ')
+ rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
+ selected, restrictions)
+ # count how many entities you'll have to index
+ count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
+ # iterate by blocks of 10000 entities
+ chunksize = 10000
+ for offset in xrange(0, count, chunksize):
+ print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
+ yield rql_template % {'limit': chunksize, 'offset': offset}
+
+Since you have access to ``req``, you can more or less fetch whatever you want.
+
+
+Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also customize the FTI process by providing your own ``get_words()``
+implementation:
+
+.. sourcecode:: python
+
+ from cubicweb.entities.adapters import IFTIndexableAdapter
+
+ class SearchIndexAdapter(IFTIndexableAdapter):
+ __regid__ = 'IFTIndexable'
+ __select__ = is_instance('MyEntityClass')
+
+ def fti_containers(self, _done=None):
+ """this should yield any entity that must be considered to
+ fulltext-index self.entity
+
+ CubicWeb's default implementation will look for yams'
+ ``fulltex_container`` property.
+ """
+ yield self.entity
+ yield self.entity.some_related_entity
+
+
+ def get_words(self):
+ # implement any logic here
+ # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
+ # for the actual signification of 'C'
+ return {'C': ['any', 'word', 'I', 'want']}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,25 @@
+.. _Part2:
+
+----------------------
+Repository development
+----------------------
+
+This part is about developing applications with the *CubicWeb*
+framework. It is not concerned with the web system, which is a
+separate layer and has its own whole chapter.
+
+.. toctree::
+ :maxdepth: 2
+ :numbered:
+
+ cubes/index
+ vreg.rst
+ datamodel/index
+ entityclasses/index
+ devcore/index
+ repo/index
+ testing.rst
+ migration.rst
+ profiling.rst
+ fti.rst
+ dataimport
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/migration.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,251 @@
+.. -*- coding: utf-8 -*-
+
+.. _migration:
+
+Migration
+=========
+
+One of the main design goals of *CubicWeb* was to support iterative and agile
+development. For this purpose, multiple actions are provided to facilitate the
+improvement of an instance, and in particular to handle the changes to be
+applied to the data model, without loosing existing data.
+
+The current version of a cube (and of cubicweb itself) is provided in the file
+`__pkginfo__.py` as a tuple of 3 integers.
+
+Migration scripts management
+----------------------------
+
+Migration scripts has to be located in the directory `migration` of your
+cube and named accordingly:
+
+::
+
+ <version n° X.Y.Z>[_<description>]_<mode>.py
+
+in which :
+
+* X.Y.Z is the model version number to which the script enables to migrate.
+
+* *mode* (between the last "_" and the extension ".py") is used for
+ distributed installation. It indicates to which part
+ of the application (RQL server, web server) the script applies.
+ Its value could be :
+
+ * `common`, applies to the RQL server as well as the web server and updates
+ files on the hard drive (configuration files migration for example).
+
+ * `web`, applies only to the web server and updates files on the hard drive.
+
+ * `repository`, applies only to the RQL server and updates files on the
+ hard drive.
+
+ * `Any`, applies only to the RQL server and updates data in the database
+ (schema and data migration for example).
+
+Again in the directory `migration`, the file `depends.map` allows to indicate
+that for the migration to a particular model version, you always have to first
+migrate to a particular *CubicWeb* version. This file can contain comments (lines
+starting with `#`) and a dependency is listed as follows: ::
+
+ <model version n° X.Y.Z> : <cubicweb version n° X.Y.Z>
+
+For example: ::
+
+ 0.12.0: 2.26.0
+ 0.13.0: 2.27.0
+ # 0.14 works with 2.27 <= cubicweb <= 2.28 at least
+ 0.15.0: 2.28.0
+
+Base context
+------------
+
+The following identifiers are pre-defined in migration scripts:
+
+* `config`, instance configuration
+
+* `interactive_mode`, boolean indicating that the script is executed in
+ an interactive mode or not
+
+* `versions_map`, dictionary of migrated versions (key are cubes
+ names, including 'cubicweb', values are (from version, to version)
+
+* `confirm(question)`, function asking the user and returning true
+ if the user answers yes, false otherwise (always returns true in
+ non-interactive mode)
+
+* `_()` is equivalent to `unicode` allowing to flag the strings to
+ internationalize in the migration scripts.
+
+In the `repository` scripts, the following identifiers are also defined:
+
+* `commit(ask_confirm=True)`, request confirming and executing a "commit"
+
+* `schema`, instance schema (readen from the database)
+
+* `fsschema`, installed schema on the file system (e.g. schema of
+ the updated model and cubicweb)
+
+* `repo`, repository object
+
+* `session`, repository session object
+
+
+New cube dependencies
+---------------------
+
+If your code depends on some new cubes, you have to add them in a migration
+script by using:
+
+* `add_cube(cube, update_database=True)`, add a cube.
+* `add_cubes(cubes, update_database=True)`, add a list of cubes.
+
+The `update_database` parameter is telling if the database schema
+should be updated or if only the relevant persistent property should be
+inserted (for the case where a new cube has been extracted from an
+existing one, so the new cube schema is actually already in there).
+
+If some of the added cubes are already used by an instance, they'll simply be
+silently skipped.
+
+To remove a cube use `drop_cube(cube, removedeps=False)`.
+
+Schema migration
+----------------
+The following functions for schema migration are available in `repository`
+scripts:
+
+* `add_attribute(etype, attrname, attrtype=None, commit=True)`, adds a new
+ attribute to an existing entity type. If the attribute type is not specified,
+ then it is extracted from the updated schema.
+
+* `drop_attribute(etype, attrname, commit=True)`, removes an attribute from an
+ existing entity type.
+
+* `rename_attribute(etype, oldname, newname, commit=True)`, renames an attribute
+
+* `add_entity_type(etype, auto=True, commit=True)`, adds a new entity type.
+ If `auto` is True, all the relations using this entity type and having a known
+ entity type on the other hand will automatically be added.
+
+* `drop_entity_type(etype, commit=True)`, removes an entity type and all the
+ relations using it.
+
+* `rename_entity_type(oldname, newname, commit=True)`, renames an entity type
+
+* `add_relation_type(rtype, addrdef=True, commit=True)`, adds a new relation
+ type. If `addrdef` is True, all the relations definitions of this type will
+ be added.
+
+* `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
+ definitions of this type.
+
+* `rename_relation_type(oldname, newname, commit=True)`, renames a relation type.
+
+* `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
+ relation definition.
+
+* `drop_relation_definition(subjtype, rtype, objtype, commit=True)`, removes
+ a relation definition.
+
+* `sync_schema_props_perms(ertype=None, syncperms=True, syncprops=True, syncrdefs=True, commit=True)`,
+ synchronizes properties and/or permissions on:
+ - the whole schema if ertype is None
+ - an entity or relation type schema if ertype is a string
+ - a relation definition if ertype is a 3-uple (subject, relation, object)
+
+* `change_relation_props(subjtype, rtype, objtype, commit=True, **kwargs)`, changes
+ properties of a relation definition by using the named parameters of the properties
+ to change.
+
+* `set_widget(etype, rtype, widget, commit=True)`, changes the widget used for the
+ relation <rtype> of entity type <etype>.
+
+* `set_size_constraint(etype, rtype, size, commit=True)`, changes the size constraints
+ for the relation <rtype> of entity type <etype>.
+
+Data migration
+--------------
+The following functions for data migration are available in `repository` scripts:
+
+* `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL
+ query, either to interrogate or update. A result set object is returned.
+
+* `add_entity(etype, *args, **kwargs)`, adds a new entity of the given type.
+ The attribute and relation values are specified as named positional
+ arguments.
+
+Workflow creation
+-----------------
+
+The following functions for workflow creation are available in `repository`
+scripts:
+
+* `add_workflow(label, workflowof, initial=False, commit=False, **kwargs)`, adds a new workflow
+ for a given type(s)
+
+You can find more details about workflows in the chapter :ref:`Workflow` .
+
+Configuration migration
+-----------------------
+
+The following functions for configuration migration are available in all
+scripts:
+
+* `option_renamed(oldname, newname)`, indicates that an option has been renamed
+
+* `option_group_change(option, oldgroup, newgroup)`, indicates that an option does not
+ belong anymore to the same group.
+
+* `option_added(oldname, newname)`, indicates that an option has been added.
+
+* `option_removed(oldname, newname)`, indicates that an option has been deleted.
+
+The `config` variable is an object which can be used to access the
+configuration values, for reading and updating, with a dictionary-like
+syntax.
+
+Example 1: migration script changing the variable 'sender-addr' in
+all-in-one.conf. The script also checks that in that the instance is
+configured with a known value for that variable, and only updates the
+value in that case.
+
+.. sourcecode:: python
+
+ wrong_addr = 'cubicweb@loiglab.fr' # known wrong address
+ fixed_addr = 'cubicweb@logilab.fr'
+ configured_addr = config.get('sender-addr')
+ # check that the address has not been hand fixed by a sysadmin
+ if configured_addr == wrong_addr:
+ config['sender-addr'] = fixed-addr
+ config.save()
+
+Example 2: checking the value of the database backend driver, which
+can be useful in case you need to issue backend-dependent raw SQL
+queries in a migration script.
+
+.. sourcecode:: python
+
+ dbdriver = config.sources()['system']['db-driver']
+ if dbdriver == "sqlserver2005":
+ # this is now correctly handled by CW :-)
+ sql('ALTER TABLE cw_Xxxx ALTER COLUMN cw_name varchar(64) NOT NULL;')
+ commit()
+ else: # postgresql
+ sync_schema_props_perms(ertype=('Xxxx', 'name', 'String'),
+ syncperms=False)
+
+
+Others migration functions
+--------------------------
+Those functions are only used for low level operations that could not be
+accomplished otherwise or to repair damaged databases during interactive
+session. They are available in `repository` scripts:
+
+* `sql(sql, args=None, ask_confirm=True)`, executes an arbitrary SQL query on the system source
+* `add_entity_type_table(etype, commit=True)`
+* `add_relation_type_table(rtype, commit=True)`
+* `uninline_relation(rtype, commit=True)`
+
+
+[FIXME] Add explanation on how to use cubicweb-ctl shell
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/profiling.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,57 @@
+.. _PROFILING:
+
+Profiling and performance
+=========================
+
+If you feel that one of your pages takes more time than it should to be
+generated, chances are that you're making too many RQL queries. Obviously,
+there are other reasons but experience tends to show this is the first thing to
+track down. Luckily, CubicWeb provides a configuration option to log RQL
+queries. In your ``all-in-one.conf`` file, set the **query-log-file** option::
+
+ # web application query log file
+ query-log-file=/home/user/myapp-rql.log
+
+Then restart your application, reload your page and stop your application.
+The file ``myapp-rql.log`` now contains the list of RQL queries that were
+executed during your test. It's a simple text file containing lines such as::
+
+ Any A WHERE X eid %(x)s, X lastname A {'x': 448} -- (0.002 sec, 0.010 CPU sec)
+ Any A WHERE X eid %(x)s, X firstname A {'x': 447} -- (0.002 sec, 0.000 CPU sec)
+
+The structure of each line is::
+
+ <RQL QUERY> <QUERY ARGS IF ANY> -- <TIME SPENT>
+
+CubicWeb also provides the **exlog** command to examine and summarize data found
+in such a file:
+
+.. sourcecode:: sh
+
+ $ cubicweb-ctl exlog /home/user/myapp-rql.log
+ 0.07 50 Any A WHERE X eid %(x)s, X firstname A {}
+ 0.05 50 Any A WHERE X eid %(x)s, X lastname A {}
+ 0.01 1 Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E employees X, X modification_date AA {}
+ 0.01 1 Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s {, }
+ 0.01 1 Any B,T,P ORDERBY lower(T) WHERE B is Bookmark,B title T, B path P, B bookmarked_by U, U eid %(x)s {}
+ 0.01 1 Any A,B,C,D WHERE A eid %(x)s,A name B,A creation_date C,A modification_date D {}
+
+This command sorts and uniquifies queries so that it's easy to see where
+is the hot spot that needs optimization.
+
+Do not neglect to set the **fetch_attrs** attribute you can define in your
+entity classes because it can greatly reduce the number of queries executed (see
+:ref:`FetchAttrs`).
+
+You should also know about the **profile** option in the ``all-in-on.conf``. If
+set, this option will make your application run in an `hotshot`_ session and
+store the results in the specified file.
+
+.. _hotshot: http://docs.python.org/library/hotshot.html#module-hotshot
+
+Last but no least, if you're using the PostgreSQL database backend, VACUUMing
+your database can significantly improve the performance of the queries (by
+updating the statistics used by the query optimizer). Nowadays, this is done
+automatically from time to time, but if you've just imported a large amount of
+data in your db, you will want to vacuum it (with the analyse option on). Read
+the documentation of your database for more information.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/hooks.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,279 @@
+.. -*- coding: utf-8 -*-
+.. _hooks:
+
+Hooks and Operations
+====================
+
+.. autodocstring:: cubicweb.server.hook
+
+
+Example using dataflow hooks
+----------------------------
+
+We will use a very simple example to show hooks usage. Let us start with the
+following schema.
+
+.. sourcecode:: python
+
+ class Person(EntityType):
+ age = Int(required=True)
+
+We would like to add a range constraint over a person's age. Let's write an hook
+(supposing yams can not handle this nativly, which is wrong). It shall be placed
+into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
+`mycube/hooks/... package` containing hooks in various modules.
+
+.. sourcecode:: python
+
+ from cubicweb import ValidationError
+ from cubicweb.predicates import is_instance
+ from cubicweb.server.hook import Hook
+
+ class PersonAgeRange(Hook):
+ __regid__ = 'person_age_range'
+ __select__ = Hook.__select__ & is_instance('Person')
+ events = ('before_add_entity', 'before_update_entity')
+
+ def __call__(self):
+ if 'age' in self.entity.cw_edited:
+ if 0 <= self.entity.age <= 120:
+ return
+ msg = self._cw._('age must be between 0 and 120')
+ raise ValidationError(self.entity.eid, {'age': msg})
+
+In our example the base `__select__` is augmented with an `is_instance` selector
+matching the desired entity type.
+
+The `events` tuple is used specify that our hook should be called before the
+entity is added or updated.
+
+Then in the hook's `__call__` method, we:
+
+* check if the 'age' attribute is edited
+* if so, check the value is in the range
+* if not, raise a validation error properly
+
+Now Let's augment our schema with new `Company` entity type with some relation to
+`Person` (in 'mycube/schema.py').
+
+.. sourcecode:: python
+
+ class Company(EntityType):
+ name = String(required=True)
+ boss = SubjectRelation('Person', cardinality='1*')
+ subsidiary_of = SubjectRelation('Company', cardinality='*?')
+
+
+We would like to constrain the company's bosses to have a minimum (legal)
+age. Let's write an hook for this, which will be fired when the `boss` relation
+is established (still supposing we could not specify that kind of thing in the
+schema).
+
+.. sourcecode:: python
+
+ class CompanyBossLegalAge(Hook):
+ __regid__ = 'company_boss_legal_age'
+ __select__ = Hook.__select__ & match_rtype('boss')
+ events = ('before_add_relation',)
+
+ def __call__(self):
+ boss = self._cw.entity_from_eid(self.eidto)
+ if boss.age < 18:
+ msg = self._cw._('the minimum age for a boss is 18')
+ raise ValidationError(self.eidfrom, {'boss': msg})
+
+.. Note::
+
+ We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the
+ proper relation type.
+
+ The essential difference with respect to an entity hook is that there is no
+ self.entity, but `self.eidfrom` and `self.eidto` hook attributes which
+ represent the subject and object **eid** of the relation.
+
+Suppose we want to check that there is no cycle by the `subsidiary_of`
+relation. This is best achieved in an operation since all relations are likely to
+be set at commit time.
+
+.. sourcecode:: python
+
+ from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype
+
+ def check_cycle(self, session, eid, rtype, role='subject'):
+ parents = set([eid])
+ parent = session.entity_from_eid(eid)
+ while parent.related(rtype, role):
+ parent = parent.related(rtype, role)[0]
+ if parent.eid in parents:
+ msg = session._('detected %s cycle' % rtype)
+ raise ValidationError(eid, {rtype: msg})
+ parents.add(parent.eid)
+
+
+ class CheckSubsidiaryCycleOp(Operation):
+
+ def precommit_event(self):
+ check_cycle(self.session, self.eidto, 'subsidiary_of')
+
+
+ class CheckSubsidiaryCycleHook(Hook):
+ __regid__ = 'check_no_subsidiary_cycle'
+ __select__ = Hook.__select__ & match_rtype('subsidiary_of')
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
+
+
+Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other
+exceptions are usually programming errors.
+
+In the above example, our hook will instantiate an operation each time the hook
+is called, i.e. each time the `subsidiary_of` relation is set. There is an
+alternative method to schedule an operation from a hook, using the
+:func:`get_instance` class method.
+
+.. sourcecode:: python
+
+ from cubicweb.server.hook import set_operation
+
+ class CheckSubsidiaryCycleHook(Hook):
+ __regid__ = 'check_no_subsidiary_cycle'
+ events = ('after_add_relation',)
+ __select__ = Hook.__select__ & match_rtype('subsidiary_of')
+
+ def __call__(self):
+ CheckSubsidiaryCycleOp.get_instance(self._cw).add_data(self.eidto)
+
+ class CheckSubsidiaryCycleOp(DataOperationMixIn, Operation):
+
+ def precommit_event(self):
+ for eid in self.get_data():
+ check_cycle(self.session, eid, self.rtype)
+
+
+Here, we call :func:`set_operation` so that we will simply accumulate eids of
+entities to check at the end in a single `CheckSubsidiaryCycleOp`
+operation. Value are stored in a set associated to the
+'subsidiary_cycle_detection' transaction data key. The set initialization and
+operation creation are handled nicely by :func:`set_operation`.
+
+A more realistic example can be found in the advanced tutorial chapter
+:ref:`adv_tuto_security_propagation`.
+
+
+Inter-instance communication
+----------------------------
+
+If your application consists of several instances, you may need some means to
+communicate between them. Cubicweb provides a publish/subscribe mechanism
+using ØMQ_. In order to use it, use
+:meth:`~cubicweb.server.cwzmq.ZMQComm.add_subscription` on the
+`repo.app_instances_bus` object. The `callback` will get the message (as a
+list). A message can be sent by calling
+:meth:`~cubicweb.server.cwzmq.ZMQComm.publish` on `repo.app_instances_bus`.
+The first element of the message is the topic which is used for filtering and
+dispatching messages.
+
+.. _ØMQ: http://www.zeromq.org/
+
+.. sourcecode:: python
+
+ class FooHook(hook.Hook):
+ events = ('server_startup',)
+ __regid__ = 'foo_startup'
+
+ def __call__(self):
+ def callback(msg):
+ self.info('received message: %s', ' '.join(msg))
+ self.repo.app_instances_bus.add_subscription('hello', callback)
+
+.. sourcecode:: python
+
+ def do_foo(self):
+ actually_do_foo()
+ self._cw.repo.app_instances_bus.publish(['hello', 'world'])
+
+The `zmq-address-pub` configuration variable contains the address used
+by the instance for sending messages, e.g. `tcp://*:1234`. The
+`zmq-address-sub` variable contains a comma-separated list of addresses
+to listen on, e.g. `tcp://localhost:1234, tcp://192.168.1.1:2345`.
+
+
+Hooks writing tips
+------------------
+
+Reminder
+~~~~~~~~
+
+You should never use the `entity.foo = 42` notation to update an entity. It will
+not do what you expect (updating the database). Instead, use the
+:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
+:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
+'before_update_entity' event.
+
+
+How to choose between a before and an after event ?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`before_*` hooks give you access to the old attribute (or relation)
+values. You can also intercept and update edited values in the case of
+entity modification before they reach the database.
+
+Else the question is: should I need to do things before or after the actual
+modification ? If the answer is "it doesn't matter", use an 'after' event.
+
+
+Validation Errors
+~~~~~~~~~~~~~~~~~
+
+When a hook which is responsible to maintain the consistency of the
+data model detects an error, it must use a specific exception named
+:exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
+:exc:`~cubicweb.ValidationError` is a programming error. Raising it
+entails aborting the current transaction.
+
+This exception is used to convey enough information up to the user
+interface. Hence its constructor is different from the default Exception
+constructor. It accepts, positionally:
+
+* an entity eid (**not the entity itself**),
+
+* a dict whose keys represent attribute (or relation) names and values
+ an end-user facing message (hence properly translated) relating the
+ problem.
+
+.. sourcecode:: python
+
+ raise ValidationError(earth.eid, {'sea_level': self._cw._('too high'),
+ 'temperature': self._cw._('too hot')})
+
+
+Checking for object created/deleted in the current transaction
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In hooks, you can use the
+:meth:`~cubicweb.server.session.Session.added_in_transaction` or
+:meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session
+object to check if an eid has been created or deleted during the hook's
+transaction.
+
+This is useful to enable or disable some stuff if some entity is being added or
+deleted.
+
+.. sourcecode:: python
+
+ if self._cw.deleted_in_transaction(self.eidto):
+ return
+
+
+Peculiarities of inlined relations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
+for details) are inserted in the database at the same time as entity attributes.
+
+This may have some side effect, for instance when creating an entity
+and setting an inlined relation in the same rql query, then at
+`before_add_relation` time, the relation will already exist in the
+database (it is otherwise not the case).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,13 @@
+.. -*- coding: utf-8 -*-
+
+Repository customization
+++++++++++++++++++++++++
+.. toctree::
+ :maxdepth: 1
+
+ sessions
+ hooks
+ notifications
+ tasks
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/notifications.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,29 @@
+.. -*- coding: utf-8 -*-
+
+Notifications management
+========================
+
+CubicWeb provides a machinery to ease notifications handling. To use it for a
+notification:
+
+* write a view inheriting from
+ :class:`~cubicweb.sobjects.notification.NotificationView`. The usual view api
+ is used to generated the email (plain text) content, and additional
+ :meth:`~cubicweb.sobjects.notification.NotificationView.subject` and
+ :meth:`~cubicweb.sobjects.notification.NotificationView.recipients` methods
+ are used to build the email's subject and
+ recipients. :class:`NotificationView` provides default implementation for both
+ methods.
+
+* write a hook for event that should trigger this notification, select the view
+ (without rendering it), and give it to
+ :func:`cubicweb.hooks.notification.notify_on_commit` so that the notification
+ will be sent if the transaction succeed.
+
+
+.. XXX explain recipient finder and provide example
+
+API details
+~~~~~~~~~~~
+.. autoclass:: cubicweb.sobjects.notification.NotificationView
+.. autofunction:: cubicweb.hooks.notification.notify_on_commit
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/sessions.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,319 @@
+.. -*- coding: utf-8 -*-
+
+Sessions
+========
+
+Sessions are objects linked to an authenticated user. The `Session.new_cnx`
+method returns a new Connection linked to that session.
+
+Connections
+===========
+
+Connections provide the `.execute` method to query the data sources, along with
+`.commit` and `.rollback` methods for transaction management.
+
+Kinds of connections
+--------------------
+
+There are two kinds of connections.
+
+* `normal connections` are the most common: they are related to users and
+ carry security checks coming with user credentials
+
+* `internal connections` have all the powers; they are also used in only a
+ few situations where you don't already have an adequate session at
+ hand, like: user authentication, data synchronisation in
+ multi-source contexts
+
+Normal connections are typically named `_cw` in most appobjects or
+sometimes just `session`.
+
+Internal connections are available from the `Repository` object and are
+to be used like this:
+
+.. sourcecode:: python
+
+ with self.repo.internal_cnx() as cnx:
+ do_stuff_with(cnx)
+ cnx.commit()
+
+Connections should always be used as context managers, to avoid leaks.
+
+
+Python/RQL API
+~~~~~~~~~~~~~~
+
+The Python API developped to interface with RQL is inspired from the standard db-api,
+but since `execute` returns its results directly, there is no `cursor` concept.
+
+.. sourcecode:: python
+
+ execute(rqlstring, args=None, build_descr=True)
+
+:rqlstring: the RQL query to execute (unicode)
+:args: if the query contains substitutions, a dictionary containing the values to use
+
+The `Connection` object owns the methods `commit` and `rollback`. You
+*should never need to use them* during the development of the web
+interface based on the *CubicWeb* framework as it determines the end
+of the transaction depending on the query execution success. They are
+however useful in other contexts such as tests or custom controllers.
+
+.. note::
+
+ If a query generates an error related to security (:exc:`Unauthorized`) or to
+ integrity (:exc:`ValidationError`), the transaction can still continue but you
+ won't be able to commit it, a rollback will be necessary to start a new
+ transaction.
+
+ Also, a rollback is automatically done if an error occurs during commit.
+
+.. note::
+
+ A :exc:`ValidationError` has a `entity` attribute. In CubicWeb,
+ this atttribute is set to the entity's eid (not a reference to the
+ entity itself).
+
+Executing RQL queries from a view or a hook
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When you're within code of the web interface, the Connection is handled by the
+request object. You should not have to access it directly, but use the
+`execute` method directly available on the request, eg:
+
+.. sourcecode:: python
+
+ rset = self._cw.execute(rqlstring, kwargs)
+
+Similarly, on the server side (eg in hooks), there is no request object (since
+you're directly inside the data-server), so you'll have to use the execute method
+of the Connection object.
+
+Proper usage of `.execute`
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Let's say you want to get T which is in configuration C, this translates to:
+
+.. sourcecode:: python
+
+ self._cw.execute('Any T WHERE T in_conf C, C eid %s' % entity.eid)
+
+But it must be written in a syntax that will benefit from the use
+of a cache on the RQL server side:
+
+.. sourcecode:: python
+
+ self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid})
+
+The syntax tree is built once for the "generic" RQL and can be re-used
+with a number of different eids. The rql IN operator is an exception
+to this rule.
+
+.. sourcecode:: python
+
+ self._cw.execute('Any T WHERE T in_conf C, C name IN (%s)'
+ % ','.join(['foo', 'bar']))
+
+Alternatively, some of the common data related to an entity can be
+obtained from the `entity.related()` method (which is used under the
+hood by the ORM when you use attribute access notation on an entity to
+get a relation. The initial request would then be translated to:
+
+.. sourcecode:: python
+
+ entity.related('in_conf', 'object')
+
+Additionally this benefits from the fetch_attrs policy (see :ref:`FetchAttrs`)
+optionally defined on the class element, which says which attributes must be
+also loaded when the entity is loaded through the ORM.
+
+.. _resultset:
+
+The `ResultSet` API
+~~~~~~~~~~~~~~~~~~~
+
+ResultSet instances are a very commonly manipulated object. They have
+a rich API as seen below, but we would like to highlight a bunch of
+methods that are quite useful in day-to-day practice:
+
+* `__str__()` (applied by `print`) gives a very useful overview of both
+ the underlying RQL expression and the data inside; unavoidable for
+ debugging purposes
+
+* `printable_rql()` returns a well formed RQL expression as a
+ string; it is very useful to build views
+
+* `entities()` returns a generator on all entities of the result set
+
+* `get_entity(row, col)` gets the entity at row, col coordinates; one
+ of the most used result set methods
+
+.. autoclass:: cubicweb.rset.ResultSet
+ :members:
+ :noindex:
+
+
+Authentication and management of sessions
+-----------------------------------------
+
+The authentication process is a ballet involving a few dancers:
+
+* through its `get_session` method the top-level application object (the
+ `CubicWebPublisher`) will open a session whenever a web request
+ comes in; it asks the `session manager` to open a session (giving
+ the web request object as context) using `open_session`
+
+ * the session manager asks its authentication manager (which is a
+ `component`) to authenticate the request (using `authenticate`)
+
+ * the authentication manager asks, in order, to its authentication
+ information retrievers, a login and an opaque object containing
+ other credentials elements (calling `authentication_information`),
+ giving the request object each time
+
+ * the default retriever (named `LoginPasswordRetriever`)
+ will in turn defer login and password fetching to the request
+ object (which, depending on the authentication mode (`cookie`
+ or `http`), will do the appropriate things and return a login
+ and a password)
+
+ * the authentication manager, on success, asks the `Repository`
+ object to connect with the found credentials (using `connect`)
+
+ * the repository object asks authentication to all of its
+ sources which support the `CWUser` entity with the given
+ credentials; when successful it can build the cwuser entity,
+ from which a regular `Session` object is made; it returns the
+ session id
+
+ * the source in turn will delegate work to an authentifier
+ class that defines the ultimate `authenticate` method (for
+ instance the native source will query the database against
+ the provided credentials)
+
+ * the authentication manager, on success, will call back _all_
+ retrievers with `authenticated` and return its authentication
+ data (on failure, it will try the anonymous login or, if the
+ configuration forbids it, raise an `AuthenticationError`)
+
+Writing authentication plugins
+------------------------------
+
+Sometimes CubicWeb's out-of-the-box authentication schemes (cookie and
+http) are not sufficient. Nowadays there is a plethora of such schemes
+and the framework cannot provide them all, but as the sequence above
+shows, it is extensible.
+
+Two levels have to be considered when writing an authentication
+plugin: the web client and the repository.
+
+We invented a scenario where it makes sense to have a new plugin in
+each side: some middleware will do pre-authentication and under the
+right circumstances add a new HTTP `x-foo-user` header to the query
+before it reaches the CubicWeb instance. For a concrete example of
+this, see the `trustedauth`_ cube.
+
+.. _`trustedauth`: http://www.cubicweb.org/project/cubicweb-trustedauth
+
+Repository authentication plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On the repository side, it is possible to register a source
+authentifier using the following kind of code:
+
+.. sourcecode:: python
+
+ from cubicweb.server.sources import native
+
+ class FooAuthentifier(native.LoginPasswordAuthentifier):
+ """ a source authentifier plugin
+ if 'foo' in authentication information, no need to check
+ password
+ """
+ auth_rql = 'Any X WHERE X is CWUser, X login %(login)s'
+
+ def authenticate(self, session, login, **kwargs):
+ """return CWUser eid for the given login
+ if this account is defined in this source,
+ else raise `AuthenticationError`
+ """
+ session.debug('authentication by %s', self.__class__.__name__)
+ if 'foo' not in kwargs:
+ return super(FooAuthentifier, self).authenticate(session, login, **kwargs)
+ try:
+ rset = session.execute(self.auth_rql, {'login': login})
+ return rset[0][0]
+ except Exception, exc:
+ session.debug('authentication failure (%s)', exc)
+ raise AuthenticationError('foo user is unknown to us')
+
+Since repository authentifiers are not appobjects, we have to register
+them through a `server_startup` hook.
+
+.. sourcecode:: python
+
+ class ServerStartupHook(hook.Hook):
+ """ register the foo authenticator """
+ __regid__ = 'fooauthenticatorregisterer'
+ events = ('server_startup',)
+
+ def __call__(self):
+ self.debug('registering foo authentifier')
+ self.repo.system_source.add_authentifier(FooAuthentifier())
+
+Web authentication plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. sourcecode:: python
+
+ class XFooUserRetriever(authentication.LoginPasswordRetriever):
+ """ authenticate by the x-foo-user http header
+ or just do normal login/password authentication
+ """
+ __regid__ = 'x-foo-user'
+ order = 0
+
+ def authentication_information(self, req):
+ """retrieve authentication information from the given request, raise
+ NoAuthInfo if expected information is not found
+ """
+ self.debug('web authenticator building auth info')
+ try:
+ login = req.get_header('x-foo-user')
+ if login:
+ return login, {'foo': True}
+ else:
+ return super(XFooUserRetriever, self).authentication_information(self, req)
+ except Exception, exc:
+ self.debug('web authenticator failed (%s)', exc)
+ raise authentication.NoAuthInfo()
+
+ def authenticated(self, retriever, req, cnx, login, authinfo):
+ """callback when return authentication information have opened a
+ repository connection successfully. Take care req has no session
+ attached yet, hence req.execute isn't available.
+
+ Here we set a flag on the request to indicate that the user is
+ foo-authenticated. Can be used by a selector
+ """
+ self.debug('web authenticator running post authentication callback')
+ cnx.foo_user = authinfo.get('foo')
+
+In the `authenticated` method we add (in an admitedly slightly hackish
+way) an attribute to the connection object. This, in turn, can be used
+to build a selector dispatching on the fact that the user was
+preauthenticated or not.
+
+.. sourcecode:: python
+
+ @objectify_selector
+ def foo_authenticated(cls, req, rset=None, **kwargs):
+ if hasattr(req.cnx, 'foo_user') and req.foo_user:
+ return 1
+ return 0
+
+Full Session and Connection API
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autoclass:: cubicweb.server.session.Session
+.. autoclass:: cubicweb.server.session.Connection
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/repo/tasks.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,9 @@
+.. -*- coding: utf-8 -*-
+
+Tasks
+=========
+
+[WRITE ME]
+
+* repository tasks
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/testing.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,559 @@
+.. -*- coding: utf-8 -*-
+
+Tests
+=====
+
+Unit tests
+----------
+
+The *CubicWeb* framework provides the
+:class:`cubicweb.devtools.testlib.CubicWebTC` test base class .
+
+Tests shall be put into the mycube/test directory. Additional test
+data shall go into mycube/test/data.
+
+It is much advised to write tests concerning entities methods,
+actions, hooks and operations, security. The
+:class:`~cubicweb.devtools.testlib.CubicWebTC` base class has
+convenience methods to help test all of this.
+
+In the realm of views, automatic tests check that views are valid
+XHTML. See :ref:`automatic_views_tests` for details.
+
+Most unit tests need a live database to work against. This is achieved
+by CubicWeb using automatically sqlite (bundled with Python, see
+http://docs.python.org/library/sqlite3.html) as a backend.
+
+The database is stored in the mycube/test/tmpdb,
+mycube/test/tmpdb-template files. If it does not (yet) exist, it will
+be built automatically when the test suite starts.
+
+.. warning::
+
+ Whenever the schema changes (new entities, attributes, relations)
+ one must delete these two files. Changes concerned only with entity
+ or relation type properties (constraints, cardinalities,
+ permissions) and generally dealt with using the
+ `sync_schema_props_perms()` function of the migration environment do
+ not need a database regeneration step.
+
+.. _hook_test:
+
+Unit test by example
+````````````````````
+
+We start with an example extracted from the keyword cube (available
+from http://www.cubicweb.org/project/cubicweb-keyword).
+
+.. sourcecode:: python
+
+ from cubicweb.devtools.testlib import CubicWebTC
+ from cubicweb import ValidationError
+
+ class ClassificationHooksTC(CubicWebTC):
+
+ def setup_database(self):
+ with self.admin_access.repo_cnx() as cnx:
+ group_etype = cnx.find('CWEType', name='CWGroup').one()
+ c1 = cnx.create_entity('Classification', name=u'classif1',
+ classifies=group_etype)
+ user_etype = cnx.find('CWEType', name='CWUser').one()
+ c2 = cnx.create_entity('Classification', name=u'classif2',
+ classifies=user_etype)
+ self.kw1eid = cnx.create_entity('Keyword', name=u'kwgroup', included_in=c1).eid
+ cnx.commit()
+
+ def test_cannot_create_cycles(self):
+ with self.admin_access.repo_cnx() as cnx:
+ kw1 = cnx.entity_from_eid(self.kw1eid)
+ # direct obvious cycle
+ with self.assertRaises(ValidationError):
+ kw1.cw_set(subkeyword_of=kw1)
+ cnx.rollback()
+ # testing indirect cycles
+ kw3 = cnx.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
+ 'SK subkeyword_of K WHERE C name "classif1", K eid %(k)s'
+ {'k': kw1}).get_entity(0,0)
+ kw3.cw_set(reverse_subkeyword_of=kw1)
+ self.assertRaises(ValidationError, cnx.commit)
+
+The test class defines a :meth:`setup_database` method which populates the
+database with initial data. Each test of the class runs with this
+pre-populated database.
+
+The test case itself checks that an Operation does its job of
+preventing cycles amongst Keyword entities.
+
+The `create_entity` method of connection (or request) objects allows
+to create an entity. You can link this entity to other entities, by
+specifying as argument, the relation name, and the entity to link, as
+value. In the above example, the `Classification` entity is linked to
+a `CWEtype` via the relation `classifies`. Conversely, if you are
+creating a `CWEtype` entity, you can link it to a `Classification`
+entity, by adding `reverse_classifies` as argument.
+
+.. note::
+
+ the :meth:`commit` method is not called automatically. You have to
+ call it explicitly if needed (notably to test operations). It is a
+ good practice to regenerate entities with :meth:`entity_from_eid`
+ after a commit to avoid request cache effects.
+
+You can see an example of security tests in the
+:ref:`adv_tuto_security`.
+
+It is possible to have these tests run continuously using `apycot`_.
+
+.. _apycot: http://www.cubicweb.org/project/apycot
+
+.. _securitytest:
+
+Managing connections or users
++++++++++++++++++++++++++++++
+
+Since unit tests are done with the SQLITE backend and this does not
+support multiple connections at a time, you must be careful when
+simulating security, changing users.
+
+By default, tests run with a user with admin privileges. Connections
+using these credentials are accessible through the `admin_access` object
+of the test classes.
+
+The `repo_cnx()` method returns a connection object that can be used as a
+context manager:
+
+.. sourcecode:: python
+
+ # admin_access is a pre-cooked session wrapping object
+ # it is built with:
+ # self.admin_access = self.new_access('admin')
+ with self.admin_access.repo_cnx() as cnx:
+ cnx.execute(...)
+ self.create_user(cnx, login='user1')
+ cnx.commit()
+
+ user1access = self.new_access('user1')
+ with user1access.web_request() as req:
+ req.execute(...)
+ req.cnx.commit()
+
+On exit of the context manager, a rollback is issued, which releases
+the connection. Don't forget to issue the `cnx.commit()` calls!
+
+.. warning::
+
+ Do not use references kept to the entities created with a
+ connection from another one!
+
+Email notifications tests
+`````````````````````````
+
+When running tests, potentially generated e-mails are not really sent
+but are found in the list `MAILBOX` of module
+:mod:`cubicweb.devtools.testlib`.
+
+You can test your notifications by analyzing the contents of this list, which
+contains objects with two attributes:
+
+* `recipients`, the list of recipients
+* `msg`, email.Message object
+
+Let us look at a simple example from the ``blog`` cube.
+
+.. sourcecode:: python
+
+ from cubicweb.devtools.testlib import CubicWebTC, MAILBOX
+
+ class BlogTestsCubicWebTC(CubicWebTC):
+ """test blog specific behaviours"""
+
+ def test_notifications(self):
+ with self.admin_access.web_request() as req:
+ cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
+ description=u'cubicweb is beautiful')
+ blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
+ content=u'cubicweb hop')
+ blog_entry_1.cw_set(entry_of=cubicweb_blog)
+ blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
+ content=u'cubicweb yes')
+ blog_entry_2.cw_set(entry_of=cubicweb_blog)
+ self.assertEqual(len(MAILBOX), 0)
+ req.cnx.commit()
+ self.assertEqual(len(MAILBOX), 2)
+ mail = MAILBOX[0]
+ self.assertEqual(mail.subject, '[data] hop')
+ mail = MAILBOX[1]
+ self.assertEqual(mail.subject, '[data] yes')
+
+Visible actions tests
+`````````````````````
+
+It is easy to write unit tests to test actions which are visible to
+a user or to a category of users. Let's take an example in the
+`conference cube`_.
+
+.. _`conference cube`: http://www.cubicweb.org/project/cubicweb-conference
+.. sourcecode:: python
+
+ class ConferenceActionsTC(CubicWebTC):
+
+ def setup_database(self):
+ with self.admin_access.repo_cnx() as cnx:
+ self.confeid = cnx.create_entity('Conference',
+ title=u'my conf',
+ url_id=u'conf',
+ start_on=date(2010, 1, 27),
+ end_on = date(2010, 1, 29),
+ call_open=True,
+ reverse_is_chair_at=chair,
+ reverse_is_reviewer_at=reviewer).eid
+
+ def test_admin(self):
+ with self.admin_access.web_request() as req:
+ rset = req.find('Conference').one()
+ self.assertListEqual(self.pactions(req, rset),
+ [('workflow', workflow.WorkflowActions),
+ ('edit', confactions.ModifyAction),
+ ('managepermission', actions.ManagePermissionsAction),
+ ('addrelated', actions.AddRelatedActions),
+ ('delete', actions.DeleteAction),
+ ('generate_badge_action', badges.GenerateBadgeAction),
+ ('addtalkinconf', confactions.AddTalkInConferenceAction)
+ ])
+ self.assertListEqual(self.action_submenu(req, rset, 'addrelated'),
+ [(u'add Track in_conf Conference object',
+ u'http://testing.fr/cubicweb/add/Track'
+ u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'
+ u'__redirectpath=conference%%2Fconf&'
+ u'__redirectvid=' % {'conf': self.confeid}),
+ ])
+
+You just have to execute a rql query corresponding to the view you want to test,
+and to compare the result of
+:meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions
+that must be visible in the interface. This is a list of tuples. The first
+element is the action's `__regid__`, the second the action's class.
+
+To test actions in a submenu, you just have to test the result of
+:meth:`~cubicweb.devtools.testlib.CubicWebTC.action_submenu` method. The last
+parameter of the method is the action's category. The result is a list of
+tuples. The first element is the action's title, and the second element the
+action's url.
+
+
+.. _automatic_views_tests:
+
+Automatic views testing
+-----------------------
+
+This is done automatically with the :class:`cubicweb.devtools.testlib.AutomaticWebTest`
+class. At cube creation time, the mycube/test/test_mycube.py file
+contains such a test. The code here has to be uncommented to be
+usable, without further modification.
+
+The ``auto_populate`` method uses a smart algorithm to create
+pseudo-random data in the database, thus enabling the views to be
+invoked and tested.
+
+Depending on the schema, hooks and operations constraints, it is not
+always possible for the automatic auto_populate to proceed.
+
+It is possible of course to completely redefine auto_populate. A
+lighter solution is to give hints (fill some class attributes) about
+what entities and relations have to be skipped by the auto_populate
+mechanism. These are:
+
+* `no_auto_populate`, may contain a list of entity types to skip
+* `ignored_relations`, may contain a list of relation types to skip
+* `application_rql`, may contain a list of rql expressions that
+ auto_populate cannot guess by itself; these must yield resultsets
+ against which views may be selected.
+
+.. warning::
+
+ Take care to not let the imported `AutomaticWebTest` in your test module
+ namespace, else both your subclass *and* this parent class will be run.
+
+Cache heavy database setup
+-------------------------------
+
+Some test suites require a complex setup of the database that takes
+seconds (or even minutes) to complete. Doing the whole setup for each
+individual test makes the whole run very slow. The ``CubicWebTC``
+class offer a simple way to prepare a specific database once for
+multiple tests. The `test_db_id` class attribute of your
+``CubicWebTC`` subclass must be set to a unique identifier and the
+:meth:`pre_setup_database` class method must build the cached content. As
+the :meth:`pre_setup_database` method is not garanteed to be called
+every time a test method is run, you must not set any class attribute
+to be used during test *there*. Databases for each `test_db_id` are
+automatically created if not already in cache. Clearing the cache is
+up to the user. Cache files are found in the :file:`data/database`
+subdirectory of your test directory.
+
+.. warning::
+
+ Take care to always have the same :meth:`pre_setup_database`
+ function for all classes with a given `test_db_id` otherwise your
+ tests will have unpredictable results depending on the first
+ encountered one.
+
+
+Testing on a real-life database
+-------------------------------
+
+The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration`
+configuration class to setup its testing environment (database driver,
+user password, application home, and so on). The `cubicweb.devtools`
+module also provides a `RealDatabaseConfiguration`
+class that will read a regular cubicweb sources file to fetch all
+this information but will also prevent the database to be initalized
+and reset between tests.
+
+For a test class to use a specific configuration, you have to set
+the `_config` class attribute on the class as in:
+
+.. sourcecode:: python
+
+ from cubicweb.devtools import RealDatabaseConfiguration
+ from cubicweb.devtools.testlib import CubicWebTC
+
+ class BlogRealDatabaseTC(CubicWebTC):
+ _config = RealDatabaseConfiguration('blog',
+ sourcefile='/path/to/realdb_sources')
+
+ def test_blog_rss(self):
+ with self.admin_access.web_request() as req:
+ rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
+ 'B created_by U, U login "logilab", B creation_date D')
+ self.view('rss', rset, req=req)
+
+
+Testing with other cubes
+------------------------
+
+Sometimes a small component cannot be tested all by itself, so one
+needs to specify other cubes to be used as part of the the unit test
+suite. This is handled by the ``bootstrap_cubes`` file located under
+``mycube/test/data``. One example from the `preview` cube::
+
+ card, file, preview
+
+The format is:
+
+* possibly several empy lines or lines starting with ``#`` (comment lines)
+* one line containing a comma-separated list of cube names.
+
+It is also possible to add a ``schema.py`` file in
+``mycube/test/data``, which will be used by the testing framework,
+therefore making new entity types and relations available to the
+tests.
+
+Literate programming
+--------------------
+
+CubicWeb provides some literate programming capabilities. The :ref:`cubicweb-ctl`
+`shell` command accepts different file formats. If your file ends with `.txt`
+or `.rst`, the file will be parsed by :mod:`doctest.testfile` with CubicWeb's
+:ref:`migration` API enabled in it.
+
+Create a `scenario.txt` file in the `test/` directory and fill with some content.
+Refer to the :mod:`doctest.testfile` `documentation`_.
+
+.. _documentation: http://docs.python.org/library/doctest.html
+
+Then, you can run it directly by::
+
+ $ cubicweb-ctl shell <cube_instance> test/scenario.txt
+
+When your scenario file is ready, put it in a new test case to be able to run
+it automatically.
+
+.. sourcecode:: python
+
+ from os.path import dirname, join
+ from logilab.common.testlib import unittest_main
+ from cubicweb.devtools.testlib import CubicWebTC
+
+ class AcceptanceTC(CubicWebTC):
+
+ def test_scenario(self):
+ self.assertDocTestFile(join(dirname(__file__), 'scenario.txt'))
+
+ if __name__ == '__main__':
+ unittest_main()
+
+Skipping a scenario
+```````````````````
+
+If you want to set up initial conditions that you can't put in your unit test
+case, you have to use a :exc:`KeyboardInterrupt` exception only because of the
+way :mod:`doctest` module will catch all the exceptions internally.
+
+ >>> if condition_not_met:
+ ... raise KeyboardInterrupt('please, check your fixture.')
+
+Passing paramaters
+``````````````````
+Using extra arguments to parametrize your scenario is possible by prepending them
+by double dashes.
+
+Please refer to the `cubicweb-ctl shell --help` usage.
+
+.. important::
+ Your scenario file must be utf-8 encoded.
+
+Test APIS
+---------
+
+Using Pytest
+````````````
+
+The `pytest` utility (shipping with `logilab-common`_, which is a
+mandatory dependency of CubicWeb) extends the Python unittest
+functionality and is the preferred way to run the CubicWeb test
+suites. Bare unittests also work the usual way.
+
+.. _logilab-common: http://www.logilab.org/project/logilab-common
+
+To use it, you may:
+
+* just launch `pytest` in your cube to execute all tests (it will
+ discover them automatically)
+* launch `pytest unittest_foo.py` to execute one test file
+* launch `pytest unittest_foo.py bar` to execute all test methods and
+ all test cases whose name contains `bar`
+
+Additionally, the `-x` option tells pytest to exit at the first error
+or failure. The `-i` option tells pytest to drop into pdb whenever an
+exception occurs in a test.
+
+When the `-x` option has been used and the run stopped on a test, it
+is possible, after having fixed the test, to relaunch pytest with the
+`-R` option to tell it to start testing again from where it previously
+failed.
+
+Using the `TestCase` base class
+```````````````````````````````
+
+The base class of CubicWebTC is logilab.common.testlib.TestCase, which
+provides a lot of convenient assertion methods.
+
+.. autoclass:: logilab.common.testlib.TestCase
+ :members:
+
+CubicWebTC API
+``````````````
+.. autoclass:: cubicweb.devtools.testlib.CubicWebTC
+ :members:
+
+
+What you need to know about request and session
+-----------------------------------------------
+
+.. image:: ../../images/request_session.png
+
+First, remember to think that some code run on a client side, some
+other on the repository side. More precisely:
+
+* client side: web interface, raw repoapi connection (cubicweb-ctl shell for
+ instance);
+
+* repository side: RQL query execution, that may trigger hooks and operation.
+
+The client interacts with the repository through a repoapi connection.
+
+
+.. note::
+
+ These distinctions are going to disappear in cubicweb 3.21 (if not
+ before).
+
+A repoapi connection is tied to a session in the repository. The connection and
+request objects are inaccessible from repository code / the session object is
+inaccessible from client code (theoretically at least).
+
+The web interface provides a request class. That `request` object provides
+access to all cubicweb resources, eg:
+
+* the registry (which itself provides access to the schema and the
+ configuration);
+
+* an underlying repoapi connection (when using req.execute, you actually call the
+ repoapi);
+
+* other specific resources depending on the client type (url generation according
+ to base url, form parameters, etc.).
+
+
+A `session` provides an api similar to a request regarding RQL execution and
+access to global resources (registry and all), but also has the following
+responsibilities:
+
+* handle transaction data, that will live during the time of a single
+ transaction. This includes the database connections that will be used to
+ execute RQL queries.
+
+* handle persistent data that may be used across different (web) requests
+
+* security and hooks control (not possible through a request)
+
+
+The `_cw` attribute
+```````````````````
+The `_cw` attribute available on every application object provides access to all
+cubicweb resources, i.e.:
+
+- For code running on the client side (eg web interface view), `_cw` is a request
+ instance.
+
+- For code running on the repository side (hooks and operation), `_cw` is a
+ Connection or Session instance.
+
+
+Beware some views may be called with a session (e.g. notifications) or with a
+request.
+
+
+Request, session and transaction
+````````````````````````````````
+
+In the web interface, an HTTP request is handled by a single request, which will
+be thrown away once the response is sent.
+
+The web publisher handles the transaction:
+
+* commit / rollback is done automatically
+
+* you should not commit / rollback explicitly, except if you really
+ need it
+
+Let's detail the process:
+
+1. an incoming RQL query comes from a client to the web stack
+
+2. the web stack opens an authenticated database connection for the
+ request, which is associated to a user session
+
+3. the query is executed (through the repository connection)
+
+4. this query may trigger hooks. Hooks and operations may execute some rql queries
+ through `cnx.execute`.
+
+5. the repository gets the result of the query in 1. If it was a RQL read query,
+ the database connection is released. If it was a write query, the connection
+ is then tied to the session until the transaction is commited or rolled back.
+
+6. results are sent back to the client
+
+This implies several things:
+
+* when using a request, or code executed in hooks, this database
+ connection handling is totally transparent
+
+* however, take care when writing tests: you are usually faking /
+ testing both the server and the client side, so you have to decide
+ when to use RepoAccess.client_cnx or RepoAccess.repo_cnx. Ask
+ yourself "where will the code I want to test be running, client or
+ repository side?". The response is usually: use a repo (since the
+ "client connection" concept is going away in a couple of releases).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devrepo/vreg.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,471 @@
+The Registry, selectors and application objects
+===============================================
+
+This chapter deals with some of the core concepts of the |cubicweb| framework
+which make it different from other frameworks (and maybe not easy to
+grasp at a first glance). To be able to do advanced development with
+|cubicweb| you need a good understanding of what is explained below.
+
+This chapter goes deep into details. You don't have to remember them
+all but keep it in mind so you can go back there later.
+
+An overview of AppObjects, the VRegistry and Selectors is given in the
+:ref:`VRegistryIntro` chapter.
+
+
+
+The :class:`CWRegistryStore`
+----------------------------
+
+The :class:`CWRegistryStore <cubicweb.cwvreg.CWRegistryStore>` can be
+seen as a two-level dictionary. It contains all dynamically loaded
+objects (subclasses of :class:`AppObject <cubicweb.appobject.AppObject>`)
+to build a |cubicweb| application. Basically:
+
+* the first level key returns a *registry*. This key corresponds to the
+ `__registry__` attribute of application object classes
+
+* the second level key returns a list of application objects which
+ share the same identifier. This key corresponds to the `__regid__`
+ attribute of application object classes.
+
+A *registry* holds a specific kind of application objects. There is
+for instance a registry for entity classes, another for views, etc...
+
+The :class:`CWRegistryStore <cubicweb.cwvreg.CWRegistryStore>` has two
+main responsibilities:
+
+- being the access point to all registries
+
+- handling the registration process at startup time, and during automatic
+ reloading in debug mode.
+
+
+Details of the recording process
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. index::
+ vregistry: registration_callback
+
+On startup, |cubicweb| loads application objects defined in its library
+and in cubes used by the instance. Application objects from the
+library are loaded first, then those provided by cubes are loaded in
+dependency order (e.g. if your cube depends on an other, objects from
+the dependency will be loaded first). The layout of the modules or packages
+in a cube is explained in :ref:`cubelayout`.
+
+For each module:
+
+* by default all objects are registered automatically
+
+* if some objects have to replace other objects, or have to be
+ included only if some condition is met, you'll have to define a
+ `registration_callback(vreg)` function in your module and explicitly
+ register **all objects** in this module, using the api defined
+ below.
+
+.. Note::
+ Once the function `registration_callback(vreg)` is implemented in a module,
+ all the objects from this module have to be explicitly registered as it
+ disables the automatic objects registration.
+
+
+API for objects registration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here are the registration methods that you can use in the
+`registration_callback` to register your objects to the
+:class:`CWRegistryStore` instance given as argument (usually named
+`vreg`):
+
+- :py:meth:`register_all() <cubicweb.cwvreg.CWRegistryStore.register_all>`
+- :py:meth:`register_and_replace() <cubicweb.cwvreg.CWRegistryStore.register_and_replace>`
+- :py:meth:`register() <cubicweb.cwvreg.CWRegistryStore.register>`
+- :py:meth:`unregister() <logilab.common.registry.RegistryStore.unregister>`
+
+Examples:
+
+.. sourcecode:: python
+
+ # web/views/basecomponents.py
+ def registration_callback(vreg):
+ # register everything in the module except SeeAlsoComponent
+ vreg.register_all(globals().itervalues(), __name__, (SeeAlsoVComponent,))
+ # conditionally register SeeAlsoVComponent
+ if 'see_also' in vreg.schema:
+ vreg.register(SeeAlsoVComponent)
+
+In this example, we register all application object classes defined in the module
+except `SeeAlsoVComponent`. This class is then registered only if the 'see_also'
+relation type is defined in the instance'schema.
+
+.. sourcecode:: python
+
+ # goa/appobjects/sessions.py
+ def registration_callback(vreg):
+ vreg.register(SessionsCleaner)
+ # replace AuthenticationManager by GAEAuthenticationManager
+ vreg.register_and_replace(GAEAuthenticationManager, AuthenticationManager)
+ # replace PersistentSessionManager by GAEPersistentSessionManager
+ vreg.register_and_replace(GAEPersistentSessionManager, PersistentSessionManager)
+
+In this example, we explicitly register classes one by one:
+
+* the `SessionCleaner` class
+* the `GAEAuthenticationManager` to replace the `AuthenticationManager`
+* the `GAEPersistentSessionManager` to replace the `PersistentSessionManager`
+
+If at some point we register a new appobject class in this module, it won't be
+registered at all without modification to the `registration_callback`
+implementation. The previous example will register it though, thanks to the call
+to the `register_all` method.
+
+
+
+Runtime objects selection
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now that we have all application objects loaded, the question is : when
+I want some specific object, for instance the primary view for a given
+entity, how do I get the proper object ? This is what we call the
+**selection mechanism**.
+
+As explained in the :ref:`Concepts` section:
+
+* each application object has a **selector**, defined by its
+ `__select__` class attribute
+
+* this selector is responsible to return a **score** for a given context
+
+ - 0 score means the object doesn't apply to this context
+
+ - else, the higher the score, the better the object suits the context
+
+* the object with the highest score is selected.
+
+.. Note::
+
+ When no single object has the highest score, an exception is raised in development
+ mode to let you know that the engine was not able to identify the view to
+ apply. This error is silenced in production mode and one of the objects with
+ the highest score is picked.
+
+ In such cases you would need to review your design and make sure
+ your selectors or appobjects are properly defined. Such an error is
+ typically caused by either forgetting to change the __regid__ in a
+ derived class, or by having copy-pasted some code.
+
+For instance, if you are selecting the primary (`__regid__ =
+'primary'`) view (`__registry__ = 'views'`) for a result set
+containing a `Card` entity, two objects will probably be selectable:
+
+* the default primary view (`__select__ = is_instance('Any')`), meaning
+ that the object is selectable for any kind of entity type
+
+* the specific `Card` primary view (`__select__ = is_instance('Card')`,
+ meaning that the object is selectable for Card entities
+
+Other primary views specific to other entity types won't be selectable in this
+case. Among selectable objects, the `is_instance('Card')` selector will return a higher
+score since it's more specific, so the correct view will be selected as expected.
+
+
+API for objects selections
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here is the selection API you'll get on every registry. Some of them, as the
+'etypes' registry, containing entity classes, extend it. In those methods,
+`*args, **kwargs` is what we call the **context**. Those arguments are given to
+selectors that will inspect their content and return a score accordingly.
+
+:py:meth:`select() <logilab.common.registry.Registry.select>`
+
+:py:meth:`select_or_none() <logilab.common.registry.Registry.select_or_none>`
+
+:py:meth:`possible_objects() <logilab.common.registry.Registry.possible_objects>`
+
+:py:meth:`object_by_id() <logilab.common.registry.Registry.object_by_id>`
+
+
+The `AppObject` class
+---------------------
+
+The :py:class:`cubicweb.appobject.AppObject` class is the base class
+for all dynamically loaded objects (application objects) accessible
+through the :py:class:`cubicweb.cwvreg.CWRegistryStore`.
+
+
+Predicates and selectors
+------------------------
+
+Predicates are scoring functions that are called by the registry to tell whenever
+an appobject can be selected in a given context. Predicates may be chained
+together using operators to build a selector. A selector is the glue that tie
+views to the data model or whatever input context. Using them appropriately is an
+essential part of the construction of well behaved cubes.
+
+Of course you may have to write your own set of predicates as your needs grows
+and you get familiar with the framework (see :ref:`CustomPredicates`).
+
+A predicate is a class testing a particular aspect of a context. A selector is
+built by combining existant predicates or even selectors.
+
+Using and combining existant predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can combine predicates using the `&`, `|` and `~` operators.
+
+When two predicates are combined using the `&` operator, it means that
+both should return a positive score. On success, the sum of scores is
+returned.
+
+When two predicates are combined using the `|` operator, it means that
+one of them should return a positive score. On success, the first
+positive score is returned.
+
+You can also "negate" a predicate by precedeing it by the `~` unary operator.
+
+Of course you can use parenthesis to balance expressions.
+
+Example
+~~~~~~~
+
+The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
+the blog entity itself.
+
+To do that, one defines a method on entity classes that returns the
+RSS stream url for a given entity. The default implementation on
+:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
+as base for all others) and a specific implementation on `Blog` will
+do what we want.
+
+But when we have a result set containing several `Blog` entities (or
+different entities), we don't know on which entity to call the
+aforementioned method. In this case, we keep the generic behaviour.
+
+Hence we have two cases here, one for a single-entity rsets, the other for
+multi-entities rsets.
+
+In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
+
+.. sourcecode:: python
+
+ class RSSIconBox(box.Box):
+ ''' just display the RSS icon on uniform result set '''
+ __select__ = box.Box.__select__ & non_final_entity()
+
+It takes into account:
+
+* the inherited selection criteria (one has to look them up in the class
+ hierarchy to know the details)
+
+* :class:`~cubicweb.predicates.non_final_entity`, which filters on result sets
+ containing non final entities (a 'final entity' being synonym for entity
+ attributes type, eg `String`, `Int`, etc)
+
+This matches our second case. Hence we have to provide a specific component for
+the first case:
+
+.. sourcecode:: python
+
+ class EntityRSSIconBox(RSSIconBox):
+ '''just display the RSS icon on uniform result set for a single entity'''
+ __select__ = RSSIconBox.__select__ & one_line_rset()
+
+Here, one adds the :class:`~cubicweb.predicates.one_line_rset` predicate, which
+filters result sets of size 1. Thus, on a result set containing multiple
+entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
+selectable. However for a result set with one entity, the `EntityRSSIconBox`
+class will have a higher score than `RSSIconBox`, which is what we wanted.
+
+Of course, once this is done, you have to:
+
+* fill in the call method of `EntityRSSIconBox`
+
+* provide the default implementation of the method returning the RSS stream url
+ on :class:`~cubicweb.entities.AnyEntity`
+
+* redefine this method on `Blog`.
+
+
+When to use selectors?
+~~~~~~~~~~~~~~~~~~~~~~
+
+Selectors are to be used whenever arises the need of dispatching on the shape or
+content of a result set or whatever else context (value in request form params,
+authenticated user groups, etc...). That is, almost all the time.
+
+Here is a quick example:
+
+.. sourcecode:: python
+
+ class UserLink(component.Component):
+ '''if the user is the anonymous user, build a link to login else a link
+ to the connected user object with a logout link
+ '''
+ __regid__ = 'loggeduserlink'
+
+ def call(self):
+ if self._cw.session.anonymous_session:
+ # display login link
+ ...
+ else:
+ # display a link to the connected user object with a loggout link
+ ...
+
+The proper way to implement this with |cubicweb| is two have two different
+classes sharing the same identifier but with different selectors so you'll get
+the correct one according to the context.
+
+.. sourcecode:: python
+
+ class UserLink(component.Component):
+ '''display a link to the connected user object with a loggout link'''
+ __regid__ = 'loggeduserlink'
+ __select__ = component.Component.__select__ & authenticated_user()
+
+ def call(self):
+ # display useractions and siteactions
+ ...
+
+ class AnonUserLink(component.Component):
+ '''build a link to login'''
+ __regid__ = 'loggeduserlink'
+ __select__ = component.Component.__select__ & anonymous_user()
+
+ def call(self):
+ # display login link
+ ...
+
+The big advantage, aside readability once you're familiar with the
+system, is that your cube becomes much more easily customizable by
+improving componentization.
+
+
+.. _CustomPredicates:
+
+Defining your own predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can use the :py:func:`objectify_predicate <logilab.common.registry.objectify_predicate>`
+decorator to easily write your own predicates as simple python
+functions.
+
+In other cases, you can take a look at the following abstract base classes:
+
+- :py:class:`ExpectedValuePredicate <cubicweb.predicates.ExpectedValuePredicate>`
+- :py:class:`EClassPredicate <cubicweb.predicates.EClassPredicate>`
+- :py:class:`EntityPredicate <cubicweb.predicates.EntityPredicate>`
+
+
+.. _DebuggingSelectors:
+
+Debugging selection
+~~~~~~~~~~~~~~~~~~~
+
+Once in a while, one needs to understand why a view (or any
+application object) is, or is not selected appropriately. Looking at
+which predicates fired (or did not) is the way. The
+:class:`traced_selection <logilab.common.registry.traced_selection>`
+context manager to help with that, *if you're running your instance in
+debug mode*.
+
+
+Base predicates
+---------------
+
+Here is a description of generic predicates provided by CubicWeb that should suit
+most of your needs.
+
+Bare predicates
+~~~~~~~~~~~~~~~
+
+Those predicates are somewhat dumb, which doesn't mean they're not (very) useful.
+
+- :py:class:`yes <cubicweb.appobject.yes>`
+- :py:class:`match_kwargs <cubicweb.predicates.match_kwargs>`
+- :py:class:`appobject_selectable <cubicweb.predicates.appobject_selectable>`
+- :py:class:`adaptable <cubicweb.predicates.adaptable>`
+- :py:class:`configuration_values <cubicweb.predicates.configuration_values>`
+
+
+Result set predicates
+~~~~~~~~~~~~~~~~~~~~~
+
+Those predicates are looking for a result set in the context ('rset' argument or
+the input context) and match or not according to its shape. Some of these
+predicates have different behaviour if a particular cell of the result set is
+specified using 'row' and 'col' arguments of the input context or not.
+
+- :py:class:`none_rset <cubicweb.predicates.none_rset>`
+- :py:class:`any_rset <cubicweb.predicates.any_rset>`
+- :py:class:`nonempty_rset <cubicweb.predicates.nonempty_rset>`
+- :py:class:`empty_rset <cubicweb.predicates.empty_rset>`
+- :py:class:`one_line_rset <cubicweb.predicates.one_line_rset>`
+- :py:class:`multi_lines_rset <cubicweb.predicates.multi_lines_rset>`
+- :py:class:`multi_columns_rset <cubicweb.predicates.multi_columns_rset>`
+- :py:class:`paginated_rset <cubicweb.predicates.paginated_rset>`
+- :py:class:`sorted_rset <cubicweb.predicates.sorted_rset>`
+- :py:class:`one_etype_rset <cubicweb.predicates.one_etype_rset>`
+- :py:class:`multi_etypes_rset <cubicweb.predicates.multi_etypes_rset>`
+
+
+Entity predicates
+~~~~~~~~~~~~~~~~~
+
+Those predicates are looking for either an `entity` argument in the input context,
+or entity found in the result set ('rset' argument or the input context) and
+match or not according to entity's (instance or class) properties.
+
+- :py:class:`non_final_entity <cubicweb.predicates.non_final_entity>`
+- :py:class:`is_instance <cubicweb.predicates.is_instance>`
+- :py:class:`score_entity <cubicweb.predicates.score_entity>`
+- :py:class:`rql_condition <cubicweb.predicates.rql_condition>`
+- :py:class:`relation_possible <cubicweb.predicates.relation_possible>`
+- :py:class:`partial_relation_possible <cubicweb.predicates.partial_relation_possible>`
+- :py:class:`has_related_entities <cubicweb.predicates.has_related_entities>`
+- :py:class:`partial_has_related_entities <cubicweb.predicates.partial_has_related_entities>`
+- :py:class:`has_permission <cubicweb.predicates.has_permission>`
+- :py:class:`has_add_permission <cubicweb.predicates.has_add_permission>`
+- :py:class:`has_mimetype <cubicweb.predicates.has_mimetype>`
+- :py:class:`is_in_state <cubicweb.predicates.is_in_state>`
+- :py:func:`on_fire_transition <cubicweb.predicates.on_fire_transition>`
+
+
+Logged user predicates
+~~~~~~~~~~~~~~~~~~~~~~
+
+Those predicates are looking for properties of the user issuing the request.
+
+- :py:class:`match_user_groups <cubicweb.predicates.match_user_groups>`
+
+
+Web request predicates
+~~~~~~~~~~~~~~~~~~~~~~
+
+Those predicates are looking for properties of *web* request, they can not be
+used on the data repository side.
+
+- :py:class:`no_cnx <cubicweb.predicates.no_cnx>`
+- :py:class:`anonymous_user <cubicweb.predicates.anonymous_user>`
+- :py:class:`authenticated_user <cubicweb.predicates.authenticated_user>`
+- :py:class:`match_form_params <cubicweb.predicates.match_form_params>`
+- :py:class:`match_search_state <cubicweb.predicates.match_search_state>`
+- :py:class:`match_context_prop <cubicweb.predicates.match_context_prop>`
+- :py:class:`match_context <cubicweb.predicates.match_context>`
+- :py:class:`match_view <cubicweb.predicates.match_view>`
+- :py:class:`primary_view <cubicweb.predicates.primary_view>`
+- :py:class:`contextual <cubicweb.predicates.contextual>`
+- :py:class:`specified_etype_implements <cubicweb.predicates.specified_etype_implements>`
+- :py:class:`attribute_edited <cubicweb.predicates.attribute_edited>`
+- :py:class:`match_transition <cubicweb.predicates.match_transition>`
+
+
+Other predicates
+~~~~~~~~~~~~~~~~
+
+- :py:class:`match_exception <cubicweb.predicates.match_exception>`
+- :py:class:`debug_mode <cubicweb.predicates.debug_mode>`
+
+You'll also find some other (very) specific predicates hidden in other modules
+than :mod:`cubicweb.predicates`.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/ajax.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,12 @@
+.. _ajax:
+
+Ajax
+----
+
+CubicWeb provides a few helpers to facilitate *javascript <-> python* communications.
+
+You can, for instance, register some python functions that will become
+callable from javascript through ajax calls. All the ajax URLs are handled
+by the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
+
+.. automodule:: cubicweb.web.views.ajaxcontroller
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/controllers.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,93 @@
+.. _controllers:
+
+Controllers
+-----------
+
+Overview
+++++++++
+
+Controllers are responsible for taking action upon user requests
+(loosely following the terminology of the MVC meta pattern).
+
+The following controllers are provided out-of-the box in CubicWeb. We
+list them by category. They are all defined in
+(:mod:`cubicweb.web.views.basecontrollers`).
+
+`Browsing`:
+
+* the View controller is associated with most browsing actions within a
+ CubicWeb application: it always instantiates a
+ :ref:`the_main_template_layout` and lets the ResultSet/Views dispatch system
+ build up the whole content; it handles :exc:`ObjectNotFound` and
+ :exc:`NoSelectableObject` errors that may bubble up to its entry point, in an
+ end-user-friendly way (but other programming errors will slip through)
+
+* the JSonpController is a wrapper around the ``ViewController`` that
+ provides jsonp_ services. Padding can be specified with the
+ ``callback`` request parameter. Only *jsonexport* / *ejsonexport*
+ views can be used. If another ``vid`` is specified, it will be
+ ignored and replaced by *jsonexport*. Request is anonymized
+ to avoid returning sensitive data and reduce the risks of CSRF attacks;
+
+* the Login/Logout controllers make effective user login or logout
+ requests
+
+
+.. _jsonp: http://en.wikipedia.org/wiki/JSONP
+
+`Edition`:
+
+* the Edit controller (see :ref:`edit_controller`) handles CRUD
+ operations in response to a form being submitted; it works in close
+ association with the Forms, to which it delegates some of the work
+
+* the ``Form validator controller`` provides form validation from Ajax
+ context, using the Edit controller, to implement the classic form
+ handling loop (user edits, hits `submit/apply`, validation occurs
+ server-side by way of the Form validator controller, and the UI is
+ decorated with failure information, either global or per-field ,
+ until it is valid)
+
+`Other`:
+
+* the ``SendMail controller`` (web/views/basecontrollers.py) is reponsible
+ for outgoing email notifications
+
+* the MailBugReport controller (web/views/basecontrollers.py) allows
+ to quickly have a `reportbug` feature in one's application
+
+* the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`
+ (:mod:`cubicweb.web.views.ajaxcontroller`) provides
+ services for Ajax calls, typically using JSON as a serialization format
+ for input, and sometimes using either JSON or XML for output. See
+ :ref:`ajax` chapter for more information.
+
+
+Registration
+++++++++++++
+
+All controllers (should) live in the 'controllers' namespace within
+the global registry.
+
+Concrete controllers
+++++++++++++++++++++
+
+Most API details should be resolved by source code inspection, as the
+various controllers have differing goals. See for instance the
+:ref:`edit_controller` chapter.
+
+:mod:`cubicweb.web.controller` contains the top-level abstract
+Controller class and its unimplemented entry point
+`publish(rset=None)` method.
+
+A handful of helpers are also provided there:
+
+* process_rql builds a result set from an rql query typically issued
+ from the browser (and available through _cw.form['rql'])
+
+* validate_cache will force cache validation handling with respect to
+ the HTTP Cache directives (that were typically originally issued
+ from a previous server -> client response); concrete Controller
+ implementations dealing with HTTP (thus, for instance, not the
+ SendMail controller) may very well call this in their publication
+ process.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/css.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,30 @@
+.. -*- coding: utf-8 -*-
+
+CSS Stylesheet
+---------------
+Conventions
+~~~~~~~~~~~
+
+.. XXX external_resources variable
+.. naming convention
+.. request.add_css
+
+
+Extending / overriding existing styles
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We cannot modify the order in which the application is reading the CSS. In
+the case we want to create new CSS style, the best is to define it a in a new
+CSS located under ``myapp/data/`` and use those new styles while writing
+customized views and templates.
+
+If you want to modify an existing CSS styling property, you will have to use
+``!important`` declaration to override the existing property. The application
+apply a higher priority on the default CSS and you can not change that.
+Customized CSS will not be read first.
+
+
+CubicWeb stylesheets
+~~~~~~~~~~~~~~~~~~~~
+
+.. XXX explain diffenrent files and main classes
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/dissection.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,316 @@
+
+.. _form_dissection:
+
+Dissection of an entity form
+----------------------------
+
+This is done (again) with a vanilla instance of the `tracker`_
+cube. We will populate the database with a bunch of entities and see
+what kind of job the automatic entity form does.
+
+.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
+
+Populating the database
+~~~~~~~~~~~~~~~~~~~~~~~
+
+We should start by setting up a bit of context: a project with two
+unpublished versions, and a ticket linked to the project and the first
+version.
+
+.. sourcecode:: python
+
+ >>> p = rql('INSERT Project P: P name "cubicweb"')
+ >>> for num in ('0.1.0', '0.2.0'):
+ ... rql('INSERT Version V: V num "%s", V version_of P WHERE P eid %%(p)s' % num, {'p': p[0][0]})
+ ...
+ <resultset 'INSERT Version V: V num "0.1.0", V version_of P WHERE P eid %(p)s' (1 rows): [765L] (('Version',))>
+ <resultset 'INSERT Version V: V num "0.2.0", V version_of P WHERE P eid %(p)s' (1 rows): [766L] (('Version',))>
+ >>> t = rql('INSERT Ticket T: T title "let us write more doc", T done_in V, '
+ 'T concerns P WHERE V num "0.1.0"', P eid %(p)s', {'p': p[0][0]})
+ >>> commit()
+
+Now let's see what the edition form builds for us.
+
+.. sourcecode:: python
+
+ >>> cnx.use_web_compatible_requests('http://fakeurl.com')
+ >>> req = cnx.request()
+ >>> form = req.vreg['forms'].select('edition', req, rset=rql('Ticket T'))
+ >>> html = form.render()
+
+.. note::
+
+ In order to play interactively with web side application objects, we have to
+ cheat a bit to have request object that will looks like HTTP request object, by
+ calling :meth:`use_web_compatible_requests()` on the connection.
+
+This creates an automatic entity form. The ``.render()`` call yields
+an html (unicode) string. The html output is shown below (with
+internal fieldset omitted).
+
+Looking at the html output
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The form enveloppe
+''''''''''''''''''
+
+.. sourcecode:: html
+
+ <div class="iformTitle"><span>main informations</span></div>
+ <div class="formBody">
+ <form action="http://crater:9999/validateform" method="post" enctype="application/x-www-form-urlencoded"
+ id="entityForm" onsubmit="return freezeFormButtons('entityForm');"
+ class="entityForm" target="eformframe">
+ <div id="progress">validating...</div>
+ <fieldset>
+ <input name="__form_id" type="hidden" value="edition" />
+ <input name="__errorurl" type="hidden" value="http://perdu.com#entityForm" />
+ <input name="__domid" type="hidden" value="entityForm" />
+ <input name="__type:763" type="hidden" value="Ticket" />
+ <input name="eid" type="hidden" value="763" />
+ <input name="__maineid" type="hidden" value="763" />
+ <input name="_cw_edited_fields:763" type="hidden"
+ value="concerns-subject,done_in-subject,priority-subject,type-subject,title-subject,description-subject,__type,_cw_generic_field" />
+ ...
+ </fieldset>
+ <iframe width="0px" height="0px" name="eformframe" id="eformframe" src="javascript: void(0);"></iframe>
+ </form>
+ </div>
+
+The main fieldset encloses a set of hidden fields containing various
+metadata, that will be used by the `edit controller` to process it
+back correctly.
+
+The `freezeFormButtons(...)` javascript callback defined on the
+``onlick`` event of the form element prevents accidental multiple
+clicks in a row.
+
+The ``action`` of the form is mapped to the ``validateform`` controller
+(situated in :mod:`cubicweb.web.views.basecontrollers`).
+
+A full explanation of the validation loop is given in
+:ref:`validation_process`.
+
+.. _attributes_section:
+
+The attributes section
+''''''''''''''''''''''
+
+We can have a look at some of the inner nodes of the form. Some fields
+are omitted as they are redundant for our purposes.
+
+.. sourcecode:: html
+
+ <fieldset class="default">
+ <table class="attributeForm">
+ <tr class="title_subject_row">
+ <th class="labelCol"><label class="required" for="title-subject:763">title</label></th>
+ <td>
+ <input id="title-subject:763" maxlength="128" name="title-subject:763" size="45"
+ tabindex="1" type="text" value="let us write more doc" />
+ </td>
+ </tr>
+ ... (description field omitted) ...
+ <tr class="priority_subject_row">
+ <th class="labelCol"><label class="required" for="priority-subject:763">priority</label></th>
+ <td>
+ <select id="priority-subject:763" name="priority-subject:763" size="1" tabindex="4">
+ <option value="important">important</option>
+ <option selected="selected" value="normal">normal</option>
+ <option value="minor">minor</option>
+ </select>
+ <div class="helper">importance</div>
+ </td>
+ </tr>
+ ... (type field omitted) ...
+ <tr class="concerns_subject_row">
+ <th class="labelCol"><label class="required" for="concerns-subject:763">concerns</label></th>
+ <td>
+ <select id="concerns-subject:763" name="concerns-subject:763" size="1" tabindex="6">
+ <option selected="selected" value="760">Foo</option>
+ </select>
+ </td>
+ </tr>
+ <tr class="done_in_subject_row">
+ <th class="labelCol"><label for="done_in-subject:763">done in</label></th>
+ <td>
+ <select id="done_in-subject:763" name="done_in-subject:763" size="1" tabindex="7">
+ <option value="__cubicweb_internal_field__"></option>
+ <option selected="selected" value="761">Foo 0.1.0</option>
+ <option value="762">Foo 0.2.0</option>
+ </select>
+ <div class="helper">version in which this ticket will be / has been done</div>
+ </td>
+ </tr>
+ </table>
+ </fieldset>
+
+
+Note that the whole form layout has been computed by the form
+renderer. It is the renderer which produces the table
+structure. Otherwise, the fields html structure is emitted by their
+associated widget.
+
+While it is called the `attributes` section of the form, it actually
+contains attributes and *mandatory relations*. For each field, we
+observe:
+
+* a dedicated row with a specific class, such as ``title_subject_row``
+ (responsability of the form renderer)
+
+* an html widget (input, select, ...) with:
+
+ * an id built from the ``rtype-role:eid`` pattern
+
+ * a name built from the same pattern
+
+ * possible values or preselected options
+
+The relations section
+'''''''''''''''''''''
+
+.. sourcecode:: html
+
+ <fieldset class="This ticket :">
+ <legend>This ticket :</legend>
+ <table class="attributeForm">
+ <tr class="_cw_generic_field_None_row">
+ <td colspan="2">
+ <table id="relatedEntities">
+ <tr><th> </th><td> </td></tr>
+ <tr id="relationSelectorRow_763" class="separator">
+ <th class="labelCol">
+ <select id="relationSelector_763" tabindex="8"
+ onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,763);">
+ <option value="">select a relation</option>
+ <option value="appeared_in_subject">appeared in</option>
+ <option value="custom_workflow_subject">custom workflow</option>
+ <option value="depends_on_object">dependency of</option>
+ <option value="depends_on_subject">depends on</option>
+ <option value="identical_to_subject">identical to</option>
+ <option value="see_also_subject">see also</option>
+ </select>
+ </th>
+ <td id="unrelatedDivs_763"></td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </fieldset>
+
+The optional relations are grouped into a drop-down combo
+box. Selection of an item triggers a javascript function which will:
+
+* show already related entities in the div of id `relatedentities`
+ using a two-colown layout, with an action to allow deletion of
+ individual relations (there are none in this example)
+
+* provide a relation selector in the div of id `relationSelector_EID`
+ to allow the user to set up relations and trigger dynamic action on
+ the last div
+
+* fill the div of id `unrelatedDivs_EID` with a dynamically computed
+ selection widget allowing direct selection of an unrelated (but
+ relatable) entity or a switch towards the `search mode` of
+ |cubicweb| which allows full browsing and selection of an entity
+ using a dedicated action situated in the left column boxes.
+
+
+The buttons zone
+''''''''''''''''
+
+Finally comes the buttons zone.
+
+.. sourcecode:: html
+
+ <table width="100%">
+ <tbody>
+ <tr>
+ <td align="center">
+ <button class="validateButton" tabindex="9" type="submit" value="validate">
+ <img alt="OK_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/ok.png" />
+ validate
+ </button>
+ </td>
+ <td style="align: right; width: 50%;">
+ <button class="validateButton"
+ onclick="postForm('__action_apply', 'button_apply', 'entityForm')"
+ tabindex="10" type="button" value="apply">
+ <img alt="APPLY_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/plus.png" />
+ apply
+ </button>
+ <button class="validateButton"
+ onclick="postForm('__action_cancel', 'button_cancel', 'entityForm')"
+ tabindex="11" type="button" value="cancel">
+ <img alt="CANCEL_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/cancel.png" />
+ cancel
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+The most notable artifacts here are the ``postForm(...)`` calls
+defined on click events on these buttons. This function basically
+submits the form.
+
+.. _validation_process:
+
+The form validation process
+---------------------------
+
+Validation loop
+~~~~~~~~~~~~~~~
+
+On form submission, the form.action is invoked. Basically, the
+``validateform`` controller is called and its output lands in the
+specified ``target``, an invisible ``<iframe>`` at the end of the
+form.
+
+Hence, the main page is not replaced, only the iframe contents. The
+``validateform`` controller only outputs a tiny javascript fragment
+which is then immediately executed.
+
+.. sourcecode:: html
+
+ <iframe width="0px" height="0px" name="eformframe" id="eformframe" src="javascript: void(0);">
+ <script type="text/javascript">
+ window.parent.handleFormValidationResponse('entityForm', null, null,
+ [false, [2164, {"name-subject": "required field"}], null],
+ null);
+ </script>
+ </iframe>
+
+The ``window.parent`` part ensures the javascript function is called
+on the right context (that is: the form element). We will describe its
+parameters:
+
+* first comes the form id (`entityForm`)
+
+* then two optional callbacks for the success and failure case
+
+* an array containing:
+
+ * a boolean which indicates status (success or failure), and then, on error:
+
+ * an array structured as ``[eid, {'rtype-role': 'error msg'}, ...]``
+
+ * on success:
+
+ * a url (string) representing the next thing to jump to
+
+Given the array structure described above, it is quite simple to
+manipulate the DOM to show the errors at appropriate places.
+
+Explanation
+~~~~~~~~~~~
+
+This mecanism may seem a bit overcomplicated but we have to deal with
+two realities:
+
+* in the (strict) XHTML world, there are no iframes (hence the dynamic
+ inclusion, tolerated by Firefox)
+
+* no (or not all) browser(s) support file input field handling through
+ ajax.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/editcontroller.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,113 @@
+.. _edit_controller:
+
+The `edit controller`
+---------------------
+
+It can be found in (:mod:`cubicweb.web.views.editcontroller`). This
+controller processes data received from an html form to create or
+update entities.
+
+Edition handling
+~~~~~~~~~~~~~~~~
+
+The parameters related to entities to edit are specified as follows
+(first seen in :ref:`attributes_section`)::
+
+ <rtype-role>:<entity eid>
+
+where entity eid could be a letter in case of an entity to create. We
+name those parameters as *qualified*.
+
+* Retrieval of entities to edit is done by using the forms parameters
+ `eid` and `__type`
+
+* For all the attributes and the relations of an entity to edit
+ (attributes and relations are handled a bit differently but these
+ details are not much relevant here) :
+
+ * using the ``rtype``, ``role`` and ``__type`` information, fetch
+ an appropriate field instance
+
+ * check if the field has been modified (if not, proceed to the next
+ relation)
+
+ * build an rql expression to update the entity
+
+At the end, all rql expressions are executed.
+
+* For each entity to edit:
+
+ * if a qualified parameter `__linkto` is specified, its value has
+ to be a string (or a list of strings) such as: ::
+
+ <relation type>:<eids>:<target>
+
+ where <target> is either `subject` or `object` and each eid could
+ be separated from the others by a `_`. Target specifies if the
+ *edited entity* is subject or object of the relation and each
+ relation specified will be inserted.
+
+ * if a qualified parameter `__clone_eid` is specified for an entity, the
+ relations of the specified entity passed as value of this parameter are
+ copied on the edited entity.
+
+ * if a qualified parameter `__delete` is specified, its value must be
+ a string or a list of string such as follows: ::
+
+ <subjects eids>:<relation type>:<objects eids>
+
+ where each eid subject or object can be seperated from the other
+ by `_`. Each specified relation will be deleted.
+
+
+* If no entity is edited but the form contains the parameters `__linkto`
+ and `eid`, this one is interpreted by using the value specified for `eid`
+ to designate the entity on which to add the relations.
+
+.. note::
+
+ * if the parameter `__action_delete` is found, all the entities specified
+ as to be edited will be deleted.
+
+ * if the parameter `__action_cancel` is found, no action is completed.
+
+ * if the parameter `__action_apply` is found, the editing is
+ applied normally but the redirection is done on the form (see
+ :ref:`RedirectionControl`).
+
+ * if no entity is found to be edited and if there is no parameter
+ `__action_delete`, `__action_cancel`, `__linkto`, `__delete` or
+ `__insert`, an error is raised.
+
+ * using the parameter `__message` in the form will allow to use its value
+ as a message to provide the user once the editing is completed.
+
+
+.. _RedirectionControl:
+
+Redirection control
+~~~~~~~~~~~~~~~~~~~
+Once editing is completed, there is still an issue left: where should we go
+now? If nothing is specified, the controller will do his job but it does not
+mean we will be happy with the result. We can control that by using the
+following parameters:
+
+* `__redirectpath`: path of the URL (relative to the root URL of the site,
+ no form parameters
+
+* `__redirectparams`: forms parameters to add to the path
+
+* `__redirectrql`: redirection RQL request
+
+* `__redirectvid`: redirection view identifier
+
+* `__errorurl`: initial form URL, used for redirecting in case a validation
+ error is raised during editing. If this one is not specified, an error page
+ is displayed instead of going back to the form (which is, if necessary,
+ responsible for displaying the errors)
+
+* `__form_id`: initial view form identifier, used if `__action_apply` is
+ found
+
+In general we use either `__redirectpath` and `__redirectparams` or
+`__redirectrql` and `__redirectvid`.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/examples.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,232 @@
+Examples
+--------
+
+(Automatic) Entity form
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Looking at some cubes available on the `cubicweb forge`_ we find some
+with form manipulation. The following example comes from the the
+`conference`_ cube. It extends the change state form for the case
+where a ``Talk`` entity is getting into ``submitted`` state. The goal
+is to select reviewers for the submitted talk.
+
+.. _`cubicweb forge`: http://www.cubicweb.org/view?rql=Any+P+ORDERBY+N+WHERE+P+name+LIKE+%22cubicweb-%25%22%2C+P+is+Project%2C+P+name+N
+.. _`conference`: http://www.cubicweb.org/project/cubicweb-conference
+
+.. sourcecode:: python
+
+ from cubicweb.web import formfields as ff, formwidgets as fwdgs
+ class SendToReviewerStatusChangeView(ChangeStateFormView):
+ __select__ = (ChangeStateFormView.__select__ &
+ is_instance('Talk') &
+ rql_condition('X in_state S, S name "submitted"'))
+
+ def get_form(self, entity, transition, **kwargs):
+ form = super(SendToReviewerStatusChangeView, self).get_form(entity, transition, **kwargs)
+ relation = ff.RelationField(name='reviews', role='object',
+ eidparam=True,
+ label=_('select reviewers'),
+ widget=fwdgs.Select(multiple=True))
+ form.append_field(relation)
+ return form
+
+Simple extension of a form can be done from within the `FormView`
+wrapping the form. FormView instances have a handy ``get_form`` method
+that returns the form to be rendered. Here we add a ``RelationField``
+to the base state change form.
+
+One notable point is the ``eidparam`` argument: it tells both the
+field and the ``edit controller`` that the field is linked to a
+specific entity.
+
+It is hence entirely possible to add ad-hoc fields that will be
+processed by some specialized instance of the edit controller.
+
+
+Ad-hoc fields form
+~~~~~~~~~~~~~~~~~~
+
+We want to define a form doing something else than editing an entity. The idea is
+to propose a form to send an email to entities in a resultset which implements
+:class:`IEmailable`. Let's take a simplified version of what you'll find in
+:mod:`cubicweb.web.views.massmailing`.
+
+Here is the source code:
+
+.. sourcecode:: python
+
+ def sender_value(form, field):
+ return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
+
+ def recipient_choices(form, field):
+ return [(e.get_email(), e.eid)
+ for e in form.cw_rset.entities()
+ if e.get_email()]
+
+ def recipient_value(form, field):
+ return [e.eid for e in form.cw_rset.entities()
+ if e.get_email()]
+
+ class MassMailingForm(forms.FieldsForm):
+ __regid__ = 'massmailing'
+
+ needs_js = ('cubicweb.widgets.js',)
+ domid = 'sendmail'
+ action = 'sendmail'
+
+ sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
+ label=_('From:'),
+ value=sender_value)
+
+ recipient = ff.StringField(widget=CheckBox(),
+ label=_('Recipients:'),
+ choices=recipient_choices,
+ value=recipients_value)
+
+ subject = ff.StringField(label=_('Subject:'), max_length=256)
+
+ mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
+ inputid='mailbody'))
+
+ form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
+ _('send email'), 'SEND_EMAIL_ICON'),
+ ImgButton('cancelbutton', "javascript: history.back()",
+ stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
+
+Let's detail what's going on up there. Our form will hold four fields:
+
+* a sender field, which is disabled and will simply contains the user's name and
+ email
+
+* a recipients field, which will be displayed as a list of users in the context
+ result set with checkboxes so user can still choose who will receive his mailing
+ by checking or not the checkboxes. By default all of them will be checked since
+ field's value return a list containing same eids as those returned by the
+ vocabulary function.
+
+* a subject field, limited to 256 characters (hence we know a
+ :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in
+ :class:`~cubicweb.web.formfields.StringField`)
+
+* a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`,
+ and whose definition won't be shown here. Notice though that we tell this form
+ need this javascript file by using `needs_js`
+
+Last but not least, we add two buttons control: one to post the form using
+javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id
+set to 'sendmail', which is our form DOM id as specified by its `domid`
+attribute), another to cancel the form which will go back to the previous page
+using another javascript call. Also we specify an image to use as button icon as a
+resource identifier (see :ref:`uiprops`) given as last argument to
+:class:`cubicweb.web.formwidgets.ImgButton`.
+
+To see this form, we still have to wrap it in a view. This is pretty simple:
+
+.. sourcecode:: python
+
+ class MassMailingFormView(form.FormViewMixIn, EntityView):
+ __regid__ = 'massmailing'
+ __select__ = is_instance(IEmailable) & authenticated_user()
+
+ def call(self):
+ form = self._cw.vreg['forms'].select('massmailing', self._cw,
+ rset=self.cw_rset)
+ form.render(w=self.w)
+
+As you see, we simply define a view with proper selector so it only apply to a
+result set containing :class:`IEmailable` entities, and so that only users in the
+managers or users group can use it. Then in the `call()` method for this view we
+simply select the above form and call its `.render()` method with our output
+stream as argument.
+
+When this form is submitted, a controller with id 'sendmail' will be called (as
+specified using `action`). This controller will be responsible to actually send
+the mail to specified recipients.
+
+Here is what it looks like:
+
+.. sourcecode:: python
+
+ class SendMailController(Controller):
+ __regid__ = 'sendmail'
+ __select__ = (authenticated_user() &
+ match_form_params('recipient', 'mailbody', 'subject'))
+
+ def publish(self, rset=None):
+ body = self._cw.form['mailbody']
+ subject = self._cw.form['subject']
+ eids = self._cw.form['recipient']
+ # eids may be a string if only one recipient was specified
+ if isinstance(eids, basestring):
+ rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
+ else:
+ rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
+ recipients = list(rset.entities())
+ msg = format_mail({'email' : self._cw.user.get_email(),
+ 'name' : self._cw.user.dc_title()},
+ recipients, body, subject)
+ if not self._cw.vreg.config.sendmails([(msg, recipients)]):
+ msg = self._cw._('could not connect to the SMTP server')
+ else:
+ msg = self._cw._('emails successfully sent')
+ raise Redirect(self._cw.build_url(__message=msg))
+
+
+The entry point of a controller is the publish method. In that case we simply get
+back post values in request's `form` attribute, get user instances according
+to eids found in the 'recipient' form value, and send email after calling
+:func:`format_mail` to get a proper email message. If we can't send email or
+if we successfully sent email, we redirect to the index page with proper message
+to inform the user.
+
+Also notice that our controller has a selector that deny access to it
+to anonymous users (we don't want our instance to be used as a spam
+relay), but also checks if the expected parameters are specified in
+forms. That avoids later defensive programming (though it's not enough
+to handle all possible error cases).
+
+To conclude our example, suppose we wish a different form layout and that existent
+renderers are not satisfying (we would check that first of course :). We would then
+have to define our own renderer:
+
+.. sourcecode:: python
+
+ class MassMailingFormRenderer(formrenderers.FormRenderer):
+ __regid__ = 'massmailing'
+
+ def _render_fields(self, fields, w, form):
+ w(u'<table class="headersform">')
+ for field in fields:
+ if field.name == 'mailbody':
+ w(u'</table>')
+ w(u'<div id="toolbar">')
+ w(u'<ul>')
+ for button in form.form_buttons:
+ w(u'<li>%s</li>' % button.render(form))
+ w(u'</ul>')
+ w(u'</div>')
+ w(u'<div>')
+ w(field.render(form, self))
+ w(u'</div>')
+ else:
+ w(u'<tr>')
+ w(u'<td class="hlabel">%s</td>' %
+ self.render_label(form, field))
+ w(u'<td class="hvalue">')
+ w(field.render(form, self))
+ w(u'</td></tr>')
+
+ def render_buttons(self, w, form):
+ pass
+
+We simply override the `_render_fields` and `render_buttons` method of the base form renderer
+to arrange fields as we desire it: here we'll have first a two columns table with label and
+value of the sender, recipients and subject field (form order respected), then form controls,
+then a div containing the textarea for the email's content.
+
+To bind this renderer to our form, we should add to our form definition above:
+
+.. sourcecode:: python
+
+ form_renderer_id = 'massmailing'
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/form.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,377 @@
+.. _webform:
+
+HTML form construction
+----------------------
+
+CubicWeb provides the somewhat usual form / field / widget / renderer abstraction
+to provide generic building blocks which will greatly help you in building forms
+properly integrated with CubicWeb (coherent display, error handling, etc...),
+while keeping things as flexible as possible.
+
+A ``form`` basically only holds a set of ``fields``, and has te be bound to a
+``renderer`` which is responsible to layout them. Each field is bound to a
+``widget`` that will be used to fill in value(s) for that field (at form
+generation time) and 'decode' (fetch and give a proper Python type to) values
+sent back by the browser.
+
+The ``field`` should be used according to the type of what you want to edit.
+E.g. if you want to edit some date, you'll have to use the
+:class:`cubicweb.web.formfields.DateField`. Then you can choose among multiple
+widgets to edit it, for instance :class:`cubicweb.web.formwidgets.TextInput` (a
+bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple
+calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery
+calendar). You can of course also write your own widget.
+
+Exploring the available forms
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A small excursion into a |cubicweb| shell is the quickest way to
+discover available forms (or application objects in general).
+
+.. sourcecode:: python
+
+ >>> from pprint import pprint
+ >>> pprint( session.vreg['forms'] )
+ {'base': [<class 'cubicweb.web.views.forms.FieldsForm'>,
+ <class 'cubicweb.web.views.forms.EntityFieldsForm'>],
+ 'changestate': [<class 'cubicweb.web.views.workflow.ChangeStateForm'>,
+ <class 'cubes.tracker.views.forms.VersionChangeStateForm'>],
+ 'composite': [<class 'cubicweb.web.views.forms.CompositeForm'>,
+ <class 'cubicweb.web.views.forms.CompositeEntityForm'>],
+ 'deleteconf': [<class 'cubicweb.web.views.editforms.DeleteConfForm'>],
+ 'edition': [<class 'cubicweb.web.views.autoform.AutomaticEntityForm'>,
+ <class 'cubicweb.web.views.workflow.TransitionEditionForm'>,
+ <class 'cubicweb.web.views.workflow.StateEditionForm'>],
+ 'logform': [<class 'cubicweb.web.views.basetemplates.LogForm'>],
+ 'massmailing': [<class 'cubicweb.web.views.massmailing.MassMailingForm'>],
+ 'muledit': [<class 'cubicweb.web.views.editforms.TableEditForm'>],
+ 'sparql': [<class 'cubicweb.web.views.sparql.SparqlForm'>]}
+
+
+The two most important form families here (for all practical purposes) are `base`
+and `edition`. Most of the time one wants alterations of the
+:class:`AutomaticEntityForm` to generate custom forms to handle edition of an
+entity.
+
+The Automatic Entity Form
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: cubicweb.web.views.autoform
+
+Anatomy of a choices function
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Let's have a look at the `ticket_done_in_choices` function given to
+the `choices` parameter of the relation tag that is applied to the
+('Ticket', 'done_in', '*') relation definition, as it is both typical
+and sophisticated enough. This is a code snippet from the `tracker`_
+cube.
+
+.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
+
+The ``Ticket`` entity type can be related to a ``Project`` and a
+``Version``, respectively through the ``concerns`` and ``done_in``
+relations. When a user is about to edit a ticket, we want to fill the
+combo box for the ``done_in`` relation with values pertinent with
+respect to the context. The important context here is:
+
+* creation or modification (we cannot fetch values the same way in
+ either case)
+
+* ``__linkto`` url parameter given in a creation context
+
+.. sourcecode:: python
+
+ from cubicweb.web import formfields
+
+ def ticket_done_in_choices(form, field):
+ entity = form.edited_entity
+ # first see if its specified by __linkto form parameters
+ linkedto = form.linked_to[('done_in', 'subject')]
+ if linkedto:
+ return linkedto
+ # it isn't, get initial values
+ vocab = field.relvoc_init(form)
+ veid = None
+ # try to fetch the (already or pending) related version and project
+ if not entity.has_eid():
+ peids = form.linked_to[('concerns', 'subject')]
+ peid = peids and peids[0]
+ else:
+ peid = entity.project.eid
+ veid = entity.done_in and entity.done_in[0].eid
+ if peid:
+ # we can complete the vocabulary with relevant values
+ rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
+ rset = form._cw.execute(
+ 'Any V, VN ORDERBY version_sort_value(VN) '
+ 'WHERE V version_of P, P eid %(p)s, V num VN, '
+ 'V in_state ST, NOT ST name "published"', {'p': peid}, 'p')
+ vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
+ if rschema.has_perm(form._cw, 'add', toeid=v.eid)
+ and v.eid != veid]
+ return vocab
+
+The first thing we have to do is fetch potential values from the ``__linkto`` url
+parameter that is often found in entity creation contexts (the creation action
+provides such a parameter with a predetermined value; for instance in this case,
+ticket creation could occur in the context of a `Version` entity). The
+:class:`~cubicweb.web.formfields.RelationField` field class provides a
+:meth:`~cubicweb.web.formfields.RelationField.relvoc_linkedto` method that gets a
+list suitably filled with vocabulary values.
+
+.. sourcecode:: python
+
+ linkedto = field.relvoc_linkedto(form)
+ if linkedto:
+ return linkedto
+
+Then, if no ``__linkto`` argument was given, we must prepare the vocabulary with
+an initial empty value (because `done_in` is not mandatory, we must allow the
+user to not select a verson) and already linked values. This is done with the
+:meth:`~cubicweb.web.formfields.RelationField.relvoc_init` method.
+
+.. sourcecode:: python
+
+ vocab = field.relvoc_init(form)
+
+But then, we have to give more: if the ticket is related to a project,
+we should provide all the non published versions of this project
+(`Version` and `Project` can be related through the `version_of`
+relation). Conversely, if we do not know yet the project, it would not
+make sense to propose all existing versions as it could potentially
+lead to incoherences. Even if these will be caught by some
+RQLConstraint, it is wise not to tempt the user with error-inducing
+candidate values.
+
+The "ticket is related to a project" part must be decomposed as:
+
+* this is a new ticket which is created is the context of a project
+
+* this is an already existing ticket, linked to a project (through the
+ `concerns` relation)
+
+* there is no related project (quite unlikely given the cardinality of
+ the `concerns` relation, so it can only mean that we are creating a
+ new ticket, and a project is about to be selected but there is no
+ ``__linkto`` argument)
+
+.. note::
+
+ the last situation could happen in several ways, but of course in a
+ polished application, the paths to ticket creation should be
+ controlled so as to avoid a suboptimal end-user experience
+
+Hence, we try to fetch the related project.
+
+.. sourcecode:: python
+
+ veid = None
+ if not entity.has_eid():
+ peids = form.linked_to[('concerns', 'subject')]
+ peid = peids and peids[0]
+ else:
+ peid = entity.project.eid
+ veid = entity.done_in and entity.done_in[0].eid
+
+We distinguish between entity creation and entity modification using
+the ``Entity.has_eid()`` method, which returns `False` on creation. At
+creation time the only way to get a project is through the
+``__linkto`` parameter. Notice that we fetch the version in which the
+ticket is `done_in` if any, for later.
+
+.. note::
+
+ the implementation above assumes that if there is a ``__linkto``
+ parameter, it is only about a project. While it makes sense most of
+ the time, it is not an absolute. Depending on how an entity creation
+ action action url is built, several outcomes could be possible
+ there
+
+If the ticket is already linked to a project, fetching it is
+trivial. Then we add the relevant version to the initial vocabulary.
+
+.. sourcecode:: python
+
+ if peid:
+ rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
+ rset = form._cw.execute(
+ 'Any V, VN ORDERBY version_sort_value(VN) '
+ 'WHERE V version_of P, P eid %(p)s, V num VN, '
+ 'V in_state ST, NOT ST name "published"', {'p': peid})
+ vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
+ if rschema.has_perm(form._cw, 'add', toeid=v.eid)
+ and v.eid != veid]
+
+.. warning::
+
+ we have to defend ourselves against lack of a project eid. Given
+ the cardinality of the `concerns` relation, there *must* be a
+ project, but this rule can only be enforced at validation time,
+ which will happen of course only after form subsmission
+
+Here, given a project eid, we complete the vocabulary with all
+unpublished versions defined in the project (sorted by number) for
+which the current user is allowed to establish the relation.
+
+
+Building self-posted form with custom fields/widgets
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes you want a form that is not related to entity edition. For those,
+you'll have to handle form posting by yourself. Here is a complete example on how
+to achieve this (and more).
+
+Imagine you want a form that selects a month period. There are no proper
+field/widget to handle this in CubicWeb, so let's start by defining them:
+
+.. sourcecode:: python
+
+ # let's have the whole import list at the beginning, even those necessary for
+ # subsequent snippets
+ from logilab.common import date
+ from logilab.mtconverter import xml_escape
+ from cubicweb.view import View
+ from cubicweb.predicates import match_kwargs
+ from cubicweb.web import RequestError, ProcessFormError
+ from cubicweb.web import formfields as fields, formwidgets as wdgs
+ from cubicweb.web.views import forms, calendar
+
+ class MonthSelect(wdgs.Select):
+ """Custom widget to display month and year. Expect value to be given as a
+ date instance.
+ """
+
+ def format_value(self, form, field, value):
+ return u'%s/%s' % (value.year, value.month)
+
+ def process_field_data(self, form, field):
+ val = super(MonthSelect, self).process_field_data(form, field)
+ try:
+ year, month = val.split('/')
+ year = int(year)
+ month = int(month)
+ return date.date(year, month, 1)
+ except ValueError:
+ raise ProcessFormError(
+ form._cw._('badly formated date string %s') % val)
+
+
+ class MonthPeriodField(fields.CompoundField):
+ """custom field composed of two subfields, 'begin_month' and 'end_month'.
+
+ It expects to be used on form that has 'mindate' and 'maxdate' in its
+ extra arguments, telling the range of month to display.
+ """
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('widget', wdgs.IntervalWidget())
+ super(MonthPeriodField, self).__init__(
+ [fields.StringField(name='begin_month',
+ choices=self.get_range, sort=False,
+ value=self.get_mindate,
+ widget=MonthSelect()),
+ fields.StringField(name='end_month',
+ choices=self.get_range, sort=False,
+ value=self.get_maxdate,
+ widget=MonthSelect())], *args, **kwargs)
+
+ @staticmethod
+ def get_range(form, field):
+ mindate = date.todate(form.cw_extra_kwargs['mindate'])
+ maxdate = date.todate(form.cw_extra_kwargs['maxdate'])
+ assert mindate <= maxdate
+ _ = form._cw._
+ months = []
+ while mindate <= maxdate:
+ label = '%s %s' % (_(calendar.MONTHNAMES[mindate.month - 1]),
+ mindate.year)
+ value = field.widget.format_value(form, field, mindate)
+ months.append( (label, value) )
+ mindate = date.next_month(mindate)
+ return months
+
+ @staticmethod
+ def get_mindate(form, field):
+ return form.cw_extra_kwargs['mindate']
+
+ @staticmethod
+ def get_maxdate(form, field):
+ return form.cw_extra_kwargs['maxdate']
+
+ def process_posted(self, form):
+ for field, value in super(MonthPeriodField, self).process_posted(form):
+ if field.name == 'end_month':
+ value = date.last_day(value)
+ yield field, value
+
+
+Here we first define a widget that will be used to select the beginning and the
+end of the period, displaying months like '<month> YYYY' but using 'YYYY/mm' as
+actual value.
+
+We then define a field that will actually hold two fields, one for the beginning
+and another for the end of the period. Each subfield uses the widget we defined
+earlier, and the outer field itself uses the standard
+:class:`IntervalWidget`. The field adds some logic:
+
+* a vocabulary generation function `get_range`, used to populate each sub-field
+
+* two 'value' functions `get_mindate` and `get_maxdate`, used to tell to
+ subfields which value they should consider on form initialization
+
+* overriding of `process_posted`, called when the form is being posted, so that
+ the end of the period is properly set to the last day of the month.
+
+Now, we can define a very simple form:
+
+.. sourcecode:: python
+
+ class MonthPeriodSelectorForm(forms.FieldsForm):
+ __regid__ = 'myform'
+ __select__ = match_kwargs('mindate', 'maxdate')
+
+ form_buttons = [wdgs.SubmitButton()]
+ form_renderer_id = 'onerowtable'
+ period = MonthPeriodField()
+
+
+where we simply add our field, set a submit button and use a very simple renderer
+(try others!). Also we specify a selector that ensures form will have arguments
+necessary to our field.
+
+Now, we need a view that will wrap the form and handle post when it occurs,
+simply displaying posted values in the page:
+
+.. sourcecode:: python
+
+ class SelfPostingForm(View):
+ __regid__ = 'myformview'
+
+ def call(self):
+ mindate, maxdate = date.date(2010, 1, 1), date.date(2012, 1, 1)
+ form = self._cw.vreg['forms'].select(
+ 'myform', self._cw, mindate=mindate, maxdate=maxdate, action='')
+ try:
+ posted = form.process_posted()
+ self.w(u'<p>posted values %s</p>' % xml_escape(repr(posted)))
+ except RequestError: # no specified period asked
+ pass
+ form.render(w=self.w, formvalues=self._cw.form)
+
+
+Notice usage of the :meth:`process_posted` method, that will return a dictionary
+of typed values (because they have been processed by the field). In our case, when
+the form is posted you should see a dictionary with 'begin_month' and 'end_month'
+as keys with the selected dates as value (as a python `date` object).
+
+
+APIs
+~~~~
+
+.. automodule:: cubicweb.web.formfields
+.. automodule:: cubicweb.web.formwidgets
+.. automodule:: cubicweb.web.views.forms
+.. automodule:: cubicweb.web.views.formrenderers
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/edition/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,15 @@
+Edition control
+===============
+
+This chapter covers the editing capabilities of |cubicweb|. It
+explains html Form construction, the Edit Controller and their
+interactions.
+
+
+.. toctree::
+ :maxdepth: 2
+
+ form
+ dissection
+ editcontroller
+ examples
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/facets.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,23 @@
+The facets system
+-----------------
+
+Facets allow to restrict searches according to some user friendly criterias.
+CubicWeb has a builtin `facet`_ system to define restrictions `filters`_ really
+as easily as possible.
+
+Here is an exemple of the facets rendering picked from our
+http://www.cubicweb.org web site:
+
+.. image:: ../../images/facet_overview.png
+
+Facets will appear on each page presenting more than one entity that may be
+filtered according to some known criteria.
+
+Base classes for facets
+~~~~~~~~~~~~~~~~~~~~~~~
+.. automodule:: cubicweb.web.facet
+
+
+.. _facet: http://en.wikipedia.org/wiki/Faceted_browser
+.. _filters: http://www.cubicweb.org/blogentry/154152
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/httpcaching.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,21 @@
+HTTP cache management
+=====================
+
+.. automodule:: cubicweb.web.httpcache
+
+Cache policies
+--------------
+.. autoclass:: cubicweb.web.httpcache.NoHTTPCacheManager
+.. autoclass:: cubicweb.web.httpcache.MaxAgeHTTPCacheManager
+.. autoclass:: cubicweb.web.httpcache.EtagHTTPCacheManager
+.. autoclass:: cubicweb.web.httpcache.EntityHTTPCacheManager
+
+Exception
+---------
+.. autoexception:: cubicweb.web.httpcache.NoEtag
+
+Helper functions
+----------------
+.. autofunction:: cubicweb.web.httpcache.set_http_cache_headers
+
+.. NOT YET AVAILABLE IN STABLE autofunction:: cubicweb.web.httpcache.lastmodified
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,24 @@
+Web side development
+====================
+
+In this chapter, we will describe the core APIs for web development in
+the *CubicWeb* framework.
+
+.. toctree::
+ :maxdepth: 2
+
+ publisher
+ controllers
+ request
+ searchbar
+ views/index
+ rtags
+ ajax
+ js
+ css
+ edition/index
+ facets
+ internationalization
+ property
+ httpcaching
+ resource
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/internationalization.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,229 @@
+.. -*- coding: utf-8 -*-
+
+.. _internationalization:
+
+Internationalization
+---------------------
+
+Cubicweb fully supports the internalization of its content and interface.
+
+Cubicweb's interface internationalization is based on the translation project `GNU gettext`_.
+
+.. _`GNU gettext`: http://www.gnu.org/software/gettext/
+
+Cubicweb' internalization involves two steps:
+
+* in your Python code and cubicweb-tal templates : mark translatable strings
+
+* in your instance : handle the translation catalog, edit translations
+
+String internationalization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+User defined string
+```````````````````
+
+In the Python code and cubicweb-tal templates translatable strings can be
+marked in one of the following ways :
+
+ * by using the *built-in* function `_`:
+
+ .. sourcecode:: python
+
+ class PrimaryView(EntityView):
+ """the full view of an non final entity"""
+ __regid__ = 'primary'
+ title = _('primary')
+
+ OR
+
+ * by using the equivalent request's method:
+
+ .. sourcecode:: python
+
+ class NoResultView(View):
+ """default view when no result has been found"""
+ __regid__ = 'noresult'
+
+ def call(self, **kwargs):
+ self.w(u'<div class="searchMessage"><strong>%s</strong></div>\n'
+ % self._cw._('No result matching query'))
+
+The goal of the *built-in* function `_` is only **to mark the
+translatable strings**, it will only return the string to translate
+itself, but not its translation (it's actually another name for the
+`unicode` builtin).
+
+In the other hand the request's method `self._cw._` is also meant to
+retrieve the proper translation of translation strings in the
+requested language.
+
+Finally you can also use the `__` attribute of request object to get a
+translation for a string *which should not itself added to the catalog*,
+usually in case where the actual msgid is created by string interpolation ::
+
+ self._cw.__('This %s' % etype)
+
+In this example ._cw.__` is used instead of ._cw._` so we don't have 'This %s' in
+messages catalogs.
+
+Translations in cubicweb-tal template can also be done with TAL tags
+`i18n:content` and `i18n:replace`.
+
+If you need to add messages on top of those that can be found in the source,
+you can create a file named `i18n/static-messages.pot`.
+
+You could put there messages not found in the python sources or
+overrides for some messages of used cubes.
+
+Generated string
+````````````````
+
+We do not need to mark the translation strings of entities/relations used by a
+particular instance's schema as they are generated automatically. String for
+various actions are also generated.
+
+For exemple the following schema:
+
+.. sourcecode:: python
+
+
+ class EntityA(EntityType):
+ relation_a2b = SubjectRelation('EntityB')
+
+ class EntityB(EntityType):
+ pass
+
+May generate the following message ::
+
+ add EntityA relation_a2b EntityB subject
+
+This message will be used in views of ``EntityA`` for creation of a new
+``EntityB`` with a preset relation ``relation_a2b`` between the current
+``EntityA`` and the new ``EntityB``. The opposite message ::
+
+ add EntityA relation_a2b EntityB object
+
+Is used for similar creation of an ``EntityA`` from a view of ``EntityB``. The
+title of they respective creation form will be ::
+
+ creating EntityB (EntityA %(linkto)s relation_a2b EntityB)
+
+ creating EntityA (EntityA relation_a2b %(linkto)s EntityA)
+
+In the translated string you can use ``%(linkto)s`` for reference to the source
+``entity``.
+
+Handling the translation catalog
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Once the internationalization is done in your code, you need to populate and
+update the translation catalog. Cubicweb provides the following commands for this
+purpose:
+
+
+* `i18ncubicweb` updates Cubicweb framework's translation
+ catalogs. Unless you actually work on the framework itself, you
+ don't need to use this command.
+
+* `i18ncube` updates the translation catalogs of *one particular cube*
+ (or of all cubes). After this command is executed you must update
+ the translation files *.po* in the "i18n" directory of your
+ cube. This command will of course not remove existing translations
+ still in use. It will mark unused translation but not remove them.
+
+* `i18ninstance` recompiles the translation catalogs of *one particular
+ instance* (or of all instances) after the translation catalogs of
+ its cubes have been updated. This command is automatically
+ called every time you create or update your instance. The compiled
+ catalogs (*.mo*) are stored in the i18n/<lang>/LC_MESSAGES of
+ instance where `lang` is the language identifier ('en' or 'fr'
+ for exemple).
+
+
+Example
+```````
+
+You have added and/or modified some translation strings in your cube
+(after creating a new view or modifying the cube's schema for exemple).
+To update the translation catalogs you need to do:
+
+1. `cubicweb-ctl i18ncube <cube>`
+2. Edit the <cube>/i18n/xxx.po files and add missing translations (empty `msgstr`)
+3. `hg ci -m "updated i18n catalogs"`
+4. `cubicweb-ctl i18ninstance <myinstance>`
+
+Editing po files
+~~~~~~~~~~~~~~~~
+
+Using a PO aware editor
+````````````````````````
+
+Many tools exist to help maintain .po (PO) files. Common editors or
+development environment provides modes for these. One can also find
+dedicated PO files editor, such as `poedit`_.
+
+.. _`poedit`: http://www.poedit.net/
+
+While usage of such a tool is commendable, PO files are perfectly
+editable with a (unicode aware) plain text editor. It is also useful
+to know their structure for troubleshooting purposes.
+
+Structure of a PO file
+``````````````````````
+
+In this section, we selectively quote passages of the `GNU gettext`_
+manual chapter on PO files, available there::
+
+ http://www.gnu.org/software/hello/manual/gettext/PO-Files.html
+
+One PO file entry has the following schematic structure::
+
+ white-space
+ # translator-comments
+ #. extracted-comments
+ #: reference...
+ #, flag...
+ #| msgid previous-untranslated-string
+ msgid untranslated-string
+ msgstr translated-string
+
+
+A simple entry can look like this::
+
+ #: lib/error.c:116
+ msgid "Unknown system error"
+ msgstr "Error desconegut del sistema"
+
+It is also possible to have entries with a context specifier. They
+look like this::
+
+ white-space
+ # translator-comments
+ #. extracted-comments
+ #: reference...
+ #, flag...
+ #| msgctxt previous-context
+ #| msgid previous-untranslated-string
+ msgctxt context
+ msgid untranslated-string
+ msgstr translated-string
+
+
+The context serves to disambiguate messages with the same
+untranslated-string. It is possible to have several entries with the
+same untranslated-string in a PO file, provided that they each have a
+different context. Note that an empty context string and an absent
+msgctxt line do not mean the same thing.
+
+Contexts and CubicWeb
+`````````````````````
+
+CubicWeb PO files have both non-contextual and contextual msgids.
+
+Contextual entries are automatically used in some cases. For instance,
+entity.dc_type(), eschema.display_name(req) or display_name(etype,
+req, form, context) methods/function calls will use them.
+
+It is also possible to explicitly use the with _cw.pgettext(context,
+msgid).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/js.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,394 @@
+.. -*- coding: utf-8 -*-
+
+Javascript
+----------
+
+*CubicWeb* uses quite a bit of javascript in its user interface and
+ships with jquery (1.3.x) and parts of the jquery UI library, plus a
+number of homegrown files and also other third party libraries.
+
+All javascript files are stored in cubicweb/web/data/. There are
+around thirty js files there. In a cube it goes to data/.
+
+Obviously one does not want javascript pieces to be loaded all at
+once, hence the framework provides a number of mechanisms and
+conventions to deal with javascript resources.
+
+Conventions
+~~~~~~~~~~~
+
+It is good practice to name cube specific js files after the name of
+the cube, like this : 'cube.mycube.js', so as to avoid name clashes.
+
+.. XXX external_resources variable (which needs love)
+
+Server-side Javascript API
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Javascript resources are typically loaded on demand, from views. The
+request object (available as self._cw from most application objects,
+for instance views and entities objects) has a few methods to do that:
+
+* `add_js(self, jsfiles, localfile=True)` which takes a sequence of
+ javascript files and writes proper entries into the HTML header
+ section. The localfile parameter allows to declare resources which
+ are not from web/data (for instance, residing on a content delivery
+ network).
+
+* `add_onload(self, jscode)` which adds one raw javascript code
+ snippet inline in the html headers. This is quite useful for setting
+ up early jQuery(document).ready(...) initialisations.
+
+Javascript events
+~~~~~~~~~~~~~~~~~
+
+* ``server-response``: this event is triggered on HTTP responses (both
+ standard and ajax). The two following extra parameters are passed
+ to callbacks :
+
+ - ``ajax``: a boolean that says if the reponse was issued by an
+ ajax request
+
+ - ``node``: the DOM node returned by the server in case of an
+ ajax request, otherwise the document itself for standard HTTP
+ requests.
+
+Important javascript AJAX APIS
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* `asyncRemoteExec` and `remoteExec` are the base building blocks for
+ doing arbitrary async (resp. sync) communications with the server
+
+* `reloadComponent` is a convenience function to replace a DOM node
+ with server supplied content coming from a specific registry (this
+ is quite handy to refresh the content of some boxes for instances)
+
+* `jQuery.fn.loadxhtml` is an important extension to jQuery which
+ allows proper loading and in-place DOM update of xhtml views. It is
+ suitably augmented to trigger necessary events, and process CubicWeb
+ specific elements such as the facet system, fckeditor, etc.
+
+
+A simple example with asyncRemoteExec
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On the python side, we have to define an
+:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The
+simplest way to do that is to use the
+:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more
+details on this, refer to :ref:`ajax`).
+
+.. sourcecode: python
+
+ from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+ # serialize output to json to get it back easily on the javascript side
+ @ajaxfunc(output_type='json')
+ def js_say_hello(self, name):
+ return u'hello %s' % name
+
+On the javascript side, we do the asynchronous call. Notice how it
+creates a `deferred` object. Proper treatment of the return value or
+error handling has to be done through the addCallback and addErrback
+methods.
+
+.. sourcecode: javascript
+
+ function asyncHello(name) {
+ var deferred = asyncRemoteExec('say_hello', name);
+ deferred.addCallback(function (response) {
+ alert(response);
+ });
+ deferred.addErrback(function (error) {
+ alert('something fishy happened');
+ });
+ }
+
+ function syncHello(name) {
+ alert( remoteExec('say_hello', name) );
+ }
+
+Anatomy of a reloadComponent call
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`reloadComponent` allows to dynamically replace some DOM node with new
+elements. It has the following signature:
+
+* `compid` (mandatory) is the name of the component to be reloaded
+
+* `rql` (optional) will be used to generate a result set given as
+ argument to the selected component
+
+* `registry` (optional) defaults to 'components' but can be any other
+ valid registry name
+
+* `nodeid` (optional) defaults to compid + 'Component' but can be any
+ explicitly specified DOM node id
+
+* `extraargs` (optional) should be a dictionary of values that will be
+ given to the cell_call method of the component
+
+A simple reloadComponent example
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The server side implementation of `reloadComponent` is the
+:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject.
+
+The following function implements a two-steps method to delete a
+standard bookmark and refresh the UI, while keeping the UI responsive.
+
+.. sourcecode:: javascript
+
+ function removeBookmark(beid) {
+ d = asyncRemoteExec('delete_bookmark', beid);
+ d.addCallback(function(boxcontent) {
+ reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
+ document.location.hash = '#header';
+ updateMessage(_("bookmark has been removed"));
+ });
+ }
+
+`reloadComponent` is called with the id of the bookmark box as
+argument, no rql expression (because the bookmarks display is actually
+independant of any dataset context), a reference to the 'boxes'
+registry (which hosts all left, right and contextual boxes) and
+finally an explicit 'bookmarks_box' nodeid argument that stipulates
+the target DOM node.
+
+Anatomy of a loadxhtml call
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`jQuery.fn.loadxhtml` is an important extension to jQuery which allows
+proper loading and in-place DOM update of xhtml views. The existing
+`jQuery.load`_ function does not handle xhtml, hence the addition. The
+API of loadxhtml is roughly similar to that of `jQuery.load`_.
+
+.. _`jQuery.load`: http://api.jquery.com/load/
+
+
+* `url` (mandatory) should be a complete url (typically referencing
+ the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`,
+ but this is not strictly mandatory)
+
+* `data` (optional) is a dictionary of values given to the
+ controller specified through an `url` argument; some keys may have a
+ special meaning depending on the choosen controller (such as `fname`
+ for the JSonController); the `callback` key, if present, must refer
+ to a function to be called at the end of loadxhtml (more on this
+ below)
+
+* `reqtype` (optional) specifies the request method to be used (get or
+ post); if the argument is 'post', then the post method is used,
+ otherwise the get method is used
+
+* `mode` (optional) is one of `replace` (the default) which means the
+ loaded node will replace the current node content, `swap` to replace
+ the current node with the loaded node, and `append` which will
+ append the loaded node to the current node content
+
+About the `callback` option:
+
+* it is called with two parameters: the current node, and a list
+ containing the loaded (and post-processed node)
+
+* whenever it returns another function, this function is called in
+ turn with the same parameters as above
+
+This mechanism allows callback chaining.
+
+
+A simple example with loadxhtml
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here we are concerned with the retrieval of a specific view to be
+injected in the live DOM. The view will be of course selected
+server-side using an entity eid provided by the client side.
+
+.. sourcecode:: python
+
+ from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+ @ajaxfunc(output_type='xhtml')
+ def frob_status(self, eid, frobname):
+ entity = self._cw.entity_from_eid(eid)
+ return entity.view('frob', name=frobname)
+
+.. sourcecode:: javascript
+
+ function updateSomeDiv(divid, eid, frobname) {
+ var params = {fname:'frob_status', eid: eid, frobname:frobname};
+ jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
+ }
+
+In this example, the url argument is the base json url of a cube
+instance (it should contain something like
+`http://myinstance/ajax?`). The actual AjaxController method name is
+encoded in the `params` dictionary using the `fname` key.
+
+A more real-life example
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+A frequent need of Web 2 applications is the delayed (or demand
+driven) loading of pieces of the DOM. This is typically achieved using
+some preparation of the initial DOM nodes, jQuery event handling and
+proper use of loadxhtml.
+
+We present here a skeletal version of the mecanism used in CubicWeb
+and available in web/views/tabs.py, in the `LazyViewMixin` class.
+
+.. sourcecode:: python
+
+ def lazyview(self, vid, rql=None):
+ """ a lazy version of wview """
+ w = self.w
+ self._cw.add_js('cubicweb.lazy.js')
+ urlparams = {'vid' : vid, 'fname' : 'view'}
+ if rql is not None:
+ urlparams['rql'] = rql
+ w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
+ vid, xml_escape(self._cw.build_url('json', **urlparams))))
+ w(u'</div>')
+ self._cw.add_onload(u"""
+ jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
+ loadNow('#lazy-%(vid)s');});"""
+ % {'event': 'load_%s' % vid, 'vid': vid})
+
+This creates a `div` with a specific event associated to it.
+
+The full version deals with:
+
+* optional parameters such as an entity eid, an rset
+
+* the ability to further reload the fragment
+
+* the ability to display a spinning wheel while the fragment is still
+ not loaded
+
+* handling of browsers that do not support ajax (search engines,
+ text-based browsers such as lynx, etc.)
+
+The javascript side is quite simple, due to loadxhtml awesomeness.
+
+.. sourcecode:: javascript
+
+ function loadNow(eltsel) {
+ var lazydiv = jQuery(eltsel);
+ lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
+ }
+
+This is all significantly different of the previous `simple example`
+(albeit this example actually comes from real-life code).
+
+Notice how the `cubicweb:loadurl` is used to convey the url
+information. The base of this url is similar to the global javascript
+JSON_BASE_URL. According to the pattern described earlier,
+the `fname` parameter refers to the standard `js_view` method of the
+JSonController. This method renders an arbitrary view provided a view
+id (or `vid`) is provided, and most likely an rql expression yielding
+a result set against which a proper view instance will be selected.
+
+The `cubicweb:loadurl` is one of the 29 attributes extensions to XHTML
+in a specific cubicweb namespace. It is a means to pass information
+without breaking HTML nor XHTML compliance and without resorting to
+ungodly hacks.
+
+Given all this, it is easy to add a small nevertheless useful feature
+to force the loading of a lazy view (for instance, a very
+computation-intensive web page could be scinded into one fast-loading
+part and a delayed part).
+
+On the server side, a simple call to a javascript function is
+sufficient.
+
+.. sourcecode:: python
+
+ def forceview(self, vid):
+ """trigger an event that will force immediate loading of the view
+ on dom readyness
+ """
+ self._cw.add_onload("triggerLoad('%s');" % vid)
+
+The browser-side definition follows.
+
+.. sourcecode:: javascript
+
+ function triggerLoad(divid) {
+ jQuery('#lazy-' + divd).trigger('load_' + divid);
+ }
+
+
+Javascript library: overview
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* jquery.* : jquery and jquery UI library
+
+* cubicweb.ajax.js : concentrates all ajax related facilities (it
+ extends jQuery with the loahxhtml function, provides a handfull of
+ high-level ajaxy operations like asyncRemoteExec, reloadComponent,
+ replacePageChunk, getDomFromResponse)
+
+* cubicweb.python.js : adds a number of practical extension to stdanrd
+ javascript objects (on Date, Array, String, some list and dictionary
+ operations), and a pythonesque way to build classes. Defines a
+ CubicWeb namespace.
+
+* cubicweb.htmlhelpers.js : a small bag of convenience functions used
+ in various other cubicweb javascript resources (baseuri, progress
+ cursor handling, popup login box, html2dom function, etc.)
+
+* cubicweb.widgets.js : provides a widget namespace and constructors
+ and helpers for various widgets (mainly facets and timeline)
+
+* cubicweb.edition.js : used by edition forms
+
+* cubicweb.preferences.js : used by the preference form
+
+* cubicweb.facets.js : used by the facets mechanism
+
+There is also javascript support for massmailing, gmap (google maps),
+fckcwconfig (fck editor), timeline, calendar, goa (CubicWeb over
+AppEngine), flot (charts drawing), tabs and bookmarks.
+
+API
+~~~
+
+.. toctree::
+ :maxdepth: 1
+
+ /js_api/index
+
+
+Testing javascript
+~~~~~~~~~~~~~~~~~~
+
+You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests
+inside the python unittest run . You simply have to define a new class that
+inherit from ``QUnitTestCase`` and register your javascript test file in the
+``all_js_tests`` lclass attribut. This ``all_js_tests`` is a sequence a
+3-tuple (<test_file, [<dependencies> ,] [<data_files>]):
+
+The <test_file> should contains the qunit test. <dependencies> defines the list
+of javascript file that must be imported before the test script. Dependencies
+are included their definition order. <data_files> are additional files copied in the
+test directory. both <dependencies> and <data_files> are optionnal.
+``jquery.js`` is preincluded in for all test.
+
+.. sourcecode:: python
+
+ from cubicweb.qunit import QUnitTestCase
+
+ class MyQUnitTest(QUnitTestCase):
+
+ all_js_tests = (
+ ("relative/path/to/my_simple_testcase.js",)
+ ("relative/path/to/my_qunit_testcase.js",(
+ "rel/path/to/dependency_1.js",
+ "rel/path/to/dependency_2.js",)),
+ ("relative/path/to/my_complexe_qunit_testcase.js",(
+ "rel/path/to/dependency_1.js",
+ "rel/path/to/dependency_2.js",
+ ),(
+ "rel/path/file_dependency.html",
+ "path/file_dependency.json")
+ ),
+ )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/property.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,15 @@
+.. _cwprops:
+
+The property mecanism
+---------------------
+
+.. XXX CWProperty and co
+
+
+Property API
+~~~~~~~~~~~~
+.. XXX feed me
+
+Registering and using your own property
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. XXX feed me
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/publisher.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,65 @@
+.. _publisher:
+
+Publisher
+---------
+
+What happens when an HTTP request is issued ?
+
+The story begins with the ``CubicWebPublisher.main_publish``
+method. We do not get upper in the bootstrap process because it is
+dependant on the used HTTP library. With `twisted`_ however,
+``cubicweb.etwist.server.CubicWebRootResource.render_request`` is the
+real entry point.
+
+.. _`twisted`: http://twistedmatrix.com/trac/
+
+What main_publish does:
+
+* get a controller id and a result set from the path (this is actually
+ delegated to the `urlpublisher` component)
+
+* the controller is then selected (if not, this is considered an
+ authorization failure and signaled as such) and called
+
+* then either a proper result is returned, in which case the
+ request/connection object issues a ``commit`` and returns the result
+
+* or error handling must happen:
+
+ * ``ValidationErrors`` pop up there and may lead to a redirect to a
+ previously arranged url or standard error handling applies
+ * an HTTP 500 error (`Internal Server Error`) is issued
+
+
+Now, let's turn to the controller. There are many of them in
+:mod:`cubicweb.web.views.basecontrollers`. We can just follow the
+default `view` controller that is selected on a `view` path. See the
+:ref:`controllers` chapter for more information on controllers.
+
+The `View` controller's entry point is the `publish` method. It does
+the following:
+
+* compute the `main` view to be applied, using either the given result
+ set or building one from a user provided rql string (`rql` and `vid`
+ can be forced from the url GET parameters), that is:
+
+ * compute the `vid` using the result set and the schema (see
+ `cubicweb.web.views.vid_from_rset`)
+ * handle all error cases that could happen in this phase
+
+* do some cache management chores
+
+* select a main template (typically `TheMainTemplate`, see chapter
+ :ref:`templates`)
+
+* call it with the result set and the computed view.
+
+What happens next actually depends on the template and the view, but
+in general this is the rendering phase.
+
+
+CubicWebPublisher API
+`````````````````````
+
+.. autoclass:: cubicweb.web.application.CubicWebPublisher
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/request.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,131 @@
+The `Request` class (`cubicweb.web.request`)
+--------------------------------------------
+
+Overview
+````````
+
+A request instance is created when an HTTP request is sent to the web
+server. It contains informations such as form parameters,
+authenticated user, etc. It is a very prevalent object and is used
+throughout all of the framework and applications, as you'll access to
+almost every resources through it.
+
+**A request represents a user query, either through HTTP or not (we
+also talk about RQL queries on the server side for example).**
+
+Here is a non-exhaustive list of attributes and methods available on
+request objects (grouped by category):
+
+* `Browser control`:
+
+ * `ie_browser`: tells if the browser belong to the Internet Explorer
+ family
+
+* `User and identification`:
+
+ * `user`, instance of `cubicweb.entities.authobjs.CWUser` corresponding to the
+ authenticated user
+
+* `Session data handling`
+
+ * `session.data` is the dictionary of the session data; it can be
+ manipulated like an ordinary Python dictionary
+
+* `Edition` (utilities for edition control):
+
+ * `cancel_edition`: resets error url and cleans up pending operations
+ * `create_entity`: utility to create an entity (from an etype,
+ attributes and relation values)
+ * `datadir_url`: returns the url to the merged external resources
+ (|cubicweb|'s `web/data` directory plus all `data` directories of
+ used cubes)
+ * `edited_eids`: returns the list of eids of entities that are
+ edited under the current http request
+ * `eid_rset(eid)`: utility which returns a result set from an eid
+ * `entity_from_eid(eid)`: returns an entity instance from the given eid
+ * `encoding`: returns the encoding of the current HTTP request
+ * `ensure_ro_rql(rql)`: ensure some rql query is a data request
+ * etype_rset
+ * `form`, dictionary containing the values of a web form
+ * `encoding`, character encoding to use in the response
+ * `next_tabindex()`: returns a monotonically growing integer used to
+ build the html tab index of forms
+
+* `HTTP`
+
+ * `authmode`: returns a string describing the authentication mode
+ (http, cookie, ...)
+ * `lang`: returns the user agents/browser's language as carried by
+ the http request
+ * `demote_to_html()`: in the context of an XHTML compliant browser,
+ this will force emission of the response as an HTML document
+ (using the http content negociation)
+
+* `Cookies handling`
+
+ * `get_cookie()`, returns a dictionary containing the value of the header
+ HTTP 'Cookie'
+ * `set_cookie(cookie, key, maxage=300)`, adds a header HTTP `Set-Cookie`,
+ with a minimal 5 minutes length of duration by default (`maxage` = None
+ returns a *session* cookie which will expire when the user closes the browser
+ window)
+ * `remove_cookie(cookie, key)`, forces a value to expire
+
+* `URL handling`
+
+ * `build_url(__vid, *args, **kwargs)`: return an absolute URL using
+ params dictionary key/values as URL parameters. Values are
+ automatically URL quoted, and the publishing method to use may be
+ specified or will be guessed.
+ * `build_url_params(**kwargs)`: returns a properly prepared (quoted,
+ separators, ...) string from the given parameters
+ * `url()`, returns the full URL of the HTTP request
+ * `base_url()`, returns the root URL of the web application
+ * `relative_path()`, returns the relative path of the request
+
+* `Web resource (.css, .js files, etc.) handling`:
+
+ * `add_css(cssfiles)`: adds the given list of css resources to the current
+ html headers
+ * `add_js(jsfiles)`: adds the given list of javascript resources to the
+ current html headers
+ * `add_onload(jscode)`: inject the given jscode fragment (a unicode
+ string) into the current html headers, wrapped inside a
+ document.ready(...) or another ajax-friendly one-time trigger event
+ * `add_header(header, values)`: adds the header/value pair to the
+ current html headers
+ * `status_out`: control the HTTP status of the response
+
+* `And more...`
+
+ * `set_content_type(content_type, filename=None)`, adds the header HTTP
+ 'Content-Type'
+ * `get_header(header)`, returns the value associated to an arbitrary header
+ of the HTTP request
+ * `set_header(header, value)`, adds an arbitrary header in the response
+ * `execute(*args, **kwargs)`, executes an RQL query and return the result set
+ * `property_value(key)`, properties management (`CWProperty`)
+ * dictionary `data` to store data to share informations between components
+ *while a request is executed*
+
+Please note that this class is abstract and that a concrete implementation
+will be provided by the *frontend* web used (in particular *twisted* as of
+today). For the views or others that are executed on the server side,
+most of the interface of `Request` is defined in the session associated
+to the client.
+
+API
+```
+
+The elements we gave in overview for above are built in three layers,
+from ``cubicweb.req.RequestSessionBase``, ``cubicweb.repoapi.Connection`` and
+``cubicweb.web.ConnectionCubicWebRequestBase``.
+
+.. autoclass:: cubicweb.req.RequestSessionBase
+ :members:
+
+.. autoclass:: cubicweb.repoapi.Connection
+ :members:
+
+.. autoclass:: cubicweb.web.request.ConnectionCubicWebRequestBase
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/resource.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,18 @@
+.. _resources:
+
+Locate resources
+----------------
+
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_resource
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_doc_file
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_all_files
+
+Static files handling
+---------------------
+
+.. autoattribute:: cubicweb.web.webconfig.WebConfiguration.static_directory
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_exists
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_open
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_add
+.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_del
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/rtags.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,27 @@
+Configuring the user interface
+------------------------------
+
+.. _relation_tags:
+
+Relation tags
+~~~~~~~~~~~~~
+.. automodule:: cubicweb.rtags
+
+.. _uicfg:
+
+The uicfg module
+~~~~~~~~~~~~~~~~
+
+.. note::
+
+ The part of uicfg that deals with primary views is in the
+ :ref:`primary_view_configuration` chapter.
+
+.. automodule:: cubicweb.web.views.uicfg
+
+
+The uihelper module
+~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: cubicweb.web.uihelper
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/searchbar.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,41 @@
+.. _searchbar:
+
+RQL search bar
+--------------
+
+The RQL search bar is a visual component, hidden by default, the tiny *search*
+input being enough for common use cases.
+
+An autocompletion helper is provided to help you type valid queries, both
+in terms of syntax and in terms of schema validity.
+
+.. autoclass:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder
+
+
+How search is performed
++++++++++++++++++++++++
+
+You can use the *rql search bar* to either type RQL queries, plain text queries
+or standard shortcuts such as *<EntityType>* or *<EntityType> <attrname> <value>*.
+
+Ultimately, all queries are translated to rql since it's the only
+language understood on the server (data) side. To transform the user
+query into RQL, CubicWeb uses the so-called *magicsearch component*,
+defined in :mod:`cubicweb.web.views.magicsearch`, which in turn
+delegates to a number of query preprocessor that are responsible of
+interpreting the user query and generating corresponding RQL.
+
+The code of the main processor loop is easy to understand:
+
+.. sourcecode:: python
+
+ for proc in self.processors:
+ try:
+ return proc.process_query(uquery, req)
+ except (RQLSyntaxError, BadRQLQuery):
+ pass
+
+The idea is simple: for each query processor, try to translate the
+query. If it fails, try with the next processor, if it succeeds,
+we're done and the RQL query will be executed.
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/basetemplates.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,161 @@
+.. -*- coding: utf-8 -*-
+
+.. _templates:
+
+Templates
+=========
+
+Templates are the entry point for the |cubicweb| view system. As seen
+in :ref:`views_base_class`, there are two kinds of views: the
+templatable and non-templatable.
+
+
+Non-templatable views
+---------------------
+
+Non-templatable views are standalone. They are responsible for all the details
+such as setting a proper content type (or mime type), the proper document
+headers, namespaces, etc. Examples are pure xml views such as RSS or Semantic Web
+views (`SIOC`_, `DOAP`_, `FOAF`_, `Linked Data`_, etc.), and views which generate
+binary files (pdf, excel files, etc.)
+
+.. _`SIOC`: http://sioc-project.org/
+.. _`DOAP`: http://trac.usefulinc.com/doap
+.. _`FOAF`: http://www.foaf-project.org/
+.. _`Linked Data`: http://linkeddata.org/
+
+
+To notice that a view is not templatable, you just have to set the
+view's class attribute `templatable` to `False`. In this case, it
+should set the `content_type` class attribute to the correct MIME
+type. By default, it is text/xhtml. Additionally, if your view
+generate a binary file, you have to set the view's class attribute
+`binary` to `True` too.
+
+
+Templatable views
+-----------------
+
+Templatable views are not concerned with such pesky details. They
+leave it to the template. Conversely, the template's main job is to:
+
+* set up the proper document header and content type
+* define the general layout of a document
+* invoke adequate views in the various sections of the document
+
+
+Look at :mod:`cubicweb.web.views.basetemplates` and you will find the base
+templates used to generate (X)HTML for your application. The most important
+template there is :class:`~cubicweb.web.views.basetemplates.TheMainTemplate`.
+
+.. _the_main_template_layout:
+
+TheMainTemplate
+~~~~~~~~~~~~~~~
+
+.. _the_main_template_sections:
+
+Layout and sections
+```````````````````
+
+A page is composed as indicated on the schema below :
+
+.. image:: ../../../images/main_template.png
+
+The sections dispatches specific views:
+
+* `header`: the rendering of the header is delegated to the
+ `htmlheader` view, whose default implementation can be found in
+ ``basetemplates.py`` and which does the following things:
+
+ * inject the favicon if there is one
+ * inject the global style sheets and javascript resources
+ * call and display a link to an rss component if there is one available
+
+ it also sets up the page title, and fills the actual
+ `header` section with top-level components, using the `header` view, which:
+
+ * tries to display a logo, the name of the application and the `breadcrumbs`
+ * provides a login status area
+ * provides a login box (hiden by default)
+
+* `left column`: this is filled with all selectable boxes matching the
+ `left` context (there is also a right column but nowadays it is
+ seldom used due to bad usability)
+
+* `contentcol`: this is the central column; it is filled with:
+
+ * the `rqlinput` view (hidden by default)
+ * the `applmessages` component
+ * the `contentheader` view which in turns dispatches all available
+ content navigation components having the `navtop` context (this
+ is used to navigate through entities implementing the IPrevNext
+ interface)
+ * the view that was given as input to the template's `call`
+ method, also dealing with pagination concerns
+ * the `contentfooter`
+
+* `footer`: adds all footer actions
+
+.. note::
+
+ How and why a view object is given to the main template is explained
+ in the :ref:`publisher` chapter.
+
+Configure the main template
+```````````````````````````
+
+You can overload some methods of the
+:class:`~cubicweb.web.views.basetemplates.TheMainTemplate`, in order to fulfil
+your needs. There are also some attributes and methods which can be defined on a
+view to modify the base template behaviour:
+
+* `paginable`: if the result set is bigger than a configurable size, your result
+ page will be paginated by default. You can set this attribute to `False` to
+ avoid this.
+
+* `binary`: boolean flag telling if the view generates some text or a binary
+ stream. Default to False. When view generates text argument given to `self.w`
+ **must be a unicode string**, encoded string otherwise.
+
+* `content_type`, view's content type, default to 'text/xhtml'
+
+* `templatable`, boolean flag telling if the view's content should be returned
+ directly (when `False`) or included in the main template layout (including
+ header, boxes and so on).
+
+* `page_title()`, method that should return a title that will be set as page
+ title in the html headers.
+
+* `html_headers()`, method that should return a list of HTML headers to be
+ included the html headers.
+
+
+You can also modify certain aspects of the main template of a page
+when building a url or setting these parameters in the req.form:
+
+* `__notemplate`, if present (whatever the value assigned), only the content view
+ is returned
+
+* `__force_display`, if present and its value is not null, no pagination whatever
+ the number of entities to display (e.g. similar effect as view's `paginable`
+ attribute described above.
+
+* `__method`, if the result set to render contains only one entity and this
+ parameter is set, it refers to a method to call on the entity by passing it the
+ dictionary of the forms parameters, before going the classic way (through step
+ 1 and 2 described juste above)
+
+* `vtitle`, a title to be set as <h1> of the content
+
+Other templates
+~~~~~~~~~~~~~~~
+
+There are also the following other standard templates:
+
+* :class:`cubicweb.web.views.basetemplates.LogInTemplate`
+* :class:`cubicweb.web.views.basetemplates.LogOutTemplate`
+* :class:`cubicweb.web.views.basetemplates.ErrorTemplate` specializes
+ :class:`~cubicweb.web.views.basetemplates.TheMainTemplate` to do
+ proper end-user output if an error occurs during the computation of
+ TheMainTemplate (it is a fallback view).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/baseviews.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,21 @@
+Base views
+----------
+
+|cubicweb| provides a lot of standard views, that can be found in
+:mod:`cubicweb.web.views` sub-modules.
+
+A certain number of views are used to build the web interface, which apply to one
+or more entities. As other appobjects, their identifier is what distinguish them
+from each others. The most generic ones, found in
+:mod:`cubicweb.web.views.baseviews`, are described below.
+
+You'll probably want to customize one or more of the described views which are
+default, generic, implementations.
+
+
+.. automodule:: cubicweb.web.views.baseviews
+
+You will also find modules providing some specific services:
+
+.. automodule:: cubicweb.web.views.navigation
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/boxes.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,36 @@
+Boxes
+-----
+
+(:mod:`cubicweb.web.views.boxes`)
+
+*sidebox*
+ This view displays usually a side box of some related entities
+ in a primary view.
+
+The action box
+~~~~~~~~~~~~~~~
+
+The ``add_related`` is an automatic menu in the action box that allows to create
+an entity automatically related to the initial entity (context in
+which the box is displayed). By default, the links generated in this
+box are computed from the schema properties of the displayed entity,
+but it is possible to explicitly specify them thanks to the
+`cubicweb.web.views.uicfg.rmode` *relation tag*:
+
+* `link`, indicates that a relation is in general created pointing
+ to an existing entity and that we should not to display a link
+ for this relation
+
+* `create`, indicates that a relation is in general created pointing
+ to new entities and that we should display a link to create a new
+ entity and link to it automatically
+
+
+If necessary, it is possible to overwrite the method
+`relation_mode(rtype, targettype, x='subject')` to dynamically
+compute a relation creation category.
+
+Please note that if at least one action belongs to the `addrelated` category,
+the automatic behavior is desactivated in favor of an explicit behavior
+(e.g. display of `addrelated` category actions only).
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/breadcrumbs.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,60 @@
+Breadcrumbs
+-----------
+
+Breadcrumbs are a navigation component to help the user locate himself
+along a path of entities.
+
+Display
+~~~~~~~
+
+Breadcrumbs are displayed by default in the header section (see
+:ref:`the_main_template_sections`). With the default main template,
+the header section is composed by the logo, the application name,
+breadcrumbs and, at the most right, the login box. Breadcrumbs are
+displayed just next to the application name, thus they begin with a
+separator.
+
+Here is the header section of the CubicWeb's forge:
+
+.. image:: ../../../images/breadcrumbs_header.png
+
+There are three breadcrumbs components defined in
+:mod:`cubicweb.web.views.ibreadcrumbs`:
+
+- `BreadCrumbEntityVComponent`: displayed for a result set with one line
+ if the entity is adaptable to ``IBreadCrumbsAdapter``.
+- `BreadCrumbETypeVComponent`: displayed for a result set with more than
+ one line, but with all entities of the same type which can adapt to
+ ``IBreadCrumbsAdapter``.
+- `BreadCrumbAnyRSetVComponent`: displayed for any other result set.
+
+Building breadcrumbs
+~~~~~~~~~~~~~~~~~~~~
+
+The ``IBreadCrumbsAdapter`` adapter is defined in the
+:mod:`cubicweb.web.views.ibreadcrumbs` module. It specifies that an
+entity which implements this interface must have a ``breadcrumbs`` and
+a ``parent_entity`` method. A default implementation for each is
+provided. This implementation expoits the ITreeAdapter.
+
+.. note::
+
+ Redefining the breadcrumbs is the hammer way to do it. Another way
+ is to define an `ITreeAdapter` adapter on an entity type. If
+ available, it will be used to compute breadcrumbs.
+
+Here is the API of the ``IBreadCrumbsAdapter`` class:
+
+.. automethod:: cubicweb.web.views.ibreadcrumbs.IBreadCrumbsAdapter.parent_entity
+.. automethod:: cubicweb.web.views.ibreadcrumbs.IBreadCrumbsAdapter.breadcrumbs
+
+If the breadcrumbs method return a list of entities, the
+``cubicweb.web.views.ibreadcrumbs.BreadCrumbView`` is used to display
+the elements.
+
+By default, for any entity, if recurs=True, breadcrumbs method returns
+a list of entities, else a list of a simple string.
+
+In order to see a hierarchical breadcrumbs, entities must have a
+``parent`` method which returns the parent entity. By default this
+method doesn't exist on entity, given that it can not be guessed.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/idownloadable.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,23 @@
+The 'download' views
+====================
+
+.. automodule:: cubicweb.web.views.idownloadable
+
+Components
+----------
+
+.. autoclass:: cubicweb.web.views.idownloadable.DownloadBox
+
+Download views
+--------------
+
+.. autoclass:: cubicweb.web.views.idownloadable.DownloadView
+.. autoclass:: cubicweb.web.views.idownloadable.DownloadLinkView
+.. autoclass:: cubicweb.web.views.idownloadable.IDownloadablePrimaryView
+.. autoclass:: cubicweb.web.views.idownloadable.IDownloadableOneLineView
+
+Embedded views
+--------------
+
+.. autoclass:: cubicweb.web.views.idownloadable.ImageView
+.. autoclass:: cubicweb.web.views.idownloadable.EHTMLView
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,28 @@
+The View system
+===============
+
+This chapter aims to describe the concept of a `view` used all along
+the development of a web application and how it has been implemented
+in |cubicweb|.
+
+
+.. toctree::
+ :maxdepth: 3
+
+ views
+ basetemplates
+ primary
+ reledit
+ baseviews
+ startup
+ boxes
+ table
+ xmlrss
+ urlpublish
+ breadcrumbs
+ idownloadable
+ wdoc
+
+.. editforms
+.. embedding
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/primary.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,276 @@
+.. _primary_view:
+
+The Primary View
+-----------------
+
+By default, *CubicWeb* provides a view that fits every available
+entity type. This is the first view you might be interested in
+modifying. It is also one of the richest and most complex.
+
+It is automatically selected on a one line result set containing an
+entity.
+
+It lives in the :mod:`cubicweb.web.views.primary` module.
+
+The *primary* view is supposed to render a maximum of informations about the
+entity.
+
+.. _primary_view_layout:
+
+Layout
+``````
+
+The primary view has the following layout.
+
+.. image:: ../../../images/primaryview_template.png
+
+.. _primary_view_configuration:
+
+Primary view configuration
+``````````````````````````
+
+If you want to customize the primary view of an entity, overriding the primary
+view class may not be necessary. For simple adjustments (attributes or relations
+display locations and styles), a much simpler way is to use uicfg.
+
+Attributes/relations display location
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In the primary view, there are three sections where attributes and
+relations can be displayed (represented in pink in the image above):
+
+* 'attributes'
+* 'relations'
+* 'sideboxes'
+
+**Attributes** can only be displayed in the attributes section (default
+ behavior). They can also be hidden. By default, attributes of type `Password`
+ and `Bytes` are hidden.
+
+For instance, to hide the ``title`` attribute of the ``Blog`` entity:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views import uicfg
+ uicfg.primaryview_section.tag_attribute(('Blog', 'title'), 'hidden')
+
+**Relations** can be either displayed in one of the three sections or hidden.
+
+For relations, there are two methods:
+
+* ``tag_object_of`` for modifying the primary view of the object
+* ``tag_subject_of`` for modifying the primary view of the subject
+
+These two methods take two arguments:
+
+* a triplet ``(subject, relation_name, object)``, where subject or object can be replaced with ``'*'``
+* the section name or ``hidden``
+
+.. sourcecode:: python
+
+ pv_section = uicfg.primaryview_section
+ # hide every relation `entry_of` in the `Blog` primary view
+ pv_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
+
+ # display `entry_of` relations in the `relations`
+ # section in the `BlogEntry` primary view
+ pv_section.tag_subject_of(('BlogEntry', 'entry_of', '*'), 'relations')
+
+
+Display content
+^^^^^^^^^^^^^^^
+
+You can use ``primaryview_display_ctrl`` to customize the display of attributes
+or relations. Values of ``primaryview_display_ctrl`` are dictionaries.
+
+
+Common keys for attributes and relations are:
+
+* ``vid``: specifies the regid of the view for displaying the attribute or the relation.
+
+ If ``vid`` is not specified, the default value depends on the section:
+ * ``attributes`` section: 'reledit' view
+ * ``relations`` section: 'autolimited' view
+ * ``sideboxes`` section: 'sidebox' view
+
+* ``order``: int used to control order within a section. When not specified,
+ automatically set according to order in which tags are added.
+
+* ``label``: label for the relations section or side box
+
+* ``showlabel``: boolean telling whether the label is displayed
+
+.. sourcecode:: python
+
+ # let us remind the schema of a blog entry
+ class BlogEntry(EntityType):
+ title = String(required=True, fulltextindexed=True, maxsize=256)
+ publish_date = Date(default='TODAY')
+ content = String(required=True, fulltextindexed=True)
+ entry_of = SubjectRelation('Blog', cardinality='?*')
+
+ # now, we want to show attributes
+ # with an order different from that in the schema definition
+ view_ctrl = uicfg.primaryview_display_ctrl
+ for index, attr in enumerate('title', 'content', 'publish_date'):
+ view_ctrl.tag_attribute(('BlogEntry', attr), {'order': index})
+
+By default, relations displayed in the 'relations' section are being displayed by
+the 'autolimited' view. This view will use comma separated values, or list view
+and/or limit your rset if there is too much items in it (and generate the "view
+all" link in this case).
+
+You can control this view by setting the following values in the
+`primaryview_display_ctrl` relation tag:
+
+* `limit`, maximum number of entities to display. The value of the
+ 'navigation.related-limit' cwproperty is used by default (which is 8 by default).
+ If None, no limit.
+
+* `use_list_limit`, number of entities until which they should be display as a list
+ (eg using the 'list' view). Below that limit, the 'csv' view is used. If None,
+ display using 'csv' anyway.
+
+* `subvid`, the subview identifier (eg view that should be used of each item in the
+ list)
+
+Notice you can also use the `filter` key to set up a callback taking the related
+result set as argument and returning it filtered, to do some arbitrary filtering
+that can't be done using rql for instance.
+
+
+.. sourcecode:: python
+
+ pv_section = uicfg.primaryview_section
+ # in `CWUser` primary view, display `created_by`
+ # relations in relations section
+ pv_section.tag_object_of(('*', 'created_by', 'CWUser'), 'relations')
+
+ # display this relation as a list, sets the label,
+ # limit the number of results and filters on comments
+ def filter_comment(rset):
+ return rset.filtered_rset(lambda x: x.e_schema == 'Comment')
+ pv_ctrl = uicfg.primaryview_display_ctrl
+ pv_ctrl.tag_object_of(('*', 'created_by', 'CWUser'),
+ {'vid': 'list', 'label': _('latest comment(s):'),
+ 'limit': True,
+ 'filter': filter_comment})
+
+.. warning:: with the ``primaryview_display_ctrl`` rtag, the subject or the
+ object of the relation is ignored for respectively ``tag_object_of`` or
+ ``tag_subject_of``. To avoid warnings during execution, they should be set to
+ ``'*'``.
+
+
+.. automodule:: cubicweb.web.views.primary
+
+
+Example of customization and creation
+`````````````````````````````````````
+
+We'll show you now an example of a ``primary`` view and how to customize it.
+
+If you want to change the way a ``BlogEntry`` is displayed, just
+override the method ``cell_call()`` of the view ``primary`` in
+``BlogDemo/views.py``.
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import is_instance
+ from cubicweb.web.views.primary import Primaryview
+
+ class BlogEntryPrimaryView(PrimaryView):
+ __select__ = PrimaryView.__select__ & is_instance('BlogEntry')
+
+ def render_entity_attributes(self, entity):
+ self.w(u'<p>published on %s</p>' %
+ entity.publish_date.strftime('%Y-%m-%d'))
+ super(BlogEntryPrimaryView, self).render_entity_attributes(entity)
+
+
+The above source code defines a new primary view for
+``BlogEntry``. The `__reid__` class attribute is not repeated there since it
+is inherited through the `primary.PrimaryView` class.
+
+The selector for this view chains the selector of the inherited class
+with its own specific criterion.
+
+The view method ``self.w()`` is used to output data. Here `lines
+08-09` output HTML for the publication date of the entry.
+
+.. image:: ../../../images/lax-book_09-new-view-blogentry_en.png
+ :alt: blog entries now look much nicer
+
+Let us now improve the primary view of a blog
+
+.. sourcecode:: python
+
+ from logilab.mtconverter import xml_escape
+ from cubicweb.predicates import is_instance, one_line_rset
+ from cubicweb.web.views.primary import Primaryview
+
+ class BlogPrimaryView(PrimaryView):
+ __regid__ = 'primary'
+ __select__ = PrimaryView.__select__ & is_instance('Blog')
+ rql = 'Any BE ORDERBY D DESC WHERE BE entry_of B, BE publish_date D, B eid %(b)s'
+
+ def render_entity_relations(self, entity):
+ rset = self._cw.execute(self.rql, {'b' : entity.eid})
+ for entry in rset.entities():
+ self.w(u'<p>%s</p>' % entry.view('inblogcontext'))
+
+ class BlogEntryInBlogView(EntityView):
+ __regid__ = 'inblogcontext'
+ __select__ = is_instance('BlogEntry')
+
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(row, col)
+ self.w(u'<a href="%s" title="%s">%s</a>' %
+ entity.absolute_url(),
+ xml_escape(entity.content[:50]),
+ xml_escape(entity.description))
+
+This happens in two places. First we override the
+render_entity_relations method of a Blog's primary view. Here we want
+to display our blog entries in a custom way.
+
+At `line 10`, a simple request is made to build a result set with all
+the entities linked to the current ``Blog`` entity by the relationship
+``entry_of``. The part of the framework handling the request knows
+about the schema and infers that such entities have to be of the
+``BlogEntry`` kind and retrieves them (in the prescribed publish_date
+order).
+
+The request returns a selection of data called a result set. Result
+set objects have an .entities() method returning a generator on
+requested entities (going transparently through the `ORM` layer).
+
+At `line 13` the view 'inblogcontext' is applied to each blog entry to
+output HTML. (Note that the 'inblogcontext' view is not defined
+whatsoever in *CubicWeb*. You are absolutely free to define whole view
+families.) We juste arrange to wrap each blogentry output in a 'p'
+html element.
+
+Next, we define the 'inblogcontext' view. This is NOT a primary view,
+with its well-defined sections (title, metadata, attribtues,
+relations/boxes). All a basic view has to define is cell_call.
+
+Since views are applied to result sets which can be tables of data, we
+have to recover the entity from its (row,col)-coordinates (`line
+20`). Then we can spit some HTML.
+
+.. warning::
+
+ Be careful: all strings manipulated in *CubicWeb* are actually
+ unicode strings. While web browsers are usually tolerant to
+ incoherent encodings they are being served, we should not abuse
+ it. Hence we have to properly escape our data. The xml_escape()
+ function has to be used to safely fill (X)HTML elements from Python
+ unicode strings.
+
+Assuming we added entries to the blog titled `MyLife`, displaying it
+now allows to read its description and all its entries.
+
+.. image:: ../../../images/lax-book_10-blog-with-two-entries_en.png
+ :alt: a blog and all its entries
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/reledit.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,149 @@
+.. _reledit:
+
+The "Click and Edit" (also `reledit`) View
+------------------------------------------
+
+The principal way to update data through the Web UI is through the
+`modify` action on entities, which brings a full form. This is
+described in the :ref:`webform` chapter.
+
+There is however another way to perform piecewise edition of entities
+and relations, using a specific `reledit` (for *relation edition*)
+view from the :mod:`cubicweb.web.views.reledit` module.
+
+This is typically applied from the default Primary View (see
+:ref:`primary_view`) on the attributes and relation section. It makes
+small editions more convenient.
+
+Of course, this can be used customely in any other view. Here come
+some explanation about its capabilities and instructions on the way to
+use it.
+
+Using `reledit`
+***************
+
+Let's start again with a simple example:
+
+.. sourcecode:: python
+
+ class Company(EntityType):
+ name = String(required=True, unique=True)
+ boss = SubjectRelation('Person', cardinality='1*')
+ status = SubjectRelation('File', cardinality='?*', composite='subject')
+
+In some view code we might want to show these attributes/relations and
+allow the user to edit each of them in turn without having to leave
+the current page. We would write code as below:
+
+.. sourcecode:: python
+
+ company.view('reledit', rtype='name', default_value='<name>') # editable name attribute
+ company.view('reledit', rtype='boss') # editable boss relation
+ company.view('reledit', rtype='status') # editable attribute-like relation
+
+If one wanted to edit the company from a boss's point of view, one
+would have to indicate the proper relation's role. By default the role
+is `subject`.
+
+.. sourcecode:: python
+
+ person.view('reledit', rtype='boss', role='object')
+
+Each of these will provide with a different editing widget. The `name`
+attribute will obviously get a text input field. The `boss` relation
+will be edited through a selection box, allowing to pick another
+`Person` as boss. The `status` relation, given that it defines Company
+as a composite entity with one file inside, will provide additional actions
+
+* to `add` a `File` when there is one
+* to `delete` the `File` (if the cardinality allows it)
+
+Moreover, editing the relation or using the `add` action leads to an
+embedded edition/creation form allowing edition of the target entity
+(which is `File` in our example) instead of merely allowing to choose
+amongst existing files.
+
+The `reledit_ctrl` rtag
+***********************
+
+The behaviour of reledited attributes/relations can be finely
+controlled using the reledit_ctrl rtag, defined in
+:mod:`cubicweb.web.views.uicfg`.
+
+This rtag provides four control variables:
+
+* ``default_value``: alternative default value
+ The default value is what is shown when there is no value.
+* ``reload``: boolean, eid (to reload to) or function taking subject
+ and returning bool/eid This is useful when editing a relation (or
+ attribute) that impacts the url or another parts of the current
+ displayed page. Defaults to false.
+* ``rvid``: alternative view id (as str) for relation or composite
+ edition Default is 'incontext' or 'csv' depending on the
+ cardinality. They can also be statically changed by subclassing
+ ClickAndEditFormView and redefining _one_rvid (resp. _many_rvid).
+* ``edit_target``: 'rtype' (to edit the relation) or 'related' (to
+ edit the related entity) This controls whether to edit the relation
+ or the target entity of the relation. Currently only one-to-one
+ relations support target entity edition. By default, the 'related'
+ option is taken whenever the relation is composite and one-to-one.
+
+Let's see how to use these controls.
+
+.. sourcecode:: python
+
+ from logilab.mtconverter import xml_escape
+ from cubicweb.web.views.uicfg import reledit_ctrl
+ reledit_ctrl.tag_attribute(('Company', 'name'),
+ {'reload': lambda x:x.eid,
+ 'default_value': xml_escape(u'<logilab tastes better>')})
+ reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'edit_target': 'related'})
+
+The `default_value` needs to be an xml escaped unicode string.
+
+The `edit_target` tag on the `boss` relation being set to `related` will
+ensure edition of the `Person` entity instead (using a standard
+automatic form) of the association of Company and Person.
+
+Finally, the `reload` key accepts either a boolean, an eid or a
+unicode string representing a url. If an eid is provided, it will be
+internally transformed into a url. The eid/url case helps when one
+needs to reload and the current url is inappropriate. A common case is
+edition of a key attribute, which is part of the current url. If one
+user changed the Company's name from `lozilab` to `logilab`, reloading
+on http://myapp/company/lozilab would fail. Providing the entity's
+eid, then, forces to reload on something like http://myapp/company/42,
+which always work.
+
+
+Disable `reledit`
+*****************
+
+By default, `reledit` is available on attributes and relations displayed in
+the 'attribute' section of the default primary view. If you want to disable
+it for some attribute or relation, you have use `uicfg`:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views.uicfg import primaryview_display_ctrl as _pvdc
+ _pvdc.tag_attribute(('Company', 'name'), {'vid': 'incontext'})
+
+To deactivate it everywhere it's used automatically, you may use the code snippet
+below somewhere in your cube's views:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views import reledit
+
+ class DeactivatedAutoClickAndEditFormView(reledit.AutoClickAndEditFormView):
+ def _should_edit_attribute(self, rschema):
+ return False
+
+ def _should_edit_attribute(self, rschema, role):
+ return False
+
+ def registration_callback(vreg):
+ vreg.register_and_replace(DeactivatedAutoClickAndEditFormView,
+ reledit.AutoClickAndEditFormView)
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/startup.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,18 @@
+Startup views
+-------------
+
+Startup views are views requiring no context, from which you usually start
+browsing (for instance the index page). The usual selectors are
+:class:`~cubicweb.predicates.none_rset` or :class:`~logilab.common.registry.yes`.
+
+You'll find here a description of startup views provided by the framework.
+
+.. automodule:: cubicweb.web.views.startup
+
+
+Other startup views:
+
+*schema*
+ A view dedicated to the display of the schema of the instance
+
+.. XXX to be continued
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/table.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,145 @@
+Table views
+-----------
+
+.. automodule:: cubicweb.web.views.tableview
+
+Example
+```````
+
+Let us take an example from the timesheet cube:
+
+.. sourcecode:: python
+
+ class ActivityResourcesTable(EntityView):
+ __regid__ = 'activity.resources.table'
+ __select__ = is_instance('Activity')
+
+ def call(self, showresource=True):
+ eids = ','.join(str(row[0]) for row in self.cw_rset)
+ rql = ('Any R,D,DUR,WO,DESCR,S,A, SN,RT,WT ORDERBY D DESC '
+ 'WHERE '
+ ' A is Activity, A done_by R, R title RT, '
+ ' A diem D, A duration DUR, '
+ ' A done_for WO, WO title WT, '
+ ' A description DESCR, A in_state S, S name SN, '
+ ' A eid IN (%s)' % eids)
+ rset = self._cw.execute(rql)
+ self.wview('resource.table', rset, 'null')
+
+ class ResourcesTable(RsetTableView):
+ __regid__ = 'resource.table'
+ # notice you may wish a stricter selector to check rql's shape
+ __select__ = is_instance('Resource')
+ # my table headers
+ headers = ['Resource', 'diem', 'duration', 'workpackage', 'description', 'state']
+ # I want a table where attributes are editable (reledit inside)
+ finalvid = 'editable-final'
+
+ cellvids = {3: 'editable-final'}
+ # display facets and actions with a menu
+ layout_args = {'display_filter': 'top',
+ 'add_view_actions': None}
+
+To obtain an editable table, you may specify the 'editable-table' view identifier
+using some of `cellvids`, `finalvid` or `nonfinalvid`.
+
+The previous example results in:
+
+.. image:: ../../../images/views-table-shadow.png
+
+In order to activate table filter mechanism, the `display_filter` option is given
+as a layout argument. A small arrow will be displayed at the table's top right
+corner. Clicking on `show filter form` action, will display the filter form as
+below:
+
+.. image:: ../../../images/views-table-filter-shadow.png
+
+By the same way, you can display additional actions for the selected entities
+by setting `add_view_actions` layout option to `True`. This will add actions
+returned by the view's :meth:`~cubicweb.web.views.tableview.TableMixIn.table_actions`.
+
+You can notice that all columns of the result set are not displayed. This is
+because of given `headers`, implying to display only columns from 0 to
+len(headers).
+
+Also Notice that the `ResourcesTable` view relies on a particular rql shape
+(which is not ensured by the way, the only checked thing is that the result set
+contains instance of the `Resource` type). That usually implies that you can't
+use this view for user specific queries (e.g. generated by facets or typed
+manually).
+
+
+So another option would be to write this view using
+:class:`~cubicweb.web.views.tableview.EntityTableView`, as below.
+
+.. sourcecode:: python
+
+ class ResourcesTable(EntityTableView):
+ __regid__ = 'resource.table'
+ __select__ = is_instance('Resource')
+ # table columns definition
+ columns = ['resource', 'diem', 'duration', 'workpackage', 'description', 'in_state']
+ # I want a table where attributes are editable (reledit inside)
+ finalvid = 'editable-final'
+ # display facets and actions with a menu
+ layout_args = {'display_filter': 'top',
+ 'add_view_actions': None}
+
+ def workpackage_cell(entity):
+ activity = entity.reverse_done_in[0]
+ activity.view('reledit', rtype='done_for', role='subject', w=w)
+ def workpackage_sortvalue(entity):
+ activity = entity.reverse_done_in[0]
+ return activity.done_for[0].sortvalue()
+
+ column_renderers = {
+ 'resource': MainEntityColRenderer(),
+ 'workpackage': EntityTableColRenderer(
+ header='Workpackage',
+ renderfunc=worpackage_cell,
+ sortfunc=worpackage_sortvalue,),
+ 'in_state': EntityTableColRenderer(
+ renderfunc=lambda w,x: w(x.cw_adapt_to('IWorkflowable').printable_state),
+ sortfunc=lambda x: x.cw_adapt_to('IWorkflowable').printable_state),
+ }
+
+Notice the following point:
+
+* `cell_<column>(w, entity)` will be searched for rendering the content of a
+ cell. If not found, `column` is expected to be an attribute of `entity`.
+
+* `cell_sortvalue_<column>(entity)` should return a typed value to use for
+ javascript sorting or None for not sortable columns (the default).
+
+* The :func:`etable_entity_sortvalue` decorator will set a 'sortvalue' function
+ for the column containing the main entity (the one given as argument to all
+ methods), which will call `entity.sortvalue()`.
+
+* You can set a column header using the :func:`etable_header_title` decorator.
+ This header will be translated. If it's not an already existing msgid, think
+ to mark it using `_()` (the example supposes headers are schema defined msgid).
+
+
+Pro/cons of each approach
+`````````````````````````
+:class:`EntityTableView` and :class:`RsetableView` provides basically the same
+set of features, though they don't share the same properties. Let's try to sum
+up pro and cons of each class.
+
+* `EntityTableView` view is:
+
+ - more verbose, but usually easier to understand
+
+ - easily extended (easy to add/remove columns for instance)
+
+ - doesn't rely on a particular rset shape. Simply give it a title and will be
+ listed in the 'possible views' box if any.
+
+* `RsetTableView` view is:
+
+ - hard to beat to display barely a result set, or for cases where some of
+ `headers`, `displaycols` or `cellvids` could be defined to enhance the table
+ while you don't care about e.g. pagination or facets.
+
+ - hardly extensible, as you usually have to change places where the view is
+ called to modify the RQL (hence the view's result set shape).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/urlpublish.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,137 @@
+.. -*- coding: utf-8 -*-
+
+URL publishing
+--------------
+
+(:mod:`cubicweb.web.views.urlpublishing`)
+
+.. automodule:: cubicweb.web.views.urlpublishing
+
+.. autoclass:: cubicweb.web.views.urlpublishing.URLPublisherComponent
+ :members:
+
+
+You can write your own *URLPathEvaluator* class to handle custom paths.
+For instance, if you want */my-card-id* to redirect to the corresponding
+card's primary view, you would write:
+
+.. sourcecode:: python
+
+ class CardWikiidEvaluator(URLPathEvaluator):
+ priority = 3 # make it be evaluated *before* RestPathEvaluator
+
+ def evaluate_path(self, req, segments):
+ if len(segments) != 1:
+ raise PathDontMatch()
+ rset = req.execute('Any C WHERE C wikiid %(w)s',
+ {'w': segments[0]})
+ if len(rset) == 0:
+ # Raise NotFound if no card is found
+ raise PathDontMatch()
+ return None, rset
+
+On the other hand, you can also deactivate some of the standard
+evaluators in your final application. The only thing you have to
+do is to unregister them, for instance in a *registration_callback*
+in your cube:
+
+.. sourcecode:: python
+
+ def registration_callback(vreg):
+ vreg.unregister(RestPathEvaluator)
+
+You can even replace the :class:`cubicweb.web.views.urlpublishing.URLPublisherComponent`
+class if you want to customize the whole toolchain process or if you want
+to plug into an early enough extension point to control your request
+parameters:
+
+.. sourcecode:: python
+
+ class SanitizerPublisherComponent(URLPublisherComponent):
+ """override default publisher component to explicitly ignore
+ unauthorized request parameters in anonymous mode.
+ """
+ unauthorized_form_params = ('rql', 'vid', '__login', '__password')
+
+ def process(self, req, path):
+ if req.session.anonymous_session:
+ self._remove_unauthorized_params(req)
+ return super(SanitizerPublisherComponent, self).process(req, path)
+
+ def _remove_unauthorized_params(self, req):
+ for param in req.form.keys():
+ if param in self.unauthorized_form_params:
+ req.form.pop(param)
+
+
+ def registration_callback(vreg):
+ vreg.register_and_replace(SanitizerPublisherComponent, URLPublisherComponent)
+
+
+.. autoclass:: cubicweb.web.views.urlpublishing.RawPathEvaluator
+.. autoclass:: cubicweb.web.views.urlpublishing.EidPathEvaluator
+.. autoclass:: cubicweb.web.views.urlpublishing.URLRewriteEvaluator
+.. autoclass:: cubicweb.web.views.urlpublishing.RestPathEvaluator
+.. autoclass:: cubicweb.web.views.urlpublishing.ActionPathEvaluator
+
+URL rewriting
+-------------
+
+(:mod:`cubicweb.web.views.urlrewrite`)
+
+.. autoclass:: cubicweb.web.views.urlrewrite.URLRewriter
+ :members:
+
+.. autoclass:: cubicweb.web.views.urlrewrite.SimpleReqRewriter
+ :members:
+
+.. autoclass:: cubicweb.web.views.urlrewrite.SchemaBasedRewriter
+ :members:
+
+
+``SimpleReqRewriter`` is enough for a certain number of simple cases. If it is not sufficient, ``SchemaBasedRewriter`` allows to do more elaborate things.
+
+Here is an example of ``SimpleReqRewriter`` usage with plain string:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views.urlrewrite import SimpleReqRewriter
+ class TrackerSimpleReqRewriter(SimpleReqRewriter):
+ rules = [
+ ('/versions', dict(vid='versionsinfo')),
+ ]
+
+When the url is `<base_url>/versions`, the view with the __regid__ `versionsinfo` is displayed.
+
+Here is an example of ``SimpleReqRewriter`` usage with regular expressions:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views.urlrewrite import (
+ SimpleReqRewriter, rgx)
+
+ class BlogReqRewriter(SimpleReqRewriter):
+ rules = [
+ (rgx('/blogentry/([a-z_]+)\.rss'),
+ dict(rql=('Any X ORDERBY CD DESC LIMIT 20 WHERE X is BlogEntry,'
+ 'X creation_date CD, X created_by U, '
+ 'U login "%(user)s"'
+ % {'user': r'\1'}), vid='rss'))
+ ]
+
+When a url matches the regular expression, the view with the __regid__
+`rss` which match the result set is displayed.
+
+Here is an example of ``SchemaBasedRewriter`` usage:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views.urlrewrite import (
+ SchemaBasedRewriter, rgx, build_rset)
+
+ class TrackerURLRewriter(SchemaBasedRewriter):
+ rules = [
+ (rgx('/project/([^/]+)/([^/]+)/tests'),
+ build_rset(rql='Version X WHERE X version_of P, P name %(project)s, X num %(num)s',
+ rgxgroups=[('project', 1), ('num', 2)], vid='versiontests')),
+ ]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/views.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,120 @@
+
+.. _Views:
+
+Principles
+----------
+
+We'll start with a description of the interface providing a basic
+understanding of the available classes and methods, then detail the
+view selection principle.
+
+A `View` is an object responsible for the rendering of data from the
+model into an end-user consummable form. They typically churn out an
+XHTML stream, but there are views concerned with email other non-html
+outputs.
+
+.. _views_base_class:
+
+Discovering possible views
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible to configure the web user interface to have a left box
+showing all the views than can be applied to the current result set.
+
+To enable this, click on your login at the top right corner. Chose
+"user preferences", then "boxes", then "possible views box" and check
+"visible = yes" before validating your changes.
+
+The views listed there we either not selected because of a lower
+score, or they were deliberately excluded by the main template logic.
+
+
+Basic class for views
+~~~~~~~~~~~~~~~~~~~~~
+
+Class :class:`~cubicweb.view.View`
+``````````````````````````````````
+
+.. autoclass:: cubicweb.view.View
+
+The basic interface for views is as follows (remember that the result
+set has a tabular structure with rows and columns, hence cells):
+
+* `render(**context)`, render the view by calling `call` or
+ `cell_call` depending on the context
+
+* `call(**kwargs)`, call the view for a complete result set or null
+ (the default implementation calls `cell_call()` on each cell of the
+ result set)
+
+* `cell_call(row, col, **kwargs)`, call the view for a given cell of a
+ result set (`row` and `col` being integers used to access the cell)
+
+* `url()`, returns the URL enabling us to get the view with the current
+ result set
+
+* `wview(__vid, rset, __fallback_vid=None, **kwargs)`, call the view of
+ identifier `__vid` on the given result set. It is possible to give a
+ fallback view identifier that will be used if the requested view is
+ not applicable to the result set.
+
+* `html_headers()`, returns a list of HTML headers to be set by the
+ main template
+
+* `page_title()`, returns the title to use in the HTML header `title`
+
+Other basic view classes
+````````````````````````
+Here are some of the subclasses of :class:`~cubicweb.view.View` defined in :mod:`cubicweb.view`
+that are more concrete as they relate to data rendering within the application:
+
+.. autoclass:: cubicweb.view.EntityView
+.. autoclass:: cubicweb.view.StartupView
+.. autoclass:: cubicweb.view.EntityStartupView
+.. autoclass:: cubicweb.view.AnyRsetView
+
+Examples of views class
+```````````````````````
+
+- Using `templatable`, `content_type` and HTTP cache configuration
+
+.. sourcecode:: python
+
+ class RSSView(XMLView):
+ __regid__ = 'rss'
+ title = _('rss')
+ templatable = False
+ content_type = 'text/xml'
+ http_cache_manager = MaxAgeHTTPCacheManager
+ cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
+
+
+- Using a custom selector
+
+.. sourcecode:: python
+
+ class SearchForAssociationView(EntityView):
+ """view called by the edition view when the user asks
+ to search for something to link to the edited eid
+ """
+ __regid__ = 'search-associate'
+ title = _('search for association')
+ __select__ = one_line_rset() & match_search_state('linksearch') & is_instance('Any')
+
+
+XML views, binaries views...
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For views generating other formats than HTML (an image generated dynamically
+for example), and which can not simply be included in the HTML page generated
+by the main template (see above), you have to:
+
+* set the attribute `templatable` of the class to `False`
+* set, through the attribute `content_type` of the class, the MIME
+ type generated by the view to `application/octet-stream` or any
+ relevant and more specialised mime type
+
+For views dedicated to binary content creation (like dynamically generated
+images), we have to set the attribute `binary` of the class to `True` (which
+implies that `templatable == False`, so that the attribute `w` of the view could be
+replaced by a binary flow instead of unicode).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/wdoc.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,15 @@
+.. -*- coding: utf-8 -*-
+
+Online documentation system
+===========================
+
+.. automodule:: cubicweb.web.views.wdoc
+
+Help views
+----------
+.. autoclass:: cubicweb.web.views.wdoc.InlineHelpView
+
+Actions
+-------
+.. autoclass:: cubicweb.web.views.wdoc.HelpAction
+.. autoclass:: cubicweb.web.views.wdoc.AboutAction
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/devweb/views/xmlrss.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,66 @@
+.. _XmlAndRss:
+
+XML and RSS views
+-----------------
+
+(:mod:`cubicweb.web.views.xmlrss`)
+
+Overview
++++++++++
+
+*rss*
+ Creates a RSS/XML view and call the view `rssitem` for each entity of
+ the result set.
+
+*rssitem*
+ Create a RSS/XML view for each entity based on the results of the dublin core
+ methods of the entity (`dc_*`)
+
+RSS Channel Example
+++++++++++++++++++++
+
+Assuming you have several blog entries, click on the title of the
+search box in the left column. A larger search box should appear. Enter:
+
+.. sourcecode:: sql
+
+ Any X ORDERBY D WHERE X is BlogEntry, X creation_date D
+
+and you get a list of blog entries.
+
+Click on your login at the top right corner. Chose "user preferences",
+then "boxes", then "possible views box" and check "visible = yes"
+before validating your changes.
+
+Enter the same query in the search box and you will see the same list,
+plus a box titled "possible views" in the left column. Click on
+"entityview", then "RSS".
+
+You just applied the "RSS" view to the RQL selection you requested.
+
+That's it, you have a RSS channel for your blog.
+
+Try again with:
+
+.. sourcecode:: sql
+
+ Any X ORDERBY D WHERE X is BlogEntry, X creation_date D,
+ X entry_of B, B title "MyLife"
+
+Another RSS channel, but a bit more focused.
+
+A last one for the road:
+
+.. sourcecode:: sql
+
+ Any C ORDERBY D WHERE C is Comment, C creation_date D LIMIT 15
+
+displayed with the RSS view, that's a channel for the last fifteen
+comments posted.
+
+[WRITE ME]
+
+* show that the RSS view can be used to display an ordered selection
+ of blog entries, thus providing a RSS channel
+
+* show that a different selection (by category) means a different channel
Binary file doc/book/en/.static/cubicweb.png has changed
Binary file doc/book/en/.static/logilab.png has changed
--- a/doc/book/en/.static/sphinx-default.css Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,861 +0,0 @@
-/**
- * Sphinx Doc Design
- */
-
-html, body {
- background: white;
-}
-
-body {
- font-family: Verdana, sans-serif;
- font-size: 100%;
- background-color: white;
- color: black;
- margin: 0;
- padding: 0;
-}
-
-/* :::: LAYOUT :::: */
-
-div.logilablogo {
- padding: 10px 10px 10px 10px;
- height:75;
-}
-
-
-div.document {
- background-color: white;
-}
-
-div.documentwrapper {
- float: left;
- width: 100%;
-}
-
-div.bodywrapper {
- margin: 0 0 0 230px;
-}
-
-div.body {
- background-color: white;
- padding: 0 20px 30px 20px;
- border-left:solid;
- border-left-color:#e2e2e2;
- border-left-width:thin;
-}
-
-div.sphinxsidebarwrapper {
- padding: 10px 5px 0 10px;
-}
-
-div.sphinxsidebar {
- float: left;
- width: 230px;
- margin-left: -100%;
- font-size: 90%;
-}
-
-div.clearer {
- clear: both;
-}
-
-div.footer {
- color: #ff4500;
- width: 100%;
- padding: 9px 0 9px 0;
- text-align: center;
- font-size: 75%;
-}
-
-div.footer a {
- color: #ff4500;
- text-decoration: underline;
-}
-
-div.related {
- background-color: #ff7700;
- color: white;
- width: 100%;
- height: 30px;
- line-height: 30px;
- font-size: 90%;
-}
-
-div.related h3 {
- display: none;
-}
-
-div.related ul {
- margin: 0;
- padding: 0 0 0 10px;
- list-style: none;
-}
-
-div.related li {
- display: inline;
-}
-
-div.related li.right {
- float: right;
- margin-right: 5px;
-}
-
-div.related a {
- color: white;
- font-weight:bold;
-}
-
-/* ::: TOC :::: */
-
-div.sphinxsidebar {
- border-style:solid;
- border-color: white;
-/* background-color:#e2e2e2;*/
- padding-bottom:5px;
-}
-
-div.sphinxsidebar h3 {
- font-family: Verdana, sans-serif;
- color: black;
- font-size: 1.2em;
- font-weight: normal;
- margin: 0;
- padding: 0;
- font-weight:bold;
- font-style:italic;
-}
-
-div.sphinxsidebar h4 {
- font-family: Verdana, sans-serif;
- color: black;
- font-size: 1.1em;
- font-weight: normal;
- margin: 5px 0 0 0;
- padding: 0;
- font-weight:bold;
- font-style:italic;
-}
-
-div.sphinxsidebar p {
- color: black;
-}
-
-div.sphinxsidebar p.topless {
- margin: 5px 10px 10px 10px;
-}
-
-div.sphinxsidebar ul {
- margin: 10px;
- padding: 0;
- list-style: none;
- color: black;
-}
-
-div.sphinxsidebar ul ul,
-div.sphinxsidebar ul.want-points {
- margin-left: 20px;
- list-style: square;
-}
-
-div.sphinxsidebar ul ul {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-div.sphinxsidebar a {
- color: black;
- text-decoration: none;
-}
-
-div.sphinxsidebar form {
- margin-top: 10px;
-}
-
-div.sphinxsidebar input {
- border: 1px solid #e2e2e2;
- font-family: sans-serif;
- font-size: 1em;
- padding-bottom: 5px;
-}
-
-/* :::: MODULE CLOUD :::: */
-div.modulecloud {
- margin: -5px 10px 5px 10px;
- padding: 10px;
- line-height: 160%;
- border: 1px solid #cbe7e5;
- background-color: #f2fbfd;
-}
-
-div.modulecloud a {
- padding: 0 5px 0 5px;
-}
-
-/* :::: SEARCH :::: */
-ul.search {
- margin: 10px 0 0 20px;
- padding: 0;
-}
-
-ul.search li {
- padding: 5px 0 5px 20px;
- background-image: url(file.png);
- background-repeat: no-repeat;
- background-position: 0 7px;
-}
-
-ul.search li a {
- font-weight: bold;
-}
-
-ul.search li div.context {
- color: #888;
- margin: 2px 0 0 30px;
- text-align: left;
-}
-
-ul.keywordmatches li.goodmatch a {
- font-weight: bold;
-}
-
-/* :::: COMMON FORM STYLES :::: */
-
-div.actions {
- padding: 5px 10px 5px 10px;
- border-top: 1px solid #cbe7e5;
- border-bottom: 1px solid #cbe7e5;
- background-color: #e0f6f4;
-}
-
-form dl {
- color: #333;
-}
-
-form dt {
- clear: both;
- float: left;
- min-width: 110px;
- margin-right: 10px;
- padding-top: 2px;
-}
-
-input#homepage {
- display: none;
-}
-
-div.error {
- margin: 5px 20px 0 0;
- padding: 5px;
- border: 1px solid #d00;
- font-weight: bold;
-}
-
-/* :::: INLINE COMMENTS :::: */
-
-div.inlinecomments {
- position: absolute;
- right: 20px;
-}
-
-div.inlinecomments a.bubble {
- display: block;
- float: right;
- background-image: url(style/comment.png);
- background-repeat: no-repeat;
- width: 25px;
- height: 25px;
- text-align: center;
- padding-top: 3px;
- font-size: 0.9em;
- line-height: 14px;
- font-weight: bold;
- color: black;
-}
-
-div.inlinecomments a.bubble span {
- display: none;
-}
-
-div.inlinecomments a.emptybubble {
- background-image: url(style/nocomment.png);
-}
-
-div.inlinecomments a.bubble:hover {
- background-image: url(style/hovercomment.png);
- text-decoration: none;
- color: #3ca0a4;
-}
-
-div.inlinecomments div.comments {
- float: right;
- margin: 25px 5px 0 0;
- max-width: 50em;
- min-width: 30em;
- border: 1px solid #2eabb0;
- background-color: #f2fbfd;
- z-index: 150;
-}
-
-div#comments {
- border: 1px solid #2eabb0;
- margin-top: 20px;
-}
-
-div#comments div.nocomments {
- padding: 10px;
- font-weight: bold;
-}
-
-div.inlinecomments div.comments h3,
-div#comments h3 {
- margin: 0;
- padding: 0;
- background-color: #2eabb0;
- color: white;
- border: none;
- padding: 3px;
-}
-
-div.inlinecomments div.comments div.actions {
- padding: 4px;
- margin: 0;
- border-top: none;
-}
-
-div#comments div.comment {
- margin: 10px;
- border: 1px solid #2eabb0;
-}
-
-div.inlinecomments div.comment h4,
-div.commentwindow div.comment h4,
-div#comments div.comment h4 {
- margin: 10px 0 0 0;
- background-color: #2eabb0;
- color: white;
- border: none;
- padding: 1px 4px 1px 4px;
-}
-
-div#comments div.comment h4 {
- margin: 0;
-}
-
-div#comments div.comment h4 a {
- color: #d5f4f4;
-}
-
-div.inlinecomments div.comment div.text,
-div.commentwindow div.comment div.text,
-div#comments div.comment div.text {
- margin: -5px 0 -5px 0;
- padding: 0 10px 0 10px;
-}
-
-div.inlinecomments div.comment div.meta,
-div.commentwindow div.comment div.meta,
-div#comments div.comment div.meta {
- text-align: right;
- padding: 2px 10px 2px 0;
- font-size: 95%;
- color: #538893;
- border-top: 1px solid #cbe7e5;
- background-color: #e0f6f4;
-}
-
-div.commentwindow {
- position: absolute;
- width: 500px;
- border: 1px solid #cbe7e5;
- background-color: #f2fbfd;
- display: none;
- z-index: 130;
-}
-
-div.commentwindow h3 {
- margin: 0;
- background-color: #2eabb0;
- color: white;
- border: none;
- padding: 5px;
- font-size: 1.5em;
- cursor: pointer;
-}
-
-div.commentwindow div.actions {
- margin: 10px -10px 0 -10px;
- padding: 4px 10px 4px 10px;
- color: #538893;
-}
-
-div.commentwindow div.actions input {
- border: 1px solid #2eabb0;
- background-color: white;
- color: #135355;
- cursor: pointer;
-}
-
-div.commentwindow div.form {
- padding: 0 10px 0 10px;
-}
-
-div.commentwindow div.form input,
-div.commentwindow div.form textarea {
- border: 1px solid #3c9ea2;
- background-color: white;
- color: black;
-}
-
-div.commentwindow div.error {
- margin: 10px 5px 10px 5px;
- background-color: #fbe5dc;
- display: none;
-}
-
-div.commentwindow div.form textarea {
- width: 99%;
-}
-
-div.commentwindow div.preview {
- margin: 10px 0 10px 0;
- background-color: #70d0d4;
- padding: 0 1px 1px 25px;
-}
-
-div.commentwindow div.preview h4 {
- margin: 0 0 -5px -20px;
- padding: 4px 0 0 4px;
- color: white;
- font-size: 1.3em;
-}
-
-div.commentwindow div.preview div.comment {
- background-color: #f2fbfd;
-}
-
-div.commentwindow div.preview div.comment h4 {
- margin: 10px 0 0 0!important;
- padding: 1px 4px 1px 4px!important;
- font-size: 1.2em;
-}
-
-/* :::: SUGGEST CHANGES :::: */
-div#suggest-changes-box input, div#suggest-changes-box textarea {
- border: 1px solid #ccc;
- background-color: white;
- color: black;
-}
-
-div#suggest-changes-box textarea {
- width: 99%;
- height: 400px;
-}
-
-
-/* :::: PREVIEW :::: */
-div.preview {
- background-image: url(style/preview.png);
- padding: 0 20px 20px 20px;
- margin-bottom: 30px;
-}
-
-
-/* :::: INDEX PAGE :::: */
-
-table.contentstable {
- width: 90%;
-}
-
-table.contentstable p.biglink {
- line-height: 150%;
-}
-
-a.biglink {
- font-size: 1.3em;
-}
-
-span.linkdescr {
- font-style: italic;
- padding-top: 5px;
- font-size: 90%;
-}
-
-/* :::: INDEX STYLES :::: */
-
-table.indextable td {
- text-align: left;
- vertical-align: top;
-}
-
-table.indextable dl, table.indextable dd {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-table.indextable tr.pcap {
- height: 10px;
-}
-
-table.indextable tr.cap {
- margin-top: 10px;
- background-color: #f2f2f2;
-}
-
-img.toggler {
- margin-right: 3px;
- margin-top: 3px;
- cursor: pointer;
-}
-
-form.pfform {
- margin: 10px 0 20px 0;
-}
-
-/* :::: GLOBAL STYLES :::: */
-
-.docwarning {
- background-color: #ffe4e4;
- padding: 10px;
- margin: 0 -20px 0 -20px;
- border-bottom: 1px solid #f66;
-}
-
-p.subhead {
- font-weight: bold;
- margin-top: 20px;
-}
-
-a {
- color: orangered;
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-div.body h1,
-div.body h2,
-div.body h3,
-div.body h4,
-div.body h5,
-div.body h6 {
- font-family: 'Verdana', sans-serif;
- background-color: white;
- font-weight: bold;
- color: black;
- border-bottom: 1px solid #ccc;
- margin: 20px -20px 10px -20px;
- padding: 3px 0 3px 10px;
-}
-
-div.body h1 { margin-top: 10pt; font-size: 150%; }
-div.body h2 { font-size: 120%; }
-div.body h3 { font-size: 100%; }
-div.body h4 { font-size: 80%; }
-div.body h5 { font-size: 600%; }
-div.body h6 { font-size: 40%; }
-
-a.headerlink {
- color: #c60f0f;
- font-size: 0.8em;
- padding: 0 4px 0 4px;
- text-decoration: none;
- visibility: hidden;
-}
-
-h1:hover > a.headerlink,
-h2:hover > a.headerlink,
-h3:hover > a.headerlink,
-h4:hover > a.headerlink,
-h5:hover > a.headerlink,
-h6:hover > a.headerlink,
-dt:hover > a.headerlink {
- visibility: visible;
-}
-
-a.headerlink:hover {
- background-color: #c60f0f;
- color: white;
-}
-
-div.body p, div.body dd, div.body li {
- text-align: justify;
- line-height: 130%;
-}
-
-div.body p.caption {
- text-align: inherit;
-}
-
-div.body td {
- text-align: left;
-}
-
-ul.fakelist {
- list-style: none;
- margin: 10px 0 10px 20px;
- padding: 0;
-}
-
-.field-list ul {
- padding-left: 1em;
-}
-
-.first {
- margin-top: 0 !important;
-}
-
-/* "Footnotes" heading */
-p.rubric {
- margin-top: 30px;
- font-weight: bold;
-}
-
-/* "Topics" */
-
-div.topic {
- background-color: #eee;
- border: 1px solid #ccc;
- padding: 0 7px 0 7px;
- margin: 10px 0 10px 0;
-}
-
-p.topic-title {
- font-size: 1.1em;
- font-weight: bold;
- margin-top: 10px;
-}
-
-/* Admonitions */
-
-div.admonition {
- margin-top: 10px;
- margin-bottom: 10px;
- padding: 7px;
-}
-
-div.admonition dt {
- font-weight: bold;
-}
-
-div.admonition dl {
- margin-bottom: 0;
-}
-
-div.admonition p {
- display: inline;
-}
-
-div.seealso {
- background-color: #ffc;
- border: 1px solid #ff6;
-}
-
-div.warning {
- background-color: #ffe4e4;
- border: 1px solid #f66;
-}
-
-div.note {
- background-color: #eee;
- border: 1px solid #ccc;
-}
-
-p.admonition-title {
- margin: 0px 10px 5px 0px;
- font-weight: bold;
- display: inline;
-}
-
-p.admonition-title:after {
- content: ":";
-}
-
-div.body p.centered {
- text-align: center;
- margin-top: 25px;
-}
-
-table.docutils {
- border: 0;
-}
-
-table.docutils td, table.docutils th {
- padding: 1px 8px 1px 0;
- border-top: 0;
- border-left: 0;
- border-right: 0;
- border-bottom: 1px solid #aaa;
-}
-
-table.field-list td, table.field-list th {
- border: 0 !important;
-}
-
-table.footnote td, table.footnote th {
- border: 0 !important;
-}
-
-.field-list ul {
- margin: 0;
- padding-left: 1em;
-}
-
-.field-list p {
- margin: 0;
-}
-
-dl {
- margin-bottom: 15px;
- clear: both;
-}
-
-dd p {
- margin-top: 0px;
-}
-
-dd ul, dd table {
- margin-bottom: 10px;
-}
-
-dd {
- margin-top: 3px;
- margin-bottom: 10px;
- margin-left: 30px;
-}
-
-.refcount {
- color: #060;
-}
-
-dt:target,
-.highlight {
- background-color: #fbe54e;
-}
-
-dl.glossary dt {
- font-weight: bold;
- font-size: 1.1em;
-}
-
-th {
- text-align: left;
- padding-right: 5px;
-}
-
-pre {
- padding: 5px;
- background-color: #efc;
- color: #333;
- border: 1px solid #ac9;
- border-left: none;
- border-right: none;
- overflow: auto;
-}
-
-td.linenos pre {
- padding: 5px 0px;
- border: 0;
- background-color: transparent;
- color: #aaa;
-}
-
-table.highlighttable {
- margin-left: 0.5em;
-}
-
-table.highlighttable td {
- padding: 0 0.5em 0 0.5em;
-}
-
-tt {
- background-color: #ecf0f3;
- padding: 0 1px 0 1px;
- font-size: 0.95em;
-}
-
-tt.descname {
- background-color: transparent;
- font-weight: bold;
- font-size: 1.2em;
-}
-
-tt.descclassname {
- background-color: transparent;
-}
-
-tt.xref, a tt {
- background-color: transparent;
- font-weight: bold;
-}
-
-.footnote:target { background-color: #ffa }
-
-h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
- background-color: transparent;
-}
-
-.optional {
- font-size: 1.3em;
-}
-
-.versionmodified {
- font-style: italic;
-}
-
-form.comment {
- margin: 0;
- padding: 10px 30px 10px 30px;
- background-color: #eee;
-}
-
-form.comment h3 {
- background-color: #326591;
- color: white;
- margin: -10px -30px 10px -30px;
- padding: 5px;
- font-size: 1.4em;
-}
-
-form.comment input,
-form.comment textarea {
- border: 1px solid #ccc;
- padding: 2px;
- font-family: sans-serif;
- font-size: 100%;
-}
-
-form.comment input[type="text"] {
- width: 240px;
-}
-
-form.comment textarea {
- width: 100%;
- height: 200px;
- margin-bottom: 10px;
-}
-
-.system-message {
- background-color: #fda;
- padding: 5px;
- border: 3px solid red;
-}
-
-/* :::: PRINT :::: */
-@media print {
- div.document,
- div.documentwrapper,
- div.bodywrapper {
- margin: 0;
- width : 100%;
- }
-
- div.sphinxsidebar,
- div.related,
- div.footer,
- div#comments div.new-comment-box,
- #top-link {
- display: none;
- }
-}
--- a/doc/book/en/.templates/layout.html Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-{%- block doctype -%}
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-{%- endblock %}
-{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %}
-{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
-{%- macro relbar() %}
- <div class="related">
- <h3>Navigation</h3>
- <ul>
- {%- for rellink in rellinks %}
- <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
- <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags }}"
- accesskey="{{ rellink[2] }}">{{ rellink[3] }}</a>
- {%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
- {%- endfor %}
- {%- block rootrellink %}
- <li><a href="{{ pathto('index') }}">{{ shorttitle }}</a>{{ reldelim1 }}</li>
- {%- endblock %}
- {%- for parent in parents %}
- <li><a href="{{ parent.link|e }}" accesskey="U">{{ parent.title }}</a>{{ reldelim1 }}</li>
- {%- endfor %}
- {%- block relbaritems %}{% endblock %}
- </ul>
- </div>
-{%- endmacro %}
-{%- macro sidebar() %}
- {%- if builder != 'htmlhelp' %}
- <div class="sphinxsidebar">
- <div class="sphinxsidebarwrapper">
- {%- block sidebarlogo %}
- {%- if logo %}
- <p class="logo"><img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/></p>
- {%- endif %}
- {%- endblock %}
- {%- block sidebartoc %}
- {%- if display_toc %}
- <h3>Table Of Contents</h3>
- {{ toc }}
- {%- endif %}
- {%- endblock %}
- {%- block sidebarrel %}
- {%- if prev %}
- <h4>Previous topic</h4>
- <p class="topless"><a href="{{ prev.link|e }}" title="previous chapter">{{ prev.title }}</a></p>
- {%- endif %}
- {%- if next %}
- <h4>Next topic</h4>
- <p class="topless"><a href="{{ next.link|e }}" title="next chapter">{{ next.title }}</a></p>
- {%- endif %}
- {%- endblock %}
- {%- if sourcename %}
- <!--<h3>This Page</h3>
- <ul class="this-page-menu">
- {%- if builder == 'web' %}
- <li><a href="#comments">Comments ({{ comments|length }} so far)</a></li>
- <li><a href="{{ pathto('@edit/' + sourcename)|e }}">Suggest Change</a></li>
- <li><a href="{{ pathto('@source/' + sourcename)|e }}">Show Source</a></li>
- {%- elif builder == 'html' %}
- <li><a href="{{ pathto('_sources/' + sourcename, true)|e }}">Show Source</a></li>
- {%- endif %}
- </ul>-->
- {%- endif %}
- {%- if customsidebar %}
- {{ rendertemplate(customsidebar) }}
- {%- endif %}
- {%- block sidebarsearch %}
- {%- if pagename != "search" %}
- <h3>{{ builder == 'web' and 'Keyword' or 'Quick' }} search</h3>
- <form class="search" action="{{ pathto('search') }}" method="get">
- <input type="text" name="q" size="18" /> <input type="submit" value="Go" />
- <input type="hidden" name="check_keywords" value="yes" />
- <input type="hidden" name="area" value="default" />
- </form>
- {%- if builder == 'web' %}
- <p style="font-size: 90%">Enter a module, class or function name.</p>
- {%- endif %}
- {%- endif %}
- {%- endblock %}
- </div>
- </div>
- {%- endif %}
-{%- endmacro -%}
-
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- {%- if builder != 'htmlhelp' %}
- {%- set titlesuffix = " — " + docstitle %}
- {%- endif %}
- <title>{{ title|striptags }}{{ titlesuffix }}</title>
- {%- if builder == 'web' %}
- <link rel="stylesheet" href="{{ pathto('index') }}?do=stylesheet{%
- if in_admin_panel %}&admin=yes{% endif %}" type="text/css" />
- {%- for link, type, title in page_links %}
- <link rel="alternate" type="{{ type|e(true) }}" title="{{ title|e(true) }}" href="{{ link|e(true) }}" />
- {%- endfor %}
- {%- else %}
- <link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
- <link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
- {%- endif %}
- {%- if builder != 'htmlhelp' %}
- <script type="text/javascript">
- var DOCUMENTATION_OPTIONS = {
- URL_ROOT: '{{ pathto("", 1) }}',
- VERSION: '{{ release }}',
- COLLAPSE_MODINDEX: false,
- FILE_SUFFIX: '{{ file_suffix }}'
- };
- </script>
- <script type="text/javascript" src="{{ pathto('_static/jquery.js', 1) }}"></script>
- <script type="text/javascript" src="{{ pathto('_static/interface.js', 1) }}"></script>
- <script type="text/javascript" src="{{ pathto('_static/doctools.js', 1) }}"></script>
- <script type="text/javascript" src="{{ pathto('_static/searchtools.js', 1) }}"></script>
- {%- if use_opensearch %}
- <link rel="search" type="application/opensearchdescription+xml"
- title="Search within {{ docstitle }}"
- href="{{ pathto('_static/opensearch.xml', 1) }}"/>
- {%- endif %}
- {%- if favicon %}
- <link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
- {%- endif %}
- {%- endif %}
-{%- block rellinks %}
- {%- if hasdoc('about') %}
- <link rel="author" title="About these documents" href="{{ pathto('about') }}" />
- {%- endif %}
- <link rel="contents" title="Global table of contents" href="{{ pathto('contents') }}" />
- <link rel="index" title="Global index" href="{{ pathto('genindex') }}" />
- <link rel="search" title="Search" href="{{ pathto('search') }}" />
- {%- if hasdoc('copyright') %}
- <link rel="copyright" title="Copyright" href="{{ pathto('copyright') }}" />
- {%- endif %}
- <link rel="top" title="{{ docstitle }}" href="{{ pathto('index') }}" />
- {%- if parents %}
- <link rel="up" title="{{ parents[-1].title|striptags }}" href="{{ parents[-1].link|e }}" />
- {%- endif %}
- {%- if next %}
- <link rel="next" title="{{ next.title|striptags }}" href="{{ next.link|e }}" />
- {%- endif %}
- {%- if prev %}
- <link rel="prev" title="{{ prev.title|striptags }}" href="{{ prev.link|e }}" />
- {%- endif %}
-{%- endblock %}
-{%- block extrahead %}{% endblock %}
- </head>
- <body>
-
-{% block logilablogo %}
-<div class="logilablogo">
- <a class="logogo" href="http://www.cubicweb.org"><img border="0" src="{{ pathto('_static/cubicweb.png', 1) }}"/></a>
- </div>
-{% endblock %}
-
-{%- block relbar1 %}{{ relbar() }}{% endblock %}
-
-{%- block sidebar1 %}{# possible location for sidebar #}{% endblock %}
-
-{%- block document %}
- <div class="document">
- <div class="documentwrapper">
- {%- if builder != 'htmlhelp' %}
- <div class="bodywrapper">
- {%- endif %}
- <div class="body">
- {% block body %}{% endblock %}
- </div>
- {%- if builder != 'htmlhelp' %}
- </div>
- {%- endif %}
- </div>
-{%- endblock %}
-
-{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
- <div class="clearer"></div>
- </div>
-
-{%- block relbar2 %}{{ relbar() }}{% endblock %}
-
-{%- block footer %}
- <div class="footer">
- {%- if hasdoc('copyright') %}
- © <a href="{{ pathto('copyright') }}">Copyright</a> {{ copyright }}.
- {%- else %}
- © Copyright {{ copyright }}.
- {%- endif %}
- {%- if last_updated %}
- Last updated on {{ last_updated }}.
- {%- endif %}
- {%- if show_sphinx %}
- Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
- {%- endif %}
- </div>
-{%- endblock %}
- </body>
-</html>
--- a/doc/book/en/MERGE_ME-tut-create-app.en.txt Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,386 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-
-Tutoriel : créer votre première application web pour Google AppEngine
-=====================================================================
-
-[TRANSLATE ME TO FRENCH]
-
-This tutorial will guide you step by step to build a blog application
-and discover the unique features of `LAX`. It assumes that you followed
-the :ref:`installation` guidelines and that both the `AppEngine SDK` and the
-`LAX` framework are setup on your computer.
-
-Creating a new application
---------------------------
-
-We choosed in this tutorial to develop a blog as an example of web application
-and will go through each required steps/actions to have it running with `LAX`.
-When you installed `LAX`, you saw a directory named ``skel``. Make a copy of
-this directory and call it ``BlogDemo``.
-
-The location of this directory does not matter. But once decided, make sure your ``PYTHONPATH`` is properly set (:ref:`installation`).
-
-
-Defining a schema
------------------
-
-With `LAX`, the schema/datamodel is the core of the application. This is where
-you will define the type of content you have to hanlde in your application.
-
-Let us start with something simple and improve on it iteratively.
-
-In schema.py, we define two entities: ``Blog`` and ``BlogEntry``.
-
-::
-
- class Blog(EntityType):
- title = String(maxsize=50, required=True)
- description = String()
-
- class BlogEntry(EntityType):
- title = String(maxsize=100, required=True)
- publish_date = Date(default='TODAY')
- text = String(fulltextindexed=True)
- category = String(vocabulary=('important','business'))
- entry_of = SubjectRelation('Blog', cardinality='?*')
-
-A Blog has a title and a description. The title is a string that is
-required by the class EntityType and must be less than 50 characters.
-The description is a string that is not constrained.
-
-A BlogEntry has a title, a publish_date and a text. The title is a
-string that is required and must be less than 100 characters. The
-publish_date is a Date with a default value of TODAY, meaning that
-when a BlogEntry is created, its publish_date will be the current day
-unless it is modified. The text is a string that will be indexed in
-the full-text index and has no constraint.
-
-A BlogEntry also has a relationship ``entry_of`` that link it to a
-Blog. The cardinality ``?*`` means that a BlogEntry can be part of
-zero or one Blog (``?`` means `zero or one`) and that a Blog can
-have any number of BlogEntry (``*`` means `any number including
-zero`). For completeness, remember that ``+`` means `one or more`.
-
-Running the application
------------------------
-
-Defining this simple schema is enough to get us started. Make sure you
-followed the setup steps described in detail in the installation
-chapter (especially visiting http://localhost:8080/_load as an
-administrator), then launch the application with the command::
-
- python dev_appserver.py BlogDemo
-
-and point your browser at http://localhost:8080/ (if it is easier for
-you, use the on-line demo at http://lax.appspot.com/).
-
-.. image:: images/lax-book.00-login.en.png
- :alt: login screen
-
-After you log in, you will see the home page of your application. It
-lists the entity types: Blog and BlogEntry. If these links read
-``blog_plural`` and ``blogentry_plural`` it is because
-internationalization (i18n) is not working for you yet. Please ignore
-this for now.
-
-.. image:: images/lax-book.01-start.en.png
- :alt: home page
-
-Creating system entities
-------------------------
-You can only create new users if you decided not to use google authentication.
-
-
-[WRITE ME : create users manages permissions etc]
-
-
-
-Creating application entites
-----------------------------
-
-Create a Blog
-~~~~~~~~~~~~~
-
-Let us create a few of these entities. Click on the [+] at the right
-of the link Blog. Call this new Blog ``Tech-blog`` and type in
-``everything about technology`` as the description, then validate the
-form by clicking on ``Validate``.
-
-.. image:: images/lax-book.02-create-blog.en.png
- :alt: from to create blog
-
-Click on the logo at top left to get back to the home page, then
-follow the Blog link that will list for you all the existing Blog.
-You should be seeing a list with a single item ``Tech-blog`` you
-just created.
-
-.. image:: images/lax-book.03-list-one-blog.en.png
- :alt: displaying a list of a single blog
-
-Clicking on this item will get you to its detailed description except
-that in this case, there is not much to display besides the name and
-the phrase ``everything about technology``.
-
-.. image:: images/lax-book.04-detail-one-blog.en.png
- :alt: displaying the detailed view of a blog
-
-Now get back to the home page by clicking on the top-left logo, then
-create a new Blog called ``MyLife`` and get back to the home page
-again to follow the Blog link for the second time. The list now
-has two items.
-
-.. image:: images/lax-book.05-list-two-blog.en.png
- :alt: displaying a list of two blogs
-
-
-Create a BlogEntry
-~~~~~~~~~~~~~~~~~~
-
-Get back to the home page and click on [+] at the right of the link
-BlogEntry. Call this new entry ``Hello World`` and type in some text
-before clicking on ``Validate``. You added a new blog entry without
-saying to what blog it belongs. There is a box on the left entitled
-``actions``, click on the menu item ``modify``. You are back to the form
-to edit the blog entry you just created, except that the form now has
-another section with a combobox titled ``add relation``. Chose
-``entry_of`` in this menu and a second combobox appears where you pick
-``MyLife``.
-
-You could also have, at the time you started to fill the form for a
-new entity BlogEntry, hit ``Apply`` instead of ``Validate`` and the
-combobox titled ``add relation`` would have showed up.
-
-.. image:: images/lax-book.06-add-relation-entryof.en.png
- :alt: editing a blog entry to add a relation to a blog
-
-Validate the changes by clicking ``Validate``. The entity BlogEntry
-that is displayed now includes a link to the entity Blog named
-``MyLife``.
-
-.. image:: images/lax-book.07-detail-one-blogentry.en.png
- :alt: displaying the detailed view of a blogentry
-
-Remember that all of this was handled by the framework and that the
-only input that was provided so far is the schema. To get a graphical
-view of the schema, run the ``laxctl genschema BlogDemo`` command as
-explained in the installation section and point your browser to the
-URL http://localhost:8080/schema
-
-.. image:: images/lax-book.08-schema.en.png
- :alt: graphical view of the schema (aka data-model)
-
-Site configuration
-------------------
-
-.. image:: images/lax-book.03-site-config-panel.en.png
-
-This panel allows you to configure the appearance of your application site.
-Six menus are available and we will go through each of them to explain how
-to use them.
-
-Navigation
-~~~~~~~~~~
-This menu provides you a way to adjust some navigation options depending on
-your needs, such as the number of entities to display by page of results.
-Follows the detailled list of available options:
-
-* navigation.combobox-limit: maximum number of entities to display in related
- combo box (sample format: 23)
-* navigation.page-size: maximum number of objects displayed by page of results
- (sample format: 23)
-* navigation.related-limit: maximum number of related entities to display in
- the primary view (sample format: 23)
-* navigation.short-line-size: maximum number of characters in short description
- (sample format: 23)
-
-UI
-~~
-This menu provides you a way to customize the user interface settings such as
-date format or encoding in the produced html.
-Follows the detailled list of available options:
-
-* ui.date-format : how to format date in the ui ("man strftime" for format description)
-* ui.datetime-format : how to format date and time in the ui ("man strftime" for format
- description)
-* ui.default-text-format : default text format for rich text fields.
-* ui.encoding : user interface encoding
-* ui.fckeditor : should html fields being edited using fckeditor (a HTML WYSIWYG editor).
- You should also select text/html as default text format to actually get fckeditor.
-* ui.float-format : how to format float numbers in the ui
-* ui.language : language of the user interface
-* ui.main-template : id of main template used to render pages
-* ui.site-title : site title, which is displayed right next to the logo in the header
-* ui.time-format : how to format time in the ui ("man strftime" for format description)
-
-
-Actions
-~~~~~~~
-This menu provides a way to configure the context in which you expect the actions
-to be displayed to the user and if you want the action to be visible or not.
-You must have notice that when you view a list of entities, an action box is
-available on the left column which display some actions as well as a drop-down
-menu for more actions.
-
-The context available are:
-
-* mainactions : actions listed in the left box
-* moreactions : actions listed in the `more` menu of the left box
-* addrelated : add actions listed in the left box
-* useractions : actions listed in the first section of drop-down menu
- accessible from the right corner user login link
-* siteactions : actions listed in the second section of drop-down menu
- accessible from the right corner user login link
-* hidden : select this to hide the specific action
-
-Boxes
-~~~~~
-The application has already a pre-defined set of boxes you can use right away.
-This configuration section allows you to place those boxes where you want in the
-application interface to customize it.
-
-The available boxes are:
-
-* actions box : box listing the applicable actions on the displayed data
-
-* boxes_blog_archives_box : box listing the blog archives
-
-* possible views box : box listing the possible views for the displayed data
-
-* rss box : RSS icon to get displayed data as a RSS thread
-
-* search box : search box
-
-* startup views box : box listing the configuration options available for
- the application site, such as `Preferences` and `Site Configuration`
-
-Components
-~~~~~~~~~~
-[WRITE ME]
-
-Contextual components
-~~~~~~~~~~~~~~~~~~~~~
-[WRITE ME]
-
-Set-up a workflow
------------------
-
-Before starting, make sure you refresh your mind by reading [link to
-definition_workflow chapter].
-
-We want to create a workflow to control the quality of the BlogEntry
-submitted on your application. When a BlogEntry is created by a user
-its state should be `submitted`. To be visible to all, it needs to
-be in the state `published`. To move from `submitted` to `published`
-we need a transition that we can name `approve_blogentry`.
-
-We do not want every user to be allowed to change the state of a
-BlogEntry. We need to define a group of user, `moderators`, and
-this group will have appropriate permissions to approve BlogEntry
-to be published and visible to all.
-
-There are two ways to create a workflow, form the user interface,
-and also by defining it in ``migration/postcreate.py``. This script
-is executed each time a new ``./bin/laxctl db-init`` is done.
-If you create the states and transitions through the user interface
-this means that next time you will need to initialize the database
-you will have to re-create all the entities.
-We strongly recommand you create the workflow in ``migration\postcreate.py``
-and we will now show you how.
-The user interface would only be a reference for you to view the states
-and transitions but is not the appropriate interface to define your
-application workflow.
-
-Update the schema
-~~~~~~~~~~~~~~~~~
-To enable a BlogEntry to have a State, we have to define a relation
-``in_state`` in the schema of BlogEntry. Please do as follows, add
-the line ``in_state (...)``::
-
- class BlogEntry(EntityType):
- title = String(maxsize=100, required=True)
- publish_date = Date(default='TODAY')
- text_format = String(meta=True, internationalizable=True, maxsize=50,
- default='text/rest', constraints=[format_constraint])
- text = String(fulltextindexed=True)
- category = String(vocabulary=('important','business'))
- entry_of = SubjectRelation('Blog', cardinality='?*')
- in_state = SubjectRelation('State', cardinality='1*')
-
-As you updated the schema, you will have re-execute ``./bin/laxctl db-init``
-to initialize the database and migrate your existing entities.
-[WRITE ABOUT MIGRATION]
-
-Create states, transitions and group permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-At the time the ``postcreate.py`` script is executed, several methods
-can be used. They are all defined in the ``class ServerMigrationHelper``.
-We will only discuss the method we use to create a wrokflow here.
-
-To define our workflow for BlogDemo, please add the following lines
-to ``migration/postcreate.py``::
-
- _ = unicode
-
- moderators = add_entity('CWGroup', name=u"moderators")
-
- submitted = add_state(_('submitted'), 'BlogEntry', initial=True)
- published = add_state(_('published'), 'BlogEntry')
-
- add_transition(_('approve_blogentry'), 'BlogEntry', (submitted,), published, ('moderators', 'managers'),)
-
- checkpoint()
-
-``add_entity`` is used here to define the new group of users that we
-need to define the transitions, `moderators`.
-If this group required by the transition is not defined before the
-transition is created, it will not create the relation `transition
-require the group moderator`.
-
-``add_state`` expects as the first argument the name of the state you are
-willing to create, then the entity type on which the state can be applied,
-and an optionnal argument to set if the state is the initial state
-of the entity type or not.
-
-``add_transition`` expects as the first argument the name of the
-transition, then the entity type on which we can apply the transition,
-then the list of possible initial states from which the transition
-can be applied, the target state of the transition, and the permissions
-(e.g. list of the groups of users who can apply the transition).
-
-.. image:: images/lax-book.03-transitions-view.en.png
-
-You can now notice that in the actions box of a BlogEntry, the state
-is now listed as well as the possible transitions from this state
-defined by the workflow. This transition, as defined in the workflow,
-will only being displayed for the users belonging to the group
-moderators of managers.
-
-Change view permission
-~~~~~~~~~~~~~~~~~~~~~~
-
-
-
-Conclusion
-----------
-
-Exercise
-~~~~~~~~
-
-Create new blog entries in ``Tech-blog``.
-
-What we learned
-~~~~~~~~~~~~~~~
-
-Creating a simple schema was enough to set up a new application that
-can store blogs and blog entries.
-
-What is next ?
-~~~~~~~~~~~~~~
-
-Although the application is fully functionnal, its look is very
-basic. In the following section we will learn to create views to
-customize how data is displayed.
-
-
--- a/doc/book/en/MERGE_ME-tut-create-gae-app.en.txt Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _tutorielGAE:
-
-Tutoriel : créer votre première application web pour Google AppEngine
-=====================================================================
-
-Ce tutoriel va vous guider pas à pas a construire une apllication web
-de gestion de Blog afin de vous faire découvrir les fonctionnalités de
-*CubicWeb*.
-
-Nous supposons que vous avec déjà suivi le guide :ref:`installationGAE`.
-
-
-Créez une nouvelle application
-------------------------------
-
-Nous choisissons dans ce tutoriel de développer un blog comme un exemple
-d'application web et nous allons expliciter toutes les étapes nécessaires
-à sa réalisation.
-
-::
-
- cubicweb-ctl newgapp blogdemo
-
-`newgapp` est la commande permettant de créer une instance *CubicWeb* pour
-le datastore.
-
-Assurez-vous que votre variable d'environnement ``PYTHONPATH`` est correctement
-initialisée (:ref:`installationGAE`)
-
-Définissez un schéma
---------------------
-
-Le modèle de données ou schéma est au coeur d'une application *CubicWeb*.
-C'est là où vous allez devoir définir le type de contenu que votre application
-devra gérer.
-
-Commençons par un schéma simple que nous améliorerons progressivemment.
-
-Une fois votre instance ``blogdemo`` crée, vous trouverez un fichier ``schema.py``
-contenant la définition des entités suivantes : ``Blog`` and ``BlogEntry``.
-
-::
-
- class Blog(EntityType):
- title = String(maxsize=50, required=True)
- description = String()
-
- class BlogEntry(EntityType):
- title = String(maxsize=100, required=True)
- publish_date = Date(default='TODAY')
- text = String(fulltextindexed=True)
- category = String(vocabulary=('important','business'))
- entry_of = SubjectRelation('Blog', cardinality='?*')
-
-
-Un ``Blog`` a un titre et une description. Le titre est une chaîne
-de caractères requise par la classe parente EntityType and ne doit
-pas excéder 50 caractères. La description est une chaîne de
-caractères sans contraintes.
-
-Une ``BlogEntry`` a un titre, une date de publication et du texte
-étant son contenu. Le titre est une chaîne de caractères qui ne
-doit pas excéder 100 caractères. La date de publication est de type Date et a
-pour valeur par défaut TODAY, ce qui signifie que lorsqu'une
-``BlogEntry`` sera créée, sa date de publication sera la date
-courante a moins de modifier ce champ. Le texte est une chaîne de
-caractères qui sera indexée en plein texte et sans contraintes.
-
-Une ``BlogEntry`` a aussi une relation nommée ``entry_of`` qui la
-relie à un ``Blog``. La cardinalité ``?*`` signifie que BlogEntry
-peut faire partie de zero a un Blog (``?`` signifie `zero ou un`) et
-qu'un Blog peut avoir une infinité de BlogEntry (``*`` signifie
-`n'importe quel nombre incluant zero`).
-Par soucis de complétude, nous rappellerons que ``+`` signifie
-`un ou plus`.
-
-Lancez l'application
---------------------
-
-Définir ce simple schéma est suffisant pour commencer. Assurez-vous
-que vous avez suivi les étapes décrites dans la section installation
-(en particulier visitez http://localhost:8080/_load en tant qu'administrateur
-afin d'initialiser le datastore), puis lancez votre application avec la commande ::
-
- python dev_appserver.py BlogDemo
-
-puis dirigez vous vers http://localhost:8080/ (ou si c'est plus facile
-vous pouvez utiliser la démo en ligne http://lax.appspot.com/).
-[FIXME] -- changer la demo en ligne en quelque chose qui marche (!)
-
-.. image:: images/lax-book.00-login.en.png
- :alt: login screen
-
-Après vous être authentifié, vous arrivez sur la page d'accueil de votre
-application. Cette page liste les types d'entités accessibles dans votre
-application, en l'occurrence : Blog et Articles. Si vous lisez ``blog_plural``
-et ``blogentry_plural`` cela signifie que l'internationalisation (i18n)
-n'a pas encore fonctionné. Ignorez cela pour le moment.
-
-.. image:: images/lax-book.01-start.en.png
- :alt: home page
-
-Créez des entités système
--------------------------
-
-Vous ne pourrez créer de nouveaux utilisateurs que dans le cas où vous
-avez choisi de ne pas utiliser l'authentification Google.
-
-
-[WRITE ME : create users manages permissions etc]
-
-
-
-Créez des entités applicatives
-------------------------------
-
-Créez un Blog
-~~~~~~~~~~~~~
-
-Créons à présent quelques entités. Cliquez sur `[+]` sur la
-droite du lien Blog. Appelez cette nouvelle entité Blog ``Tech-Blog``
-et tapez pour la description ``everything about technology``,
-puis validez le formulaire d'édition en cliquant sur le bouton
-``Validate``.
-
-
-.. image:: images/lax-book.02-create-blog.en.png
- :alt: from to create blog
-
-En cliquant sur le logo situé dans le coin gauche de la fenêtre,
-vous allez être redirigé vers la page d'accueil. Ensuite, si vous allez
-sur le lien Blog, vous devriez voir la liste des entités Blog, en particulier
-celui que vous venez juste de créer ``Tech-Blog``.
-
-.. image:: images/lax-book.03-list-one-blog.en.png
- :alt: displaying a list of a single blog
-
-Si vous cliquez sur ``Tech-Blog`` vous devriez obtenir une description
-détaillée, ce qui dans notre cas, n'est rien de plus que le titre
-et la phrase ``everything about technology``
-
-
-.. image:: images/lax-book.04-detail-one-blog.en.png
- :alt: displaying the detailed view of a blog
-
-Maintenant retournons sur la page d'accueil et créons un nouveau
-Blog ``MyLife`` et retournons sur la page d'accueil, puis suivons
-le lien Blog et nous constatons qu'à présent deux blogs sont listés.
-
-.. image:: images/lax-book.05-list-two-blog.en.png
- :alt: displaying a list of two blogs
-
-Créons un article
-~~~~~~~~~~~~~~~~~
-
-Revenons sur la page d'accueil et cliquons sur `[+]` Ã droite du lien
-`articles`. Appellons cette nouvelle entité ``Hello World`` et introduisons
-un peut de texte avant de ``Valider``. Vous venez d'ajouter un article
-sans avoir précisé à quel Blog il appartenait. Dans la colonne de gauche
-se trouve une boite intitulé ``actions``, cliquez sur le menu ``modifier``.
-Vous êtes de retour sur le formulaire d'édition de l'article que vous
-venez de créer, à ceci près que ce formulaire a maintenant une nouvelle
-section intitulée ``ajouter relation``. Choisissez ``entry_of`` dans ce menu,
-cela va faire apparaitre une deuxième menu déroulant dans lequel vous
-allez pouvoir séléctionner le Blog ``MyLife``.
-
-Vous auriez pu aussi, au moment où vous avez crée votre article, sélectionner
-``appliquer`` au lieu de ``valider`` et le menu ``ajouter relation`` serait apparu.
-
-.. image:: images/lax-book.06-add-relation-entryof.en.png
- :alt: editing a blog entry to add a relation to a blog
-
-Validez vos modifications en cliquant sur ``Valider``. L'entité article
-qui est listée contient maintenant un lien vers le Blog auquel il
-appartient, ``MyLife``.
-
-.. image:: images/lax-book.07-detail-one-blogentry.en.png
- :alt: displaying the detailed view of a blogentry
-
-Rappelez-vous que pour le moment, tout a été géré par la plate-forme
-*CubicWeb* et que la seule chose qui a été fournie est le schéma de
-données. D'ailleurs pour obtenir une vue graphique du schéma, exécutez
-la commande ``laxctl genschema blogdemo`` et vous pourrez visualiser
-votre schéma a l'URL suivante : http://localhost:8080/schema
-
-.. image:: images/lax-book.08-schema.en.png
- :alt: graphical view of the schema (aka data-model)
-
-
-Change view permission
-~~~~~~~~~~~~~~~~~~~~~~
-
-
-
-Conclusion
-----------
-
-Exercise
-~~~~~~~~
-
-Create new blog entries in ``Tech-blog``.
-
-What we learned
-~~~~~~~~~~~~~~~
-
-Creating a simple schema was enough to set up a new application that
-can store blogs and blog entries.
-
-What is next ?
-~~~~~~~~~~~~~~
-
-Although the application is fully functionnal, its look is very
-basic. In the following section we will learn to create views to
-customize how data is displayed.
-
-
--- a/doc/book/en/_themes/cubicweb/layout.html Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-{% extends "basic/layout.html" %}
-
-{%- block extrahead %}
-<!--[if lte IE 6]>
-<link rel="stylesheet" href="{{ pathto('_static/ie6.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
-<![endif]-->
-{%- if theme_favicon %}
-<link rel="shortcut icon" href="{{ pathto('_static/'+theme_favicon, 1) }}"/>
-{%- endif %}
-
-{%- if theme_canonical_url %}
-<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
-{%- endif %}
-{% endblock %}
-
-{% block header %}
-
-{% if theme_in_progress|tobool %}
- <img style="position: fixed; display: block; width: 165px; height: 165px; bottom: 60px; right: 0; border: 0;" src="{{ pathto('_static/in_progress.png', 1) }}" alt="Documentation in progress" />
-{% endif %}
-
-{% if theme_outdated|tobool %}
- <div style="bottom: 60px; right: 20px;position: fixed;"><a href="{{ latest_url }}" class="btn btn-large btn-danger"><strong>></strong> Read the latest version of this page</a></div>
-{% endif %}
-
-<div class="header-small">
- {%- if theme_logo %}
- {% set img, ext = theme_logo.split('.', -1) %}
- <div class="logo-small">
- <a href="{{ pathto(master_doc) }}">
- <img class="logo" src="{{ pathto('_static/%s-small.%s' % (img, ext), 1)}}" alt="Logo"/>
- </a>
- </div>
- {%- endif %}
-</div>
-{% endblock %}
-
-{%- macro relbar() %}
-<div class="related">
- <h3>{{ _('Navigation') }}</h3>
- <ul>
- {%- for rellink in rellinks %}
- <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
- <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags|e }}"
- {{ accesskey(rellink[2]) }}>{{ rellink[3] }}</a>
- {%- if not loop.first %}{{ reldelim2 }}{% endif %}
- </li>
- {%- endfor %}
- {%- block rootrellink %}
- <li><a href="{{ pathto(master_doc) }}">{{ docstitle|e }}</a>{{ reldelim1 }}</li>
- {%- endblock %}
- {%- for parent in parents %}
- <li><a href="{{ parent.link|e }}" {% if loop.last %}{{ accesskey("U") }}{% endif %}>{{ parent.title }}</a>{{ reldelim1 }}</li>
- {%- endfor %}
- {%- block relbaritems %} {% endblock %}
- </ul>
-</div>
-{%- endmacro %}
-
-{%- block sidebarlogo %}{%- endblock %}
-{%- block sidebarsourcelink %}{%- endblock %}
--- a/doc/book/en/_themes/cubicweb/static/cubicweb.css_t Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-/*
- * cubicweb.css_t
- * ~~~~~~~~~~~~~~
- *
- * Sphinx stylesheet -- cubicweb theme.
- *
- * :copyright: Copyright 2014 by the Cubicweb team, see AUTHORS.
- * :license: LGPL, see LICENSE for details.
- *
- */
-
-@import url("pyramid.css");
-
-div.header-small {
- background-image: linear-gradient(white, #e2e2e2);
- border-bottom: 1px solid #bbb;
-}
-
-div.logo-small {
- padding: 10px;
-}
-
-img.logo {
- width: 150px;
-}
-
-div.related a {
- color: #e6820e;
-}
-
-a, a .pre {
- color: #e6820e;
-}
--- a/doc/book/en/_themes/cubicweb/static/cubicweb.ico Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-../../../../../../web/data/favicon.ico
\ No newline at end of file
--- a/doc/book/en/_themes/cubicweb/static/logo-cubicweb-small.svg Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-logo-cubicweb.svg
\ No newline at end of file
--- a/doc/book/en/_themes/cubicweb/static/logo-cubicweb.svg Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-../../../../../../web/data/logo-cubicweb.svg
\ No newline at end of file
--- a/doc/book/en/_themes/cubicweb/theme.conf Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-[theme]
-inherit = pyramid
-pygments_style = sphinx.pygments_styles.PyramidStyle
-stylesheet = cubicweb.css
-
-
-[options]
-logo = logo-cubicweb.svg
-favicon = cubicweb.ico
-in_progress = false
-outdated = false
-canonical_url =
--- a/doc/book/en/additionnal_services/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-Additional services
-===================
-
-In this chapter, we introduce services crossing the *web -
-repository - administration* organisation of the first parts of the
-CubicWeb book. Those services can be either proper services (like the
-undo functionality) or mere *topical cross-sections* across CubicWeb.
-
-.. toctree::
- :maxdepth: 2
-
- undo
-
-
--- a/doc/book/en/additionnal_services/undo.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,337 +0,0 @@
-Undoing changes in CubicWeb
----------------------------
-
-Many desktop applications offer the possibility for the user to
-undo its last changes : this *undo feature* has now been
-integrated into the CubicWeb framework. This document will
-introduce you to the *undo feature* both from the end-user and the
-application developer point of view.
-
-But because a semantic web application and a common desktop
-application are not the same thing at all, especially as far as
-undoing is concerned, we will first introduce *what* is the *undo
-feature* for now.
-
-What's *undoing* in a CubicWeb application
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-What is an *undo feature* is quite intuitive in the context of a
-desktop application. But it is a bit subtler in the context of a
-Semantic Web application. This section introduces some of the main
-differences between a classical desktop and a Semantic Web
-applications to keep in mind in order to state precisely *what we
-want*.
-
-The notion transactions
-```````````````````````
-
-A CubicWeb application acts upon an *Entity-Relationship* model,
-described by a schema. This allows to ensure some data integrity
-properties. It also implies that changes are made by all-or-none
-groups called *transactions*, such that the data integrity is
-preserved whether the transaction is completely applied *or* none
-of it is applied.
-
-A transaction can thus include more actions than just those
-directly required by the main purpose of the user. For example,
-when a user *just* writes a new blog entry, the underlying
-*transaction* holds several *actions* as illustrated below :
-
-* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
-
- #. Created Blog entry : Torototo
- #. Added relation : Torototo owned by admin
- #. Added relation : Torototo blog entry of Undo Blog
- #. Added relation : Torototo in state draft (draft)
- #. Added relation : Torototo created by admin
-
-Because of the very nature (all-or-none) of the transactions, the
-"undoable stuff" are the transactions and not the actions !
-
-Public and private actions within a transaction
-```````````````````````````````````````````````
-
-Actually, within the *transaction* "Created Blog entry :
-Torototo", two of those *actions* are said to be *public* and
-the others are said to be *private*. *Public* here means that the
-public actions (1 and 3) were directly requested by the end user ;
-whereas *private* means that the other actions (2, 4, 5) were
-triggered "under the hood" to fulfill various requirements for the
-user operation (ensuring integrity, security, ... ).
-
-And because quite a lot of actions can be triggered by a "simple"
-end-user request, most of which the end-user is not (and does not
-need or wish to be) aware, only the so-called public actions will
-appear [1]_ in the description of the an undoable transaction.
-
-* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
-
- #. Created Blog entry : Torototo
- #. Added relation : Torototo blog entry of Undo Blog
-
-But note that both public and private actions will be undone
-together when the transaction is undone.
-
-(In)dependent transactions : the simple case
-````````````````````````````````````````````
-
-A CubicWeb application can be used *simultaneously* by different users
-(whereas a single user works on an given office document at a
-given time), so that there is not always a single history
-time-line in the CubicWeb case. Moreover CubicWeb provides
-security through the mechanism of *permissions* granted to each
-user. This can lead to some transactions *not* being undoable in
-some contexts.
-
-In the simple case two (unprivileged) users Alice and Bob make
-relatively independent changes : then both Alice and Bob can undo
-their changes. But in some case there is a clean dependency
-between Alice's and Bob's actions or between actions of one of
-them. For example let's suppose that :
-
-- Alice has created a blog,
-- then has published a first post inside,
-- then Bob has published a second post in the same blog,
-- and finally Alice has updated its post contents.
-
-Then it is clear that Alice can undo her contents changes and Bob
-can undo his post creation independently. But Alice can not undo
-her post creation while she has not first undone her changes.
-It is also clear that Bob should *not* have the
-permissions to undo any of Alice's transactions.
-
-
-More complex dependencies between transactions
-``````````````````````````````````````````````
-
-But more surprising things can quickly happen. Going back to the
-previous example, Alice *can* undo the creation of the blog after
-Bob has published its post in it ! But this is possible only
-because the schema does not *require* for a post to be in a
-blog. Would the *blog entry of* relation have been mandatory, then
-Alice could not have undone the blog creation because it would
-have broken integrity constraint for Bob's post.
-
-When a user attempts to undo a transaction the system will check
-whether a later transaction has explicit dependency on the
-would-be-undone transaction. In this case the system will not even
-attempt the undo operation and inform the user.
-
-If no such dependency is detected the system will attempt the undo
-operation but it can fail, typically because of integrity
-constraint violations. In such a case the undo operation is
-completely [3]_ rollbacked.
-
-
-The *undo feature* for CubicWeb end-users
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The exposition of the undo feature to the end-user through a Web
-interface is still quite basic and will be improved toward a
-greater usability. But it is already fully functional. For now
-there are two ways to access the *undo feature* as long as the it
-has been activated in the instance configuration file with the
-option *undo-support=yes*.
-
-Immediately after having done the change to be canceled through
-the **undo** link in the message. This allows to undo an
-hastily action immediately. For example, just after having
-validated the creation of the blog entry *A second blog entry* we
-get the following message, allowing to undo the creation.
-
-.. image:: /images/undo_mesage_w600.png
- :width: 600px
- :alt: Screenshot of the undo link in the message
- :align: center
-
-At any time we can access the **undo-history view** accessible from the
-start-up page.
-
-.. image:: /images/undo_startup-link_w600.png
- :width: 600px
- :alt: Screenshot of the startup menu with access to the history view
- :align: center
-
-This view will provide inspection of the transaction and their (public)
-actions. Each transaction provides its own **undo** link. Only the
-transactions the user has permissions to see and undo will be shown.
-
-.. image:: /images/undo_history-view_w600.png
- :width: 600px
- :alt: Screenshot of the undo history main view
- :align: center
-
-If the user attempts to undo a transaction which can't be undone or
-whose undoing fails, then a message will explain the situation and
-no partial undoing will be left behind.
-
-This is all for the end-user side of the undo mechanism : this is
-quite simple indeed ! Now, in the following section, we are going
-to introduce the developer side of the undo mechanism.
-
-The *undo feature* for CubicWeb application developers
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A word of warning : this section is intended for developers,
-already having some knowledge of what's under CubicWeb's hood. If
-it is not *yet* the case, please refer to CubicWeb documentation
-http://docs.cubicweb.org/ .
-
-Overview
-````````
-
-The core of the undo mechanisms is at work in the *native source*,
-beyond the RQL. This does mean that *transactions* and *actions*
-are *no entities*. Instead they are represented at the SQL level
-and exposed through the *DB-API* supported by the repository
-*Connection* objects.
-
-Once the *undo feature* has been activated in the instance
-configuration file with the option *undo-support=yes*, each
-mutating operation (cf. [2]_) will be recorded in some special SQL
-table along with its associated transaction. Transaction are
-identified by a *txuuid* through which the functions of the
-*DB-API* handle them.
-
-On the web side the last commited transaction *txuuid* is
-remembered in the request's data to allow for imediate undoing
-whereas the *undo-history view* relies upon the *DB-API* to list
-the accessible transactions. The actual undoing is performed by
-the *UndoController* accessible at URL of the form
-`www.my.host/my/instance/undo?txuuid=...`
-
-The repository side
-```````````````````
-
-Please refer to the file `cubicweb/server/sources/native.py` and
-`cubicweb/transaction.py` for the details.
-
-The undoing information is mainly stored in three SQL tables:
-
-`transactions`
- Stores the txuuid, the user eid and the date-and-time of
- the transaction. This table is referenced by the two others.
-
-`tx_entity_actions`
- Stores the undo information for actions on entities.
-
-`tx_relation_actions`
- Stores the undo information for the actions on relations.
-
-When the undo support is activated, entries are added to those
-tables for each mutating operation on the data repository, and are
-deleted on each transaction undoing.
-
-Those table are accessible through the following methods of the
-repository `Connection` object :
-
-`undoable_transactions`
- Returns a list of `Transaction` objects accessible to the user
- and according to the specified filter(s) if any.
-
-`tx_info`
- Returns a `Transaction` object from a `txuuid`
-
-`undo_transaction`
- Returns the list of `Action` object for the given `txuuid`.
-
- NB: By default it only return *public* actions.
-
-The web side
-````````````
-
-The exposure of the *undo feature* to the end-user through the Web
-interface relies on the *DB-API* introduced above. This implies
-that the *transactions* and *actions* are not *entities* linked by
-*relations* on which the usual views can be applied directly.
-
-That's why the file `cubicweb/web/views/undohistory.py` defines
-some dedicated views to access the undo information :
-
-`UndoHistoryView`
- This is a *StartupView*, the one accessible from the home
- page of the instance which list all transactions.
-
-`UndoableTransactionView`
- This view handles the display of a single `Transaction` object.
-
-`UndoableActionBaseView`
- This (abstract) base class provides private methods to build
- the display of actions whatever their nature.
-
-`Undoable[Add|Remove|Create|Delete|Update]ActionView`
- Those views all inherit from `UndoableActionBaseView` and
- each handles a specific kind of action.
-
-`UndoableActionPredicate`
- This predicate is used as a *selector* to pick the appropriate
- view for actions.
-
-Apart from this main *undo-history view* a `txuuid` is stored in
-the request's data `last_undoable_transaction` in order to allow
-immediate undoing of a hastily validated operation. This is
-handled in `cubicweb/web/application.py` in the `main_publish` and
-`add_undo_link_to_msg` methods for the storing and displaying
-respectively.
-
-Once the undo information is accessible, typically through a
-`txuuid` in an *undo* URL, the actual undo operation can be
-performed by the `UndoController` defined in
-`cubicweb/web/views/basecontrollers.py`. This controller basically
-extracts the `txuuid` and performs a call to `undo_transaction` and
-in case of an undo-specific error, lets the top level publisher
-handle it as a validation error.
-
-
-Conclusion
-~~~~~~~~~~
-
-The undo mechanism relies upon a low level recording of the
-mutating operation on the repository. Those records are accessible
-through some method added to the *DB-API* and exposed to the
-end-user either through a whole history view of through an
-immediate undoing link in the message box.
-
-The undo feature is functional but the interface and configuration
-options are still quite reduced. One major improvement would be to
-be able to filter with a finer grain which transactions or actions
-one wants to see in the *undo-history view*. Another critical
-improvement would be to enable the undo feature on a part only of
-the entity-relationship schema to avoid storing too much useless
-data and reduce the underlying overhead.
-
-But both functionality are related to the strong design choice not
-to represent transactions and actions as entities and
-relations. This has huge benefits in terms of safety and conceptual
-simplicity but prevents from using lots of convenient CubicWeb
-features such as *facets* to access undo information.
-
-Before developing further the undo feature or eventually revising
-this design choice, it appears that some return of experience is
-strongly needed. So don't hesitate to try the undo feature in your
-application and send us some feedback.
-
-
-Notes
-~~~~~
-
-.. [1] The end-user Web interface could be improved to enable
- user to choose whether he wishes to see private actions.
-
-.. [2] There is only five kind of elementary actions (beyond
- merely accessing data for reading):
-
- * **C** : creating an entity
- * **D** : deleting an entity
- * **U** : updating an entity attributes
- * **A** : adding a relation
- * **R** : removing a relation
-
-.. [3] Meaning none of the actions in the transaction is
- undone. Depending upon the application, it might make sense
- to enable *partial* undo. That is to say undo in which some
- actions could not be undo without preventing to undo the
- others actions in the transaction (as long as it does not
- break schema integrity). This is not forbidden by the
- back-end but is deliberately not supported by the front-end
- (for now at least).
--- a/doc/book/en/admin/additional-tips.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-
-.. _Additional Tips:
-
-Backups (mostly with postgresql)
---------------------------------
-
-It is always a good idea to backup. If your system does not do that,
-you should set it up. Note that whenever you do an upgrade,
-`cubicweb-ctl` offers you to backup your database. There are a number
-of ways for doing backups.
-
-Using postgresql (and only that)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Before you
-go ahead, make sure the following permissions are correct ::
-
- # chgrp postgres /var/lib/cubicweb/backup
- # chmod g+ws /var/lib/cubicweb/backup
- # chgrp postgres /etc/cubicweb.d/*<instance>*/sources
- # chmod g+r /etc/cubicweb.d/*<instance>*/sources
-
-Simply use the pg_dump in a cron installed for `postgres` user on the database server::
-
- # m h dom mon dow command
- 0 2 * * * pg_dump -Fc --username=cubicweb --no-owner <instance> > /var/backups/<instance>-$(date '+%Y-%m-%d_%H:%M:%S').dump
-
-Using :command:`cubicweb-ctl db-dump`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The CubicWeb way is to use the :command:`db-dump` command. For that,
-you have to put your passwords in a user-only-readable file at the
-home directory of root user. The file is `.pgpass` (`chmod 0600`), in
-this case for a socket run connection to PostgreSQL ::
-
- /var/run/postgresql:5432:<instance>:<database user>:<database password>
-
-The postgres documentation for the `.pgpass` format can be found `here`_
-
-Then add the following command to the crontab of the user (`crontab -e`)::
-
- # m h dom mon dow command
- 0 2 * * * cubicweb-ctl db-dump <instance>
-
-
-Backup ninja
-~~~~~~~~~~~~
-
-You can use a combination `backup-ninja`_ (which has a postgres script in the
-example directory), `backuppc`)_ (for versionning).
-
-Please note that in the *CubicWeb way* it adds a second location for your
-password which is error-prone.
-
-.. _`here` : http://www.postgresql.org/docs/current/static/libpq-pgpass.html
-.. _`backup-ninja` : https://labs.riseup.net/code/projects/show/backupninja/
-.. _`backuppc` : http://backuppc.sourceforge.net/
-
-.. warning::
-
- Remember that these indications will fail you whenever you use
- another database backend than postgres. Also it does properly handle
- externally managed data such as files (using the Bytes File System
- Storage).
--- a/doc/book/en/admin/config.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,255 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _ConfigEnv:
-
-Set-up of a *CubicWeb* environment
-==================================
-
-You can `configure the database`_ system of your choice:
-
- - `PostgreSQL configuration`_
- - `MySql configuration`_
- - `SQLServer configuration`_
- - `SQLite configuration`_
-
-For advanced features, have a look to:
-
- - `Pyro configuration`_
- - `Cubicweb resources configuration`_
-
-.. _`configure the database`: DatabaseInstallation_
-.. _`PostgreSQL configuration`: PostgresqlConfiguration_
-.. _`MySql configuration`: MySqlConfiguration_
-.. _`SQLServer configuration`: SQLServerConfiguration_
-.. _`SQLite configuration`: SQLiteConfiguration_
-.. _`Pyro configuration`: PyroConfiguration_
-.. _`Cubicweb resources configuration`: RessourcesConfiguration_
-
-
-
-.. _RessourcesConfiguration:
-
-Cubicweb resources configuration
---------------------------------
-
-.. autodocstring:: cubicweb.cwconfig
-
-
-.. _DatabaseInstallation:
-
-Databases configuration
------------------------
-
-Each instance can be configured with its own database connection information,
-that will be stored in the instance's :file:`sources` file. The database to use
-will be chosen when creating the instance. CubicWeb is known to run with
-Postgresql (recommended), MySQL, SQLServer and SQLite.
-
-Other possible sources of data include CubicWeb, Subversion, LDAP and Mercurial,
-but at least one relational database is required for CubicWeb to work. You do
-not need to install a backend that you do not intend to use for one of your
-instances. SQLite is not fit for production use, but it works well for testing
-and ships with Python, which saves installation time when you want to get
-started quickly.
-
-.. _PostgresqlConfiguration:
-
-PostgreSQL
-~~~~~~~~~~
-
-Many Linux distributions ship with the appropriate PostgreSQL packages.
-Basically, you need to install the following packages:
-
-* `postgresql` and `postgresql-client`, which will pull the respective
- versioned packages (e.g. `postgresql-9.1` and `postgresql-client-9.1`) and,
- optionally,
-* a `postgresql-plpython-X.Y` package with a version corresponding to that of
- the aforementioned packages (e.g. `postgresql-plpython-9.1`).
-
-If you run postgres version prior to 8.3, you'll also need the
-`postgresql-contrib-8.X` package for full-text search extension.
-
-If you run postgres on another host than the |cubicweb| repository, you should
-install the `postgresql-client` package on the |cubicweb| host, and others on the
-database host.
-
-For extra details concerning installation, please refer to the `PostgreSQL
-project online documentation`_.
-
-.. _`PostgreSQL project online documentation`: http://www.postgresql.org/docs
-
-
-Database cluster
-++++++++++++++++
-
-If you already have an existing cluster and PostgreSQL server running, you do
-not need to execute the initilization step of your PostgreSQL database unless
-you want a specific cluster for |cubicweb| databases or if your existing
-cluster doesn't use the UTF8 encoding (see note below).
-
-To initialize a PostgreSQL cluster, use the command ``initdb``::
-
- $ initdb -E UTF8 -D /path/to/pgsql
-
-Notice the encoding specification. This is necessary since |cubicweb| usually
-want UTF8 encoded database. If you use a cluster with the wrong encoding, you'll
-get error like::
-
- new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)
- HINT: Use the same encoding as in the template database, or use template0 as template.
-
-Once initialized, start the database server PostgreSQL with the command::
-
- $ postgres -D /path/to/psql
-
-If you cannot execute this command due to permission issues, please make sure
-that your username has write access on the database. ::
-
- $ chown username /path/to/pgsql
-
-Database authentication
-+++++++++++++++++++++++
-
-The database authentication is configured in `pg_hba.conf`. It can be either set
-to `ident sameuser` or `md5`. If set to `md5`, make sure to use an existing
-user of your database. If set to `ident sameuser`, make sure that your client's
-operating system user name has a matching user in the database. If not, please
-do as follow to create a user::
-
- $ su
- $ su - postgres
- $ createuser -s -P username
-
-The option `-P` (for password prompt), will encrypt the password with the
-method set in the configuration file :file:`pg_hba.conf`. If you do not use this
-option `-P`, then the default value will be null and you will need to set it
-with::
-
- $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
-
-The above login/password will be requested when you will create an instance with
-`cubicweb-ctl create` to initialize the database of your instance.
-
-Notice that the `cubicweb-ctl db-create` does database initialization that
-may requires a postgres superuser. That's why a login/password is explicitly asked
-at this step, so you can use there a superuser without using this user when running
-the instance. Things that require special privileges at this step:
-
-* database creation, require the 'create database' permission
-* install the plpython extension language (require superuser)
-* install the tsearch extension for postgres version prior to 8.3 (require superuser)
-
-To avoid using a super user each time you create an install, a nice trick is to
-install plpython (and tsearch when needed) on the special `template1` database,
-so they will be installed automatically when cubicweb databases are created
-without even with needs for special access rights. To do so, run ::
-
- # Installation of plpythonu language by default ::
- $ createlang -U pgadmin plpythonu template1
- $ psql -U pgadmin template1
- template1=# update pg_language set lanpltrusted=TRUE where lanname='plpythonu';
-
-Where `pgadmin` is a postgres superuser. The last command is necessary since by
-default plpython is an 'untrusted' language and as such can't be used by non
-superuser. This update fix that problem by making it trusted.
-
-To install the tsearch plain-text index extension on postgres prior to 8.3, run::
-
- cat /usr/share/postgresql/8.X/contrib/tsearch2.sql | psql -U username template1
-
-
-.. _MySqlConfiguration:
-
-MySql
-~~~~~
-
-You must add the following lines in ``/etc/mysql/my.cnf`` file::
-
- transaction-isolation=READ-COMMITTED
- default-storage-engine=INNODB
- default-character-set=utf8
- max_allowed_packet = 128M
-
-.. Note::
- It is unclear whether mysql supports indexed string of arbitrary length or
- not.
-
-
-.. _SQLServerConfiguration:
-
-SQLServer
-~~~~~~~~~
-
-As of this writing, support for SQLServer 2005 is functional but incomplete. You
-should be able to connect, create a database and go quite far, but some of the
-SQL generated from RQL queries is still currently not accepted by the
-backend. Porting to SQLServer 2008 is also an item on the backlog.
-
-The `source` configuration file may look like this (specific parts only are
-shown)::
-
- [system]
- db-driver=sqlserver2005
- db-user=someuser
- # database password not needed
- #db-password=toto123
- #db-create/init may ask for a pwd: just say anything
- db-extra-arguments=Trusted_Connection
- db-encoding=utf8
-
-
-You need to change the default settings on the database by running::
-
- ALTER DATABASE <databasename> SET READ_COMMITTED_SNAPSHOT ON;
-
-The ALTER DATABASE command above requires some permissions that your
-user may not have. In that case you will have to ask your local DBA to
-run the query for you.
-
-You can check that the setting is correct by running the following
-query which must return '1'::
-
- SELECT is_read_committed_snapshot_on
- FROM sys.databases WHERE name='<databasename>';
-
-
-
-.. _SQLiteConfiguration:
-
-SQLite
-~~~~~~
-
-SQLite has the great advantage of requiring almost no configuration. Simply
-use 'sqlite' as db-driver, and set path to the dabase as db-name. Don't specify
-anything for db-user and db-password, they will be ignore anyway.
-
-.. Note::
- SQLite is great for testing and to play with cubicweb but is not suited for
- production environments.
-
-
-.. _PyroConfiguration:
-
-Pyro configuration
-------------------
-
-Pyro name server
-~~~~~~~~~~~~~~~~
-
-If you want to use Pyro to access your instance remotely, or to have multi-source
-or distributed configuration, it is required to have a Pyro name server running
-on your network. By default it is detected by a broadcast request, but you can
-specify a location in the instance's configuration file.
-
-To do so, you need to :
-
-* be sure to have installed it (see :ref:`InstallDependencies`)
-
-* launch the pyro name server with `pyro-nsd start` before starting cubicweb
-
-* under debian, edit the file :file:`/etc/default/pyro-nsd` so that the name
- server pyro will be launched automatically when the machine fire up
-
-Note that you can use the pyro server without a running pyro nameserver.
-Refer to `pyro-ns-host` server configuration option for details.
-
--- a/doc/book/en/admin/create-instance.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Creation of your first instance
-===============================
-
-Instance creation
------------------
-
-Now that we created a cube, we can create an instance and access it via a web
-browser. We will use a `all-in-one` configuration to simplify things ::
-
- cubicweb-ctl create -c all-in-one mycube myinstance
-
-.. note::
- Please note that we created a new cube for a demo purposes but
- you could have used an existing cube available in our standard library
- such as blog or person for example.
-
-A series of questions will be prompted to you, the default answer is usually
-sufficient. You can anyway modify the configuration later on by editing
-configuration files. When a login/password are requested to access the database
-please use the credentials you created at the time you configured the database
-(:ref:`PostgresqlConfiguration`).
-
-It is important to distinguish here the user used to access the database and the
-user used to login to the cubicweb instance. When an instance starts, it uses
-the login/password for the database to get the schema and handle low level
-transaction. But, when :command:`cubicweb-ctl create` asks for a manager
-login/psswd of *CubicWeb*, it refers to the user you will use during the
-development to administrate your web instance. It will be possible, later on,
-to use this user to create other users for your final web instance.
-
-
-Instance administration
------------------------
-
-start / stop
-~~~~~~~~~~~~
-
-When this command is completed, the definition of your instance is
-located in :file:`~/etc/cubicweb.d/myinstance/*`. To launch it, you
-just type ::
-
- cubicweb-ctl start -D myinstance
-
-The option `-D` specifies the *debug mode* : the instance is not
-running in server mode and does not disconnect from the terminal,
-which simplifies debugging in case the instance is not properly
-launched. You can see how it looks by visiting the URL
-`http://localhost:8080` (the port number depends of your
-configuration). To login, please use the cubicweb administrator
-login/password you defined when you created the instance.
-
-To shutdown the instance, Crtl-C in the terminal window is enough.
-If you did not use the option `-D`, then type ::
-
- cubicweb-ctl stop myinstance
-
-This is it! All is settled down to start developping your data model...
-
-.. note::
-
- The output of `cubicweb-ctl start -D myinstance` can be
- overwhelming. It is possible to reduce the log level with the
- `--loglevel` parameter as in `cubicweb-ctl start -D myinstance -l
- info` to filter out all logs under `info` gravity.
-
-upgrade
-~~~~~~~
-
-A manual upgrade step is necessary whenever a new version of CubicWeb or
-a cube is installed, in order to synchronise the instance's
-configuration and schema with the new code. The command is::
-
- cubicweb-ctl upgrade myinstance
-
-A series of questions will be asked. It always starts with a proposal
-to make a backup of your sources (where it applies). Unless you know
-exactly what you are doing (i.e. typically fiddling in debug mode, but
-definitely NOT migrating a production instance), you should answer YES
-to that.
-
-The remaining questions concern the migration steps of |cubicweb|,
-then of the cubes that form the whole application, in reverse
-dependency order.
-
-In principle, if the migration scripts have been properly written and
-tested, you should answer YES to all questions.
-
-Somtimes, typically while debugging a migration script, something goes
-wrong and the migration fails. Unfortunately the databse may be in an
-incoherent state. You have two options here:
-
-* fix the bug, restore the database and restart the migration process
- from scratch (quite recommended in a production environement)
-
-* try to replay the migration up to the last successful commit, that
- is answering NO to all questions up to the step that failed, and
- finish by answering YES to the remaining questions.
-
--- a/doc/book/en/admin/cubicweb-ctl.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _cubicweb-ctl:
-
-``cubicweb-ctl`` tool
-=====================
-
-`cubicweb-ctl` is the swiss knife to manage *CubicWeb* instances.
-The general syntax is ::
-
- cubicweb-ctl <command> [options command] <arguments commands>
-
-To view available commands ::
-
- cubicweb-ctl
- cubicweb-ctl --help
-
-Please note that the commands available depends on the *CubicWeb* packages
-and cubes that have been installed.
-
-To view the help menu on specific command ::
-
- cubicweb-ctl <command> --help
-
-Listing available cubes and instance
--------------------------------------
-
-* ``list``, provides a list of the available configuration, cubes
- and instances.
-
-
-Creation of a new cube
------------------------
-
-Create your new cube cube ::
-
- cubicweb-ctl newcube
-
-This will create a new cube in
-``/path/to/grshell-cubicweb/cubes/<mycube>`` for a Mercurial
-installation, or in ``/usr/share/cubicweb/cubes`` for a debian
-packages installation.
-
-Create an instance
--------------------
-
-You must ensure `~/etc/cubicweb.d/` exists prior to this. On windows, the
-'~' part will probably expand to 'Documents and Settings/user'.
-
-To create an instance from an existing cube, execute the following
-command ::
-
- cubicweb-ctl create <cube_name> <instance_name>
-
-This command will create the configuration files of an instance in
-``~/etc/cubicweb.d/<instance_name>``.
-
-The tool ``cubicweb-ctl`` executes the command ``db-create`` and
-``db-init`` when you run ``create`` so that you can complete an
-instance creation in a single command. But of course it is possible
-to issue these separate commands separately, at a later stage.
-
-Command to create/initialize an instance database
--------------------------------------------------
-
-* ``db-create``, creates the system database of an instance (tables and
- extensions only)
-* ``db-init``, initializes the system database of an instance
- (schema, groups, users, workflows...)
-
-Commands to control instances
------------------------------
-
-* ``start``, starts one or more or all instances
-
-of special interest::
-
- start -D
-
-will start in debug mode (under windows, starting without -D will not
-work; you need instead to setup your instance as a service).
-
-* ``stop``, stops one or more or all instances
-* ``restart``, restarts one or more or all instances
-* ``status``, returns the status of the instance(s)
-
-Commands to maintain instances
-------------------------------
-
-* ``upgrade``, launches the existing instances migration when a new version
- of *CubicWeb* or the cubes installed is available
-* ``shell``, opens a (Python based) migration shell for manual maintenance of the instance
-* ``db-dump``, creates a dump of the system database
-* ``db-restore``, restores a dump of the system database
-* ``db-check``, checks data integrity of an instance. If the automatic correction
- is activated, it is recommanded to create a dump before this operation.
-* ``schema-sync``, synchronizes the persistent schema of an instance with
- the instance schema. It is recommanded to create a dump before this operation.
-
-Commands to maintain i18n catalogs
-----------------------------------
-* ``i18ncubicweb``, regenerates messages catalogs of the *CubicWeb* library
-* ``i18ncube``, regenerates the messages catalogs of a cube
-* ``i18ninstance``, recompiles the messages catalogs of an instance.
- This is automatically done while upgrading.
-
-See also chapter :ref:`internationalization`.
-
-Other commands
---------------
-* ``delete``, deletes an instance (configuration files and database)
--- a/doc/book/en/admin/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Part3:
-
---------------
-Administration
---------------
-
-This part is for installation and administration of the *CubicWeb* framework and
-instances based on that framework.
-
-.. toctree::
- :maxdepth: 1
- :numbered:
-
- setup
- setup-windows
- config
- cubicweb-ctl
- create-instance
- instance-config
- site-config
- multisources
- ldap
- pyro
- migration
- additional-tips
- rql-logs
-
--- a/doc/book/en/admin/instance-config.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,226 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-
-Configure an instance
-=====================
-
-While creating an instance, a configuration file is generated in::
-
- $ (CW_INSTANCES_DIR) / <instance> / <configuration name>.conf
-
-For example::
-
- /etc/cubicweb.d/myblog/all-in-one.conf
-
-It is a simple text file in the INI format
-(http://en.wikipedia.org/wiki/INI_file). In the following description,
-each option name is prefixed with its own section and followed by its
-default value if necessary, e.g. "`<section>.<option>` [value]."
-
-.. _`WebServerConfig`:
-
-Configuring the Web server
---------------------------
-:`web.auth-model` [cookie]:
- authentication mode, cookie or http
-:`web.realm`:
- realm of the instance in http authentication mode
-:`web.http-session-time` [0]:
- period of inactivity of an HTTP session before it closes automatically.
- Duration in seconds, 0 meaning no expiration (or more exactly at the
- closing of the browser client)
-
-:`main.anonymous-user`, `main.anonymous-password`:
- login and password to use to connect to the RQL server with
- HTTP anonymous connection. CWUser account should exist.
-
-:`main.base-url`:
- url base site to be used to generate the urls of web pages
-
-Https configuration
-```````````````````
-It is possible to make a site accessible for anonymous http connections
-and https for authenticated users. This requires to
-use apache (for example) for redirection and the variable `main.https-url`
-of configuration file.
-
-For this to work you have to activate the following apache modules :
-
-* rewrite
-* proxy
-* http_proxy
-
-The command on Debian based systems for that is ::
-
- a2enmod rewrite http_proxy proxy
- /etc/init.d/apache2 restart
-
-:Example:
-
- For an apache redirection of a site accessible via `http://localhost/demo`
- and `https://localhost/demo` and actually running on port 8080, it
- takes to the http:::
-
- ProxyPreserveHost On
- RewriteEngine On
- RewriteCond %{REQUEST_URI} ^/demo
- RewriteRule ^/demo$ /demo/
- RewriteRule ^/demo/(.*) http://127.0.0.1:8080/$1 [L,P]
-
- and for the https:::
-
- ProxyPreserveHost On
- RewriteEngine On
- RewriteCond %{REQUEST_URI} ^/ demo
- RewriteRule ^/demo$/demo/
- RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]
-
-
- and we will file in the all-in-one.conf of the instance:::
-
- base-url = http://localhost/demo
- https-url = https://localhost/demo
-
-Notice that if you simply want a site accessible through https, not *both* http
-and https, simply set `base-url` to the https url and the first section into your
-apache configuration (as you would have to do for an http configuration with an
-apache front-end).
-
-Setting up the web client
--------------------------
-:`web.embed-allowed`:
- regular expression matching sites which could be "embedded" in
- the site (controllers 'embed')
-:`web.submit-url`:
- url where the bugs encountered in the instance can be mailed to
-
-
-RQL server configuration
-------------------------
-:`main.host`:
- host name if it can not be detected correctly
-:`main.pid-file`:
- file where will be written the server pid
-:`main.uid`:
- user account to use for launching the server when it is
- root launched by init
-:`main.session-time [30*60]`:
- timeout of a RQL session
-:`main.query-log-file`:
- file where all requests RQL executed by the server are written
-
-
-Pyro configuration for the instance
------------------------------------
-Web server side:
-
-:`pyro.pyro-instance-id`:
- pyro identifier of RQL server (e.g. the instance name)
-
-RQL server side:
-
-:`main.pyro-server`:
- boolean to switch on/off pyro server-side
-
-:`pyro.pyro-host`:
- pyro host:port number. If no port is specified, it is assigned
- automatically.
-
-RQL and web servers side:
-
-:`pyro.pyro-ns-host`:
- hostname hosting pyro server name. If no value is
- specified, it is located by a request from broadcast
-
-:`pyro.pyro-ns-group`:
- pyro group in which to save the instance (will default to 'cubicweb')
-
-
-Configuring e-mail
-------------------
-RQL and web server side:
-
-:`email.mangle-mails [no]`:
- indicates whether the email addresses must be displayed as is or
- transformed
-
-RQL server side:
-
-:`email.smtp-host [mail]`:
- hostname hosting the SMTP server to use for outgoing mail
-:`email.smtp-port [25]`:
- SMTP server port to use for outgoing mail
-:`email.sender-name`:
- name to use for outgoing mail of the instance
-:`email.sender-addr`:
- address for outgoing mail of the instance
-:`email.default dest-addrs`:
- destination addresses by default, if used by the configuration of the
- dissemination of the model (separated by commas)
-:`email.supervising-addrs`:
- destination addresses of e-mails of supervision (separated by
- commas)
-
-
-Configuring logging
--------------------
-:`main.log-threshold`:
- level of filtering messages (DEBUG, INFO, WARNING, ERROR)
-:`main.log-file`:
- file to write messages
-
-
-.. _PersistentProperties:
-
-Configuring persistent properties
----------------------------------
-Other configuration settings are in the form of entities `CWProperty`
-in the database. It must be edited via the web interface or by
-RQL queries.
-
-:`ui.encoding`:
- Character encoding to use for the web
-:`navigation.short-line-size`:
- number of characters for "short" display
-:`navigation.page-size`:
- maximum number of entities to show per results page
-:`navigation.related-limit`:
- number of related entities to show up on primary entity view
-:`navigation.combobox-limit`:
- number of entities unrelated to show up on the drop-down lists of
- the sight on an editing entity view
-
-Cross-Origin Resource Sharing
------------------------------
-
-CubicWeb provides some support for the CORS_ protocol. For now, the
-provided implementation only deals with access to a CubicWeb instance
-as a whole. Support for a finer granularity may be considered in the
-future.
-
-Specificities of the provided implementation:
-
-- ``Access-Control-Allow-Credentials`` is always true
-- ``Access-Control-Allow-Origin`` header in response will never be
- ``*``
-- ``Access-Control-Expose-Headers`` can be configured globally (see below)
-- ``Access-Control-Max-Age`` can be configured globally (see below)
-- ``Access-Control-Allow-Methods`` can be configured globally (see below)
-- ``Access-Control-Allow-Headers`` can be configured globally (see below)
-
-
-A few parameters can be set to configure the CORS_ capabilities of CubicWeb.
-
-.. _CORS: http://www.w3.org/TR/cors/
-
-:`access-control-allow-origin`:
- comma-separated list of allowed origin domains or "*" for any domain
-:`access-control-allow-methods`:
- comma-separated list of allowed HTTP methods
-:`access-control-max-age`:
- maximum age of cross-origin resource sharing (in seconds)
-:`access-control-allow-headers`:
- comma-separated list of allowed HTTP custom headers (used in simple requests)
-:`access-control-expose-headers`:
- comma-separated list of allowed HTTP custom headers (used in preflight requests)
-
--- a/doc/book/en/admin/ldap.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,134 +0,0 @@
-.. _LDAP:
-
-LDAP integration
-================
-
-Overview
---------
-
-Using LDAP as a source for user credentials and information is quite
-easy. The most difficult part lies in building an LDAP schema or
-using an existing one.
-
-At cube creation time, one is asked if more sources are wanted. LDAP
-is one possible option at this time. Of course, it is always possible
-to set it up later using the `CWSource` entity type, which we discuss
-there.
-
-It is possible to add as many LDAP sources as wanted, which translates
-in as many `CWSource` entities as needed.
-
-The general principle of the LDAP source is, given a proper
-configuration, to create local users matching the users available in
-the directory and deriving local user attributes from directory users
-attributes. Then a periodic task ensures local user information
-synchronization with the directory.
-
-Users handled by such a source should not be edited directly from
-within the application instance itself. Rather, updates should happen
-at the LDAP server level.
-
-Credential checks are _always_ done against the LDAP server.
-
-.. Note::
-
- There are currently two ldap source types: the older `ldapuser` and
- the newer `ldapfeed`. The older will be deprecated anytime soon, as
- the newer has now gained all the features of the old and does not
- suffer from some of its illnesses.
-
- The ldapfeed creates real `CWUser` entities, and then
- activate/deactivate them depending on their presence/absence in the
- corresponding LDAP source. Their attribute and state
- (activated/deactivated) are hence managed by the source mechanism;
- they should not be altered by other means (as such alterations may
- be overridden in some subsequent source synchronisation).
-
-
-Configuration of an LDAPfeed source
------------------------------------
-
-Additional sources are created at cube creation time or later through the
-user interface.
-
-Configure an `ldapfeed` source from the user interface under `Manage` then
-`data sources`:
-
-* At this point `type` has been set to `ldapfeed`.
-
-* The `parser` attribute shall be set to `ldapfeed`.
-
-* The `url` attribute shall be set to an URL such as ldap://ldapserver.domain/.
-
-* The `configuration` attribute contains many options. They are described in
- detail in the next paragraph.
-
-
-Options of an LDAPfeed source
------------------------------
-
-Let us enumerate the options by categories (LDAP server connection,
-LDAP schema mapping information).
-
-LDAP server connection options:
-
-* `auth-mode`, (choices are simple, cram_md5, digest_md5, gssapi, support
- for the later being partial as of now)
-
-* `auth-realm`, realm to use when using gssapi/kerberos authentication
-
-* `data-cnx-dn`, user dn to use to open data connection to the ldap (eg
- used to respond to rql queries)
-
-* `data-cnx-password`, password to use to open data connection to the
- ldap (eg used to respond to rql queries)
-
-If the LDAP server accepts anonymous binds, then it is possible to
-leave data-cnx-dn and data-cnx-password empty. This is, however, quite
-unlikely in practice. Beware that the LDAP server might hide attributes
-such as "userPassword" while the rest of the attributes remain visible
-through an anonymous binding.
-
-LDAP schema mapping options:
-
-* `user-base-dn`, base DN to lookup for users
-
-* `user-scope`, user search scope (valid values: "BASE", "ONELEVEL",
- "SUBTREE")
-
-* `user-classes`, classes of user (with Active Directory, you want to
- say "user" here)
-
-* `user-filter`, additional filters to be set in the ldap query to
- find valid users
-
-* `user-login-attr`, attribute used as login on authentication (with
- Active Directory, you want to use "sAMAccountName" here)
-
-* `user-default-group`, name of a group in which ldap users will be by
- default. You can set multiple groups by separating them by a comma
-
-* `user-attrs-map`, map from ldap user attributes to cubicweb
- attributes (with Active Directory, you want to use
- sAMAccountName:login,mail:email,givenName:firstname,sn:surname)
-
-
-Other notes
------------
-
-* Cubicweb is able to start if ldap cannot be reached, even on
- cubicweb-ctl start ... If some source ldap server cannot be used
- while an instance is running, the corresponding users won't be
- authenticated but their status will not change (e.g. they will not
- be deactivated)
-
-* The user-base-dn is a key that helps cubicweb map CWUsers to LDAP
- users: beware updating it
-
-* When a user is removed from an LDAP source, it is deactivated in the
- CubicWeb instance; when a deactivated user comes back in the LDAP
- source, it (automatically) is activated again
-
-* You can use the :class:`CWSourceHostConfig` to have variants for a source
- configuration according to the host the instance is running on. To do so
- go on the source's view from the sources management view.
--- a/doc/book/en/admin/migration.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Migrating cubicweb instances - benefits from a distributed architecture
-=======================================================================
-
-Migrate apache & cubicweb
--------------------------
-
-**Aim** : do the migration for N cubicweb instances hosted on a server to another with no downtime.
-
-**Prerequisites** : have an explicit definition of the database host (not default or localhost). In our case, the database is hosted on another host. You are not migrating your pyro server. You are not using multisource (more documentation on that soon).
-
-**Steps** :
-
-1. *on new machine* : install your environment (*pseudocode*) ::
-
- apt-get install cubicweb cubicweb-applications apache2
-
-2. *on old machine* : copy your cubicweb and apache configuration to the new machine ::
-
- scp /etc/cubicweb.d/ newmachine:/etc/cubicweb.d/
- scp /etc/apache2/sites-available/ newmachine:/etc/apache2/sites-available/
-
-3. *on new machine* : give new ids to pyro registration so the new instances can register ::
-
- cd /etc/cubicweb.d/ ; sed -i.bck 's/^pyro-instance-id=.*$/\02/' */all-in-one.conf
-
-4. *on new machine* : start your instances ::
-
- cubicweb start
-
-5. *on new machine* : enable sites and modules for apache and start it, test it using by modifying your /etc/host file.
-
-6. change dns entry from your oldmachine to newmachine
-
-7. shutdown your *old machine* (if it doesn't host other services or your database)
-
-8. That's it.
-
-**Possible enhancements** : use right from the start a pound server behind your apache, that way you can add backends and smoothily migrate by shuting down backends that pound will take into account.
-
-Migrate apache & cubicweb with pyro
------------------------------------
-
-FIXME TODO
-
--- a/doc/book/en/admin/multisources.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-Multiple sources of data
-========================
-
-Data sources include SQL, LDAP, RQL, mercurial and subversion.
-
-.. XXX feed me
--- a/doc/book/en/admin/pyro.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-.. _UsingPyro:
-
-Working with a distributed client (using Pyro)
-==============================================
-
-In some circumstances, it is practical to split the repository and
-web-client parts of the application for load-balancing reasons. Or
-one wants to access the repository from independant scripts to consult
-or update the database.
-
-Prerequisites
--------------
-
-For this to work, several steps have to be taken in order.
-
-You must first ensure that the appropriate software is installed and
-running (see :ref:`ConfigEnv`)::
-
- pyro-nsd -x -p 6969
-
-Then you have to set appropriate options in your configuration. For
-instance::
-
- pyro-server=yes
- pyro-ns-host=localhost:6969
-
- pyro-instance-id=myinstancename
-
-Connect to the CubicWeb repository from a python script
--------------------------------------------------------
-
-Assuming pyro-nsd is running and your instance is configured with ``pyro-server=yes``,
-you will be able to use :mod:`cubicweb.dbapi` api to initiate the connection.
-
-.. note::
- Regardless of whether your instance is pyro activated or not, you can still
- achieve this by using cubicweb-ctl shell scripts in a simpler way, as by default
- it creates a repository 'in-memory' instead of connecting through pyro. That
- also means you've to be on the host where the instance is running.
-
-Finally, the client (for instance a python script) must connect specifically
-as in the following example code:
-
-.. sourcecode:: python
-
- from cubicweb import dbapi
-
- cnx = dbapi.connect(database='instance-id', user='admin', password='admin')
- cnx.load_appobjects()
- cur = cnx.cursor()
- for name in (u'Personal', u'Professional', u'Computers'):
- cur.execute('INSERT Tag T: T name %(n)s', {'n': name})
- cnx.commit()
-
-Calling :meth:`cubicweb.dbapi.load_appobjects`, will populate the
-cubicweb registries (see :ref:`VRegistryIntro`) with the application
-objects installed on the host where the script runs. You'll then be
-allowed to use the ORM goodies and custom entity methods and views. Of
-course this is optional, without it you can still get the repository
-data through the connection but in a roughly way: only RQL cursors
-will be available, e.g. you can't even build entity objects from the
-result set.
--- a/doc/book/en/admin/rql-logs.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-RQL logs
-========
-
-You can configure the *CubicWeb* instance to keep a log
-of the queries executed against your database. To do so,
-edit the configuration file of your instance
-``.../etc/cubicweb.d/myapp/all-in-one.conf`` and uncomment the
-variable ``query-log-file``::
-
- # web instance query log file
- query-log-file=/tmp/rql-myapp.log
-
--- a/doc/book/en/admin/setup-windows.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _SetUpWindowsEnv:
-
-Installing a development environement on Windows
-================================================
-
-Setting up a Windows development environment is not too complicated
-but it requires a series of small steps.
-
-We propose an example of a typical |cubicweb| installation on Windows
-from sources. We assume everything goes into ``C:\\`` and for any
-package, without version specification, "the latest is
-the greatest".
-
-Mind that adjusting the installation drive should be straightforward.
-
-
-
-Install the required elements
------------------------------
-
-|cubicweb| requires some base elements that must be installed to run
-correctly. So, first of all, you must install them :
-
-* python >= 2.6 and < 3
- (`Download Python <http://www.python.org/download/>`_).
- You can also consider the Python(x,y) distribution
- (`Download Python(x,y) <http://code.google.com/p/pythonxy/wiki/Downloads>`_)
- as it makes things easier for Windows user by wrapping in a single installer
- python 2.7 plus numerous useful third-party modules and
- applications (including Eclipse + pydev, which is an arguably good
- IDE for Python under Windows).
-
-* `Twisted <http://twistedmatrix.com/trac/>`_ is an event-driven
- networking engine
- (`Download Twisted <http://twistedmatrix.com/trac/>`_)
-
-* `lxml <http://codespeak.net/lxml/>`_ library
- (version >=2.2.1) allows working with XML and HTML
- (`Download lxml <http://pypi.python.org/pypi/lxml/2.2.1>`_)
-
-* `Postgresql <http://www.postgresql.org/>`_,
- an object-relational database system
- (`Download Postgresql <http://www.enterprisedb.com/products/pgdownload.do#windows>`_)
- and its python drivers
- (`Download psycopg <http://www.stickpeople.com/projects/python/win-psycopg/#Version2>`_)
-
-* A recent version of `gettext`
- (`Download gettext <http://download.logilab.org/pub/gettext/gettext-0.17-win32-setup.exe>`_).
-
-* `rql <http://www.logilab.org/project/rql>`_,
- the recent version of the Relationship Query Language parser.
-
-Install optional elements
--------------------------
-
-We recommend you to install the following elements. They are not
-mandatory but they activate very interesting features in |cubicweb|:
-
-* `python-ldap <http://pypi.python.org/pypi/python-ldap>`_
- provides access to LDAP/Active directory directories
- (`Download python-ldap <http://www.osuch.org/python-ldap>`_).
-
-* `graphviz <http://www.graphviz.org/>`_
- which allow schema drawings.
- (`Download graphviz <http://www.graphviz.org/Download_windows.php>`_).
- It is quite recommended (albeit not mandatory).
-
-Other elements will activate more features once installed. Take a look
-at :ref:`InstallDependencies`.
-
-Useful tools
-------------
-
-Some additional tools could be useful to develop :ref:`cubes <AvailableCubes>`
-with the framework.
-
-* `mercurial <http://mercurial.selenic.com/>`_ and its standard windows GUI
- (`TortoiseHG <http://tortoisehg.bitbucket.org/>`_) allow you to get the source
- code of |cubicweb| from control version repositories. So you will be able to
- get the latest development version and pre-release bugfixes in an easy way
- (`Download mercurial <http://bitbucket.org/tortoisehg/stable/wiki/download>`_).
-
-* You can also consider the ssh client `Putty` in order to peruse
- mercurial over ssh (`Download <http://www.putty.org/>`_).
-
-* If you are an Eclipse user, mercurial can be integrated using the
- `MercurialEclipse` plugin
- (`Home page <http://www.vectrace.com/mercurialeclipse/>`_).
-
-Getting the sources
--------------------
-
-There are two ways to get the sources of |cubicweb| and its
-:ref:`cubes <AvailableCubes>`:
-
-* download the latest release (:ref:`SourceInstallation`)
-* get the development version using Mercurial
- (:ref:`MercurialInstallation`)
-
-Environment variables
----------------------
-
-You will need some convenience environment variables once all is set up. These
-variables are settable through the GUI by getting at the `System properties`
-window (by righ-clicking on `My Computer` -> `properties`).
-
-In the `advanced` tab, there is an `Environment variables` button. Click on
-it. That opens a small window allowing edition of user-related and system-wide
-variables.
-
-We will consider only user variables. First, the ``PATH`` variable. Assuming
-you are logged as user *Jane*, add the following paths, separated by
-semi-colons::
-
- C:\Documents and Settings\Jane\My Documents\Python\cubicweb\cubicweb\bin
- C:\Program Files\Graphviz2.24\bin
-
-The ``PYTHONPATH`` variable should also contain::
-
- C:\Documents and Settings\Jane\My Documents\Python\cubicweb\
-
-From now, on a fresh `cmd` shell, you should be able to type::
-
- cubicweb-ctl list
-
-... and get a meaningful output.
-
-Running an instance as a service
---------------------------------
-
-This currently assumes that the instances configurations is located at
-``C:\\etc\\cubicweb.d``. For a cube 'my_instance', you will find
-``C:\\etc\\cubicweb.d\\my_instance\\win32svc.py``.
-
-Now, register your instance as a windows service with::
-
- win32svc install
-
-Then start the service with::
-
- net start cubicweb-my_instance
-
-In case this does not work, you should be able to see error reports in
-the application log, using the windows event log viewer.
--- a/doc/book/en/admin/setup.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,269 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _SetUpEnv:
-
-Installation of a *CubicWeb* environment
-========================================
-
-Official releases are available from the `CubicWeb.org forge`_ and from
-`PyPI`_. Since CubicWeb is developed using `Agile software development
-<http://en.wikipedia.org/wiki/Agile_software_development>`_ techniques, releases
-happen frequently. In a version numbered X.Y.Z, X changes after a few years when
-the API breaks, Y changes after a few weeks when features are added and Z
-changes after a few days when bugs are fixed.
-
-Depending on your needs, you will chose a different way to install CubicWeb on
-your system:
-
-- `Installation on Debian/Ubuntu`_
-- `Installation on Windows`_
-- `Installation in a virtualenv`_
-- `Installation with pip`_
-- `Installation with easy_install`_
-- `Installation from tarball`_
-
-If you are a power-user and need the very latest features, you will
-
-- `Install from version control`_
-
-Once the software is installed, move on to :ref:`ConfigEnv` for better control
-and advanced features of |cubicweb|.
-
-.. _`Installation on Debian/Ubuntu`: DebianInstallation_
-.. _`Installation on Windows`: WindowsInstallation_
-.. _`Installation in a virtualenv`: VirtualenvInstallation_
-.. _`Installation with pip`: PipInstallation_
-.. _`Installation with easy_install`: EasyInstallInstallation_
-.. _`Installation from tarball`: TarballInstallation_
-.. _`Install from version control`: MercurialInstallation_
-
-
-.. _DebianInstallation:
-
-Debian/Ubuntu install
----------------------
-
-|cubicweb| is packaged for Debian/Ubuntu (and derived
-distributions). Their integrated package-management system make
-installation and upgrade much easier for users since
-dependencies (like databases) are automatically installed.
-
-Depending on the distribution you are using, add the appropriate line to your
-`list of sources` (for example by editing ``/etc/apt/sources.list``).
-
-For Debian 7.0 Wheezy (stable)::
-
- deb http://download.logilab.org/production/ wheezy/
-
-For Debian Sid (unstable)::
-
- deb http://download.logilab.org/production/ sid/
-
-For Ubuntu 12.04 Precise Pangolin (Long Term Support) and newer::
-
- deb http://download.logilab.org/production/ precise/
-
-The repositories are signed with the `Logilab's gnupg key`_. You can download
-and register the key to avoid warnings::
-
- wget -q http://download.logilab.org/logilab-dists-key.asc -O- | sudo apt-key add -
-
-Update your list of packages and perform the installation::
-
- apt-get update
- apt-get install cubicweb cubicweb-dev
-
-``cubicweb`` installs the framework itself, allowing you to create new
-instances. ``cubicweb-dev`` installs the development environment
-allowing you to develop new cubes.
-
-There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
-list of available cubes using ``apt-cache search cubicweb`` or at the
-`CubicWeb.org forge`_.
-
-.. note::
-
- `cubicweb-dev` will install basic sqlite support. You can easily setup
- :ref:`cubicweb with other database <DatabaseInstallation>` using the following
- virtual packages :
-
- * `cubicweb-postgresql-support` contains the necessary dependencies for
- using :ref:`cubicweb with postgresql datatabase <PostgresqlConfiguration>`
-
- * `cubicweb-mysql-support` contains the necessary dependencies for using
- :ref:`cubicweb with mysql database <MySqlConfiguration>`.
-
-.. _`list of sources`: http://wiki.debian.org/SourcesList
-.. _`Logilab's gnupg key`: http://download.logilab.org/logilab-dists-key.asc
-.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
-
-.. _WindowsInstallation:
-
-Windows Install
----------------
-
-You need to have `python`_ version >= 2.5 and < 3 installed.
-
-If you want an automated install, your best option is probably the
-:ref:`EasyInstallInstallation`. EasyInstall is a tool that helps users to
-install python packages along with their dependencies, searching for suitable
-pre-compiled binaries on the `The Python Package Index`_.
-
-If you want better control over the process as well as a suitable development
-environment or if you are having problems with `easy_install`, read on to
-:ref:`SetUpWindowsEnv`.
-
-.. _python: http://www.python.org/
-.. _`The Python Package Index`: http://pypi.python.org
-
-.. _VirtualenvInstallation:
-
-`Virtualenv` install
---------------------
-
-|cubicweb| can be safely installed, used and contained inside a
-`virtualenv`_. You can use either :ref:`pip <PipInstallation>` or
-:ref:`easy_install <EasyInstallInstallation>` to install |cubicweb|
-inside an activated virtual environment.
-
-.. _PipInstallation:
-
-`pip` install
--------------
-
-`pip <http://pip.openplans.org/>`_ is a python tool that helps downloading,
-building, installing, and managing Python packages and their dependencies. It
-is fully compatible with `virtualenv`_ and installs the packages from sources
-published on the `The Python Package Index`_.
-
-.. _`virtualenv`: http://virtualenv.openplans.org/
-
-A working compilation chain is needed to build the modules that include C
-extensions. If you really do not want to compile anything, installing `lxml <http://lxml.de/>`_,
-`Twisted Web <http://twistedmatrix.com/trac/wiki/Downloads/>`_ and `libgecode
-<http://www.gecode.org/>`_ will help.
-
-For Debian, these minimal dependencies can be obtained by doing::
-
- apt-get install gcc python-pip python-dev python-lxml
-
-or, if you prefer to get as much as possible from pip::
-
- apt-get install gcc python-pip python-dev libxslt1-dev libxml2-dev
-
-For Windows, you can install pre-built packages (possible `source
-<http://www.lfd.uci.edu/~gohlke/pythonlibs/>`_). For a minimal setup, install:
-
-- pip http://www.lfd.uci.edu/~gohlke/pythonlibs/#pip
-- setuptools http://www.lfd.uci.edu/~gohlke/pythonlibs/#setuptools
-- libxml-python http://www.lfd.uci.edu/~gohlke/pythonlibs/#libxml-python>
-- lxml http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml and
-- twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
-
-Make sure to choose the correct architecture and version of Python.
-
-Finally, install |cubicweb| and its dependencies, by running::
-
- pip install cubicweb
-
-Many other :ref:`cubes <AvailableCubes>` are available. A list is available at
-`PyPI <http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
-or at the `CubicWeb.org forge`_.
-
-For example, installing the *blog cube* is achieved by::
-
- pip install cubicweb-blog
-
-.. _EasyInstallInstallation:
-
-`easy_install` install
-----------------------
-
-.. note::
-
- If you are not a Windows user and you have a compilation environment, we
- recommend you to use the PipInstallation_.
-
-`easy_install`_ is a python utility that helps downloading, installing, and
-managing python packages and their dependencies.
-
-Install |cubicweb| and its dependencies, run::
-
- easy_install cubicweb
-
-There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
-list of available cubes on `PyPI
-<http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
-or at the `CubicWeb.org Forge`_.
-
-For example, installing the *blog cube* is achieved by::
-
- easy_install cubicweb-blog
-
-.. note::
-
- If you encounter problem with :ref:`cubes <AvailableCubes>` installation,
- consider using :ref:`PipInstallation` which is more stable
- but can not installed pre-compiled binaries.
-
-.. _`easy_install`: http://packages.python.org/distribute/easy_install.html
-
-
-.. _SourceInstallation:
-
-Install from source
--------------------
-
-.. _TarballInstallation:
-
-You can download the archive containing the sources from
-`http://download.logilab.org/pub/cubicweb/ <http://download.logilab.org/pub/cubicweb/>`_.
-
-Make sure you also have all the :ref:`InstallDependencies`.
-
-Once uncompressed, you can install the framework from inside the uncompressed
-folder with::
-
- python setup.py install
-
-Or you can run |cubicweb| directly from the source directory by
-setting the :ref:`resource mode <RessourcesConfiguration>` to `user`. This will
-ease the development with the framework.
-
-There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
-list of availble cubes at the `CubicWeb.org Forge`_.
-
-
-.. _MercurialInstallation:
-
-Install from version control system
------------------------------------
-
-To keep-up with on-going development, clone the :ref:`Mercurial
-<MercurialPresentation>` repository::
-
- hg clone -u stable http://hg.logilab.org/cubicweb # stable branch
- hg clone http://hg.logilab.org/cubicweb # development branch
-
-To get many of CubicWeb's dependencies and a nice set of base cubes, run the
-`clone_deps.py` script located in `cubicweb/bin/`::
-
- python cubicweb/bin/clone_deps.py
-
-(Windows users should replace slashes with antislashes).
-
-This script will clone a set of mercurial repositories into the
-directory containing the ``cubicweb`` repository, and update them to the
-latest published version tag (if any).
-
-.. note::
-
- In every cloned repositories, a `hg tags` will display a list of
- tags in reverse chronological order. One reasonnable option is to go to a
- tagged version: the latest published version or example, as done by
- the `clone_deps` script)::
-
- hg update cubicweb-version-3.12.2
-
-Make sure you also have all the :ref:`InstallDependencies`.
-
--- a/doc/book/en/admin/site-config.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-User interface for web site configuration
-=========================================
-
-.. image:: ../images/lax-book_03-site-config-panel_en.png
-
-This panel allows you to configure the appearance of your instance site.
-Six menus are available and we will go through each of them to explain how
-to use them.
-
-Navigation
-~~~~~~~~~~
-This menu provides you a way to adjust some navigation options depending on
-your needs, such as the number of entities to display by page of results.
-Follows the detailled list of available options :
-
-* navigation.combobox-limit : maximum number of entities to display in related
- combo box (sample format: 23)
-* navigation.page-size : maximum number of objects displayed by page of results
- (sample format: 23)
-* navigation.related-limit : maximum number of related entities to display in
- the primary view (sample format: 23)
-* navigation.short-line-size : maximum number of characters in short description
- (sample format: 23)
-
-UI
-~~
-This menu provides you a way to customize the user interface settings such as
-date format or encoding in the produced html.
-Follows the detailled list of available options :
-
-* ui.date-format : how to format date in the ui ("man strftime" for format description)
-* ui.datetime-format : how to format date and time in the ui ("man strftime" for format
- description)
-* ui.default-text-format : default text format for rich text fields.
-* ui.encoding : user interface encoding
-* ui.fckeditor :should html fields being edited using fckeditor (a HTML WYSIWYG editor).
- You should also select text/html as default text format to actually get fckeditor.
-* ui.float-format : how to format float numbers in the ui
-* ui.language : language of the user interface
-* ui.main-template : id of main template used to render pages
-* ui.site-title : site title, which is displayed right next to the logo in the header
-* ui.time-format : how to format time in the ui ("man strftime" for format description)
-
-
-Actions
-~~~~~~~
-This menu provides a way to configure the context in which you expect the actions
-to be displayed to the user and if you want the action to be visible or not.
-You must have notice that when you view a list of entities, an action box is
-available on the left column which display some actions as well as a drop-down
-menu for more actions.
-
-The context available are :
-
-* mainactions : actions listed in the left box
-* moreactions : actions listed in the `more` menu of the left box
-* addrelated : add actions listed in the left box
-* useractions : actions listed in the first section of drop-down menu
- accessible from the right corner user login link
-* siteactions : actions listed in the second section of drop-down menu
- accessible from the right corner user login link
-* hidden : select this to hide the specific action
-
-Boxes
-~~~~~
-The instance has already a pre-defined set of boxes you can use right away.
-This configuration section allows you to place those boxes where you want in the
-instance interface to customize it.
-
-The available boxes are :
-
-* actions box : box listing the applicable actions on the displayed data
-
-* boxes_blog_archives_box : box listing the blog archives
-
-* possible views box : box listing the possible views for the displayed data
-
-* rss box : RSS icon to get displayed data as a RSS thread
-
-* search box : search box
-
-* startup views box : box listing the configuration options available for
- the instance site, such as `Preferences` and `Site Configuration`
-
-Components
-~~~~~~~~~~
-[WRITE ME]
-
-Contextual components
-~~~~~~~~~~~~~~~~~~~~~
-[WRITE ME]
-
--- a/doc/book/en/annexes/depends.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _InstallDependencies:
-
-Installation dependencies
-=========================
-
-When you run CubicWeb from source, either by downloading the tarball or
-cloning the mercurial tree, here is the list of tools and libraries you need
-to have installed in order for CubicWeb to work:
-
-* yapps - http://theory.stanford.edu/~amitp/yapps/ -
- http://pypi.python.org/pypi/Yapps2
-
-* pygraphviz - http://networkx.lanl.gov/pygraphviz/ -
- http://pypi.python.org/pypi/pygraphviz
-
-* docutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
-
-* lxml - http://codespeak.net/lxml - http://pypi.python.org/pypi/lxml
-
-* twisted - http://twistedmatrix.com/ - http://pypi.python.org/pypi/Twisted
-
-* logilab-common - http://www.logilab.org/project/logilab-common -
- http://pypi.python.org/pypi/logilab-common/
-
-* logilab-database - http://www.logilab.org/project/logilab-database -
- http://pypi.python.org/pypi/logilab-database/
-
-* logilab-constraint - http://www.logilab.org/project/logilab-constraint -
- http://pypi.python.org/pypi/constraint/
-
-* logilab-mtconverter - http://www.logilab.org/project/logilab-mtconverter -
- http://pypi.python.org/pypi/logilab-mtconverter
-
-* rql - http://www.logilab.org/project/rql - http://pypi.python.org/pypi/rql
-
-* yams - http://www.logilab.org/project/yams - http://pypi.python.org/pypi/yams
-
-* indexer - http://www.logilab.org/project/indexer -
- http://pypi.python.org/pypi/indexer
-
-* passlib - https://code.google.com/p/passlib/ -
- http://pypi.python.org/pypi/passlib
-
-If you're using a Postgresql database (recommended):
-
-* psycopg2 - http://initd.org/projects/psycopg2 - http://pypi.python.org/pypi/psycopg2
-* plpythonu extension
-
-Other optional packages:
-
-* fyzz - http://www.logilab.org/project/fyzz -
- http://pypi.python.org/pypi/fyzz *to activate Sparql querying*
-
-
-Any help with the packaging of CubicWeb for more than Debian/Ubuntu (including
-eggs, buildouts, etc) will be greatly appreciated.
--- a/doc/book/en/annexes/docstrings-conventions.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-Javascript docstrings
-=====================
-
-Whereas in Python source code we only need to include a module docstrings
-using the directive `.. automodule:: mypythonmodule`, we will have to
-explicitely define Javascript modules and functions in the doctrings since
-there is no native directive to include Javascript files.
-
-Rest generation
----------------
-
-`pyjsrest` is a small utility parsing Javascript doctrings and generating the
-corresponding Restructured file used by Sphinx to generate HTML documentation.
-This script will have the following structure::
-
- ===========
- filename.js
- ===========
- .. module:: filename.js
-
-We use the `.. module::` directive to register a javascript library
-as a Python module for Sphinx. This provides an entry in the module index.
-
-The contents of the docstring found in the javascript file will be added as is
-following the module declaration. No treatment will be done on the doctring.
-All the documentation structure will be in the docstrings and will comply
-with the following rules.
-
-Docstring structure
--------------------
-
-Basically we document javascript with RestructuredText docstring
-following the same convention as documenting Python code.
-
-The doctring in Javascript files must be contained in standard
-Javascript comment signs, starting with `/**` and ending with `*/`,
-such as::
-
- /**
- * My comment starts here.
- * This is the second line prefixed with a `*`.
- * ...
- * ...
- * All the follwing line will be prefixed with a `*` followed by a space.
- * ...
- * ...
- */
-
-
-Comments line prefixed by `//` will be ignored. They are reserved for source
-code comments dedicated to developers.
-
-
-Javscript functions docstring
------------------------------
-
-By default, the `function` directive describes a module-level function.
-
-`function` directive
-~~~~~~~~~~~~~~~~~~~~
-
-Its purpose is to define the function prototype such as::
-
- .. function:: loadxhtml(url, data, reqtype, mode)
-
-If any namespace is used, we should add it in the prototype for now,
-until we define an appropriate directive::
-
- .. function:: jQuery.fn.loadxhtml(url, data, reqtype, mode)
-
-Function parameters
-~~~~~~~~~~~~~~~~~~~
-
-We will define function parameters as a bulleted list, where the
-parameter name will be backquoted and followed by its description.
-
-Example of a javascript function docstring::
-
- .. function:: loadxhtml(url, data, reqtype, mode)
-
- cubicweb loadxhtml plugin to make jquery handle xhtml response
-
- fetches `url` and replaces this's content with the result
-
- Its arguments are:
-
- * `url`
-
- * `mode`, how the replacement should be done (default is 'replace')
- Possible values are :
- - 'replace' to replace the node's content with the generated HTML
- - 'swap' to replace the node itself with the generated HTML
- - 'append' to append the generated HTML to the node's content
-
-
-Optional parameter specification
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Javascript functions handle arguments not listed in the function signature.
-In the javascript code, they will be flagged using `/* ... */`. In the docstring,
-we flag those optional arguments the same way we would define it in
-Python::
-
- .. function:: asyncRemoteExec(fname, arg1=None, arg2=None)
-
-
--- a/doc/book/en/annexes/faq.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,439 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Frequently Asked Questions (FAQ)
-================================
-
-
-Generalities
-````````````
-
-Why do you use the LGPL license to prevent me from doing X ?
-------------------------------------------------------------
-
-LGPL means that *if* you redistribute your application, you need to
-redistribute the changes you made to CubicWeb under the LGPL licence.
-
-Publishing a web site has nothing to do with redistributing source
-code according to the terms of the LGPL. A fair amount of companies
-use modified LGPL code for internal use. And someone could publish a
-*CubicWeb* component under a BSD licence for others to plug into a
-LGPL framework without any problem. The only thing we are trying to
-prevent here is someone taking the framework and packaging it as
-closed source to his own clients.
-
-Why does not CubicWeb have a template language ?
-------------------------------------------------
-
-There are enough template languages out there. You can use your
-preferred template language if you want. [explain how to use a
-template language]
-
-*CubicWeb* does not define its own templating language as this was
-not our goal. Based on our experience, we realized that
-we could gain productivity by letting designers use design tools
-and developpers develop without the use of the templating language
-as an intermediary that could not be anyway efficient for both parties.
-Python is the templating language that we use in *CubicWeb*, but again,
-it does not prevent you from using a templating language.
-
-Moreover, CubicWeb currently supports `simpletal`_ out of the box and
-it is also possible to use the `cwtags`_ library to build html trees
-using the `with statement`_ with more comfort than raw strings.
-
-.. _`simpletal`: http://www.owlfish.com/software/simpleTAL/
-.. _`cwtags`: http://www.cubicweb.org/project/cwtags
-.. _`with statement`: http://www.python.org/dev/peps/pep-0343/
-
-Why do you think using pure python is better than using a template language ?
------------------------------------------------------------------------------
-
-Python is an Object Oriented Programming language and as such it
-already provides a consistent and strong architecture and syntax
-a templating language would not reach.
-
-Using Python instead of a template langage for describing the user interface
-makes it to maintain with real functions/classes/contexts without the need of
-learning a new dialect. By using Python, we use standard OOP techniques and
-this is a key factor in a robust application.
-
-CubicWeb looks pretty recent. Is it stable ?
---------------------------------------------
-
-It is constantly evolving, piece by piece. The framework has evolved since
-2001 and data has been migrated from one schema to the other ever since. There
-is a well-defined way to handle data and schema migration.
-
-You can see the roadmap there:
-http://www.cubicweb.org/project/cubicweb?tab=projectroadmap_tab.
-
-
-Why is the RQL query language looking similar to X ?
-----------------------------------------------------
-
-It may remind you of SQL but it is higher level than SQL, more like
-SPARQL. Except that SPARQL did not exist when we started the project.
-With version 3.4, CubicWeb has support for SPARQL.
-
-The RQL language is what is going to make a difference with django-
-like frameworks for several reasons.
-
-1. accessing data is *much* easier with it. One can write complex
- queries with RQL that would be tedious to define and hard to maintain
- using an object/filter suite of method calls.
-
-2. it offers an abstraction layer allowing your applications to run
- on multiple back-ends. That means not only various SQL backends
- (postgresql, sqlite, mysql), but also multiple databases at the
- same time, and also non-SQL data stores like LDAP directories and
- subversion/mercurial repositories (see the `vcsfile`
- component). Google App Engine is yet another supported target for
- RQL.
-
-Which ajax library is CubicWeb using ?
---------------------------------------
-
-CubicWeb uses jQuery_ and provides a few helpers on top of that. Additionally,
-some jQuery plugins are provided (some are provided in specific cubes).
-
-.. _jQuery: http://jquery.com
-
-
-Development
-```````````
-
-How to change the instance logo ?
----------------------------------
-
-The logo is managed by css. You must provide a custom css that will contain
-the code below:
-
-::
-
- #logo {
- background-image: url("logo.jpg");
- }
-
-
-``logo.jpg`` is in ``mycube/data`` directory.
-
-How to create an anonymous user ?
----------------------------------
-
-This allows to browse the site without being authenticated. In the
-``all-in-one.conf`` file of your instance, define the anonymous user
-as follows ::
-
- # login of the CubicWeb user account to use for anonymous user (if you want to
- # allow anonymous)
- anonymous-user=anon
-
- # password of the CubicWeb user account matching login
- anonymous-password=anon
-
-You also must ensure that this `anon` user is a registered user of
-the DB backend. If not, you can create through the administation
-interface of your instance by adding a user with in the group `guests`.
-
-.. note::
- While creating a new instance, you can decide to allow access
- to anonymous user, which will automatically execute what is
- decribed above.
-
-How to load data from a python script ?
----------------------------------------
-Please, refer to :ref:`UsingPyro`.
-
-
-How to format an entity date attribute ?
-----------------------------------------
-
-If your schema has an attribute of type `Date` or `Datetime`, you usually want to
-format it when displaying it. First, you should define your preferred format
-using the site configuration panel
-``http://appurl/view?vid=systempropertiesform`` and then set ``ui.date`` and/or
-``ui.datetime``. Then in the view code, use:
-
-.. sourcecode:: python
-
- entity.printable_value(date_attribute)
-
-which will always return a string whatever the attribute's type (so it's
-recommended also for other attribute types). By default it expects to generate
-HTML, so it deals with rich text formating, xml escaping...
-
-How to update a database after a schema modification ?
-------------------------------------------------------
-
-It depends on what has been modified in the schema.
-
-* update the permissions and properties of an entity or a relation:
- ``sync_schema_props_perms('MyEntityOrRelation')``.
-
-* add an attribute: ``add_attribute('MyEntityType', 'myattr')``.
-
-* add a relation: ``add_relation_definition('SubjRelation', 'MyRelation', 'ObjRelation')``.
-
-I get `NoSelectableObject` exceptions, how do I debug selectors ?
------------------------------------------------------------------
-
-You just need to put the appropriate context manager around view/component
-selection. One standard place for components is in cubicweb/vregistry.py:
-
-.. sourcecode:: python
-
- def possible_objects(self, *args, **kwargs):
- """return an iterator on possible objects in this registry for the given
- context
- """
- from logilab.common.registry import traced_selection
- with traced_selection():
- for appobjects in self.itervalues():
- try:
- yield self._select_best(appobjects, *args, **kwargs)
- except NoSelectableObject:
- continue
-
-This will yield additional WARNINGs, like this::
-
- 2009-01-09 16:43:52 - (cubicweb.selectors) WARNING: selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
-
-For views, you can put this context in `cubicweb/web/views/basecontrollers.py` in
-the `ViewController`:
-
-.. sourcecode:: python
-
- def _select_view_and_rset(self, rset):
- ...
- try:
- from logilab.common.registry import traced_selection
- with traced_selection():
- view = self._cw.vreg['views'].select(vid, req, rset=rset)
- except ObjectNotFound:
- self.warning("the view %s could not be found", vid)
- req.set_message(req._("The view %s could not be found") % vid)
- vid = vid_from_rset(req, rset, self._cw.vreg.schema)
- view = self._cw.vreg['views'].select(vid, req, rset=rset)
- ...
-
-I get "database is locked" when executing tests
------------------------------------------------
-
-If you have "database is locked" as error when you are executing security tests,
-it is usually because commit or rollback are missing before login() calls.
-
-You can also use a context manager, to avoid such errors, as described
-here: :ref:`securitytest`.
-
-
-What are hooks used for ?
--------------------------
-
-Hooks are executed around (actually before or after) events. The most common
-events are data creation, update and deletion. They permit additional constraint
-checking (those not expressible at the schema level), pre and post computations
-depending on data movements.
-
-As such, they are a vital part of the framework.
-
-Other kinds of hooks, called Operations, are available
-for execution just before commit.
-
-For more information, read :ref:`hooks` section.
-
-
-Configuration
-`````````````
-
-How to configure a LDAP source ?
---------------------------------
-
-See :ref:`LDAP`.
-
-How to import LDAP users in |cubicweb| ?
-----------------------------------------
-
- Here is a useful script which enables you to import LDAP users
- into your *CubicWeb* instance by running the following:
-
-.. sourcecode:: python
-
- import os
- import pwd
- import sys
-
- from logilab.database import get_connection
-
- def getlogin():
- """avoid using os.getlogin() because of strange tty/stdin problems
- (man 3 getlogin)
- Another solution would be to use $LOGNAME, $USER or $USERNAME
- """
- return pwd.getpwuid(os.getuid())[0]
-
-
- try:
- database = sys.argv[1]
- except IndexError:
- print 'USAGE: python ldap2system.py <database>'
- sys.exit(1)
-
- if raw_input('update %s db ? [y/n]: ' % database).strip().lower().startswith('y'):
- cnx = get_connection(user=getlogin(), database=database)
- cursor = cnx.cursor()
-
- insert = ('INSERT INTO euser (creation_date, eid, modification_date, login, '
- ' firstname, surname, last_login_time, upassword) '
- "VALUES (%(mtime)s, %(eid)s, %(mtime)s, %(login)s, %(firstname)s, "
- "%(surname)s, %(mtime)s, './fqEz5LeZnT6');")
- update = "UPDATE entities SET source='system' WHERE eid=%(eid)s;"
- cursor.execute("SELECT eid,type,source,extid,mtime FROM entities WHERE source!='system'")
- for eid, type, source, extid, mtime in cursor.fetchall():
- if type != 'CWUser':
- print "don't know what to do with entity type", type
- continue
- if source != 'ldapuser':
- print "don't know what to do with source type", source
- continue
- ldapinfos = dict(x.strip().split('=') for x in extid.split(','))
- login = ldapinfos['uid']
- firstname = ldapinfos['uid'][0].upper()
- surname = ldapinfos['uid'][1:].capitalize()
- if login != 'jcuissinat':
- args = dict(eid=eid, type=type, source=source, login=login,
- firstname=firstname, surname=surname, mtime=mtime)
- print args
- cursor.execute(insert, args)
- cursor.execute(update, args)
-
- cnx.commit()
- cnx.close()
-
-
-Security
-````````
-
-How to reset the password for user joe ?
-----------------------------------------
-
-If you want to reset the admin password for ``myinstance``, do::
-
- $ cubicweb-ctl reset-admin-pwd myinstance
-
-You need to generate a new encrypted password::
-
- $ python
- >>> from cubicweb.server.utils import crypt_password
- >>> crypt_password('joepass')
- 'qHO8282QN5Utg'
- >>>
-
-and paste it in the database::
-
- $ psql mydb
- mydb=> update cw_cwuser set cw_upassword='qHO8282QN5Utg' where cw_login='joe';
- UPDATE 1
-
-if you're running over SQL Server, you need to use the CONVERT
-function to convert the string to varbinary(255). The SQL query is
-therefore::
-
- update cw_cwuser set cw_upassword=CONVERT(varbinary(255), 'qHO8282QN5Utg') where cw_login='joe';
-
-Be careful, the encryption algorithm is different on Windows and on
-Unix. You cannot therefore use a hash generated on Unix to fill in a
-Windows database, nor the other way round.
-
-
-You can prefer use a migration script similar to this shell invocation instead::
-
- $ cubicweb-ctl shell <instance>
- >>> from cubicweb import Binary
- >>> from cubicweb.server.utils import crypt_password
- >>> crypted = crypt_password('joepass')
- >>> rset = rql('Any U WHERE U is CWUser, U login "joe"')
- >>> joe = rset.get_entity(0,0)
- >>> joe.cw_set(upassword=Binary(crypted))
-
-Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file.
-
-The more experimented people would use RQL request directly::
-
- >>> rql('SET X upassword %(a)s WHERE X is CWUser, X login "joe"',
- ... {'a': crypted})
-
-I've just created a user in a group and it doesn't work !
----------------------------------------------------------
-
-You are probably getting errors such as ::
-
- remove {'PR': 'Project', 'C': 'CWUser'} from solutions since your_user has no read access to cost
-
-This is because you have to put your user in the "users" group. The user has to
-be in both groups.
-
-How is security implemented ?
-------------------------------
-
-The basis for security is a mapping from operations to groups or
-arbitrary RQL expressions. These mappings are scoped to entities and
-relations.
-
-This is an example for an Entity Type definition:
-
-.. sourcecode:: python
-
- class Version(EntityType):
- """a version is defining the content of a particular project's
- release"""
- # definition of attributes is voluntarily missing
- __permissions__ = {'read': ('managers', 'users', 'guests',),
- 'update': ('managers', 'logilab', 'owners'),
- 'delete': ('managers',),
- 'add': ('managers', 'logilab',
- ERQLExpression('X version_of PROJ, U in_group G, '
- 'PROJ require_permission P, '
- 'P name "add_version", P require_group G'),)}
-
-The above means that permission to read a Version is granted to any
-user that is part of one of the groups 'managers', 'users', 'guests'.
-The 'add' permission is granted to users in group 'managers' or
-'logilab' or to users in group G, if G is linked by a permission
-entity named "add_version" to the version's project.
-
-An example for a Relation Definition (RelationType both defines a
-relation type and implicitly one relation definition, on which the
-permissions actually apply):
-
-.. sourcecode:: python
-
- class version_of(RelationType):
- """link a version to its project. A version is necessarily linked
- to one and only one project. """
- # some lines voluntarily missing
- __permissions__ = {'read': ('managers', 'users', 'guests',),
- 'delete': ('managers', ),
- 'add': ('managers', 'logilab',
- RRQLExpression('O require_permission P, P name "add_version", '
- 'U in_group G, P require_group G'),) }
-
-The main difference lies in the basic available operations (there is
-no 'update' operation) and the usage of an RRQLExpression (rql
-expression for a relation) instead of an ERQLExpression (rql
-expression for an entity).
-
-You can find additional information in the section :ref:`securitymodel`.
-
-Is it possible to bypass security from the UI (web front) part ?
-----------------------------------------------------------------
-
-No. Only Hooks/Operations can do that.
-
-Can PostgreSQL and CubicWeb authentication work with kerberos ?
-----------------------------------------------------------------
-
-If you have PostgreSQL set up to accept kerberos authentication, you can set
-the db-host, db-name and db-user parameters in the `sources` configuration
-file while leaving the password blank. It should be enough for your
-instance to connect to postgresql with a kerberos ticket.
-
-
--- a/doc/book/en/annexes/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Part4:
-
-----------
-Appendixes
-----------
-
-The following chapters are reference material.
-
-.. toctree::
- :maxdepth: 1
- :numbered:
-
- faq
- rql/index
- mercurial
- depends
- docstrings-conventions
--- a/doc/book/en/annexes/mercurial.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _MercurialPresentation:
-
-Introducing Mercurial
-=====================
-
-Introduction
-````````````
-Mercurial_ manages a distributed repository containing revisions
-trees (each revision indicates the changes required to obtain the
-next, and so on). Locally, we have a repository containing revisions
-tree, and a working directory. It is possible
-to put in its working directory, one of the versions of its local repository,
-modify and then push it in its repository.
-It is also possible to get revisions from another repository or to export
-its own revisions from the local repository to another repository.
-
-.. _Mercurial: http://www.selenic.com/mercurial/
-
-In contrast to CVS/Subversion, we usually create a repository per
-project to manage.
-
-In a collaborative development, we usually create a central repository
-accessible to all developers of the project. These central repository is used
-as a reference. According to their needs, everyone can have a local repository,
-that they will have to synchronize with the central repository from time to time.
-
-
-Major commands
-``````````````
-* Create a local repository::
-
- hg clone ssh://myhost//home/src/repo
-
-* See the contents of the local repository (graphical tool in Qt)::
-
- hgview
-
-* Add a sub-directory or file in the current directory::
-
- hg add subdir
-
-* Move to the working directory a specific revision (or last
- revision) from the local repository::
-
- hg update [identifier-revision]
- hg up [identifier-revision]
-
-* Get in its local repository, the tree of revisions contained in a
- remote repository (this does not change the local directory)::
-
- hg pull ssh://myhost//home/src/repo
- hg pull -u ssh://myhost//home/src/repo # equivalent to pull + update
-
-* See what are the heads of branches of the local repository if a `pull`
- returned a new branch::
-
- hg heads
-
-* Submit the working directory in the local repository (and create a new
- revision)::
-
- hg commit
- hg ci
-
-* Merge with the mother revision of local directory, another revision from
- the local respository (the new revision will be then two mothers
- revisions)::
-
- hg merge identifier-revision
-
-* Export to a remote repository, the tree of revisions in its content
- local respository (this does not change the local directory)::
-
- hg push ssh://myhost//home/src/repo
-
-* See what local revisions are not in another repository::
-
- hg outgoing ssh://myhost//home/src/repo
-
-* See what are the revisions of a repository not found locally::
-
- hg incoming ssh://myhost//home/src/repo
-
-* See what is the revision of the local repository which has been taken out
- from the working directory and amended::
-
- hg parent
-
-* See the differences between the working directory and the mother revision
- of the local repository, possibly to submit them in the local repository::
-
- hg diff
- hg commit-tool
- hg ct
-
-
-Best Practices
-``````````````
-* Remember to `hg pull -u` regularly, and particularly before
- a `hg commit`.
-
-* Remember to `hg push` when your repository contains a version
- relatively stable of your changes.
-
-* If a `hg pull -u` created a new branch head:
-
- 1. find its identifier with `hg head`
- 2. merge with `hg merge`
- 3. `hg ci`
- 4. `hg push`
-
-Installation of the guestrepo extension
-```````````````````````````````````````
-
-Set up the guestrepo extension by getting a copy of the sources
-from https://bitbucket.org/selinc/guestrepo and adding the following
-lines to your ``~/.hgrc``: ::
-
- [extensions]
- guestrepo=/path/to/guestrepo/guestrepo
-
-
-More information
-````````````````
-
-For more information about Mercurial, please refer to the Mercurial project online documentation_.
-
-.. _documentation: http://www.selenic.com/mercurial/wiki/
-
Binary file doc/book/en/annexes/rql/Graph-ex.gif has changed
--- a/doc/book/en/annexes/rql/debugging.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _DEBUGGING:
-
-Debugging RQL
--------------
-
-Available levels
-~~~~~~~~~~~~~~~~
-
-Server debugging flags. They may be combined using binary operators.
-
-.. autodata:: cubicweb.server.DBG_NONE
-.. autodata:: cubicweb.server.DBG_RQL
-.. autodata:: cubicweb.server.DBG_SQL
-.. autodata:: cubicweb.server.DBG_REPO
-.. autodata:: cubicweb.server.DBG_MS
-.. autodata:: cubicweb.server.DBG_HOOKS
-.. autodata:: cubicweb.server.DBG_OPS
-.. autodata:: cubicweb.server.DBG_MORE
-.. autodata:: cubicweb.server.DBG_ALL
-
-
-Enable verbose output
-~~~~~~~~~~~~~~~~~~~~~
-
-To debug your RQL statements, it can be useful to enable a verbose output:
-
-.. sourcecode:: python
-
- from cubicweb import server
- server.set_debug(server.DBG_RQL|server.DBG_SQL|server.DBG_ALL)
-
-.. autofunction:: cubicweb.server.set_debug
-
-Another example showing how to debug hooks at a specific code site:
-
-.. sourcecode:: python
-
- from cubicweb.server import debugged, DBG_HOOKS
- with debugged(DBG_HOOKS):
- person.cw_set(works_for=company)
-
-
-Detect largest RQL queries
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-See `Profiling and performance` chapter (see :ref:`PROFILING`).
-
-
-API
-~~~
-
-.. autoclass:: cubicweb.server.debugged
-
--- a/doc/book/en/annexes/rql/implementation.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,159 +0,0 @@
-
-
-Implementation
---------------
-
-BNF grammar
-~~~~~~~~~~~
-
-The terminal elements are in capital letters, non-terminal in lowercase.
-The value of the terminal elements (between quotes) is a Python regular
-expression.
-::
-
- statement ::= (select | delete | insert | update) ';'
-
-
- # select specific rules
- select ::= 'DISTINCT'? E_TYPE selected_terms restriction? group? sort?
-
- selected_terms ::= expression ( ',' expression)*
-
- group ::= 'GROUPBY' VARIABLE ( ',' VARIABLE)*
-
- sort ::= 'ORDERBY' sort_term ( ',' sort_term)*
-
- sort_term ::= VARIABLE sort_method =?
-
- sort_method ::= 'ASC' | 'DESC'
-
-
- # delete specific rules
- delete ::= 'DELETE' (variables_declaration | relations_declaration) restriction?
-
-
- # insert specific rules
- insert ::= 'INSERT' variables_declaration ( ':' relations_declaration)? restriction?
-
-
- # update specific rules
- update ::= 'SET' relations_declaration restriction
-
-
- # common rules
- variables_declaration ::= E_TYPE VARIABLE (',' E_TYPE VARIABLE)*
-
- relations_declaration ::= simple_relation (',' simple_relation)*
-
- simple_relation ::= VARIABLE R_TYPE expression
-
- restriction ::= 'WHERE' relations
-
- relations ::= relation (LOGIC_OP relation)*
- | '(' relations')'
-
- relation ::= 'NOT'? VARIABLE R_TYPE COMP_OP? expression
- | 'NOT'? R_TYPE VARIABLE 'IN' '(' expression (',' expression)* ')'
-
- expression ::= var_or_func_or_const (MATH_OP var_or_func_or_const) *
- | '(' expression ')'
-
- var_or_func_or_const ::= VARIABLE | function | constant
-
- function ::= FUNCTION '(' expression ( ',' expression) * ')'
-
- constant ::= KEYWORD | STRING | FLOAT | INT
-
- # tokens
- LOGIC_OP ::= ',' | 'OR' | 'AND'
- MATH_OP ::= '+' | '-' | '/' | '*'
- COMP_OP ::= '>' | '>=' | '=' | '<=' | '<' | '~=' | 'LIKE'
-
- FUNCTION ::= 'MIN' | 'MAX' | 'SUM' | 'AVG' | 'COUNT' | 'UPPER' | 'LOWER'
-
- VARIABLE ::= '[A-Z][A-Z0-9]*'
- E_TYPE ::= '[A-Z]\w*'
- R_TYPE ::= '[a-z_]+'
-
- KEYWORD ::= 'TRUE' | 'FALSE' | 'NULL' | 'TODAY' | 'NOW'
- STRING ::= "'([^'\]|\\.)*'" |'"([^\"]|\\.)*\"'
- FLOAT ::= '\d+\.\d*'
- INT ::= '\d+'
-
-
-Internal representation (syntactic tree)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The tree research does not contain the selected variables
-(e.g. there is only what follows "WHERE").
-
-The insertion tree does not contain the variables inserted or relations
-defined on these variables (e.g. there is only what follows "WHERE").
-
-The removal tree does not contain the deleted variables and relations
-(e.g. there is only what follows the "WHERE").
-
-The update tree does not contain the variables and relations updated
-(e.g. there is only what follows the "WHERE").
-
-::
-
- Select ((Relationship | And | Or)?, Group?, Sort?)
- Insert (Relations | And | Or)?
- Delete (Relationship | And | Or)?
- Update (Relations | And | Or)?
-
- And ((Relationship | And | Or), (Relationship | And | Or))
- Or ((Relationship | And | Or), (Relationship | And | Or))
-
- Relationship ((VariableRef, Comparison))
-
- Comparison ((Function | MathExpression | Keyword | Constant | VariableRef) +)
-
- Function (())
- MathExpression ((MathExpression | Keyword | Constant | VariableRef), (MathExpression | Keyword | Constant | VariableRef))
-
- Group (VariableRef +)
- Sort (SortTerm +)
- SortTerm (VariableRef +)
-
- VariableRef ()
- Variable ()
- Keyword ()
- Constant ()
-
-
-Known limitations
-~~~~~~~~~~~~~~~~~
-
-- The current implementation does not support linking two relations of type 'is'
- with an OR. I do not think that the negation is supported on this type of
- relation (XXX to be confirmed).
-
-- missing COALESCE and certainly other things...
-
-- writing an rql query requires knowledge of the used schema (with real relation
- names and entities, not those viewed in the user interface). On the other
- hand, we cannot really bypass that, and it is the job of a user interface to
- hide the RQL.
-
-
-Topics
-~~~~~~
-
-It would be convenient to express the schema matching
-relations (non-recursive rules)::
-
- Document class Type <-> Document occurence_of Fiche class Type
- Sheet class Type <-> Form collection Collection class Type
-
-Therefore 1. becomes::
-
- Document X where
- X class C, C name 'Cartoon'
- X owned_by U, U login 'syt'
- X available true
-
-I'm not sure that we should handle this at RQL level ...
-
-There should also be a special relation 'anonymous'.
--- a/doc/book/en/annexes/rql/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-.. _RQLChapter:
-
-Relation Query Language (RQL)
-=============================
-
-This chapter describes the Relation Query Language syntax and its implementation in CubicWeb.
-
-.. toctree::
- :maxdepth: 2
-
- intro
- language
- debugging
- implementation
--- a/doc/book/en/annexes/rql/intro.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,162 +0,0 @@
-
-.. _rql_intro:
-
-Introduction
-------------
-
-Goals of RQL
-~~~~~~~~~~~~
-
-The goal is to have a semantic language in order to:
-
-- query relations in a clear syntax
-- empowers access to data repository manipulation
-- making attributes/relations browsing easy
-
-As such, attributes will be regarded as cases of special relations (in
-terms of usage, the user should see no syntactic difference between an
-attribute and a relation).
-
-Comparison with existing languages
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-SQL
-```
-
-RQL may remind of SQL but works at a higher abstraction level (the *CubicWeb*
-framework generates SQL from RQL to fetch data from relation databases). RQL is
-focused on browsing relations. The user needs only to know about the *CubicWeb*
-data model he is querying, but not about the underlying SQL model.
-
-Sparql
-``````
-
-The query language most similar to RQL is SPARQL_, defined by the W3C to serve
-for the semantic web.
-
-Versa
-`````
-
-We should look in more detail, but here are already some ideas for the moment
-... Versa_ is the language most similar to what we wanted to do, but the model
-underlying data being RDF, there are some things such as namespaces or
-handling of the RDF types which does not interest us. On the functionality
-level, Versa_ is very comprehensive including through many functions of
-conversion and basic types manipulation, which we may want to look at one time
-or another. Finally, the syntax is a little esoteric.
-
-Datalog
-```````
-
-Datalog_ is a prolog derived query langage which applies to relational
-databases. It is more expressive than RQL in that it accepts either
-extensional_ and intensional_ predicates (or relations). As of now,
-RQL only deals with intensional relations.
-
-The different types of queries
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Search (`Any`)
- Extract entities and attributes of entities.
-
-Insert entities (`INSERT`)
- Insert new entities or relations in the database.
- It can also directly create relationships for the newly created entities.
-
-Update entities, create relations (`SET`)
- Update existing entities in the database,
- or create relations between existing entities.
-
-Delete entities or relationship (`DELETE`)
- Remove entities or relations existing in the database.
-
-
-RQL relation expressions
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-RQL expressions apply to a live database defined by a
-:ref:`datamodel_definition`. Apart from the main type, or head, of the
-expression (search, insert, etc.) the most common constituent of an
-RQL expression is a (set of) relation expression(s).
-
-An RQL relation expression contains three components:
-
-* the subject, which is an entity type
-* the predicate, which is a relation definition (an arc of the schema)
-* the object, which is either an attribute or a relation to another entity
-
-.. image:: Graph-ex.gif
- :alt: <subject> <predicate> <object>
- :align: center
-
-.. warning::
-
- A relation is always expressed in the order: ``subject``,
- ``predicate``, ``object``.
-
- It is important to determine if the entity type is subject or object
- to construct a valid expression. Inverting the subject/object is an
- error since the relation cannot be found in the schema.
-
- If one does not have access to the code, one can find the order by
- looking at the schema image in manager views (the subject is located
- at the beginning of the arrow).
-
-An example of two related relation expressions::
-
- P works_for C, P name N
-
-RQL variables represent typed entities. The type of entities is
-either automatically inferred (by looking at the possible relation
-definitions, see :ref:`RelationDefinition`) or explicitely constrained
-using the ``is`` meta relation.
-
-In the example above, we barely need to look at the schema. If
-variable names (in the RQL expression) and relation type names (in the
-schema) are expresssively designed, the human reader can infer as much
-as the |cubicweb| querier.
-
-The ``P`` variable is used twice but it always represent the same set
-of entities. Hence ``P works_for C`` and ``P name N`` must be
-compatible in the sense that all the Ps (which *can* refer to
-different entity types) must accept the ``works_for`` and ``name``
-relation types. This does restrict the set of possible values of P.
-
-Adding another relation expression::
-
- P works_for C, P name N, C name "logilab"
-
-This further restricts the possible values of P through an indirect
-constraint on the possible values of ``C``. The RQL-level unification_
-happening there is translated to one (or several) joins_ at the
-database level.
-
-.. note::
-
- In |cubicweb|, the term `relation` is often found without ambiguity
- instead of `predicate`. This predicate is also known as the
- `property` of the triple in `RDF concepts`_
-
-
-RQL Operators
-~~~~~~~~~~~~~
-
-An RQL expression's head can be completed using various operators such
-as ``ORDERBY``, ``GROUPBY``, ``HAVING``, ``LIMIT`` etc.
-
-RQL relation expressions can be grouped with ``UNION`` or
-``WITH``. Predicate oriented keywords such as ``EXISTS``, ``OR``,
-``NOT`` are available.
-
-The complete zoo of RQL operators is described extensively in the
-following chapter (:ref:`RQL`).
-
-.. _RDF concepts: http://www.w3.org/TR/rdf-concepts/
-.. _Versa: http://wiki.xml3k.org/Versa
-.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/
-.. _unification: http://en.wikipedia.org/wiki/Unification_(computing)
-.. _joins: http://en.wikipedia.org/wiki/Join_(SQL)
-.. _Datalog: http://en.wikipedia.org/wiki/Datalog
-.. _intensional: http://en.wikipedia.org/wiki/Intensional_definition
-.. _extensional: http://en.wikipedia.org/wiki/Extension_(predicate_logic)
-
--- a/doc/book/en/annexes/rql/language.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,804 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _RQL:
-
-RQL syntax
-----------
-
-.. _RQLKeywords:
-
-Reserved keywords
-~~~~~~~~~~~~~~~~~
-
-::
-
- AND, ASC, BEING, DELETE, DESC, DISTINCT, EXISTS, FALSE, GROUPBY,
- HAVING, ILIKE, INSERT, LIKE, LIMIT, NOT, NOW, NULL, OFFSET,
- OR, ORDERBY, SET, TODAY, TRUE, UNION, WHERE, WITH
-
-The keywords are not case sensitive. You should not use them when defining your
-schema, or as RQL variable names.
-
-
-.. _RQLCase:
-
-Case
-~~~~
-
-* Variables should be all upper-cased.
-
-* Relation should be all lower-cased and match exactly names of relations defined
- in the schema.
-
-* Entity types should start with an upper cased letter and be followed by at least
- a lower cased latter.
-
-
-.. _RQLVariables:
-
-Variables and typing
-~~~~~~~~~~~~~~~~~~~~
-
-Entities and values to browse and/or select are represented in the query by
-*variables* that must be written in capital letters.
-
-With RQL, we do not distinguish between entities and attributes. The value of an
-attribute is considered as an entity of a particular type (see below), linked to
-one (real) entity by a relation called the name of the attribute, where the
-entity is the subject and the attribute the object.
-
-The possible type(s) for each variable is derived from the schema according to
-the constraints expressed above and thanks to the relations between each
-variable.
-
-We can restrict the possible types for a variable using the special relation
-**is** in the restrictions.
-
-
-.. _VirtualRelations:
-
-Virtual relations
-~~~~~~~~~~~~~~~~~
-
-Those relations may only be used in RQL query but are not actual attributes of
-your entities.
-
-* `has_text`: relation to use to query the full text index (only for entities
- having fulltextindexed attributes).
-
-* `identity`: relation to use to tell that a RQL variable is the same as another
- when you've to use two different variables for querying purpose. On the
- opposite it's also useful together with the ``NOT`` operator to tell that two
- variables should not identify the same entity
-
-
-.. _RQLLiterals:
-
-Literal expressions
-~~~~~~~~~~~~~~~~~~~
-
-Bases types supported by RQL are those supported by yams schema. Literal values
-are expressed as explained below:
-
-* string should be between double or single quotes. If the value contains a
- quote, it should be preceded by a backslash '\\'
-
-* floats separator is dot '.'
-
-* boolean values are ``TRUE`` and ``FALSE`` keywords
-
-* date and time should be expressed as a string with ISO notation : YYYY/MM/DD
- [hh:mm], or using keywords ``TODAY`` and ``NOW``
-
-You may also use the ``NULL`` keyword, meaning 'unspecified'.
-
-
-.. _RQLOperators:
-
-Operators
-~~~~~~~~~
-
-.. _RQLLogicalOperators:
-
-Logical operators
-`````````````````
-::
-
- AND, OR, NOT, ','
-
-',' is equivalent to 'AND' but with the smallest among the priority of logical
-operators (see :ref:`RQLOperatorsPriority`).
-
-.. _RQLMathematicalOperators:
-
-Mathematical operators
-``````````````````````
-
-+----------+---------------------+-----------+--------+
-| Operator | Description | Example | Result |
-+==========+=====================+===========+========+
-| `+` | addition | 2 + 3 | 5 |
-+----------+---------------------+-----------+--------+
-| `-` | subtraction | 2 - 3 | -1 |
-+----------+---------------------+-----------+--------+
-| `*` | multiplication | 2 * 3 | 6 |
-+----------+---------------------+-----------+--------+
-| / | division | 4 / 2 | 2 |
-+----------+---------------------+-----------+--------+
-| % | modulo (remainder) | 5 % 4 | 1 |
-+----------+---------------------+-----------+--------+
-| ^ | exponentiation | 2.0 ^ 3.0 | 8 |
-+----------+---------------------+-----------+--------+
-| & | bitwise AND | 91 & 15 | 11 |
-+----------+---------------------+-----------+--------+
-| `|` | bitwise OR | 32 | 3 | 35 |
-+----------+---------------------+-----------+--------+
-| # | bitwise XOR | 17 # 5 | 20 |
-+----------+---------------------+-----------+--------+
-| ~ | bitwise NOT | ~1 | -2 |
-+----------+---------------------+-----------+--------+
-| << | bitwise shift left | 1 << 4 | 16 |
-+----------+---------------------+-----------+--------+
-| >> | bitwise shift right | 8 >> 2 | 2 |
-+----------+---------------------+-----------+--------+
-
-
-Notice integer division truncates results depending on the backend behaviour. For
-instance, postgresql does.
-
-
-.. _RQLComparisonOperators:
-
-Comparison operators
-````````````````````
- ::
-
- =, !=, <, <=, >=, >, IN
-
-
-The syntax to use comparison operators is:
-
- `VARIABLE attribute <operator> VALUE`
-
-The `=` operator is the default operator and can be omitted, i.e. :
-
- `VARIABLE attribute = VALUE`
-
-is equivalent to
-
- `VARIABLE attribute VALUE`
-
-
-The operator `IN` provides a list of possible values:
-
-.. sourcecode:: sql
-
- Any X WHERE X name IN ('chauvat', 'fayolle', 'di mascio', 'thenault')
-
-
-.. _RQLStringOperators:
-
-String operators
-````````````````
-::
-
- LIKE, ILIKE, ~=, REGEXP
-
-The ``LIKE`` string operator can be used with the special character `%` in
-a string as wild-card:
-
-.. sourcecode:: sql
-
- -- match every entity whose name starts with 'Th'
- Any X WHERE X name ~= 'Th%'
- -- match every entity whose name endswith 'lt'
- Any X WHERE X name LIKE '%lt'
- -- match every entity whose name contains a 'l' and a 't'
- Any X WHERE X name LIKE '%l%t%'
-
-``ILIKE`` is the case insensitive version of ``LIKE``. It's not
-available on all backend (e.g. sqlite doesn't support it). If not available for
-your backend, ``ILIKE`` will behave like ``LIKE``.
-
-`~=` is a shortcut version of ``ILIKE``, or of ``LIKE`` when the
-former is not available on the back-end.
-
-
-The ``REGEXP`` is an alternative to ``LIKE`` that supports POSIX
-regular expressions:
-
-.. sourcecode:: sql
-
- -- match entities whose title starts with a digit
- Any X WHERE X title REGEXP "^[0-9].*"
-
-
-The underlying SQL operator used is back-end-dependent :
-
-- the ``~`` operator is used for postgresql,
-- the ``REGEXP`` operator for mysql and sqlite.
-
-Other back-ends are not supported yet.
-
-
-.. _RQLOperatorsPriority:
-
-Operators priority
-``````````````````
-
-#. `(`, `)`
-#. `^`, `<<`, `>>`
-#. `*`, `/`, `%`, `&`
-#. `+`, `-`, `|`, `#`
-#. `NOT`
-#. `AND`
-#. `OR`
-#. `,`
-
-
-.. _RQLSearchQuery:
-
-Search Query
-~~~~~~~~~~~~
-
-Simplified grammar of search query: ::
-
- [ `DISTINCT`] `Any` V1 (, V2) \*
- [ `GROUPBY` V1 (, V2) \*] [ `ORDERBY` <orderterms>]
- [ `LIMIT` <value>] [ `OFFSET` <value>]
- [ `WHERE` <triplet restrictions>]
- [ `WITH` V1 (, V2)\* BEING (<query>)]
- [ `HAVING` <other restrictions>]
- [ `UNION` <query>]
-
-Selection
-`````````
-
-The fist occuring clause is the selection of terms that should be in the result
-set. Terms may be variable, literals, function calls, arithmetic, etc. and each
-term is separated by a comma.
-
-There will be as much column in the result set as term in this clause, respecting
-order.
-
-Syntax for function call is somewhat intuitive, for instance:
-
-.. sourcecode:: sql
-
- Any UPPER(N) WHERE P firstname N
-
-
-Grouping and aggregating
-````````````````````````
-
-The ``GROUPBY`` keyword is followed by a list of terms on which results
-should be grouped. They are usually used with aggregate functions, responsible to
-aggregate values for each group (see :ref:`RQLAggregateFunctions`).
-
-For grouped queries, all selected variables must be either aggregated (i.e. used
-by an aggregate function) or grouped (i.e. listed in the ``GROUPBY``
-clause).
-
-
-Sorting
-```````
-
-The ``ORDERBY`` keyword if followed by the definition of the selection
-order: variable or column number followed by sorting method (``ASC``,
-``DESC``), ``ASC`` being the default. If the sorting method is not
-specified, then the sorting is ascendant (`ASC`).
-
-
-Pagination
-``````````
-
-The ``LIMIT`` and ``OFFSET`` keywords may be respectively used to
-limit the number of results and to tell from which result line to start (for
-instance, use `LIMIT 20` to get the first 20 results, then `LIMIT 20 OFFSET 20`
-to get the next 20.
-
-
-Restrictions
-````````````
-
-The ``WHERE`` keyword introduce one of the "main" part of the query, where
-you "define" variables and add some restrictions telling what you're interested
-in.
-
-It's a list of triplets "subject relation object", e.g. `V1 relation
-(V2 | <static value>)`. Triplets are separated using :ref:`RQLLogicalOperators`.
-
-.. note::
-
- About the negation operator (``NOT``):
-
- * ``NOT X relation Y`` is equivalent to ``NOT EXISTS(X relation Y)``
-
- * ``Any X WHERE NOT X owned_by U`` means "entities that have no relation
- ``owned_by``".
-
- * ``Any X WHERE NOT X owned_by U, U login "syt"`` means "the entity have no
- relation ``owned_by`` with the user syt". They may have a relation "owned_by"
- with another user.
-
-In this clause, you can also use ``EXISTS`` when you want to know if some
-expression is true and do not need the complete set of elements that make it
-true. Testing for existence is much faster than fetching the complete set of
-results, especially when you think about using ``OR`` against several expressions. For instance
-if you want to retrieve versions which are in state "ready" or tagged by
-"priority", you should write :
-
-.. sourcecode:: sql
-
- Any X ORDERBY PN,N
- WHERE X num N, X version_of P, P name PN,
- EXISTS(X in_state S, S name "ready")
- OR EXISTS(T tags X, T name "priority")
-
-not
-
-.. sourcecode:: sql
-
- Any X ORDERBY PN,N
- WHERE X num N, X version_of P, P name PN,
- (X in_state S, S name "ready")
- OR (T tags X, T name "priority")
-
-Both queries aren't at all equivalent :
-
-* the former will retrieve all versions, then check for each one which are in the
- matching state of or tagged by the expected tag,
-
-* the later will retrieve all versions, state and tags (cartesian product!),
- compute join and then exclude each row which are in the matching state or
- tagged by the expected tag. This implies that you won't get any result if the
- in_state or tag tables are empty (ie there is no such relation in the
- application). This is usually NOT what you want.
-
-Another common case where you may want to use ``EXISTS`` is when you
-find yourself using ``DISTINCT`` at the beginning of your query to
-remove duplicate results. The typical case is when you have a
-multivalued relation such as Version version_of Project and you want
-to retrieve projects which have a version:
-
-.. sourcecode:: sql
-
- Any P WHERE V version_of P
-
-will return each project number of versions times. So you may be
-tempted to use:
-
-.. sourcecode:: sql
-
- DISTINCT Any P WHERE V version_of P
-
-This will work, but is not efficient, as it will use the ``SELECT
-DISTINCT`` SQL predicate, which needs to retrieve all projects, then
-sort them and discard duplicates, which can have a very high cost for
-large result sets. So the best way to write this is:
-
-.. sourcecode:: sql
-
- Any P WHERE EXISTS(V version_of P)
-
-
-You can also use the question mark (`?`) to mark optional relations. This allows
-you to select entities related **or not** to another. It is a similar concept
-to `Left outer join`_:
-
- the result of a left outer join (or simply left join) for table A and B
- always contains all records of the "left" table (A), even if the
- join-condition does not find any matching record in the "right" table (B).
-
-You must use the `?` behind a variable to specify that the relation to
-that variable is optional. For instance:
-
-- Bugs of a project attached or not to a version
-
- .. sourcecode:: sql
-
- Any X, V WHERE X concerns P, P eid 42, X corrected_in V?
-
- You will get a result set containing all the project's tickets, with either the
- version in which it's fixed or None for tickets not related to a version.
-
-
-- All cards and the project they document if any
-
- .. sourcecode:: sql
-
- Any C, P WHERE C is Card, P? documented_by C
-
-Notice you may also use outer join:
-
-- on the RHS of attribute relation, e.g.
-
- .. sourcecode:: sql
-
- Any X WHERE X ref XR, Y name XR?
-
- so that Y is outer joined on X by ref/name attributes comparison
-
-
-- on any side of an ``HAVING`` expression, e.g.
-
- .. sourcecode:: sql
-
- Any X WHERE X creation_date XC, Y creation_date YC
- HAVING YEAR(XC)=YEAR(YC)?
-
- so that Y is outer joined on X by comparison of the year extracted from their
- creation date.
-
- .. sourcecode:: sql
-
- Any X WHERE X creation_date XC, Y creation_date YC
- HAVING YEAR(XC)?=YEAR(YC)
-
- would outer join X on Y instead.
-
-
-Having restrictions
-```````````````````
-
-The ``HAVING`` clause, as in SQL, may be used to restrict a query
-according to value returned by an aggregate function, e.g.
-
-.. sourcecode:: sql
-
- Any X GROUPBY X WHERE X relation Y HAVING COUNT(Y) > 10
-
-It may however be used for something else: In the ``WHERE`` clause, we are
-limited to triplet expressions, so some things may not be expressed there. Let's
-take an example : if you want to get people whose upper-cased first name equals to
-another person upper-cased first name. There is no proper way to express this
-using triplet, so you should use something like:
-
-.. sourcecode:: sql
-
- Any X WHERE X firstname XFN, Y firstname YFN, NOT X identity Y HAVING UPPER(XFN) = UPPER(YFN)
-
-Another example: imagine you want person born in 2000:
-
-.. sourcecode:: sql
-
- Any X WHERE X birthday XB HAVING YEAR(XB) = 2000
-
-Notice that while we would like this to work without the HAVING clause, this
-can't be currently be done because it introduces an ambiguity in RQL's grammar
-that can't be handled by Yapps_, the parser's generator we're using.
-
-
-Sub-queries
-```````````
-
-The ``WITH`` keyword introduce sub-queries clause. Each sub-query has the
-form:
-
- V1(,V2) BEING (rql query)
-
-Variables at the left of the ``BEING`` keyword defines into which
-variables results from the sub-query will be mapped to into the outer query.
-Sub-queries are separated from each other using a comma.
-
-Let's say we want to retrieve for each project its number of versions and its
-number of tickets. Due to the nature of relational algebra behind the scene, this
-can't be achieved using a single query. You have to write something along the
-line of:
-
-.. sourcecode:: sql
-
- Any X, VC, TC WHERE X identity XX
- WITH X, VC BEING (Any X, COUNT(V) GROUPBY X WHERE V version_of X),
- XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
-
-Notice that we can't reuse a same variable name as alias for two different
-sub-queries, hence the usage of 'X' and 'XX' in this example, which are then
-unified using the special `identity` relation (see :ref:`VirtualRelations`).
-
-.. warning::
-
- Sub-queries define a new variable scope, so even if a variable has the same name
- in the outer query and in the sub-query, they technically **aren't** the same
- variable. So:
-
- .. sourcecode:: sql
-
- Any W, REF WITH W, REF BEING
- (Any W, REF WHERE W is Workcase, W ref REF,
- W concerned_by D, D name "Logilab")
-
- could be written:
-
- .. sourcecode:: sql
-
- Any W, REF WITH W, REF BEING
- (Any W1, REF1 WHERE W1 is Workcase, W1 ref REF1,
- W1 concerned_by D, D name "Logilab")
-
- Also, when a variable is coming from a sub-query, you currently can't reference
- its attribute or inlined relations in the outer query, you've to fetch them in
- the sub-query. For instance, let's say we want to sort by project name in our
- first example, we would have to write:
-
- .. sourcecode:: sql
-
-
- Any X, VC, TC ORDERBY XN WHERE X identity XX
- WITH X, XN, VC BEING (Any X, COUNT(V) GROUPBY X,XN WHERE V version_of X, X name XN),
- XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
-
- instead of:
-
- .. sourcecode:: sql
-
- Any X, VC, TC ORDERBY XN WHERE X identity XX, X name XN,
- WITH X, XN, VC BEING (Any X, COUNT(V) GROUPBY X WHERE V version_of X),
- XX, TC BEING (Any X, COUNT(T) GROUPBY X WHERE T ticket_of X)
-
- which would result in a SQL execution error.
-
-
-Union
-`````
-
-You may get a result set containing the concatenation of several queries using
-the ``UNION``. The selection of each query should have the same number of
-columns.
-
-.. sourcecode:: sql
-
- (Any X, XN WHERE X is Person, X surname XN) UNION (Any X,XN WHERE X is Company, X name XN)
-
-
-.. _RQLFunctions:
-
-Available functions
-~~~~~~~~~~~~~~~~~~~
-
-Below is the list of aggregate and transformation functions that are supported
-nativly by the framework. Notice that cubes may define additional functions.
-
-.. _RQLAggregateFunctions:
-
-Aggregate functions
-```````````````````
-+------------------------+----------------------------------------------------------+
-| ``COUNT(Any)`` | return the number of rows |
-+------------------------+----------------------------------------------------------+
-| ``MIN(Any)`` | return the minimum value |
-+------------------------+----------------------------------------------------------+
-| ``MAX(Any)`` | return the maximum value |
-+------------------------+----------------------------------------------------------+
-| ``AVG(Any)`` | return the average value |
-+------------------------+----------------------------------------------------------+
-| ``SUM(Any)`` | return the sum of values |
-+------------------------+----------------------------------------------------------+
-| ``COMMA_JOIN(String)`` | return each value separated by a comma (for string only) |
-+------------------------+----------------------------------------------------------+
-
-All aggregate functions above take a single argument. Take care some aggregate
-functions (e.g. ``MAX``, ``MIN``) may return `None` if there is no
-result row.
-
-.. _RQLStringFunctions:
-
-String transformation functions
-```````````````````````````````
-
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``UPPER(String)`` | upper case the string |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``LOWER(String)`` | lower case the string |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``LENGTH(String)`` | return the length of the string |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``SUBSTRING(String, start, length)`` | extract from the string a string starting at given index and of |
-| | given length |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``LIMIT_SIZE(String, max size)`` | if the length of the string is greater than given max size, |
-| | strip it and add ellipsis ("..."). The resulting string will |
-| | hence have max size + 3 characters |
-+-----------------------------------------------+-----------------------------------------------------------------+
-| ``TEXT_LIMIT_SIZE(String, format, max size)`` | similar to the above, but allow to specify the MIME type of the |
-| | text contained by the string. Supported formats are text/html, |
-| | text/xhtml and text/xml. All others will be considered as plain |
-| | text. For non plain text format, sgml tags will be first removed|
-| | before limiting the string. |
-+-----------------------------------------------+-----------------------------------------------------------------+
-
-.. _RQLDateFunctions:
-
-Date extraction functions
-`````````````````````````
-
-+----------------------+----------------------------------------+
-| ``YEAR(Date)`` | return the year of a date or datetime |
-+----------------------+----------------------------------------+
-| ``MONTH(Date)`` | return the month of a date or datetime |
-+----------------------+----------------------------------------+
-| ``DAY(Date)`` | return the day of a date or datetime |
-+----------------------+----------------------------------------+
-| ``HOUR(Datetime)`` | return the hours of a datetime |
-+----------------------+----------------------------------------+
-| ``MINUTE(Datetime)`` | return the minutes of a datetime |
-+----------------------+----------------------------------------+
-| ``SECOND(Datetime)`` | return the seconds of a datetime |
-+----------------------+----------------------------------------+
-| ``WEEKDAY(Date)`` | return the day of week of a date or |
-| | datetime. Sunday == 1, Saturday == 7. |
-+----------------------+----------------------------------------+
-
-.. _RQLOtherFunctions:
-
-Other functions
-```````````````
-+-------------------+--------------------------------------------------------------------+
-| ``ABS(num)`` | return the absolute value of a number |
-+-------------------+--------------------------------------------------------------------+
-| ``RANDOM()`` | return a pseudo-random value from 0.0 to 1.0 |
-+-------------------+--------------------------------------------------------------------+
-| ``FSPATH(X)`` | expect X to be an attribute whose value is stored in a |
-| | :class:`BFSStorage` and return its path on the file system |
-+-------------------+--------------------------------------------------------------------+
-| ``FTIRANK(X)`` | expect X to be an entity used in a has_text relation, and return a |
-| | number corresponding to the rank order of each resulting entity |
-+-------------------+--------------------------------------------------------------------+
-| ``CAST(Type, X)`` | expect X to be an attribute and return it casted into the given |
-| | final type |
-+-------------------+--------------------------------------------------------------------+
-
-
-.. _RQLExamples:
-
-Examples
-~~~~~~~~
-
-- *Search for the object of identifier 53*
-
- .. sourcecode:: sql
-
- Any X WHERE X eid 53
-
-- *Search material such as comics, owned by syt and available*
-
- .. sourcecode:: sql
-
- Any X WHERE X is Document,
- X occurence_of F, F class C, C name 'Comics',
- X owned_by U, U login 'syt',
- X available TRUE
-
-- *Looking for people working for eurocopter interested in training*
-
- .. sourcecode:: sql
-
- Any P WHERE P is Person, P work_for S, S name 'Eurocopter',
- P interested_by T, T name 'training'
-
-- *Search note less than 10 days old written by jphc or ocy*
-
- .. sourcecode:: sql
-
- Any N WHERE N is Note, N written_on D, D day> (today -10),
- N written_by P, P name 'jphc' or P name 'ocy'
-
-- *Looking for people interested in training or living in Paris*
-
- .. sourcecode:: sql
-
- Any P WHERE P is Person, EXISTS(P interested_by T, T name 'training') OR
- (P city 'Paris')
-
-- *The surname and firstname of all people*
-
- .. sourcecode:: sql
-
- Any N, P WHERE X is Person, X name N, X firstname P
-
- Note that the selection of several entities generally force
- the use of "Any" because the type specification applies otherwise
- to all the selected variables. We could write here
-
- .. sourcecode:: sql
-
- String N, P WHERE X is Person, X name N, X first_name P
-
-
- Note: You can not specify several types with * ... where X is FirstType or X is SecondType*.
- To specify several types explicitly, you have to do
-
-
- .. sourcecode:: sql
-
- Any X WHERE X is IN (FirstType, SecondType)
-
-
-.. _RQLInsertQuery:
-
-Insertion query
-~~~~~~~~~~~~~~~
-
- `INSERT` <entity type> V1 (, <entity type> V2) \ * `:` <assignments>
- [ `WHERE` <restriction>]
-
-:assignments:
- list of relations to assign in the form `V1 relationship V2 | <static value>`
-
-The restriction can define variables used in assignments.
-
-Caution, if a restriction is specified, the insertion is done for
-*each line result returned by the restriction*.
-
-- *Insert a new person named 'foo'*
-
- .. sourcecode:: sql
-
- INSERT Person X: X name 'foo'
-
-- *Insert a new person named 'foo', another called 'nice' and a 'friend' relation
- between them*
-
- .. sourcecode:: sql
-
- INSERT Person X, Person Y: X name 'foo', Y name 'nice', X friend Y
-
-- *Insert a new person named 'foo' and a 'friend' relation with an existing
- person called 'nice'*
-
- .. sourcecode:: sql
-
- INSERT Person X: X name 'foo', X friend Y WHERE Y name 'nice'
-
-.. _RQLSetQuery:
-
-Update and relation creation queries
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- `SET` <assignements>
- [ `WHERE` <restriction>]
-
-Caution, if a restriction is specified, the update is done *for
-each result line returned by the restriction*.
-
-- *Renaming of the person named 'foo' to 'bar' with the first name changed*
-
- .. sourcecode:: sql
-
- SET X name 'bar', X firstname 'original' WHERE X is Person, X name 'foo'
-
-- *Insert a relation of type 'know' between objects linked by
- the relation of type 'friend'*
-
- .. sourcecode:: sql
-
- SET X know Y WHERE X friend Y
-
-
-.. _RQLDeleteQuery:
-
-Deletion query
-~~~~~~~~~~~~~~
-
- `DELETE` (<entity type> V) | (V1 relation v2 ),...
- [ `WHERE` <restriction>]
-
-Caution, if a restriction is specified, the deletion is made *for
-each line result returned by the restriction*.
-
-- *Deletion of the person named 'foo'*
-
- .. sourcecode:: sql
-
- DELETE Person X WHERE X name 'foo'
-
-- *Removal of all relations of type 'friend' from the person named 'foo'*
-
- .. sourcecode:: sql
-
- DELETE X friend Y WHERE X is Person, X name 'foo'
-
-
-.. _Yapps: http://theory.stanford.edu/~amitp/yapps/
-.. _Left outer join: http://en.wikipedia.org/wiki/Join_(SQL)#Left_outer_join
-
--- a/doc/book/en/conf.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,225 +0,0 @@
-# -*- coding: utf-8 -*-
-# copyright 2003-2014 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/>.
-"""
-
-"""
-#
-# Cubicweb documentation build configuration file, created by
-# sphinx-quickstart on Fri Oct 31 09:10:36 2008.
-#
-# This file is execfile()d with the current directory set to its containing dir.
-#
-# The contents of this file are pickled, so don't put values in the namespace
-# that aren't pickleable (module imports are okay, they're removed automatically).
-#
-# All configuration values have a default value; values that are commented out
-# serve to show the default value.
-
-from os import path as osp
-
-path = __file__
-path = osp.dirname(path) #./doc/book/en
-path = osp.dirname(path) #./doc/book/
-path = osp.dirname(path) #./doc/
-path = osp.dirname(path) #./
-path = osp.join(path,'__pkginfo__.py') #./__pkginfo__.py
-cw = {}
-execfile(path,{},cw)
-
-# If your extensions are in another directory, add it here. If the directory
-# is relative to the documentation root, use os.path.abspath to make it
-# absolute, like shown here.
-#sys.path.append(os.path.abspath('some/directory'))
-
-# General configuration
-# ---------------------
-
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = [
- 'sphinx.ext.autodoc',
- 'sphinx.ext.viewcode',
- 'logilab.common.sphinx_ext',
- ]
-
-autoclass_content = 'both'
-
-# Add any paths that contain templates here, relative to this directory.
-#templates_path = []
-
-# The suffix of source filenames.
-source_suffix = '.rst'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General substitutions.
-project = 'CubicWeb'
-copyright = '2001-2014, Logilab'
-
-# The default replacements for |version| and |release|, also used in various
-# other places throughout the built documents.
-#
-# The short X.Y version.
-version = '.'.join(str(n) for n in cw['numversion'][:2])
-# The full version, including alpha/beta/rc tags.
-release = cw['version']
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-#today = ''
-# Else, today_fmt is used as the format for a strftime call.
-today_fmt = '%B %d, %Y'
-
-# List of documents that shouldn't be included in the build.
-unused_docs = []
-
-# List of directories, relative to source directories, that shouldn't be searched
-# for source files.
-#exclude_dirs = []
-
-# The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-#add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-#show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-
-# Options for HTML output
-# -----------------------
-
-# The style sheet to use for HTML and HTML Help pages. A file of that name
-# must exist either in Sphinx' static/ path, or in one of the custom paths
-# given in html_static_path.
-#html_style = 'sphinx-default.css'
-
-# The name for this set of Sphinx documents. If None, it defaults to
-# "<project> v<release> documentation".
-html_title = '%s %s' % (project, release)
-
-html_theme_path = ['_themes']
-html_theme = 'cubicweb'
-
-# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
-
-# The name of an image file (within the static path) to place at the top of
-# the sidebar.
-#html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-#html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['.static']
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-#html_additional_pages = {}
-
-# If false, no module index is generated.
-html_use_modindex = True
-
-# If false, no index is generated.
-#html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-#html_split_index = False
-
-# If true, the reST sources are included in the HTML build as _sources/<name>.
-#html_copy_source = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it. The value of this option must be the
-# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
-
-# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-html_file_suffix = '.html'
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'Cubicwebdoc'
-
-
-# Options for LaTeX output
-# ------------------------
-
-# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
-
-# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, document class [howto/manual]).
-latex_documents = [
- ('index', 'Cubicweb.tex', 'Cubicweb Documentation',
- 'Logilab', 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_use_modindex = True
-
-#aafig_format = dict(latex='pdf', html='svg', text=None)
-
-rst_epilog = """
-.. |cubicweb| replace:: *CubicWeb*
-.. |yams| replace:: *Yams*
-.. |rql| replace:: *RQL*
-"""
--- a/doc/book/en/devrepo/cubes/available-cubes.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-.. _AvailableCubes:
-
-Available cubes
----------------
-
-An instance is made of several basic cubes. In the set of available
-basic cubes we can find for example:
-
-Base entity types
-~~~~~~~~~~~~~~~~~
-* addressbook_: PhoneNumber and PostalAddress
-* card_: Card, generic documenting card
-* event_: Event (define events, display them in calendars)
-* file_: File (to allow users to upload and store binary or text files)
-* link_: Link (to collect links to web resources)
-* mailinglist_: MailingList (to reference a mailing-list and the URLs
- for its archives and its admin interface)
-* person_: Person (easily mixed with addressbook)
-* task_: Task (something to be done between start and stop date)
-* zone_: Zone (to define places within larger places, for example a
- city in a state in a country)
-
-
-Classification
-~~~~~~~~~~~~~~
-* folder_: Folder (to organize things by grouping them in folders)
-* keyword_: Keyword (to define classification schemes)
-* tag_: Tag (to tag anything)
-
-Other features
-~~~~~~~~~~~~~~
-* basket_: Basket (like a shopping cart)
-* blog_: a blogging system using Blog and BlogEntry entity types
-* comment_: system to attach comment threads to entities)
-* email_: archiving management for emails (`Email`, `Emailpart`,
- `Emailthread`), trigger action in cubicweb through email
-
-
-
-
-
-.. _addressbook: http://www.cubicweb.org/project/cubicweb-addressbook
-.. _basket: http://www.cubicweb.org/project/cubicweb-basket
-.. _card: http://www.cubicweb.org/project/cubicweb-card
-.. _blog: http://www.cubicweb.org/project/cubicweb-blog
-.. _comment: http://www.cubicweb.org/project/cubicweb-comment
-.. _email: http://www.cubicweb.org/project/cubicweb-email
-.. _event: http://www.cubicweb.org/project/cubicweb-event
-.. _file: http://www.cubicweb.org/project/cubicweb-file
-.. _folder: http://www.cubicweb.org/project/cubicweb-folder
-.. _keyword: http://www.cubicweb.org/project/cubicweb-keyword
-.. _link: http://www.cubicweb.org/project/cubicweb-link
-.. _mailinglist: http://www.cubicweb.org/project/cubicweb-mailinglist
-.. _person: http://www.cubicweb.org/project/cubicweb-person
-.. _tag: http://www.cubicweb.org/project/cubicweb-tag
-.. _task: http://www.cubicweb.org/project/cubicweb-task
-.. _zone: http://www.cubicweb.org/project/cubicweb-zone
-
-To declare the use of a cube, once installed, add the name of the cube
-and its dependency relation in the `__depends_cubes__` dictionary
-defined in the file `__pkginfo__.py` of your own component.
-
-.. note::
- The listed cubes above are available as debian-packages on `CubicWeb's forge`_.
-
-.. _`CubicWeb's forge`: http://www.cubicweb.org/project?vtitle=All%20cubicweb%20projects
--- a/doc/book/en/devrepo/cubes/cc-newcube.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-Creating a new cube from scratch
---------------------------------
-
-Let's start by creating the cube environment in which we will develop ::
-
- cd ~/cubes
- # use cubicweb-ctl to generate a template for the cube
- # will ask some questions, most with nice default
- cubicweb-ctl newcube mycube
- # makes the cube source code managed by mercurial
- cd mycube
- hg init
- hg add .
- hg ci
-
-If all went well, you should see the cube you just created in the list
-returned by ``cubicweb-ctl list`` in the *Available cubes* section.
-If not, please refer to :ref:`ConfigurationEnv`.
-
-To reuse an existing cube, add it to the list named
-``__depends_cubes__`` which is defined in :file:`__pkginfo__.py`.
-This variable is used for the instance packaging (dependencies handled
-by system utility tools such as APT) and to find used cubes when the
-database for the instance is created (import_erschema('MyCube') will
-not properly work otherwise).
-
-On a Unix system, the available cubes are usually stored in the
-directory :file:`/usr/share/cubicweb/cubes`. If you are using the
-cubicweb mercurial repository (:ref:`SourceInstallation`), the cubes
-are searched in the directory
-:file:`/path/to/cubicweb_toplevel/cubes`. In this configuration
-cubicweb itself ought to be located at
-:file:`/path/to/cubicweb_toplevel/cubicweb`.
-
-.. note::
-
- Please note that if you do not wish to use default directory for your cubes
- library, you should set the :envvar:`CW_CUBES_PATH` environment variable to
- add extra directories where cubes will be search, and you'll then have to use
- the option `--directory` to specify where you would like to place the source
- code of your cube:
-
- ``cubicweb-ctl newcube --directory=/path/to/cubes/library mycube``
-
-
-.. XXX resurrect once live-server is back
-.. Usage of :command:`cubicweb-ctl liveserver`
-.. -------------------------------------------
-
-.. To quickly test a new cube, you can also use the `liveserver` command for cubicweb-ctl
-.. which allows to create an instance in memory (using an SQLite database by
-.. default) and make it accessible through a web server ::
-
-.. cubicweb-ctl live-server mycube
-
-.. or by using an existing database (SQLite or Postgres)::
-
-.. cubicweb-ctl live-server -s myfile_sources mycube
--- a/doc/book/en/devrepo/cubes/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-Cubes
-=====
-
-This chapter describes how to define your own cubes and reuse already available cubes.
-
-.. toctree::
- :maxdepth: 1
-
- layout.rst
- cc-newcube.rst
- available-cubes.rst
--- a/doc/book/en/devrepo/cubes/layout.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-
-.. _foundationsCube:
-
-.. _cubelayout:
-
-Standard structure for a cube
------------------------------
-
-A cube is structured as follows:
-
-::
-
- mycube/
- |
- |-- data/
- | |-- cubes.mycube.css
- | |-- cubes.mycube.js
- | `-- external_resources
- |
- |-- debian/
- | |-- changelog
- | |-- compat
- | |-- control
- | |-- copyright
- | |-- cubicweb-mycube.prerm
- | `-- rules
- |
- |-- entities.py
- |
- |-- i18n/
- | |-- en.po
- | |-- es.po
- | `-- fr.po
- |
- |-- __init__.py
- |
- |-- MANIFEST.in
- |
- |-- migration/
- | |-- postcreate.py
- | `-- precreate.py
- |
- |-- __pkginfo__.py
- |
- |-- schema.py
- |
- |-- setup.py
- |
- |-- site_cubicweb.py
- |
- |-- hooks.py
- |
- |-- test/
- | |-- data/
- | | `-- bootstrap_cubes
- | |-- pytestconf.py
- | |-- realdb_test_mycube.py
- | `-- test_mycube.py
- |
- `-- views.py
-
-
-We can use subpackages instead of python modules for ``views.py``, ``entities.py``,
-``schema.py`` or ``hooks.py``. For example, we could have:
-
-::
-
- mycube/
- |
- |-- entities.py
- |-- hooks.py
- `-- views/
- |-- __init__.py
- |-- forms.py
- |-- primary.py
- `-- widgets.py
-
-
-where :
-
-* ``schema`` contains the schema definition (server side only)
-* ``entities`` contains the entity definitions (server side and web interface)
-* ``hooks`` contains hooks and/or views notifications (server side only)
-* ``views`` contains the web interface components (web interface only)
-* ``test`` contains tests related to the cube (not installed)
-* ``i18n`` contains message catalogs for supported languages (server side and
- web interface)
-* ``data`` contains data files for static content (images, css,
- javascript code)...(web interface only)
-* ``migration`` contains initialization files for new instances (``postcreate.py``)
- and a file containing dependencies of the component depending on the version
- (``depends.map``)
-* ``debian`` contains all the files managing debian packaging (you will find
- the usual files ``control``, ``rules``, ``changelog``... not installed)
-* file ``__pkginfo__.py`` provides component meta-data, especially the distribution
- and the current version (server side and web interface) or sub-cubes used by
- the cube.
-
-
-At least you should have the file ``__pkginfo__.py``.
-
-
-The :file:`__init__.py` and :file:`site_cubicweb.py` files
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX WRITEME
-
-The :file:`__pkginfo__.py` file
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-It contains metadata describing your cube, mostly useful for packaging.
-
-Two important attributes of this module are __depends__ and __recommends__
-dictionaries that indicates what should be installed (and each version if
-necessary) for the cube to work.
-
-Dependency on other cubes are expected to be of the form 'cubicweb-<cubename>'.
-
-When an instance is created, dependencies are automatically installed, while
-recommends are not.
-
-Recommends may be seen as a kind of 'weak dependency'. Eg, the most important
-effect of recommending a cube is that, if cube A recommends cube B, the cube B
-will be loaded before the cube A (same thing happend when A depends on B).
-
-Having this behaviour is sometime desired: on schema creation, you may rely on
-something defined in the other's schema; on database creation, on something
-created by the other's postcreate, and so on.
-
-
-:file:`migration/precreate.py` and :file:`migration/postcreate.py`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX detail steps of instance creation
-
-
-External resources such as image, javascript and css files
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX naming convention external_resources file
-
-
-Out-of the box testing
-~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX MANIFEST.in, __pkginfo__.include_dirs, debian
-
-
-Packaging and distribution
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX MANIFEST.in, __pkginfo__.include_dirs, debian
-
--- a/doc/book/en/devrepo/dataimport.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-. -*- coding: utf-8 -*-
-
-.. _dataimport:
-
-Dataimport
-==========
-
-*CubicWeb* is designed to manipulate huge of amount of data, and provides helper functions to do so.
-These functions insert data within different levels of the *CubicWeb* API,
-allowing different speed/security tradeoffs. Those keeping all the *CubicWeb* hooks
-and security will be slower but the possible errors in insertion
-(bad data types, integrity error, ...) will be raised.
-
-These dataimport function are provided in the file `dataimport.py`.
-
-All the stores have the following API::
-
- >>> store = ObjectStore()
- >>> user = store.create_entity('CWUser', login=u'johndoe')
- >>> group = store.create_entity('CWUser', name=u'unknown')
- >>> store.relate(user.eid, 'in_group', group.eid)
-
-
-ObjectStore
------------
-
-This store keeps objects in memory for *faster* validation. It may be useful
-in development mode. However, as it will not enforce the constraints of the schema,
-it may miss some problems.
-
-
-
-RQLObjectStore
---------------
-
-This store works with an actual RQL repository, and it may be used in production mode.
-
-
-NoHookRQLObjectStore
---------------------
-
-This store works similarly to the *RQLObjectStore* but bypasses some *CubicWeb* hooks to be faster.
-
-
-SQLGenObjectStore
------------------
-
-This store relies on *COPY FROM*/execute many sql commands to directly push data using SQL commands
-rather than using the whole *CubicWeb* API. For now, **it only works with PostgresSQL** as it requires
-the *COPY FROM* command.
-
-The API is similar to the other stores, but **it requires a flush** after some imports to copy data
-in the database (these flushes may be multiples through the processes, or be done only once at the
-end if there is no memory issue)::
-
- >>> store = SQLGenObjectStore(session)
- >>> store.create_entity('Person', ...)
- >>> store.flush()
--- a/doc/book/en/devrepo/datamodel/baseschema.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-.. _pre_defined_entity_types:
-
-Pre-defined entities in the library
------------------------------------
-
-The library defines a set of entity schemas that are required by the system
-or commonly used in *CubicWeb* instances.
-
-
-Entity types used to store the schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* _`CWEType`, entity type
-* _`CWRType`, relation type
-* _`CWRelation`, relation definition
-* _`CWAttribute`, attribute relation definition
-* _`CWConstraint`, `CWConstraintType`, `RQLExpression`
-
-Entity types used to manage users and permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* _`CWUser`, system users
-* _`CWGroup`, users groups
-
-Entity types used to manage workflows
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* :ref:`Workflow <Workflow>`, workflow entity, linked to some entity types which may use this workflow
-* _`State`, workflow state
-* _`Transition`, workflow transition
-* _`TrInfo`, record of a transition trafic for an entity
-
-Other entity types
-~~~~~~~~~~~~~~~~~~
-* _`CWCache`, cache entities used to improve performances
-* _`CWProperty`, used to configure the instance
-
-* _`EmailAddress`, email address, used by the system to send notifications
- to the users and also used by others optionnals schemas
-
-* _`Bookmark`, an entity type used to allow a user to customize his links within
- the instance
-
-* _`ExternalUri`, used for semantic web site to indicate that an entity is the
- same as another from an external site
--- a/doc/book/en/devrepo/datamodel/define-workflows.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Workflow:
-
-Defining a Workflow
-===================
-
-General
--------
-
-A workflow describes how certain entities have to evolve between different
-states. Hence we have a set of states, and a "transition graph", i.e. a set of
-possible transitions from one state to another state.
-
-We will define a simple workflow for a blog, with only the following two states:
-`submitted` and `published`. You may want to take a look at :ref:`TutosBase` if
-you want to quickly setup an instance running a blog.
-
-Setting up a workflow
----------------------
-
-We want to create a workflow to control the quality of the BlogEntry
-submitted on the instance. When a BlogEntry is created by a user
-its state should be `submitted`. To be visible to all, it has to
-be in the state `published`. To move it from `submitted` to `published`,
-we need a transition that we can call `approve_blogentry`.
-
-A BlogEntry state should not be modifiable by every user.
-So we have to define a group of users, `moderators`, and
-this group will have appropriate permissions to publish a BlogEntry.
-
-There are two ways to create a workflow: from the user interface, or
-by defining it in ``migration/postcreate.py``. This script is executed
-each time a new ``cubicweb-ctl db-init`` is done. We strongly
-recommend to create the workflow in ``migration/postcreate.py`` and we
-will now show you how. Read `Two bits of warning`_ to understand why.
-
-The state of an entity is managed by the `in_state` attribute which
-can be added to your entity schema by inheriting from
-`cubicweb.schema.WorkflowableEntityType`.
-
-
-About our example of BlogEntry, we must have:
-
-.. sourcecode:: python
-
- from cubicweb.schema import WorkflowableEntityType
-
- class BlogEntry(WorkflowableEntityType):
- ...
-
-
-Creating states, transitions and group permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The :mod:`postcreate` script is executed in a special environment,
-adding several |cubicweb| primitives that can be used.
-
-They are all defined in the :class:`ServerMigrationHelper` class.
-We will only discuss the methods we use to create a workflow in this example.
-
-A workflow is a collection of entities of type ``State`` and of type
-``Transition`` which are standard *CubicWeb* entity types.
-
-To define a workflow for BlogDemo, please add the following lines
-to ``migration/postcreate.py``:
-
-.. sourcecode:: python
-
- _ = unicode
-
- moderators = add_entity('CWGroup', name=u"moderators")
-
-This adds the `moderators` user group.
-
-.. sourcecode:: python
-
- wf = add_workflow(u'blog publication workflow', 'BlogEntry')
-
-At first, instanciate a new workflow object with a gentle description
-and the concerned entity types (this one can be a tuple for multiple
-value).
-
-.. sourcecode:: python
-
- submitted = wf.add_state(_('submitted'), initial=True)
- published = wf.add_state(_('published'))
-
-This will create two entities of type ``State``, one with name
-'submitted', and the other with name 'published'.
-
-``add_state`` expects as first argument the name of the state you want
-to create and an optional argument to say if it is supposed to be the
-initial state of the entity type.
-
-.. sourcecode:: python
-
- wf.add_transition(_('approve_blogentry'), (submitted,), published, ('moderators', 'managers'),)
-
-This will create an entity of type ``Transition`` with name
-`approve_blogentry` which will be linked to the ``State`` entities
-created before.
-
-``add_transition`` expects
-
- * as the first argument: the name of the transition
- * then the list of states on which the transition can be triggered,
- * the target state of the transition,
- * and the permissions
- (e.g. a list of user groups who can apply the transition; the user
- has to belong to at least one of the listed group to perform the action).
-
-.. sourcecode:: python
-
- checkpoint()
-
-.. note::
- Do not forget to add the `_()` in front of all states and
- transitions names while creating a workflow so that they will be
- identified by the i18n catalog scripts.
-
-In addition to the user groups (one of which the user needs to belong
-to), we could have added a RQL condition. In this case, the user can
-only perform the action if the two conditions are satisfied.
-
-If we use an RQL condition on a transition, we can use the following variables:
-
-* `X`, the entity on which we may pass the transition
-* `U`, the user executing that may pass the transition
-
-
-.. image:: ../../images/03-transitions-view_en.png
-
-You can notice that in the action box of a BlogEntry, the state is now
-listed as well as the possible transitions for the current state
-defined by the workflow.
-
-The transitions will only be displayed for users having the right permissions.
-In our example, the transition `approve_blogentry` will only be displayed
-for the users belonging to the group `moderators` or `managers`.
-
-
-Two bits of warning
-~~~~~~~~~~~~~~~~~~~
-
-We could perfectly use the administration interface to do these
-operations. It is a convenient thing to do at times (when doing
-development, to quick-check things). But it is not recommended beyond
-that because it is a bit complicated to do it right and it will be
-only local to your instance (or, said a bit differently, such a
-workflow only exists in an instance database). Furthermore, you cannot
-write unit tests against deployed instances, and experience shows it
-is mandatory to have tests for any mildly complicated workflow
-setup.
-
-Indeed, if you create the states and transitions through the user
-interface, next time you initialize the database you will have to
-re-create all the workflow entities. The user interface should only be
-a reference for you to view the states and transitions, but is not the
-appropriate interface to define your application workflow.
--- a/doc/book/en/devrepo/datamodel/definition.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,912 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _datamodel_definition:
-
-Yams *schema*
--------------
-
-The **schema** is the core piece of a *CubicWeb* instance as it
-defines and handles the data model. It is based on entity types that
-are either already defined in `Yams`_ and the *CubicWeb* standard
-library; or more specific types defined in cubes. The schema for a
-cube is defined in a `schema` python module or package.
-
-.. _`Yams`: http://www.logilab.org/project/yams
-
-.. _datamodel_overview:
-
-Overview
-~~~~~~~~
-
-The core idea of the yams schema is not far from the classical
-`Entity-relationship`_ model. But while an E/R model (or `logical
-model`) traditionally has to be manually translated to a lower-level
-data description language (such as the SQL `create table`
-sublanguage), also often described as the `physical model`, no such
-step is required with |yams| and |cubicweb|.
-
-.. _`Entity-relationship`: http://en.wikipedia.org/wiki/Entity-relationship_model
-
-This is because in addition to high-level, logical |yams| models, one
-uses the |rql| data manipulation language to query, insert, update and
-delete data. |rql| abstracts as much of the underlying SQL database as
-a |yams| schema abstracts from the physical layout. The vagaries of
-SQL are avoided.
-
-As a bonus point, such abstraction make it quite comfortable to build
-or use different backends to which |rql| queries apply.
-
-So, as in the E/R formalism, the building blocks are ``entities``
-(:ref:`EntityType`), ``relationships`` (:ref:`RelationType`,
-:ref:`RelationDefinition`) and ``attributes`` (handled like relation
-with |yams|).
-
-Let us detail a little the divergences between E/R and |yams|:
-
-* all relationship are binary which means that to represent a
- non-binary relationship, one has to use an entity,
-* relationships do not support attributes (yet, see:
- http://www.cubicweb.org/ticket/341318), hence the need to reify it
- as an entity if need arises,
-* all entities have an `eid` attribute (an integer) that is its
- primary key (but it is possible to declare uniqueness on other
- attributes)
-
-Also |yams| supports the notions of:
-
-* entity inheritance (quite experimental yet, and completely
- undocumented),
-* relation type: that is, relationships can be established over a set
- of couple of entity types (henre the distinction made between
- `RelationType` and `RelationDefinition` below)
-
-Finally |yams| has a few concepts of its own:
-
-* relationships being oriented and binary, we call the left hand
- entity type the `subject` and the right hand entity type the
- `object`
-
-.. note::
-
- The |yams| schema is available at run time through the .schema
- attribute of the `vregistry`. It's an instance of
- :class:`cubicweb.schema.Schema`, which extends
- :class:`yams.schema.Schema`.
-
-.. _EntityType:
-
-Entity type
-~~~~~~~~~~~
-
-An entity type is an instance of :class:`yams.schema.EntitySchema`. Each entity type has
-a set of attributes and relations, and some permissions which define who can add, read,
-update or delete entities of this type.
-
-The following built-in types are available: ``String``,
-``Int``, ``BigInt``, ``Float``, ``Decimal``, ``Boolean``,
-``Date``, ``Datetime``, ``Time``, ``Interval``, ``Byte`` and
-``Password``. They can only be used as attributes of an other entity
-type.
-
-There is also a `RichString` kindof type:
-
- .. autoclass:: yams.buildobjs.RichString
-
-The ``__unique_together__`` class attribute is a list of tuples of names of
-attributes or inlined relations. For each tuple, CubicWeb ensures the unicity
-of the combination. For example:
-
-.. sourcecode:: python
-
- class State(EntityType):
- __unique_together__ = [('name', 'state_of')]
-
- name = String(required=True)
- state_of = SubjectRelation('Workflow', cardinality='1*',
- composite='object', inlined=True)
-
-
-You can find more base entity types in
-:ref:`pre_defined_entity_types`.
-
-.. XXX yams inheritance
-
-.. _RelationType:
-
-Relation type
-~~~~~~~~~~~~~
-
-A relation type is an instance of
-:class:`yams.schema.RelationSchema`. A relation type is simply a
-semantic definition of a kind of relationship that may occur in an
-application.
-
-It may be referenced by zero, one or more relation definitions.
-
-It is important to choose a good name, at least to avoid conflicts
-with some semantically different relation defined in other cubes
-(since there's only a shared name space for these names).
-
-A relation type holds the following properties (which are hence shared
-between all relation definitions of that type):
-
-* `inlined`: boolean handling the physical optimization for archiving
- the relation in the subject entity table, instead of creating a specific
- table for the relation. This applies to relations where cardinality
- of subject->relation->object is 0..1 (`?`) or 1..1 (`1`) for *all* its relation
- definitions.
-
-* `symmetric`: boolean indicating that the relation is symmetrical, which
- means that `X relation Y` implies `Y relation X`.
-
-.. _RelationDefinition:
-
-Relation definition
-~~~~~~~~~~~~~~~~~~~
-
-A relation definition is an instance of
-:class:`yams.schema.RelationDefinition`. It is a complete triplet
-"<subject entity type> <relation type> <object entity type>".
-
-When creating a new instance of that class, the corresponding
-:class:`RelationType` instance is created on the fly if necessary.
-
-Properties
-``````````
-
-The available properties for relation definitions are enumerated
-here. There are several kind of properties, as some relation
-definitions are actually attribute definitions, and other are not.
-
-Some properties may be completely optional, other may have a default
-value.
-
-Common properties for attributes and relations:
-
-* `description`: an unicode string describing an attribute or a
- relation. By default this string will be used in the editing form of
- the entity, which means that it is supposed to help the end-user and
- should be flagged by the function `_` to be properly
- internationalized.
-
-* `constraints`: a list of conditions/constraints that the relation has to
- satisfy (c.f. `Constraints`_)
-
-* `cardinality`: a two character string specifying the cardinality of
- the relation. The first character defines the cardinality of the
- relation on the subject, and the second on the object. When a
- relation can have multiple subjects or objects, the cardinality
- applies to all, not on a one-to-one basis (so it must be
- consistent...). Default value is '**'. The possible values are
- inspired from regular expression syntax:
-
- * `1`: 1..1
- * `?`: 0..1
- * `+`: 1..n
- * `*`: 0..n
-
-Attributes properties:
-
-* `unique`: boolean indicating if the value of the attribute has to be
- unique or not within all entities of the same type (false by
- default)
-
-* `indexed`: boolean indicating if an index needs to be created for
- this attribute in the database (false by default). This is useful
- only if you know that you will have to run numerous searches on the
- value of this attribute.
-
-* `default`: default value of the attribute. In case of date types, the values
- which could be used correspond to the RQL keywords `TODAY` and `NOW`.
-
-* `metadata`: Is also accepted as an argument of the attribute contructor. It is
- not really an attribute property. see `Metadata`_ for details.
-
-Properties for `String` attributes:
-
-* `fulltextindexed`: boolean indicating if the attribute is part of
- the full text index (false by default) (*applicable on the type
- `Byte` as well*)
-
-* `internationalizable`: boolean indicating if the value of the
- attribute is internationalizable (false by default)
-
-Relation properties:
-
-* `composite`: string indicating that the subject (composite ==
- 'subject') is composed of the objects of the relations. For the
- opposite case (when the object is composed of the subjects of the
- relation), we just set 'object' as value. The composition implies
- that when the relation is deleted (so when the composite is deleted,
- at least), the composed are also deleted.
-
-* `fulltext_container`: string indicating if the value if the full
- text indexation of the entity on one end of the relation should be
- used to find the entity on the other end. The possible values are
- 'subject' or 'object'. For instance the use_email relation has that
- property set to 'subject', since when performing a full text search
- people want to find the entity using an email address, and not the
- entity representing the email address.
-
-Constraints
-```````````
-
-By default, the available constraint types are:
-
-General Constraints
-......................
-
-* `SizeConstraint`: allows to specify a minimum and/or maximum size on
- string (generic case of `maxsize`)
-
-* `BoundaryConstraint`: allows to specify a minimum and/or maximum value
- on numeric types and date
-
-.. sourcecode:: python
-
- from yams.constraints import BoundaryConstraint, TODAY, NOW, Attribute
-
- class DatedEntity(EntityType):
- start = Date(constraints=[BoundaryConstraint('>=', TODAY())])
- end = Date(constraints=[BoundaryConstraint('>=', Attribute('start'))])
-
- class Before(EntityType);
- last_time = DateTime(constraints=[BoundaryConstraint('<=', NOW())])
-
-* `IntervalBoundConstraint`: allows to specify an interval with
- included values
-
-.. sourcecode:: python
-
- class Node(EntityType):
- latitude = Float(constraints=[IntervalBoundConstraint(-90, +90)])
-
-* `UniqueConstraint`: identical to "unique=True"
-
-* `StaticVocabularyConstraint`: identical to "vocabulary=(...)"
-
-Constraints can be dependent on a fixed value (90, Date(2015,3,23)) or a variable.
-In this second case, yams can handle :
-
-* `Attribute`: compare to the value of another attribute.
-* `TODAY`: compare to the current Date.
-* `NOW`: compare to the current Datetime.
-
-RQL Based Constraints
-......................
-
-RQL based constraints may take three arguments. The first one is the ``WHERE``
-clause of a RQL query used by the constraint. The second argument ``mainvars``
-is the ``Any`` clause of the query. By default this include `S` reserved for the
-subject of the relation and `O` for the object. Additional variables could be
-specified using ``mainvars``. The argument expects a single string with all
-variable's name separated by spaces. The last one, ``msg``, is the error message
-displayed when the constraint fails. As RQLVocabularyConstraint never fails the
-third argument is not available.
-
-* `RQLConstraint`: allows to specify a RQL query that has to be satisfied
- by the subject and/or the object of relation. In this query the variables
- `S` and `O` are reserved for the relation subject and object entities.
-
-* `RQLVocabularyConstraint`: similar to the previous type of constraint except
- that it does not express a "strong" constraint, which means it is only used to
- restrict the values listed in the drop-down menu of editing form, but it does
- not prevent another entity to be selected.
-
-* `RQLUniqueConstraint`: allows to the specify a RQL query that ensure that an
- attribute is unique in a specific context. The Query must **never** return more
- than a single result to be satisfied. In this query the variables `S` is
- reserved for the relation subject entity. The other variables should be
- specified with the second constructor argument (mainvars). This constraint type
- should be used when __unique_together__ doesn't fit.
-
-.. XXX note about how to add new constraint
-
-.. _securitymodel:
-
-The security model
-~~~~~~~~~~~~~~~~~~
-
-The security model of `CubicWeb` is based on `Access Control List`.
-The main principles are:
-
-* users and groups of users
-* a user belongs to at least one group of user
-* permissions (`read`, `update`, `create`, `delete`)
-* permissions are assigned to groups (and not to users)
-
-For *CubicWeb* in particular:
-
-* we associate rights at the entities/relations schema level
-
-* the default groups are: `managers`, `users` and `guests`
-
-* users belong to the `users` group
-
-* there is a virtual group called `owners` to which we can associate only
- `delete` and `update` permissions
-
- * we can not add users to the `owners` group, they are implicitly added to it
- according to the context of the objects they own
-
- * the permissions of this group are only checked on `update`/`delete` actions
- if all the other groups the user belongs to do not provide those permissions
-
-Setting permissions is done with the class attribute `__permissions__`
-of entity types and relation definitions. The value of this attribute
-is a dictionary where the keys are the access types (action), and the
-values are the authorized groups or rql expressions.
-
-For an entity type, the possible actions are `read`, `add`, `update` and
-`delete`.
-
-For a relation, the possible actions are `read`, `add`, and `delete`.
-
-For an attribute, the possible actions are `read`, `add` and `update`,
-and they are a refinement of an entity type permission.
-
-.. note::
-
- By default, the permissions of an entity type attributes are
- equivalent to the permissions of the entity type itself.
-
- It is possible to provide custom attribute permissions which are
- stronger than, or are more lenient than the entity type
- permissions.
-
- In a situation where all attributes were given custom permissions,
- the entity type permissions would not be checked, except for the
- `delete` action.
-
-For each access type, a tuple indicates the name of the authorized groups and/or
-one or multiple RQL expressions to satisfy to grant access. The access is
-provided if the user is in one of the listed groups or if one of the RQL condition
-is satisfied.
-
-Default permissions
-```````````````````
-
-The default permissions for ``EntityType`` are:
-
-.. sourcecode:: python
-
- __permissions__ = {
- 'read': ('managers', 'users', 'guests',),
- 'update': ('managers', 'owners',),
- 'delete': ('managers', 'owners'),
- 'add': ('managers', 'users',)
- }
-
-The default permissions for relations are:
-
-.. sourcecode:: python
-
- __permissions__ = {'read': ('managers', 'users', 'guests',),
- 'delete': ('managers', 'users'),
- 'add': ('managers', 'users',)}
-
-The default permissions for attributes are:
-
-.. sourcecode:: python
-
- __permissions__ = {'read': ('managers', 'users', 'guests',),
- 'add': ('managers', ERQLExpression('U has_add_permission X'),
- 'update': ('managers', ERQLExpression('U has_update_permission X')),}
-
-.. note::
-
- The default permissions for attributes are not syntactically
- equivalent to the default permissions of the entity types, but the
- rql expressions work by delegating to the entity type permissions.
-
-
-The standard user groups
-````````````````````````
-
-* `guests`
-
-* `users`
-
-* `managers`
-
-* `owners`: virtual group corresponding to the entity's owner.
- This can only be used for the actions `update` and `delete` of an entity
- type.
-
-It is also possible to use specific groups if they are defined in the precreate
-script of the cube (``migration/precreate.py``). Defining groups in postcreate
-script or later makes them unavailable for security purposes (in this case, an
-`sync_schema_props_perms` command has to be issued in a CubicWeb shell).
-
-
-Use of RQL expression for write permissions
-```````````````````````````````````````````
-
-It is possible to define RQL expression to provide update permission (`add`,
-`delete` and `update`) on entity type / relation definitions. An rql expression
-is a piece of query (corresponds to the WHERE statement of an RQL query), and the
-expression will be considered as satisfied if it returns some results. They can
-not be used in `read` permission.
-
-To use RQL expression in entity type permission:
-
-* you have to use the class :class:`~cubicweb.schema.ERQLExpression`
-
-* in this expression, the variables `X` and `U` are pre-defined references
- respectively on the current entity (on which the action is verified) and on the
- user who send the request
-
-For RQL expressions on a relation type, the principles are the same except for
-the following:
-
-* you have to use the class :class:`~cubicweb.schema.RRQLExpression` instead of
- :class:`~cubicweb.schema.ERQLExpression`
-
-* in the expression, the variables `S`, `O` and `U` are pre-defined references to
- respectively the subject and the object of the current relation (on which the
- action is being verified) and the user who executed the query
-
-To define security for attributes of an entity (non-final relation), you have to
-use the class :class:`~cubicweb.schema.ERQLExpression` in which `X` represents
-the entity the attribute belongs to.
-
-It is possible to use in those expression a special relation
-`has_<ACTION>_permission` where the subject is the user (eg 'U') and the object
-is any variable representing an entity (usually 'X' in
-:class:`~cubicweb.schema.ERQLExpression`, 'S' or 'O' in
-:class:`~cubicweb.schema.RRQLExpression`), meaning that the user needs to have
-permission to execute the action <ACTION> on the entities represented by this
-variable. It's recommanded to use this feature whenever possible since it
-simplify greatly complex security definition and upgrade.
-
-
-.. sourcecode:: python
-
- class my_relation(RelationDefinition):
- __permissions__ = {'read': ('managers', 'users'),
- 'add': ('managers', RRQLExpression('U has_update_permission S')),
- 'delete': ('managers', RRQLExpression('U has_update_permission S'))
- }
-
-In the above example, user will be allowed to add/delete `my_relation` if he has
-the `update` permission on the subject of the relation.
-
-.. note::
-
- Potentially, the `use of an RQL expression to add an entity or a relation` can
- cause problems for the user interface, because if the expression uses the
- entity or the relation to create, we are not able to verify the permissions
- before we actually added the entity (please note that this is not a problem for
- the RQL server at all, because the permissions checks are done after the
- creation). In such case, the permission check methods
- (CubicWebEntitySchema.check_perm and has_perm) can indicate that the user is
- not allowed to create this entity while it would obtain the permission. To
- compensate this problem, it is usually necessary in such case to use an action
- that reflects the schema permissions but which check properly the permissions
- so that it would show up only if possible.
-
-
-Use of RQL expression for reading rights
-````````````````````````````````````````
-
-The principles are the same but with the following restrictions:
-
-* you can not use rql expression for the `read` permission of relations and
- attributes,
-
-* you can not use special `has_<ACTION>_permission` relation in the rql
- expression.
-
-
-Important notes about write permissions checking
-````````````````````````````````````````````````
-
-Write permissions (e.g. 'add', 'update', 'delete') are checked in core hooks.
-
-When a permission is checked slightly vary according to if it's an entity or
-relation, and if the relation is an attribute relation or not). It's important to
-understand that since according to when a permission is checked, values returned
-by rql expressions may changes, hence the permission being granted or not.
-
-Here are the current rules:
-
-1. permission to add/update entity and its attributes are checked on
- commit
-
-2. permission to delete an entity is checked in 'before_delete_entity' hook
-
-3. permission to add a relation is checked either:
-
- - in 'before_add_relation' hook if the relation type is in the
- `BEFORE_ADD_RELATIONS` set
-
- - else at commit time if the relation type is in the `ON_COMMIT_ADD_RELATIONS`
- set
-
- - else in 'after_add_relation' hook (the default)
-
-4. permission to delete a relation is checked in 'before_delete_relation' hook
-
-Last but not least, remember queries issued from hooks and operation are by
-default 'unsafe', eg there are no read or write security checks.
-
-See :mod:`cubicweb.hooks.security` for more details.
-
-
-.. _yams_example:
-
-
-Derived attributes and relations
---------------------------------
-
-.. note:: **TODO** Check organisation of the whole chapter of the documentation
-
-Cubicweb offers the possibility to *query* data using so called
-*computed* relations and attributes. Those are *seen* by RQL requests
-as normal attributes and relations but are actually derived from other
-attributes and relations. In a first section we'll informally review
-two typical use cases. Then we see how to use computed attributes and
-relations in your schema. Last we will consider various significant
-aspects of their implementation and the impact on their usage.
-
-Motivating use cases
-~~~~~~~~~~~~~~~~~~~~
-
-Computed (or reified) relations
-```````````````````````````````
-
-It often arises that one must represent a ternary relation, or a
-family of relations. For example, in the context of an exhibition
-catalog you might want to link all *contributors* to the *work* they
-contributed to, but this contribution can be as *illustrator*,
-*author*, *performer*, ...
-
-The classical way to describe this kind of information within an
-entity-relationship schema is to *reify* the relation, that is turn
-the relation into a entity. In our example the schema will have a
-*Contribution* entity type used to represent the family of the
-contribution relations.
-
-
-.. sourcecode:: python
-
- class ArtWork(EntityType):
- name = String()
- ...
-
- class Person(EntityType):
- name = String()
- ...
-
- class Contribution(EntityType):
- contributor = SubjectRelation('Person', cardinality='1*', inlined=True)
- manifestation = SubjectRelation('ArtWork')
- role = SubjectRelation('Role')
-
- class Role(EntityType):
- name = String()
-
-But then, in order to query the illustrator(s) ``I`` of a work ``W``,
-one has to write::
-
- Any I, W WHERE C is Contribution, C contributor I, C manifestation W,
- C role R, R name 'illustrator'
-
-whereas we would like to be able to simply write::
-
- Any I, W WHERE I illustrator_of W
-
-This is precisely what the computed relations allow.
-
-
-Computed (or synthesized) attribute
-```````````````````````````````````
-
-Assuming a trivial schema for describing employees in companies, one
-can be interested in the total of salaries payed by a company for
-all its employees. One has to write::
-
- Any C, SUM(SA) GROUPBY S WHERE E works_for C, E salary SA
-
-whereas it would be most convenient to simply write::
-
- Any C, TS WHERE C total_salary TS
-
-And this is again what computed attributes provide.
-
-
-Using computed attributes and relations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Computed (or reified) relations
-```````````````````````````````
-
-In the above case we would define the *computed relation*
-``illustrator_of`` in the schema by:
-
-.. sourcecode:: python
-
- class illustrator_of(ComputedRelation):
- rule = ('C is Contribution, C contributor S, C manifestation O,'
- 'C role R, R name "illustrator"')
-
-You will note that:
-
-* the ``S`` and ``O`` RQL variables implicitly identify the subject and
- object of the defined computed relation, akin to what happens in
- RRQLExpression
-* the possible subject and object entity types are inferred from the rule;
-* computed relation definitions always have empty *add* and *delete* permissions
-* *read* permissions can be defined, permissions from the relations used in the
- rewrite rule **are not considered** ;
-* nothing else may be defined on the `ComputedRelation` subclass beside
- description, permissions and rule (e.g. no cardinality, composite, etc.,).
- `BadSchemaDefinition` is raised on attempt to specify other attributes;
-* computed relations can not be used in 'SET' and 'DELETE' rql queries
- (`BadQuery` exception raised).
-
-
-NB: The fact that the *add* and *delete* permissions are *empty* even
-for managers is expected to make the automatic UI not attempt to edit
-them.
-
-Computed (or synthesized) attributes
-````````````````````````````````````
-
-In the above case we would define the *computed attribute*
-``total_salary`` on the ``Company`` entity type in the schema by:
-
-.. sourcecode:: python
-
- class Company(EntityType):
- name = String()
- total_salary = Int(formula='Any SUM(SA) GROUPBY E WHERE P works_for X, E salary SA')
-
-* the ``X`` RQL variable implicitly identifies the entity holding the
- computed attribute, akin to what happens in ERQLExpression;
-* the type inferred from the formula is checked against the declared type, and
- `BadSchemaDefinition` is raised if they don't match;
-* the computed attributes always have empty *update* permissions
-* `BadSchemaDefinition` is raised on attempt to set 'update' permissions;
-* 'read' permissions can be defined, permissions regarding the formula
- **are not considered**;
-* other attribute's property (inlined, ...) can be defined as for normal attributes;
-* Similarly to computed relation, computed attribute can't be used in 'SET' and
- 'DELETE' rql queries (`BadQuery` exception raised).
-
-
-API and implementation
-~~~~~~~~~~~~~~~~~~~~~~
-
-Representation in the data backend
-``````````````````````````````````
-
-Computed relations have no direct representation at the SQL table
-level. Instead, each time a query is issued the query is rewritten to
-replace the computed relation by its equivalent definition and the
-resulting rewritten query is performed in the usual way.
-
-On the contrary, computed attributes are represented as a column in the
-table for their host entity type, just like normal attributes. Their
-value is kept up-to-date with respect to their defintion by a system
-of hooks (also called triggers in most RDBMS) which recomputes them
-when the relations and attributes they depend on are modified.
-
-Yams API
-````````
-
-When accessing the schema through the *yams API* (not when defining a
-schema in a ``schema.py`` file) the computed attributes and relations
-are represented as follows:
-
-relations
- The ``yams.RelationSchema`` class has a new ``rule`` attribute
- holding the rule as a string. If this attribute is set all others
- must not be set.
-attributes
- A new property ``formula`` is added on class
- ``yams.RelationDefinitionSchema`` alomng with a new keyword
- argument ``formula`` on the initializer.
-
-Migration
-`````````
-
-The migrations are to be handled as summarized in the array below.
-
-+------------+---------------------------------------------------+---------------------------------------+
-| | Computed rtype | Computed attribute |
-+============+===================================================+=======================================+
-| add | * add_relation_type | * add_attribute |
-| | * add_relation_definition should trigger an error | * add_relation_definition |
-+------------+---------------------------------------------------+---------------------------------------+
-| modify | * sync_schema_prop_perms: | * sync_schema_prop_perms: |
-| | checks the rule is | |
-| (rule or | synchronized with the database | - empty the cache, |
-| formula) | | - check formula, |
-| | | - make sure all the values get |
-| | | updated |
-+------------+---------------------------------------------------+---------------------------------------+
-| del | * drop_relation_type | * drop_attribute |
-| | * drop_relation_definition should trigger an error| * drop_relation_definition |
-+------------+---------------------------------------------------+---------------------------------------+
-
-
-Defining your schema using yams
--------------------------------
-
-Entity type definition
-~~~~~~~~~~~~~~~~~~~~~~
-
-An entity type is defined by a Python class which inherits from
-:class:`yams.buildobjs.EntityType`. The class definition contains the
-description of attributes and relations for the defined entity type.
-The class name corresponds to the entity type name. It is expected to
-be defined in the module ``mycube.schema``.
-
-:Note on schema definition:
-
- The code in ``mycube.schema`` is not meant to be executed. The class
- EntityType mentioned above is different from the EntitySchema class
- described in the previous chapter. EntityType is a helper class to
- make Entity definition easier. Yams will process EntityType classes
- and create EntitySchema instances from these class definitions. Similar
- manipulation happen for relations.
-
-When defining a schema using python files, you may use the following shortcuts:
-
-- `required`: boolean indicating if the attribute is required, ed subject cardinality is '1'
-
-- `vocabulary`: specify static possible values of an attribute
-
-- `maxsize`: integer providing the maximum size of a string (no limit by default)
-
-For example:
-
-.. sourcecode:: python
-
- class Person(EntityType):
- """A person with the properties and the relations necessary for my
- application"""
-
- last_name = String(required=True, fulltextindexed=True)
- first_name = String(required=True, fulltextindexed=True)
- title = String(vocabulary=('Mr', 'Mrs', 'Miss'))
- date_of_birth = Date()
- works_for = SubjectRelation('Company', cardinality='?*')
-
-
-The entity described above defines three attributes of type String,
-last_name, first_name and title, an attribute of type Date for the date of
-birth and a relation that connects a `Person` to another entity of type
-`Company` through the semantic `works_for`.
-
-
-
-:Naming convention:
-
- Entity class names must start with an uppercase letter. The common
- usage is to use ``CamelCase`` names.
-
- Attribute and relation names must start with a lowercase letter. The
- common usage is to use ``underscore_separated_words``. Attribute and
- relation names starting with a single underscore are permitted, to
- denote a somewhat "protected" or "private" attribute.
-
- In any case, identifiers starting with "CW" or "cw" are reserved for
- internal use by the framework.
-
- .. _Metadata:
-
- Some attribute using the name of another attribute as prefix are considered
- metadata. For example, if an EntityType have both a ``data`` and
- ``data_format`` attribute, ``data_format`` is view as the ``format`` metadata
- of ``data``. Later the :meth:`cw_attr_metadata` method will allow you to fetch
- metadata related to an attribute. There are only three valid metadata names:
- ``format``, ``encoding`` and ``name``.
-
-
-The name of the Python attribute corresponds to the name of the attribute
-or the relation in *CubicWeb* application.
-
-An attribute is defined in the schema as follows::
-
- attr_name = AttrType(*properties, metadata={})
-
-where
-
-* `AttrType`: is one of the type listed in EntityType_,
-
-* `properties`: is a list of the attribute needs to satisfy (see `Properties`_
- for more details),
-
-* `metadata`: is a dictionary of meta attributes related to ``attr_name``.
- Dictionary keys are the name of the meta attribute. Dictionary values
- attributes objects (like the content of ``AttrType``). For each entry of the
- metadata dictionary a ``<attr_name>_<key> = <value>`` attribute is
- automaticaly added to the EntityType. see `Metadata`_ section for details
- about valid key.
-
-
- ---
-
-While building your schema
-
-* it is possible to use the attribute `meta` to flag an entity type as a `meta`
- (e.g. used to describe/categorize other entities)
-
-.. XXX the paragraph below needs clarification and / or moving out in
-.. another place
-
-*Note*: if you end up with an `if` in the definition of your entity, this probably
-means that you need two separate entities that implement the `ITree` interface and
-get the result from `.children()` which ever entity is concerned.
-
-.. Inheritance
-.. ```````````
-.. XXX feed me
-
-
-Definition of relations
-~~~~~~~~~~~~~~~~~~~~~~~
-
-.. XXX add note about defining relation type / definition
-
-A relation is defined by a Python class heriting `RelationType`. The name
-of the class corresponds to the name of the type. The class then contains
-a description of the properties of this type of relation, and could as well
-contain a string for the subject and a string for the object. This allows to create
-new definition of associated relations, (so that the class can have the
-definition properties from the relation) for example ::
-
- class locked_by(RelationType):
- """relation on all entities indicating that they are locked"""
- inlined = True
- cardinality = '?*'
- subject = '*'
- object = 'CWUser'
-
-If provided, the `subject` and `object` attributes denote the subject
-and object of the various relation definitions related to the relation
-type. Allowed values for these attributes are:
-
-* a string corresponding to an entity type
-* a tuple of string corresponding to multiple entity types
-* the '*' special string, meaning all types of entities
-
-When a relation is not inlined and not symmetrical, and it does not require
-specific permissions, it can be defined using a `SubjectRelation`
-attribute in the EntityType class. The first argument of `SubjectRelation` gives
-the entity type for the object of the relation.
-
-:Naming convention:
-
- Although this way of defining relations uses a Python class, the
- naming convention defined earlier prevails over the PEP8 conventions
- used in the framework: relation type class names use
- ``underscore_separated_words``.
-
-:Historical note:
-
- It has been historically possible to use `ObjectRelation` which
- defines a relation in the opposite direction. This feature is
- deprecated and therefore should not be used in newly written code.
-
-:Future deprecation note:
-
- In an even more remote future, it is quite possible that the
- SubjectRelation shortcut will become deprecated, in favor of the
- RelationType declaration which offers some advantages in the context
- of reusable cubes.
-
-
-
-
-Handling schema changes
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Also, it should be clear that to properly handle data migration, an
-instance's schema is stored in the database, so the python schema file
-used to defined it is only read when the instance is created or
-upgraded.
-
-.. XXX complete me
--- a/doc/book/en/devrepo/datamodel/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-Data model
-==========
-
-This chapter describes how you define a schema and how to make it evolves as the time goes.
-
-.. toctree::
- :maxdepth: 1
-
- definition
- metadata
- baseschema
- define-workflows
--- a/doc/book/en/devrepo/datamodel/metadata.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-
-Metadata
---------
-
-.. index::
- schema: meta-data;
- schema: eid; creation_date; modification_data; cwuri
- schema: created_by; owned_by; is; is_instance;
-
-Each entity type in |cubicweb| has at least the following meta-data attributes and relations:
-
-`eid`
- entity's identifier which is unique in an instance. We usually call this identifier `eid` for historical reason.
-
-`creation_date`
- Date and time of the creation of the entity.
-
-`modification_date`
- Date and time of the latest modification of an entity.
-
-`cwuri`
- Reference URL of the entity, which is not expected to change.
-
-`created_by`
- Relation to the :ref:`users <CWUser>` who has created the entity
-
-`owned_by`
- Relation to :ref:`users <CWUser>` whom the entity belongs; usually the creator but not
- necessary, and it could have multiple owners notably for permission control
-
-`is`
- Relation to the :ref:`entity type <CWEType>` of which type the entity is.
-
-`is_instance`
- Relation to the :ref:`entity types <CWEType>` of which type the
- entity is an instance of.
-
--- a/doc/book/en/devrepo/devcore/dbapi.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,133 +0,0 @@
-.. _dbapi:
-
-Python/RQL API
-~~~~~~~~~~~~~~
-
-The Python API developped to interface with RQL is inspired from the standard db-api,
-with a Connection object having the methods cursor, rollback and commit essentially.
-The most important method is the `execute` method of a cursor.
-
-.. sourcecode:: python
-
- execute(rqlstring, args=None, build_descr=True)
-
-:rqlstring: the RQL query to execute (unicode)
-:args: if the query contains substitutions, a dictionary containing the values to use
-
-The `Connection` object owns the methods `commit` and `rollback`. You
-*should never need to use them* during the development of the web
-interface based on the *CubicWeb* framework as it determines the end
-of the transaction depending on the query execution success. They are
-however useful in other contexts such as tests or custom controllers.
-
-.. note::
-
- If a query generates an error related to security (:exc:`Unauthorized`) or to
- integrity (:exc:`ValidationError`), the transaction can still continue but you
- won't be able to commit it, a rollback will be necessary to start a new
- transaction.
-
- Also, a rollback is automatically done if an error occurs during commit.
-
-.. note::
-
- A :exc:`ValidationError` has a `entity` attribute. In CubicWeb,
- this atttribute is set to the entity's eid (not a reference to the
- entity itself).
-
-Executing RQL queries from a view or a hook
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-When you're within code of the web interface, the db-api like connexion is
-handled by the request object. You should not have to access it directly, but
-use the `execute` method directly available on the request, eg:
-
-.. sourcecode:: python
-
- rset = self._cw.execute(rqlstring, kwargs)
-
-Similarly, on the server side (eg in hooks), there is no db-api connexion (since
-you're directly inside the data-server), so you'll have to use the execute method
-of the session object.
-
-
-Proper usage of `.execute`
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Let's say you want to get T which is in configuration C, this translates to:
-
-.. sourcecode:: python
-
- self._cw.execute('Any T WHERE T in_conf C, C eid %s' % entity.eid)
-
-But it must be written in a syntax that will benefit from the use
-of a cache on the RQL server side:
-
-.. sourcecode:: python
-
- self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid})
-
-The syntax tree is built once for the "generic" RQL and can be re-used
-with a number of different eids. There rql IN operator is an exception
-to this rule.
-
-.. sourcecode:: python
-
- self._cw.execute('Any T WHERE T in_conf C, C name IN (%s)'
- % ','.join(['foo', 'bar']))
-
-Alternativelly, some of the common data related to an entity can be
-obtained from the `entity.related()` method (which is used under the
-hood by the orm when you use attribute access notation on an entity to
-get a relation. The initial request would then be translated to:
-
-.. sourcecode:: python
-
- entity.related('in_conf', 'object')
-
-Additionnaly this benefits from the fetch_attrs policy (see
-:ref:`FetchAttrs`) eventually defined on the class element, which says
-which attributes must be also loaded when the entity is loaded through
-the orm.
-
-
-.. _resultset:
-
-The `ResultSet` API
-~~~~~~~~~~~~~~~~~~~
-
-ResultSet instances are a very commonly manipulated object. They have
-a rich API as seen below, but we would like to highlight a bunch of
-methods that are quite useful in day-to-day practice:
-
-* `__str__()` (applied by `print`) gives a very useful overview of both
- the underlying RQL expression and the data inside; unavoidable for
- debugging purposes
-
-* `printable_rql()` produces back a well formed RQL expression as a
- string; it is very useful to build views
-
-* `entities()` returns a generator on all entities of the result set
-
-* `get_entity(row, col)` gets the entity at row, col coordinates; one
- of the most used result set method
-
-.. autoclass:: cubicweb.rset.ResultSet
- :members:
-
-
-The `Cursor` and `Connection` API
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The whole cursor API is developped below.
-
-.. note::
-
- In practice you'll usually use the `.execute` method on the _cw object of
- appobjects. Usage of other methods is quite rare.
-
-.. autoclass:: cubicweb.dbapi.Cursor
- :members:
-
-.. autoclass:: cubicweb.dbapi.Connection
- :members:
--- a/doc/book/en/devrepo/devcore/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-Core APIs
-=========
-
-.. toctree::
- :maxdepth: 1
-
- dbapi.rst
- reqbase.rst
-
--- a/doc/book/en/devrepo/devcore/reqbase.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-Request and ResultSet methods
------------------------------
-
-Those are methods you'll find on both request objects and on
-repository session.
-
-Request methods
-~~~~~~~~~~~~~~~
-
-`URL handling`:
-
-* `build_url(*args, **kwargs)`, returns an absolute URL based on the
- given arguments. The *controller* supposed to handle the response,
- can be specified through the first positional parameter (the
- connection is theoretically done automatically :).
-
-`Data formatting`:
-
-* `format_date(date, date_format=None, time=False)` returns a string for a
- date time according to instance's configuration
-
-* `format_time(time)` returns a string for a date time according to
- instance's configuration
-
-`And more...`:
-
-* `tal_render(template, variables)`, renders a precompiled page template with
- variables in the given dictionary as context
-
-
-Result set methods
-~~~~~~~~~~~~~~~~~~
-
-* `get_entity(row, col)`, returns the entity corresponding to the data position
- in the *result set*
-
-* `complete_entity(row, col, skip_bytes=True)`, is equivalent to `get_entity` but
- also call the method `complete()` on the entity before returning it
-
-
--- a/doc/book/en/devrepo/entityclasses/adapters.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-.. _adapters:
-
-Interfaces and Adapters
------------------------
-
-Interfaces are the same thing as object-oriented programming `interfaces`_.
-Adapter refers to a well-known `adapter`_ design pattern that helps separating
-concerns in object oriented applications.
-
-.. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
-.. _`adapter`: http://en.wikipedia.org/wiki/Adapter_pattern
-
-In |cubicweb| adapters provide logical functionalities to entity types.
-
-Definition of an adapter is quite trivial. An excerpt from cubicweb
-itself (found in :mod:`cubicweb.entities.adapters`):
-
-.. sourcecode:: python
-
-
- 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 this default implementation
- """
- __regid__ = 'ITree'
-
- child_role = 'subject'
- parent_role = 'object'
-
- def children_rql(self):
- """returns RQL to get children """
- return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
-
-The adapter object has ``self.entity`` attribute which represents the
-entity being adapted.
-
-.. Note::
-
- Adapters came with the notion of service identified by the registry identifier
- of an adapters, hence dropping the need for explicit interface and the
- :class:`cubicweb.predicates.implements` selector. You should instead use
- :class:`cubicweb.predicates.is_instance` when you want to select on an entity
- type, or :class:`cubicweb.predicates.adaptable` when you want to select on a
- service.
-
-
-Specializing and binding an adapter
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. sourcecode:: python
-
- from cubicweb.entities.adapters import ITreeAdapter
-
- class MyEntityITreeAdapter(ITreeAdapter):
- __select__ = is_instance('MyEntity')
- tree_relation = 'filed_under'
-
-The ITreeAdapter here provides a default implementation. The
-tree_relation class attribute is actually used by this implementation
-to help implement correct behaviour.
-
-Here we provide a specific implementation which will be bound for
-``MyEntity`` entity type (the `adaptee`).
-
-
-.. _interfaces_to_adapters:
-
-Converting code from Interfaces/Mixins to Adapters
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here we go with a small example. Before:
-
-.. sourcecode:: python
-
- from cubicweb.predicates import implements
- from cubicweb.interfaces import ITree
- from cubicweb.mixins import ITreeMixIn
-
- class MyEntity(ITreeMixIn, AnyEntity):
- __implements__ = AnyEntity.__implements__ + (ITree,)
-
-
- class ITreeView(EntityView):
- __select__ = implements('ITree')
- def cell_call(self, row, col):
- entity = self.cw_rset.get_entity(row, col)
- children = entity.children()
-
-After:
-
-.. sourcecode:: python
-
- from cubicweb.predicates import adaptable, is_instance
- from cubicweb.entities.adapters import ITreeAdapter
-
- class MyEntityITreeAdapter(ITreeAdapter):
- __select__ = is_instance('MyEntity')
-
- class ITreeView(EntityView):
- __select__ = adaptable('ITree')
- def cell_call(self, row, col):
- entity = self.cw_rset.get_entity(row, col)
- itree = entity.cw_adapt_to('ITree')
- children = itree.children()
-
-As we can see, the interface/mixin duality disappears and the entity
-class itself is completely freed from these concerns. When you want
-to use the ITree interface of an entity, call its `cw_adapt_to` method
-to get an adapter for this interface, then access to members of the
-interface on the adapter
-
-Let's look at an example where we defined everything ourselves. We
-start from:
-
-.. sourcecode:: python
-
- class IFoo(Interface):
- def bar(self, *args):
- raise NotImplementedError
-
- class MyEntity(AnyEntity):
- __regid__ = 'MyEntity'
- __implements__ = AnyEntity.__implements__ + (IFoo,)
-
- def bar(self, *args):
- return sum(captain.age for captain in self.captains)
-
- class FooView(EntityView):
- __regid__ = 'mycube.fooview'
- __select__ = implements('IFoo')
-
- def cell_call(self, row, col):
- entity = self.cw_rset.get_entity(row, col)
- self.w('bar: %s' % entity.bar())
-
-Converting to:
-
-.. sourcecode:: python
-
- class IFooAdapter(EntityAdapter):
- __regid__ = 'IFoo'
- __select__ = is_instance('MyEntity')
-
- def bar(self, *args):
- return sum(captain.age for captain in self.entity.captains)
-
- class FooView(EntityView):
- __regid__ = 'mycube.fooview'
- __select__ = adaptable('IFoo')
-
- def cell_call(self, row, col):
- entity = self.cw_rset.get_entity(row, col)
- self.w('bar: %s' % entity.cw_adapt_to('IFoo').bar())
-
-.. note::
-
- When migrating an entity method to an adapter, the code can be moved as is
- except for the `self` of the entity class, which in the adapter must become `self.entity`.
-
-Adapters defined in the library
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: cubicweb.entities.adapters
- :members:
-
-More are defined in web/views.
--- a/doc/book/en/devrepo/entityclasses/application-logic.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,177 +0,0 @@
-How to use entities objects and adapters
-----------------------------------------
-
-The previous chapters detailed the classes and methods available to
-the developer at the so-called `ORM`_ level. However they say little
-about the common patterns of usage of these objects.
-
-.. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping
-
-Entities objects (and their adapters) are used in the repository and
-web sides of CubicWeb. On the repository side of things, one should
-manipulate them in Hooks and Operations.
-
-Hooks and Operations provide support for the implementation of rules
-such as computed attributes, coherency invariants, etc (they play the
-same role as database triggers, but in a way that is independent of
-the actual data sources).
-
-So a lot of an application's business rules will be written in Hooks
-(or Operations).
-
-On the web side, views also typically operate using entity
-objects. Obvious entity methods for use in views are the Dublin Core
-methods like ``dc_title``. For separation of concerns reasons, one
-should ensure no ui logic pervades the entities level, and also no
-business logic should creep into the views.
-
-In the duration of a transaction, entities objects can be instantiated
-many times, in views and hooks, even for the same database entity. For
-instance, in a classic CubicWeb deployment setup, the repository and
-the web front-end are separated process communicating over the
-wire. There is no way state can be shared between these processes
-(there is a specific API for that). Hence, it is not possible to use
-entity objects as messengers between these components of an
-application. It means that an attribute set as in ``obj.x = 42``,
-whether or not x is actually an entity schema attribute, has a short
-life span, limited to the hook, operation or view within which the
-object was built.
-
-Setting an attribute or relation value can be done in the context of a
-Hook/Operation, using the ``obj.cw_set(x=42)`` notation or a plain
-RQL ``SET`` expression.
-
-In views, it would be preferable to encapsulate the necessary logic in
-a method of an adapter for the concerned entity class(es). But of
-course, this advice is also reasonable for Hooks/Operations, though
-the separation of concerns here is less stringent than in the case of
-views.
-
-This leads to the practical role of objects adapters: it's where an
-important part of the application logic lies (the other part being
-located in the Hook/Operations).
-
-Anatomy of an entity class
---------------------------
-
-We can look now at a real life example coming from the `tracker`_
-cube. Let us begin to study the ``entities/project.py`` content.
-
-.. sourcecode:: python
-
- from cubicweb.entities.adapters import ITreeAdapter
-
- class ProjectAdapter(ITreeAdapter):
- __select__ = is_instance('Project')
- tree_relation = 'subproject_of'
-
- class Project(AnyEntity):
- __regid__ = 'Project'
- fetch_attrs, cw_fetch_order = fetch_config(('name', 'description',
- 'description_format', 'summary'))
-
- TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
-
- def dc_title(self):
- return self.name
-
-The fact that the `Project` entity type implements an ``ITree``
-interface is materialized by the ``ProjectAdapter`` class (inheriting
-the pre-defined ``ITreeAdapter`` whose ``__regid__`` is of course
-``ITree``), which will be selected on `Project` entity types because
-of its selector. On this adapter, we redefine the ``tree_relation``
-attribute of the ``ITreeAdapter`` class.
-
-This is typically used in views concerned with the representation of
-tree-like structures (CubicWeb provides several such views).
-
-It is important that the views themselves try not to implement this
-logic, not only because such views would be hardly applyable to other
-tree-like relations, but also because it is perfectly fine and useful
-to use such an interface in Hooks.
-
-In fact, Tree nature is a property of the data model that cannot be
-fully and portably expressed at the level of database entities (think
-about the transitive closure of the child relation). This is a further
-argument to implement it at entity class level.
-
-``fetch_attrs`` configures which attributes should be pre-fetched when using ORM
-methods retrieving entity of this type. In a same manner, the ``cw_fetch_order`` is
-a class method allowing to control sort order. More on this in :ref:`FetchAttrs`.
-
-We can observe the big ``TICKET_DEFAULT_STATE_RESTR`` is a pure
-application domain piece of data. There is, of course, no limitation
-to the amount of class attributes of this kind.
-
-The ``dc_title`` method provides a (unicode string) value likely to be
-consumed by views, but note that here we do not care about output
-encodings. We care about providing data in the most universal format
-possible, because the data could be used by a web view (which would be
-responsible of ensuring XHTML compliance), or a console or file
-oriented output (which would have the necessary context about the
-needed byte stream encoding).
-
-.. note::
-
- The Dublin Core `dc_xxx` methods are not moved to an adapter as they
- are extremely prevalent in CubicWeb and assorted cubes and should be
- available for all entity types.
-
-Let us now dig into more substantial pieces of code, continuing the
-Project class.
-
-.. sourcecode:: python
-
- def latest_version(self, states=('published',), reverse=None):
- """returns the latest version(s) for the project in one of the given
- states.
-
- when no states specified, returns the latest published version.
- """
- order = 'DESC'
- if reverse is not None:
- warn('reverse argument is deprecated',
- DeprecationWarning, stacklevel=1)
- if reverse:
- order = 'ASC'
- rset = self.versions_in_state(states, order, True)
- if rset:
- return rset.get_entity(0, 0)
- return None
-
- def versions_in_state(self, states, order='ASC', limit=False):
- """returns version(s) for the project in one of the given states, sorted
- by version number.
-
- If limit is true, limit result to one version.
- If reverse, versions are returned from the smallest to the greatest.
- """
- if limit:
- order += ' LIMIT 1'
- rql = 'Any V,N ORDERBY version_sort_value(N) %s ' \
- 'WHERE V num N, V in_state S, S name IN (%s), ' \
- 'V version_of P, P eid %%(p)s' % (order, ','.join(repr(s) for s in states))
- return self._cw.execute(rql, {'p': self.eid})
-
-.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker/
-
-These few lines exhibit the important properties we want to outline:
-
-* entity code is concerned with the application domain
-
-* it is NOT concerned with database consistency (this is the realm of
- Hooks/Operations); in other words, it assumes a consistent world
-
-* it is NOT (directly) concerned with end-user interfaces
-
-* however it can be used in both contexts
-
-* it does not create or manipulate the internal object's state
-
-* it plays freely with RQL expression as needed
-
-* it is not concerned with internationalization
-
-* it does not raise exceptions
-
-
--- a/doc/book/en/devrepo/entityclasses/data-as-objects.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +0,0 @@
-Access to persistent data
---------------------------
-
-Python-level access to persistent data is provided by the
-:class:`Entity <cubicweb.entity>` class.
-
-.. XXX this part is not clear. refactor it.
-
-An entity class is bound to a schema entity type. Descriptors are added when
-classes are registered in order to initialize the class according to its schema:
-
-* the attributes defined in the schema appear as attributes of these classes
-
-* the relations defined in the schema appear as attributes of these classes,
- but are lists of instances
-
-`Formatting and output generation`:
-
-* :meth:`view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
- (and returns an unicode string)
-
-* :meth:`absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
-
-* :meth:`rest_path()`, returns a relative REST URL to get the entity
-
-* :meth:`printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
- returns a string enabling the display of an attribute value in a given format
- (the value is automatically recovered if necessary)
-
-`Data handling`:
-
-* :meth:`as_rset()`, converts the entity into an equivalent result set simulating the
- request `Any X WHERE X eid _eid_`
-
-* :meth:`complete(skip_bytes=True)`, executes a request that recovers at
- once all the missing attributes of an entity
-
-* :meth:`get_value(name)`, returns the value associated to the attribute name given
- in parameter
-
-* :meth:`related(rtype, role='subject', limit=None, entities=False)`,
- returns a list of entities related to the current entity by the
- relation given in parameter
-
-* :meth:`unrelated(rtype, targettype, role='subject', limit=None)`,
- returns a result set corresponding to the entities not (yet)
- related to the current entity by the relation given in parameter
- and satisfying its constraints
-
-* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the
- corresponding values given named parameters. To set a relation where this
- entity is the object of the relation, use `reverse_<relation>` as argument
- name. Values may be an entity, a list of entities, or None (meaning that all
- relations of the given type from or to this object should be deleted).
-
-* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid
- given in the parameters on the current entity
-
-* :meth:`cw_delete()` allows to delete the entity
-
-
-The :class:`AnyEntity` class
-----------------------------
-
-To provide a specific behavior for each entity, we can define a class
-inheriting from `cubicweb.entities.AnyEntity`. In general, we define this class
-in `mycube.entities` module (or in a submodule if we want to split code among
-multiple files) so that it will be available on both server and client side.
-
-The class `AnyEntity` is a sub-class of Entity that add methods to it,
-and helps specializing (by further subclassing) the handling of a
-given entity type.
-
-Most methods defined for `AnyEntity`, in addition to `Entity`, add
-support for the `Dublin Core`_ metadata.
-
-.. _`Dublin Core`: http://dublincore.org/
-
-`Standard meta-data (Dublin Core)`:
-
-* :meth:`dc_title()`, returns a unicode string corresponding to the
- meta-data `Title` (used by default is the first non-meta attribute
- of the entity schema)
-
-* :meth:`dc_long_title()`, same as dc_title but can return a more
- detailed title
-
-* :meth:`dc_description(format='text/plain')`, returns a unicode string
- corresponding to the meta-data `Description` (looks for a
- description attribute by default)
-
-* :meth:`dc_authors()`, returns a unicode string corresponding to the meta-data
- `Authors` (owners by default)
-
-* :meth:`dc_creator()`, returns a unicode string corresponding to the
- creator of the entity
-
-* :meth:`dc_date(date_format=None)`, returns a unicode string corresponding to
- the meta-data `Date` (update date by default)
-
-* :meth:`dc_type(form='')`, returns a string to display the entity type by
- specifying the preferred form (`plural` for a plural form)
-
-* :meth:`dc_language()`, returns the language used by the entity
-
-Inheritance
------------
-
-When describing a data model, entities can inherit from other entities as is
-common in object-oriented programming.
-
-You have the possibility to redefine whatever pleases you, as follow:
-
-.. sourcecode:: python
-
- from cubes.OTHER_CUBE import entities
-
- class EntityExample(entities.EntityExample):
-
- def dc_long_title(self):
- return '%s (%s)' % (self.name, self.description)
-
-The most specific entity definition will always the one used by the
-ORM. For instance, the new EntityExample above in mycube replaces the
-one in OTHER_CUBE. These types are stored in the `etype` section of
-the `vregistry`.
-
-Notice this is different than yams schema inheritance, which is an
-experimental undocumented feature.
-
-
-Application logic
------------------
-
-While a lot of custom behaviour and application logic can be
-implemented using entity classes, the programmer must be aware that
-adding new attributes and method on an entity class adds may shadow
-schema-level attribute or relation definitions.
-
-To keep entities clean (mostly data structures plus a few universal
-methods such as listed above), one should use `adapters` (see
-:ref:`adapters`).
--- a/doc/book/en/devrepo/entityclasses/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-Data as objects
-===============
-
-In this chapter, we will introduce the objects that are used to handle
-the logic associated to the data stored in the database.
-
-.. toctree::
- :maxdepth: 1
-
- data-as-objects
- load-sort
- adapters
- application-logic
--- a/doc/book/en/devrepo/entityclasses/load-sort.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-
-.. _FetchAttrs:
-
-Loaded attributes and default sorting management
-````````````````````````````````````````````````
-
-* The class attribute `fetch_attrs` allows to define in an entity class a list of
- names of attributes that should be automatically loaded when entities of this
- type are fetched from the database using ORM methods retrieving entity of this
- type (such as :meth:`related` and :meth:`unrelated`). You can also put relation
- names in there, but we are limited to *subject relations of cardinality `?` or
- `1`*.
-
-* The :meth:`cw_fetch_order` and :meth:`cw_fetch_unrelated_order` class methods
- are respectively responsible to control how entities will be sorted when:
-
- - retrieving all entities of a given type, or entities related to another
-
- - retrieving a list of entities for use in drop-down lists enabling relations
- creation in the editing view of an entity
-
-By default entities will be listed on their modification date descending,
-i.e. you'll get entities recently modified first. While this is usually a good
-default in drop-down list, you'll probably want to change `cw_fetch_order`.
-
-This may easily be done using the :func:`~cubicweb.entities.fetch_config`
-function, which simplifies the definition of attributes to load and sorting by
-returning a list of attributes to pre-load (considering automatically the
-attributes of `AnyEntity`) and a sorting function as described below:
-
-.. autofunction:: cubicweb.entities.fetch_config
-
-In you want something else (such as sorting on the result of a registered
-procedure), here is the prototype of those methods:
-
-
-.. automethod:: cubicweb.entity.Entity.cw_fetch_order
-
-.. automethod:: cubicweb.entity.Entity.cw_fetch_unrelated_order
-
--- a/doc/book/en/devrepo/fti.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,159 +0,0 @@
-.. _fti:
-
-Full Text Indexing in CubicWeb
-------------------------------
-
-When an attribute is tagged as *fulltext-indexable* in the datamodel,
-CubicWeb will automatically trigger hooks to update the internal
-fulltext index (i.e the ``appears`` SQL table) each time this attribute
-is modified.
-
-CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
-fulltext on demand:
-
-.. sourcecode:: bash
-
- cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
-
-You can also rebuild the fulltext index for a given set of entity types:
-
-.. sourcecode:: bash
-
- cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
-
-In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
-will be rebuilt.
-
-
-Standard FTI process
-~~~~~~~~~~~~~~~~~~~~
-
-Considering an entity type ``ET``, the default *fti* process is to :
-
-1. fetch all entities of type ``ET``
-
-2. for each entity, adapt it to ``IFTIndexable`` (see
- :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
-
-3. call
- :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
- the adapter which is supposed to return a dictionary *weight* ->
- *list of words* as expected by
- :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
- tokenization of each attribute value is done by
- :meth:`~logilab.database.fti.tokenize`.
-
-
-See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
-
-
-Yams and ``fulltext_container``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-It is possible in the datamodel to indicate that fulltext-indexed
-attributes defined for an entity type will be used to index not the
-entity itself but a related entity. This is especially useful for
-composite entities. Let's take a look at (a simplified version of)
-the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
-
-.. sourcecode:: python
-
- class CWUser(WorkflowableEntityType):
- login = String(required=True, unique=True, maxsize=64)
- upassword = Password(required=True)
-
- class EmailAddress(EntityType):
- address = String(required=True, fulltextindexed=True,
- indexed=True, unique=True, maxsize=128)
-
-
- class use_email_relation(RelationDefinition):
- name = 'use_email'
- subject = 'CWUser'
- object = 'EmailAddress'
- cardinality = '*?'
- composite = 'subject'
-
-
-The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
-and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
-in your application, if you use fulltext search to look for an email address, CubicWeb
-will return the ``EmailAddress`` itself. But the objects we'd like to index
-are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
-
-The simplest way to achieve that is to tag the ``use_email`` relation in
-the datamodel:
-
-.. sourcecode:: python
-
- class use_email(RelationType):
- fulltext_container = 'subject'
-
-
-Customizing how entities are fetched during ``db-rebuild-fti``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-``db-rebuild-fti`` will call the
-:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
-method on your entity type.
-
-.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
-
-Now, suppose you've got a _huge_ table to index, you probably don't want to
-get all entities at once. So here's a simple customized example that will
-process block of 10000 entities:
-
-.. sourcecode:: python
-
-
- class MyEntityClass(AnyEntity):
- __regid__ = 'MyEntityClass'
-
- @classmethod
- def cw_fti_index_rql_queries(cls, req):
- # get the default RQL method and insert LIMIT / OFFSET instructions
- base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
- selected, restrictions = base_rql.split(' WHERE ')
- rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
- selected, restrictions)
- # count how many entities you'll have to index
- count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
- # iterate by blocks of 10000 entities
- chunksize = 10000
- for offset in xrange(0, count, chunksize):
- print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
- yield rql_template % {'limit': chunksize, 'offset': offset}
-
-Since you have access to ``req``, you can more or less fetch whatever you want.
-
-
-Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-You can also customize the FTI process by providing your own ``get_words()``
-implementation:
-
-.. sourcecode:: python
-
- from cubicweb.entities.adapters import IFTIndexableAdapter
-
- class SearchIndexAdapter(IFTIndexableAdapter):
- __regid__ = 'IFTIndexable'
- __select__ = is_instance('MyEntityClass')
-
- def fti_containers(self, _done=None):
- """this should yield any entity that must be considered to
- fulltext-index self.entity
-
- CubicWeb's default implementation will look for yams'
- ``fulltex_container`` property.
- """
- yield self.entity
- yield self.entity.some_related_entity
-
-
- def get_words(self):
- # implement any logic here
- # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
- # for the actual signification of 'C'
- return {'C': ['any', 'word', 'I', 'want']}
--- a/doc/book/en/devrepo/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-.. _Part2:
-
-----------------------
-Repository development
-----------------------
-
-This part is about developing applications with the *CubicWeb*
-framework. It is not concerned with the web system, which is a
-separate layer and has its own whole chapter.
-
-.. toctree::
- :maxdepth: 2
- :numbered:
-
- cubes/index
- vreg.rst
- datamodel/index
- entityclasses/index
- devcore/index
- repo/index
- testing.rst
- migration.rst
- profiling.rst
- fti.rst
- dataimport
--- a/doc/book/en/devrepo/migration.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,250 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _migration:
-
-Migration
-=========
-
-One of the main design goals of *CubicWeb* was to support iterative and agile
-development. For this purpose, multiple actions are provided to facilitate the
-improvement of an instance, and in particular to handle the changes to be
-applied to the data model, without loosing existing data.
-
-The current version of a cube (and of cubicweb itself) is provided in the file
-`__pkginfo__.py` as a tuple of 3 integers.
-
-Migration scripts management
-----------------------------
-
-Migration scripts has to be located in the directory `migration` of your
-cube and named accordingly:
-
-::
-
- <version n° X.Y.Z>[_<description>]_<mode>.py
-
-in which :
-
-* X.Y.Z is the model version number to which the script enables to migrate.
-
-* *mode* (between the last "_" and the extension ".py") is used for
- distributed installation. It indicates to which part
- of the application (RQL server, web server) the script applies.
- Its value could be :
-
- * `common`, applies to the RQL server as well as the web server and updates
- files on the hard drive (configuration files migration for example).
-
- * `web`, applies only to the web server and updates files on the hard drive.
-
- * `repository`, applies only to the RQL server and updates files on the
- hard drive.
-
- * `Any`, applies only to the RQL server and updates data in the database
- (schema and data migration for example).
-
-Again in the directory `migration`, the file `depends.map` allows to indicate
-that for the migration to a particular model version, you always have to first
-migrate to a particular *CubicWeb* version. This file can contain comments (lines
-starting with `#`) and a dependency is listed as follows: ::
-
- <model version n° X.Y.Z> : <cubicweb version n° X.Y.Z>
-
-For example: ::
-
- 0.12.0: 2.26.0
- 0.13.0: 2.27.0
- # 0.14 works with 2.27 <= cubicweb <= 2.28 at least
- 0.15.0: 2.28.0
-
-Base context
-------------
-
-The following identifiers are pre-defined in migration scripts:
-
-* `config`, instance configuration
-
-* `interactive_mode`, boolean indicating that the script is executed in
- an interactive mode or not
-
-* `versions_map`, dictionary of migrated versions (key are cubes
- names, including 'cubicweb', values are (from version, to version)
-
-* `confirm(question)`, function asking the user and returning true
- if the user answers yes, false otherwise (always returns true in
- non-interactive mode)
-
-* `_()` is equivalent to `unicode` allowing to flag the strings to
- internationalize in the migration scripts.
-
-In the `repository` scripts, the following identifiers are also defined:
-
-* `commit(ask_confirm=True)`, request confirming and executing a "commit"
-
-* `schema`, instance schema (readen from the database)
-
-* `fsschema`, installed schema on the file system (e.g. schema of
- the updated model and cubicweb)
-
-* `repo`, repository object
-
-* `session`, repository session object
-
-
-New cube dependencies
----------------------
-
-If your code depends on some new cubes, you have to add them in a migration
-script by using:
-
-* `add_cube(cube, update_database=True)`, add a cube.
-* `add_cubes(cubes, update_database=True)`, add a list of cubes.
-
-The `update_database` parameter is telling if the database schema
-should be updated or if only the relevant persistent property should be
-inserted (for the case where a new cube has been extracted from an
-existing one, so the new cube schema is actually already in there).
-
-If some of the added cubes are already used by an instance, they'll simply be
-silently skipped.
-
-
-Schema migration
-----------------
-The following functions for schema migration are available in `repository`
-scripts:
-
-* `add_attribute(etype, attrname, attrtype=None, commit=True)`, adds a new
- attribute to an existing entity type. If the attribute type is not specified,
- then it is extracted from the updated schema.
-
-* `drop_attribute(etype, attrname, commit=True)`, removes an attribute from an
- existing entity type.
-
-* `rename_attribute(etype, oldname, newname, commit=True)`, renames an attribute
-
-* `add_entity_type(etype, auto=True, commit=True)`, adds a new entity type.
- If `auto` is True, all the relations using this entity type and having a known
- entity type on the other hand will automatically be added.
-
-* `drop_entity_type(etype, commit=True)`, removes an entity type and all the
- relations using it.
-
-* `rename_entity_type(oldname, newname, commit=True)`, renames an entity type
-
-* `add_relation_type(rtype, addrdef=True, commit=True)`, adds a new relation
- type. If `addrdef` is True, all the relations definitions of this type will
- be added.
-
-* `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
- definitions of this type.
-
-* `rename_relation_type(oldname, newname, commit=True)`, renames a relation type.
-
-* `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
- relation definition.
-
-* `drop_relation_definition(subjtype, rtype, objtype, commit=True)`, removes
- a relation definition.
-
-* `sync_schema_props_perms(ertype=None, syncperms=True, syncprops=True, syncrdefs=True, commit=True)`,
- synchronizes properties and/or permissions on:
- - the whole schema if ertype is None
- - an entity or relation type schema if ertype is a string
- - a relation definition if ertype is a 3-uple (subject, relation, object)
-
-* `change_relation_props(subjtype, rtype, objtype, commit=True, **kwargs)`, changes
- properties of a relation definition by using the named parameters of the properties
- to change.
-
-* `set_widget(etype, rtype, widget, commit=True)`, changes the widget used for the
- relation <rtype> of entity type <etype>.
-
-* `set_size_constraint(etype, rtype, size, commit=True)`, changes the size constraints
- for the relation <rtype> of entity type <etype>.
-
-Data migration
---------------
-The following functions for data migration are available in `repository` scripts:
-
-* `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL
- query, either to interrogate or update. A result set object is returned.
-
-* `add_entity(etype, *args, **kwargs)`, adds a new entity of the given type.
- The attribute and relation values are specified as named positional
- arguments.
-
-Workflow creation
------------------
-
-The following functions for workflow creation are available in `repository`
-scripts:
-
-* `add_workflow(label, workflowof, initial=False, commit=False, **kwargs)`, adds a new workflow
- for a given type(s)
-
-You can find more details about workflows in the chapter :ref:`Workflow` .
-
-Configuration migration
------------------------
-
-The following functions for configuration migration are available in all
-scripts:
-
-* `option_renamed(oldname, newname)`, indicates that an option has been renamed
-
-* `option_group_change(option, oldgroup, newgroup)`, indicates that an option does not
- belong anymore to the same group.
-
-* `option_added(oldname, newname)`, indicates that an option has been added.
-
-* `option_removed(oldname, newname)`, indicates that an option has been deleted.
-
-The `config` variable is an object which can be used to access the
-configuration values, for reading and updating, with a dictionary-like
-syntax.
-
-Example 1: migration script changing the variable 'sender-addr' in
-all-in-one.conf. The script also checks that in that the instance is
-configured with a known value for that variable, and only updates the
-value in that case.
-
-.. sourcecode:: python
-
- wrong_addr = 'cubicweb@loiglab.fr' # known wrong address
- fixed_addr = 'cubicweb@logilab.fr'
- configured_addr = config.get('sender-addr')
- # check that the address has not been hand fixed by a sysadmin
- if configured_addr == wrong_addr:
- config['sender-addr'] = fixed-addr
- config.save()
-
-Example 2: checking the value of the database backend driver, which
-can be useful in case you need to issue backend-dependent raw SQL
-queries in a migration script.
-
-.. sourcecode:: python
-
- dbdriver = config.sources()['system']['db-driver']
- if dbdriver == "sqlserver2005":
- # this is now correctly handled by CW :-)
- sql('ALTER TABLE cw_Xxxx ALTER COLUMN cw_name varchar(64) NOT NULL;')
- commit()
- else: # postgresql
- sync_schema_props_perms(ertype=('Xxxx', 'name', 'String'),
- syncperms=False)
-
-
-Others migration functions
---------------------------
-Those functions are only used for low level operations that could not be
-accomplished otherwise or to repair damaged databases during interactive
-session. They are available in `repository` scripts:
-
-* `sql(sql, args=None, ask_confirm=True)`, executes an arbitrary SQL query on the system source
-* `add_entity_type_table(etype, commit=True)`
-* `add_relation_type_table(rtype, commit=True)`
-* `uninline_relation(rtype, commit=True)`
-
-
-[FIXME] Add explanation on how to use cubicweb-ctl shell
--- a/doc/book/en/devrepo/profiling.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-.. _PROFILING:
-
-Profiling and performance
-=========================
-
-If you feel that one of your pages takes more time than it should to be
-generated, chances are that you're making too many RQL queries. Obviously,
-there are other reasons but experience tends to show this is the first thing to
-track down. Luckily, CubicWeb provides a configuration option to log RQL
-queries. In your ``all-in-one.conf`` file, set the **query-log-file** option::
-
- # web application query log file
- query-log-file=/home/user/myapp-rql.log
-
-Then restart your application, reload your page and stop your application.
-The file ``myapp-rql.log`` now contains the list of RQL queries that were
-executed during your test. It's a simple text file containing lines such as::
-
- Any A WHERE X eid %(x)s, X lastname A {'x': 448} -- (0.002 sec, 0.010 CPU sec)
- Any A WHERE X eid %(x)s, X firstname A {'x': 447} -- (0.002 sec, 0.000 CPU sec)
-
-The structure of each line is::
-
- <RQL QUERY> <QUERY ARGS IF ANY> -- <TIME SPENT>
-
-CubicWeb also provides the **exlog** command to examine and summarize data found
-in such a file:
-
-.. sourcecode:: sh
-
- $ cubicweb-ctl exlog /home/user/myapp-rql.log
- 0.07 50 Any A WHERE X eid %(x)s, X firstname A {}
- 0.05 50 Any A WHERE X eid %(x)s, X lastname A {}
- 0.01 1 Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E employees X, X modification_date AA {}
- 0.01 1 Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s {, }
- 0.01 1 Any B,T,P ORDERBY lower(T) WHERE B is Bookmark,B title T, B path P, B bookmarked_by U, U eid %(x)s {}
- 0.01 1 Any A,B,C,D WHERE A eid %(x)s,A name B,A creation_date C,A modification_date D {}
-
-This command sorts and uniquifies queries so that it's easy to see where
-is the hot spot that needs optimization.
-
-Do not neglect to set the **fetch_attrs** attribute you can define in your
-entity classes because it can greatly reduce the number of queries executed (see
-:ref:`FetchAttrs`).
-
-You should also know about the **profile** option in the ``all-in-on.conf``. If
-set, this option will make your application run in an `hotshot`_ session and
-store the results in the specified file.
-
-.. _hotshot: http://docs.python.org/library/hotshot.html#module-hotshot
-
-Last but no least, if you're using the PostgreSQL database backend, VACUUMing
-your database can significantly improve the performance of the queries (by
-updating the statistics used by the query optimizer). Nowadays, this is done
-automatically from time to time, but if you've just imported a large amount of
-data in your db, you will want to vacuum it (with the analyse option on). Read
-the documentation of your database for more information.
--- a/doc/book/en/devrepo/repo/hooks.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,279 +0,0 @@
-.. -*- coding: utf-8 -*-
-.. _hooks:
-
-Hooks and Operations
-====================
-
-.. autodocstring:: cubicweb.server.hook
-
-
-Example using dataflow hooks
-----------------------------
-
-We will use a very simple example to show hooks usage. Let us start with the
-following schema.
-
-.. sourcecode:: python
-
- class Person(EntityType):
- age = Int(required=True)
-
-We would like to add a range constraint over a person's age. Let's write an hook
-(supposing yams can not handle this nativly, which is wrong). It shall be placed
-into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
-`mycube/hooks/... package` containing hooks in various modules.
-
-.. sourcecode:: python
-
- from cubicweb import ValidationError
- from cubicweb.predicates import is_instance
- from cubicweb.server.hook import Hook
-
- class PersonAgeRange(Hook):
- __regid__ = 'person_age_range'
- __select__ = Hook.__select__ & is_instance('Person')
- events = ('before_add_entity', 'before_update_entity')
-
- def __call__(self):
- if 'age' in self.entity.cw_edited:
- if 0 <= self.entity.age <= 120:
- return
- msg = self._cw._('age must be between 0 and 120')
- raise ValidationError(self.entity.eid, {'age': msg})
-
-In our example the base `__select__` is augmented with an `is_instance` selector
-matching the desired entity type.
-
-The `events` tuple is used specify that our hook should be called before the
-entity is added or updated.
-
-Then in the hook's `__call__` method, we:
-
-* check if the 'age' attribute is edited
-* if so, check the value is in the range
-* if not, raise a validation error properly
-
-Now Let's augment our schema with new `Company` entity type with some relation to
-`Person` (in 'mycube/schema.py').
-
-.. sourcecode:: python
-
- class Company(EntityType):
- name = String(required=True)
- boss = SubjectRelation('Person', cardinality='1*')
- subsidiary_of = SubjectRelation('Company', cardinality='*?')
-
-
-We would like to constrain the company's bosses to have a minimum (legal)
-age. Let's write an hook for this, which will be fired when the `boss` relation
-is established (still supposing we could not specify that kind of thing in the
-schema).
-
-.. sourcecode:: python
-
- class CompanyBossLegalAge(Hook):
- __regid__ = 'company_boss_legal_age'
- __select__ = Hook.__select__ & match_rtype('boss')
- events = ('before_add_relation',)
-
- def __call__(self):
- boss = self._cw.entity_from_eid(self.eidto)
- if boss.age < 18:
- msg = self._cw._('the minimum age for a boss is 18')
- raise ValidationError(self.eidfrom, {'boss': msg})
-
-.. Note::
-
- We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the
- proper relation type.
-
- The essential difference with respect to an entity hook is that there is no
- self.entity, but `self.eidfrom` and `self.eidto` hook attributes which
- represent the subject and object **eid** of the relation.
-
-Suppose we want to check that there is no cycle by the `subsidiary_of`
-relation. This is best achieved in an operation since all relations are likely to
-be set at commit time.
-
-.. sourcecode:: python
-
- from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype
-
- def check_cycle(self, session, eid, rtype, role='subject'):
- parents = set([eid])
- parent = session.entity_from_eid(eid)
- while parent.related(rtype, role):
- parent = parent.related(rtype, role)[0]
- if parent.eid in parents:
- msg = session._('detected %s cycle' % rtype)
- raise ValidationError(eid, {rtype: msg})
- parents.add(parent.eid)
-
-
- class CheckSubsidiaryCycleOp(Operation):
-
- def precommit_event(self):
- check_cycle(self.session, self.eidto, 'subsidiary_of')
-
-
- class CheckSubsidiaryCycleHook(Hook):
- __regid__ = 'check_no_subsidiary_cycle'
- __select__ = Hook.__select__ & match_rtype('subsidiary_of')
- events = ('after_add_relation',)
-
- def __call__(self):
- CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
-
-
-Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other
-exceptions are usually programming errors.
-
-In the above example, our hook will instantiate an operation each time the hook
-is called, i.e. each time the `subsidiary_of` relation is set. There is an
-alternative method to schedule an operation from a hook, using the
-:func:`get_instance` class method.
-
-.. sourcecode:: python
-
- from cubicweb.server.hook import set_operation
-
- class CheckSubsidiaryCycleHook(Hook):
- __regid__ = 'check_no_subsidiary_cycle'
- events = ('after_add_relation',)
- __select__ = Hook.__select__ & match_rtype('subsidiary_of')
-
- def __call__(self):
- CheckSubsidiaryCycleOp.get_instance(self._cw).add_data(self.eidto)
-
- class CheckSubsidiaryCycleOp(DataOperationMixIn, Operation):
-
- def precommit_event(self):
- for eid in self.get_data():
- check_cycle(self.session, eid, self.rtype)
-
-
-Here, we call :func:`set_operation` so that we will simply accumulate eids of
-entities to check at the end in a single `CheckSubsidiaryCycleOp`
-operation. Value are stored in a set associated to the
-'subsidiary_cycle_detection' transaction data key. The set initialization and
-operation creation are handled nicely by :func:`set_operation`.
-
-A more realistic example can be found in the advanced tutorial chapter
-:ref:`adv_tuto_security_propagation`.
-
-
-Inter-instance communication
-----------------------------
-
-If your application consists of several instances, you may need some means to
-communicate between them. Cubicweb provides a publish/subscribe mechanism
-using ØMQ_. In order to use it, use
-:meth:`~cubicweb.server.cwzmq.ZMQComm.add_subscription` on the
-`repo.app_instances_bus` object. The `callback` will get the message (as a
-list). A message can be sent by calling
-:meth:`~cubicweb.server.cwzmq.ZMQComm.publish` on `repo.app_instances_bus`.
-The first element of the message is the topic which is used for filtering and
-dispatching messages.
-
-.. _ØMQ: http://www.zeromq.org/
-
-.. sourcecode:: python
-
- class FooHook(hook.Hook):
- events = ('server_startup',)
- __regid__ = 'foo_startup'
-
- def __call__(self):
- def callback(msg):
- self.info('received message: %s', ' '.join(msg))
- self.repo.app_instances_bus.add_subscription('hello', callback)
-
-.. sourcecode:: python
-
- def do_foo(self):
- actually_do_foo()
- self._cw.repo.app_instances_bus.publish(['hello', 'world'])
-
-The `zmq-address-pub` configuration variable contains the address used
-by the instance for sending messages, e.g. `tcp://*:1234`. The
-`zmq-address-sub` variable contains a comma-separated list of addresses
-to listen on, e.g. `tcp://localhost:1234, tcp://192.168.1.1:2345`.
-
-
-Hooks writing tips
-------------------
-
-Reminder
-~~~~~~~~
-
-You should never use the `entity.foo = 42` notation to update an entity. It will
-not do what you expect (updating the database). Instead, use the
-:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
-:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
-'before_update_entity' event.
-
-
-How to choose between a before and an after event ?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-`before_*` hooks give you access to the old attribute (or relation)
-values. You can also intercept and update edited values in the case of
-entity modification before they reach the database.
-
-Else the question is: should I need to do things before or after the actual
-modification ? If the answer is "it doesn't matter", use an 'after' event.
-
-
-Validation Errors
-~~~~~~~~~~~~~~~~~
-
-When a hook which is responsible to maintain the consistency of the
-data model detects an error, it must use a specific exception named
-:exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
-:exc:`~cubicweb.ValidationError` is a programming error. Raising it
-entails aborting the current transaction.
-
-This exception is used to convey enough information up to the user
-interface. Hence its constructor is different from the default Exception
-constructor. It accepts, positionally:
-
-* an entity eid (**not the entity itself**),
-
-* a dict whose keys represent attribute (or relation) names and values
- an end-user facing message (hence properly translated) relating the
- problem.
-
-.. sourcecode:: python
-
- raise ValidationError(earth.eid, {'sea_level': self._cw._('too high'),
- 'temperature': self._cw._('too hot')})
-
-
-Checking for object created/deleted in the current transaction
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In hooks, you can use the
-:meth:`~cubicweb.server.session.Session.added_in_transaction` or
-:meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session
-object to check if an eid has been created or deleted during the hook's
-transaction.
-
-This is useful to enable or disable some stuff if some entity is being added or
-deleted.
-
-.. sourcecode:: python
-
- if self._cw.deleted_in_transaction(self.eidto):
- return
-
-
-Peculiarities of inlined relations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
-for details) are inserted in the database at the same time as entity attributes.
-
-This may have some side effect, for instance when creating an entity
-and setting an inlined relation in the same rql query, then at
-`before_add_relation` time, the relation will already exist in the
-database (it is otherwise not the case).
--- a/doc/book/en/devrepo/repo/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Repository customization
-++++++++++++++++++++++++
-.. toctree::
- :maxdepth: 1
-
- sessions
- hooks
- notifications
- tasks
-
-
--- a/doc/book/en/devrepo/repo/notifications.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Notifications management
-========================
-
-CubicWeb provides a machinery to ease notifications handling. To use it for a
-notification:
-
-* write a view inheriting from
- :class:`~cubicweb.sobjects.notification.NotificationView`. The usual view api
- is used to generated the email (plain text) content, and additional
- :meth:`~cubicweb.sobjects.notification.NotificationView.subject` and
- :meth:`~cubicweb.sobjects.notification.NotificationView.recipients` methods
- are used to build the email's subject and
- recipients. :class:`NotificationView` provides default implementation for both
- methods.
-
-* write a hook for event that should trigger this notification, select the view
- (without rendering it), and give it to
- :func:`cubicweb.hooks.notification.notify_on_commit` so that the notification
- will be sent if the transaction succeed.
-
-
-.. XXX explain recipient finder and provide example
-
-API details
-~~~~~~~~~~~
-.. autoclass:: cubicweb.sobjects.notification.NotificationView
-.. autofunction:: cubicweb.hooks.notification.notify_on_commit
--- a/doc/book/en/devrepo/repo/sessions.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Sessions
-========
-
-Sessions are objects linked to an authenticated user. The `Session.new_cnx`
-method returns a new Connection linked to that session.
-
-Connections
-===========
-
-Connections provide the `.execute` method to query the data sources.
-
-Kinds of connections
---------------------
-
-There are two kinds of connections.
-
-* `normal connections` are the most common: they are related to users and
- carry security checks coming with user credentials
-
-* `internal connections` have all the powers; they are also used in only a
- few situations where you don't already have an adequate session at
- hand, like: user authentication, data synchronisation in
- multi-source contexts
-
-Normal connections are typically named `_cw` in most appobjects or
-sometimes just `session`.
-
-Internal connections are available from the `Repository` object and are
-to be used like this:
-
-.. sourcecode:: python
-
- with self.repo.internal_cnx() as cnx:
- do_stuff_with(cnx)
- cnx.commit()
-
-Connections should always be used as context managers, to avoid leaks.
-
-Authentication and management of sessions
------------------------------------------
-
-The authentication process is a ballet involving a few dancers:
-
-* through its `get_session` method the top-level application object (the
- `CubicWebPublisher`) will open a session whenever a web request
- comes in; it asks the `session manager` to open a session (giving
- the web request object as context) using `open_session`
-
- * the session manager asks its authentication manager (which is a
- `component`) to authenticate the request (using `authenticate`)
-
- * the authentication manager asks, in order, to its authentication
- information retrievers, a login and an opaque object containing
- other credentials elements (calling `authentication_information`),
- giving the request object each time
-
- * the default retriever (named `LoginPasswordRetriever`)
- will in turn defer login and password fetching to the request
- object (which, depending on the authentication mode (`cookie`
- or `http`), will do the appropriate things and return a login
- and a password)
-
- * the authentication manager, on success, asks the `Repository`
- object to connect with the found credentials (using `connect`)
-
- * the repository object asks authentication to all of its
- sources which support the `CWUser` entity with the given
- credentials; when successful it can build the cwuser entity,
- from which a regular `Session` object is made; it returns the
- session id
-
- * the source in turn will delegate work to an authentifier
- class that defines the ultimate `authenticate` method (for
- instance the native source will query the database against
- the provided credentials)
-
- * the authentication manager, on success, will call back _all_
- retrievers with `authenticated` and return its authentication
- data (on failure, it will try the anonymous login or, if the
- configuration forbids it, raise an `AuthenticationError`)
-
-Writing authentication plugins
-------------------------------
-
-Sometimes CubicWeb's out-of-the-box authentication schemes (cookie and
-http) are not sufficient. Nowadays there is a plethora of such schemes
-and the framework cannot provide them all, but as the sequence above
-shows, it is extensible.
-
-Two levels have to be considered when writing an authentication
-plugin: the web client and the repository.
-
-We invented a scenario where it makes sense to have a new plugin in
-each side: some middleware will do pre-authentication and under the
-right circumstances add a new HTTP `x-foo-user` header to the query
-before it reaches the CubicWeb instance. For a concrete example of
-this, see the `trustedauth`_ cube.
-
-.. _`trustedauth`: http://www.cubicweb.org/project/cubicweb-trustedauth
-
-Repository authentication plugins
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-On the repository side, it is possible to register a source
-authentifier using the following kind of code:
-
-.. sourcecode:: python
-
- from cubicweb.server.sources import native
-
- class FooAuthentifier(native.LoginPasswordAuthentifier):
- """ a source authentifier plugin
- if 'foo' in authentication information, no need to check
- password
- """
- auth_rql = 'Any X WHERE X is CWUser, X login %(login)s'
-
- def authenticate(self, session, login, **kwargs):
- """return CWUser eid for the given login
- if this account is defined in this source,
- else raise `AuthenticationError`
- """
- session.debug('authentication by %s', self.__class__.__name__)
- if 'foo' not in kwargs:
- return super(FooAuthentifier, self).authenticate(session, login, **kwargs)
- try:
- rset = session.execute(self.auth_rql, {'login': login})
- return rset[0][0]
- except Exception, exc:
- session.debug('authentication failure (%s)', exc)
- raise AuthenticationError('foo user is unknown to us')
-
-Since repository authentifiers are not appobjects, we have to register
-them through a `server_startup` hook.
-
-.. sourcecode:: python
-
- class ServerStartupHook(hook.Hook):
- """ register the foo authenticator """
- __regid__ = 'fooauthenticatorregisterer'
- events = ('server_startup',)
-
- def __call__(self):
- self.debug('registering foo authentifier')
- self.repo.system_source.add_authentifier(FooAuthentifier())
-
-Web authentication plugins
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. sourcecode:: python
-
- class XFooUserRetriever(authentication.LoginPasswordRetriever):
- """ authenticate by the x-foo-user http header
- or just do normal login/password authentication
- """
- __regid__ = 'x-foo-user'
- order = 0
-
- def authentication_information(self, req):
- """retrieve authentication information from the given request, raise
- NoAuthInfo if expected information is not found
- """
- self.debug('web authenticator building auth info')
- try:
- login = req.get_header('x-foo-user')
- if login:
- return login, {'foo': True}
- else:
- return super(XFooUserRetriever, self).authentication_information(self, req)
- except Exception, exc:
- self.debug('web authenticator failed (%s)', exc)
- raise authentication.NoAuthInfo()
-
- def authenticated(self, retriever, req, cnx, login, authinfo):
- """callback when return authentication information have opened a
- repository connection successfully. Take care req has no session
- attached yet, hence req.execute isn't available.
-
- Here we set a flag on the request to indicate that the user is
- foo-authenticated. Can be used by a selector
- """
- self.debug('web authenticator running post authentication callback')
- cnx.foo_user = authinfo.get('foo')
-
-In the `authenticated` method we add (in an admitedly slightly hackish
-way) an attribute to the connection object. This, in turn, can be used
-to build a selector dispatching on the fact that the user was
-preauthenticated or not.
-
-.. sourcecode:: python
-
- @objectify_selector
- def foo_authenticated(cls, req, rset=None, **kwargs):
- if hasattr(req.cnx, 'foo_user') and req.foo_user:
- return 1
- return 0
-
-Full Session and Connection API
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. autoclass:: cubicweb.server.session.Session
-.. autoclass:: cubicweb.server.session.Connection
--- a/doc/book/en/devrepo/repo/tasks.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Tasks
-=========
-
-[WRITE ME]
-
-* repository tasks
-
--- a/doc/book/en/devrepo/testing.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,559 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Tests
-=====
-
-Unit tests
-----------
-
-The *CubicWeb* framework provides the
-:class:`cubicweb.devtools.testlib.CubicWebTC` test base class .
-
-Tests shall be put into the mycube/test directory. Additional test
-data shall go into mycube/test/data.
-
-It is much advised to write tests concerning entities methods,
-actions, hooks and operations, security. The
-:class:`~cubicweb.devtools.testlib.CubicWebTC` base class has
-convenience methods to help test all of this.
-
-In the realm of views, automatic tests check that views are valid
-XHTML. See :ref:`automatic_views_tests` for details.
-
-Most unit tests need a live database to work against. This is achieved
-by CubicWeb using automatically sqlite (bundled with Python, see
-http://docs.python.org/library/sqlite3.html) as a backend.
-
-The database is stored in the mycube/test/tmpdb,
-mycube/test/tmpdb-template files. If it does not (yet) exist, it will
-be built automatically when the test suite starts.
-
-.. warning::
-
- Whenever the schema changes (new entities, attributes, relations)
- one must delete these two files. Changes concerned only with entity
- or relation type properties (constraints, cardinalities,
- permissions) and generally dealt with using the
- `sync_schema_props_perms()` function of the migration environment do
- not need a database regeneration step.
-
-.. _hook_test:
-
-Unit test by example
-````````````````````
-
-We start with an example extracted from the keyword cube (available
-from http://www.cubicweb.org/project/cubicweb-keyword).
-
-.. sourcecode:: python
-
- from cubicweb.devtools.testlib import CubicWebTC
- from cubicweb import ValidationError
-
- class ClassificationHooksTC(CubicWebTC):
-
- def setup_database(self):
- with self.admin_access.repo_cnx() as cnx:
- group_etype = cnx.find('CWEType', name='CWGroup').one()
- c1 = cnx.create_entity('Classification', name=u'classif1',
- classifies=group_etype)
- user_etype = cnx.find('CWEType', name='CWUser').one()
- c2 = cnx.create_entity('Classification', name=u'classif2',
- classifies=user_etype)
- self.kw1eid = cnx.create_entity('Keyword', name=u'kwgroup', included_in=c1).eid
- cnx.commit()
-
- def test_cannot_create_cycles(self):
- with self.admin_access.repo_cnx() as cnx:
- kw1 = cnx.entity_from_eid(self.kw1eid)
- # direct obvious cycle
- with self.assertRaises(ValidationError):
- kw1.cw_set(subkeyword_of=kw1)
- cnx.rollback()
- # testing indirect cycles
- kw3 = cnx.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
- 'SK subkeyword_of K WHERE C name "classif1", K eid %(k)s'
- {'k': kw1}).get_entity(0,0)
- kw3.cw_set(reverse_subkeyword_of=kw1)
- self.assertRaises(ValidationError, cnx.commit)
-
-The test class defines a :meth:`setup_database` method which populates the
-database with initial data. Each test of the class runs with this
-pre-populated database.
-
-The test case itself checks that an Operation does its job of
-preventing cycles amongst Keyword entities.
-
-The `create_entity` method of connection (or request) objects allows
-to create an entity. You can link this entity to other entities, by
-specifying as argument, the relation name, and the entity to link, as
-value. In the above example, the `Classification` entity is linked to
-a `CWEtype` via the relation `classifies`. Conversely, if you are
-creating a `CWEtype` entity, you can link it to a `Classification`
-entity, by adding `reverse_classifies` as argument.
-
-.. note::
-
- the :meth:`commit` method is not called automatically. You have to
- call it explicitly if needed (notably to test operations). It is a
- good practice to regenerate entities with :meth:`entity_from_eid`
- after a commit to avoid request cache effects.
-
-You can see an example of security tests in the
-:ref:`adv_tuto_security`.
-
-It is possible to have these tests run continuously using `apycot`_.
-
-.. _apycot: http://www.cubicweb.org/project/apycot
-
-.. _securitytest:
-
-Managing connections or users
-+++++++++++++++++++++++++++++
-
-Since unit tests are done with the SQLITE backend and this does not
-support multiple connections at a time, you must be careful when
-simulating security, changing users.
-
-By default, tests run with a user with admin privileges. Connections
-using these credentials are accessible through the `admin_access` object
-of the test classes.
-
-The `repo_cnx()` method returns a connection object that can be used as a
-context manager:
-
-.. sourcecode:: python
-
- # admin_access is a pre-cooked session wrapping object
- # it is built with:
- # self.admin_access = self.new_access('admin')
- with self.admin_access.repo_cnx() as cnx:
- cnx.execute(...)
- self.create_user(cnx, login='user1')
- cnx.commit()
-
- user1access = self.new_access('user1')
- with user1access.web_request() as req:
- req.execute(...)
- req.cnx.commit()
-
-On exit of the context manager, a rollback is issued, which releases
-the connection. Don't forget to issue the `cnx.commit()` calls!
-
-.. warning::
-
- Do not use references kept to the entities created with a
- connection from another one!
-
-Email notifications tests
-`````````````````````````
-
-When running tests, potentially generated e-mails are not really sent
-but are found in the list `MAILBOX` of module
-:mod:`cubicweb.devtools.testlib`.
-
-You can test your notifications by analyzing the contents of this list, which
-contains objects with two attributes:
-
-* `recipients`, the list of recipients
-* `msg`, email.Message object
-
-Let us look at a simple example from the ``blog`` cube.
-
-.. sourcecode:: python
-
- from cubicweb.devtools.testlib import CubicWebTC, MAILBOX
-
- class BlogTestsCubicWebTC(CubicWebTC):
- """test blog specific behaviours"""
-
- def test_notifications(self):
- with self.admin_access.web_request() as req:
- cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
- description=u'cubicweb is beautiful')
- blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
- content=u'cubicweb hop')
- blog_entry_1.cw_set(entry_of=cubicweb_blog)
- blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
- content=u'cubicweb yes')
- blog_entry_2.cw_set(entry_of=cubicweb_blog)
- self.assertEqual(len(MAILBOX), 0)
- req.cnx.commit()
- self.assertEqual(len(MAILBOX), 2)
- mail = MAILBOX[0]
- self.assertEqual(mail.subject, '[data] hop')
- mail = MAILBOX[1]
- self.assertEqual(mail.subject, '[data] yes')
-
-Visible actions tests
-`````````````````````
-
-It is easy to write unit tests to test actions which are visible to
-a user or to a category of users. Let's take an example in the
-`conference cube`_.
-
-.. _`conference cube`: http://www.cubicweb.org/project/cubicweb-conference
-.. sourcecode:: python
-
- class ConferenceActionsTC(CubicWebTC):
-
- def setup_database(self):
- with self.admin_access.repo_cnx() as cnx:
- self.confeid = cnx.create_entity('Conference',
- title=u'my conf',
- url_id=u'conf',
- start_on=date(2010, 1, 27),
- end_on = date(2010, 1, 29),
- call_open=True,
- reverse_is_chair_at=chair,
- reverse_is_reviewer_at=reviewer).eid
-
- def test_admin(self):
- with self.admin_access.web_request() as req:
- rset = req.find('Conference').one()
- self.assertListEqual(self.pactions(req, rset),
- [('workflow', workflow.WorkflowActions),
- ('edit', confactions.ModifyAction),
- ('managepermission', actions.ManagePermissionsAction),
- ('addrelated', actions.AddRelatedActions),
- ('delete', actions.DeleteAction),
- ('generate_badge_action', badges.GenerateBadgeAction),
- ('addtalkinconf', confactions.AddTalkInConferenceAction)
- ])
- self.assertListEqual(self.action_submenu(req, rset, 'addrelated'),
- [(u'add Track in_conf Conference object',
- u'http://testing.fr/cubicweb/add/Track'
- u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'
- u'__redirectpath=conference%%2Fconf&'
- u'__redirectvid=' % {'conf': self.confeid}),
- ])
-
-You just have to execute a rql query corresponding to the view you want to test,
-and to compare the result of
-:meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions
-that must be visible in the interface. This is a list of tuples. The first
-element is the action's `__regid__`, the second the action's class.
-
-To test actions in a submenu, you just have to test the result of
-:meth:`~cubicweb.devtools.testlib.CubicWebTC.action_submenu` method. The last
-parameter of the method is the action's category. The result is a list of
-tuples. The first element is the action's title, and the second element the
-action's url.
-
-
-.. _automatic_views_tests:
-
-Automatic views testing
------------------------
-
-This is done automatically with the :class:`cubicweb.devtools.testlib.AutomaticWebTest`
-class. At cube creation time, the mycube/test/test_mycube.py file
-contains such a test. The code here has to be uncommented to be
-usable, without further modification.
-
-The ``auto_populate`` method uses a smart algorithm to create
-pseudo-random data in the database, thus enabling the views to be
-invoked and tested.
-
-Depending on the schema, hooks and operations constraints, it is not
-always possible for the automatic auto_populate to proceed.
-
-It is possible of course to completely redefine auto_populate. A
-lighter solution is to give hints (fill some class attributes) about
-what entities and relations have to be skipped by the auto_populate
-mechanism. These are:
-
-* `no_auto_populate`, may contain a list of entity types to skip
-* `ignored_relations`, may contain a list of relation types to skip
-* `application_rql`, may contain a list of rql expressions that
- auto_populate cannot guess by itself; these must yield resultsets
- against which views may be selected.
-
-.. warning::
-
- Take care to not let the imported `AutomaticWebTest` in your test module
- namespace, else both your subclass *and* this parent class will be run.
-
-Cache heavy database setup
--------------------------------
-
-Some test suites require a complex setup of the database that takes
-seconds (or even minutes) to complete. Doing the whole setup for each
-individual test makes the whole run very slow. The ``CubicWebTC``
-class offer a simple way to prepare a specific database once for
-multiple tests. The `test_db_id` class attribute of your
-``CubicWebTC`` subclass must be set to a unique identifier and the
-:meth:`pre_setup_database` class method must build the cached content. As
-the :meth:`pre_setup_database` method is not garanteed to be called
-every time a test method is run, you must not set any class attribute
-to be used during test *there*. Databases for each `test_db_id` are
-automatically created if not already in cache. Clearing the cache is
-up to the user. Cache files are found in the :file:`data/database`
-subdirectory of your test directory.
-
-.. warning::
-
- Take care to always have the same :meth:`pre_setup_database`
- function for all classes with a given `test_db_id` otherwise your
- tests will have unpredictable results depending on the first
- encountered one.
-
-
-Testing on a real-life database
--------------------------------
-
-The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration`
-configuration class to setup its testing environment (database driver,
-user password, application home, and so on). The `cubicweb.devtools`
-module also provides a `RealDatabaseConfiguration`
-class that will read a regular cubicweb sources file to fetch all
-this information but will also prevent the database to be initalized
-and reset between tests.
-
-For a test class to use a specific configuration, you have to set
-the `_config` class attribute on the class as in:
-
-.. sourcecode:: python
-
- from cubicweb.devtools import RealDatabaseConfiguration
- from cubicweb.devtools.testlib import CubicWebTC
-
- class BlogRealDatabaseTC(CubicWebTC):
- _config = RealDatabaseConfiguration('blog',
- sourcefile='/path/to/realdb_sources')
-
- def test_blog_rss(self):
- with self.admin_access.web_request() as req:
- rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
- 'B created_by U, U login "logilab", B creation_date D')
- self.view('rss', rset, req=req)
-
-
-Testing with other cubes
-------------------------
-
-Sometimes a small component cannot be tested all by itself, so one
-needs to specify other cubes to be used as part of the the unit test
-suite. This is handled by the ``bootstrap_cubes`` file located under
-``mycube/test/data``. One example from the `preview` cube::
-
- card, file, preview
-
-The format is:
-
-* possibly several empy lines or lines starting with ``#`` (comment lines)
-* one line containing a comma-separated list of cube names.
-
-It is also possible to add a ``schema.py`` file in
-``mycube/test/data``, which will be used by the testing framework,
-therefore making new entity types and relations available to the
-tests.
-
-Literate programming
---------------------
-
-CubicWeb provides some literate programming capabilities. The :ref:`cubicweb-ctl`
-`shell` command accepts different file formats. If your file ends with `.txt`
-or `.rst`, the file will be parsed by :mod:`doctest.testfile` with CubicWeb's
-:ref:`migration` API enabled in it.
-
-Create a `scenario.txt` file in the `test/` directory and fill with some content.
-Refer to the :mod:`doctest.testfile` `documentation`_.
-
-.. _documentation: http://docs.python.org/library/doctest.html
-
-Then, you can run it directly by::
-
- $ cubicweb-ctl shell <cube_instance> test/scenario.txt
-
-When your scenario file is ready, put it in a new test case to be able to run
-it automatically.
-
-.. sourcecode:: python
-
- from os.path import dirname, join
- from logilab.common.testlib import unittest_main
- from cubicweb.devtools.testlib import CubicWebTC
-
- class AcceptanceTC(CubicWebTC):
-
- def test_scenario(self):
- self.assertDocTestFile(join(dirname(__file__), 'scenario.txt'))
-
- if __name__ == '__main__':
- unittest_main()
-
-Skipping a scenario
-```````````````````
-
-If you want to set up initial conditions that you can't put in your unit test
-case, you have to use a :exc:`KeyboardInterrupt` exception only because of the
-way :mod:`doctest` module will catch all the exceptions internally.
-
- >>> if condition_not_met:
- ... raise KeyboardInterrupt('please, check your fixture.')
-
-Passing paramaters
-``````````````````
-Using extra arguments to parametrize your scenario is possible by prepending them
-by double dashes.
-
-Please refer to the `cubicweb-ctl shell --help` usage.
-
-.. important::
- Your scenario file must be utf-8 encoded.
-
-Test APIS
----------
-
-Using Pytest
-````````````
-
-The `pytest` utility (shipping with `logilab-common`_, which is a
-mandatory dependency of CubicWeb) extends the Python unittest
-functionality and is the preferred way to run the CubicWeb test
-suites. Bare unittests also work the usual way.
-
-.. _logilab-common: http://www.logilab.org/project/logilab-common
-
-To use it, you may:
-
-* just launch `pytest` in your cube to execute all tests (it will
- discover them automatically)
-* launch `pytest unittest_foo.py` to execute one test file
-* launch `pytest unittest_foo.py bar` to execute all test methods and
- all test cases whose name contains `bar`
-
-Additionally, the `-x` option tells pytest to exit at the first error
-or failure. The `-i` option tells pytest to drop into pdb whenever an
-exception occurs in a test.
-
-When the `-x` option has been used and the run stopped on a test, it
-is possible, after having fixed the test, to relaunch pytest with the
-`-R` option to tell it to start testing again from where it previously
-failed.
-
-Using the `TestCase` base class
-```````````````````````````````
-
-The base class of CubicWebTC is logilab.common.testlib.TestCase, which
-provides a lot of convenient assertion methods.
-
-.. autoclass:: logilab.common.testlib.TestCase
- :members:
-
-CubicWebTC API
-``````````````
-.. autoclass:: cubicweb.devtools.testlib.CubicWebTC
- :members:
-
-
-What you need to know about request and session
------------------------------------------------
-
-.. image:: ../images/request_session.png
-
-First, remember to think that some code run on a client side, some
-other on the repository side. More precisely:
-
-* client side: web interface, raw repoapi connection (cubicweb-ctl shell for
- instance);
-
-* repository side: RQL query execution, that may trigger hooks and operation.
-
-The client interacts with the repository through a repoapi connection.
-
-
-.. note::
-
- These distinctions are going to disappear in cubicweb 3.21 (if not
- before).
-
-A repoapi connection is tied to a session in the repository. The connection and
-request objects are inaccessible from repository code / the session object is
-inaccessible from client code (theoretically at least).
-
-The web interface provides a request class. That `request` object provides
-access to all cubicweb resources, eg:
-
-* the registry (which itself provides access to the schema and the
- configuration);
-
-* an underlying repoapi connection (when using req.execute, you actually call the
- repoapi);
-
-* other specific resources depending on the client type (url generation according
- to base url, form parameters, etc.).
-
-
-A `session` provides an api similar to a request regarding RQL execution and
-access to global resources (registry and all), but also has the following
-responsibilities:
-
-* handle transaction data, that will live during the time of a single
- transaction. This includes the database connections that will be used to
- execute RQL queries.
-
-* handle persistent data that may be used across different (web) requests
-
-* security and hooks control (not possible through a request)
-
-
-The `_cw` attribute
-```````````````````
-The `_cw` attribute available on every application object provides access to all
-cubicweb resources, i.e.:
-
-- For code running on the client side (eg web interface view), `_cw` is a request
- instance.
-
-- For code running on the repository side (hooks and operation), `_cw` is a
- Connection or Session instance.
-
-
-Beware some views may be called with a session (e.g. notifications) or with a
-request.
-
-
-Request, session and transaction
-````````````````````````````````
-
-In the web interface, an HTTP request is handled by a single request, which will
-be thrown away once the response is sent.
-
-The web publisher handles the transaction:
-
-* commit / rollback is done automatically
-
-* you should not commit / rollback explicitly, except if you really
- need it
-
-Let's detail the process:
-
-1. an incoming RQL query comes from a client to the web stack
-
-2. the web stack opens an authenticated database connection for the
- request, which is associated to a user session
-
-3. the query is executed (through the repository connection)
-
-4. this query may trigger hooks. Hooks and operations may execute some rql queries
- through `cnx.execute`.
-
-5. the repository gets the result of the query in 1. If it was a RQL read query,
- the database connection is released. If it was a write query, the connection
- is then tied to the session until the transaction is commited or rolled back.
-
-6. results are sent back to the client
-
-This implies several things:
-
-* when using a request, or code executed in hooks, this database
- connection handling is totally transparent
-
-* however, take care when writing tests: you are usually faking /
- testing both the server and the client side, so you have to decide
- when to use RepoAccess.client_cnx or RepoAccess.repo_cnx. Ask
- yourself "where will the code I want to test be running, client or
- repository side?". The response is usually: use a repo (since the
- "client connection" concept is going away in a couple of releases).
--- a/doc/book/en/devrepo/vreg.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-The Registry, selectors and application objects
-===============================================
-
-This chapter deals with some of the core concepts of the |cubicweb| framework
-which make it different from other frameworks (and maybe not easy to
-grasp at a first glance). To be able to do advanced development with
-|cubicweb| you need a good understanding of what is explained below.
-
-This chapter goes deep into details. You don't have to remember them
-all but keep it in mind so you can go back there later.
-
-An overview of AppObjects, the VRegistry and Selectors is given in the
-:ref:`VRegistryIntro` chapter.
-
-.. autodocstring:: cubicweb.cwvreg
-.. autodocstring:: cubicweb.predicates
-.. automodule:: cubicweb.appobject
-
-Base predicates
----------------
-
-Predicates are scoring functions that are called by the registry to tell whenever
-an appobject can be selected in a given context. Predicates may be chained
-together using operators to build a selector. A selector is the glue that tie
-views to the data model or whatever input context. Using them appropriately is an
-essential part of the construction of well behaved cubes.
-
-Of course you may have to write your own set of predicates as your needs grows
-and you get familiar with the framework (see :ref:`CustomPredicates`).
-
-Here is a description of generic predicates provided by CubicWeb that should suit
-most of your needs.
-
-Bare predicates
-~~~~~~~~~~~~~~~
-Those predicates are somewhat dumb, which doesn't mean they're not (very) useful.
-
-.. autoclass:: cubicweb.appobject.yes
-.. autoclass:: cubicweb.predicates.match_kwargs
-.. autoclass:: cubicweb.predicates.appobject_selectable
-.. autoclass:: cubicweb.predicates.adaptable
-.. autoclass:: cubicweb.predicates.configuration_values
-
-
-Result set predicates
-~~~~~~~~~~~~~~~~~~~~~
-Those predicates are looking for a result set in the context ('rset' argument or
-the input context) and match or not according to its shape. Some of these
-predicates have different behaviour if a particular cell of the result set is
-specified using 'row' and 'col' arguments of the input context or not.
-
-.. autoclass:: cubicweb.predicates.none_rset
-.. autoclass:: cubicweb.predicates.any_rset
-.. autoclass:: cubicweb.predicates.nonempty_rset
-.. autoclass:: cubicweb.predicates.empty_rset
-.. autoclass:: cubicweb.predicates.one_line_rset
-.. autoclass:: cubicweb.predicates.multi_lines_rset
-.. autoclass:: cubicweb.predicates.multi_columns_rset
-.. autoclass:: cubicweb.predicates.paginated_rset
-.. autoclass:: cubicweb.predicates.sorted_rset
-.. autoclass:: cubicweb.predicates.one_etype_rset
-.. autoclass:: cubicweb.predicates.multi_etypes_rset
-
-
-Entity predicates
-~~~~~~~~~~~~~~~~~
-Those predicates are looking for either an `entity` argument in the input context,
-or entity found in the result set ('rset' argument or the input context) and
-match or not according to entity's (instance or class) properties.
-
-.. autoclass:: cubicweb.predicates.non_final_entity
-.. autoclass:: cubicweb.predicates.is_instance
-.. autoclass:: cubicweb.predicates.score_entity
-.. autoclass:: cubicweb.predicates.rql_condition
-.. autoclass:: cubicweb.predicates.relation_possible
-.. autoclass:: cubicweb.predicates.partial_relation_possible
-.. autoclass:: cubicweb.predicates.has_related_entities
-.. autoclass:: cubicweb.predicates.partial_has_related_entities
-.. autoclass:: cubicweb.predicates.has_permission
-.. autoclass:: cubicweb.predicates.has_add_permission
-.. autoclass:: cubicweb.predicates.has_mimetype
-.. autoclass:: cubicweb.predicates.is_in_state
-.. autofunction:: cubicweb.predicates.on_fire_transition
-
-
-Logged user predicates
-~~~~~~~~~~~~~~~~~~~~~~
-Those predicates are looking for properties of the user issuing the request.
-
-.. autoclass:: cubicweb.predicates.match_user_groups
-
-
-Web request predicates
-~~~~~~~~~~~~~~~~~~~~~~
-Those predicates are looking for properties of *web* request, they can not be
-used on the data repository side.
-
-.. autoclass:: cubicweb.predicates.no_cnx
-.. autoclass:: cubicweb.predicates.anonymous_user
-.. autoclass:: cubicweb.predicates.authenticated_user
-.. autoclass:: cubicweb.predicates.match_form_params
-.. autoclass:: cubicweb.predicates.match_search_state
-.. autoclass:: cubicweb.predicates.match_context_prop
-.. autoclass:: cubicweb.predicates.match_context
-.. autoclass:: cubicweb.predicates.match_view
-.. autoclass:: cubicweb.predicates.primary_view
-.. autoclass:: cubicweb.predicates.contextual
-.. autoclass:: cubicweb.predicates.specified_etype_implements
-.. autoclass:: cubicweb.predicates.attribute_edited
-.. autoclass:: cubicweb.predicates.match_transition
-
-
-Other predicates
-~~~~~~~~~~~~~~~~
-.. autoclass:: cubicweb.predicates.match_exception
-.. autoclass:: cubicweb.predicates.debug_mode
-
-You'll also find some other (very) specific predicates hidden in other modules
-than :mod:`cubicweb.predicates`.
--- a/doc/book/en/devweb/ajax.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-.. _ajax:
-
-Ajax
-----
-
-CubicWeb provides a few helpers to facilitate *javascript <-> python* communications.
-
-You can, for instance, register some python functions that will become
-callable from javascript through ajax calls. All the ajax URLs are handled
-by the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
-
-.. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/controllers.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-.. _controllers:
-
-Controllers
------------
-
-Overview
-++++++++
-
-Controllers are responsible for taking action upon user requests
-(loosely following the terminology of the MVC meta pattern).
-
-The following controllers are provided out-of-the box in CubicWeb. We
-list them by category. They are all defined in
-(:mod:`cubicweb.web.views.basecontrollers`).
-
-`Browsing`:
-
-* the View controller is associated with most browsing actions within a
- CubicWeb application: it always instantiates a
- :ref:`the_main_template_layout` and lets the ResultSet/Views dispatch system
- build up the whole content; it handles :exc:`ObjectNotFound` and
- :exc:`NoSelectableObject` errors that may bubble up to its entry point, in an
- end-user-friendly way (but other programming errors will slip through)
-
-* the JSonpController is a wrapper around the ``ViewController`` that
- provides jsonp_ services. Padding can be specified with the
- ``callback`` request parameter. Only *jsonexport* / *ejsonexport*
- views can be used. If another ``vid`` is specified, it will be
- ignored and replaced by *jsonexport*. Request is anonymized
- to avoid returning sensitive data and reduce the risks of CSRF attacks;
-
-* the Login/Logout controllers make effective user login or logout
- requests
-
-
-.. _jsonp: http://en.wikipedia.org/wiki/JSONP
-
-`Edition`:
-
-* the Edit controller (see :ref:`edit_controller`) handles CRUD
- operations in response to a form being submitted; it works in close
- association with the Forms, to which it delegates some of the work
-
-* the ``Form validator controller`` provides form validation from Ajax
- context, using the Edit controller, to implement the classic form
- handling loop (user edits, hits `submit/apply`, validation occurs
- server-side by way of the Form validator controller, and the UI is
- decorated with failure information, either global or per-field ,
- until it is valid)
-
-`Other`:
-
-* the ``SendMail controller`` (web/views/basecontrollers.py) is reponsible
- for outgoing email notifications
-
-* the MailBugReport controller (web/views/basecontrollers.py) allows
- to quickly have a `reportbug` feature in one's application
-
-* the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`
- (:mod:`cubicweb.web.views.ajaxcontroller`) provides
- services for Ajax calls, typically using JSON as a serialization format
- for input, and sometimes using either JSON or XML for output. See
- :ref:`ajax` chapter for more information.
-
-
-Registration
-++++++++++++
-
-All controllers (should) live in the 'controllers' namespace within
-the global registry.
-
-Concrete controllers
-++++++++++++++++++++
-
-Most API details should be resolved by source code inspection, as the
-various controllers have differing goals. See for instance the
-:ref:`edit_controller` chapter.
-
-:mod:`cubicweb.web.controller` contains the top-level abstract
-Controller class and its unimplemented entry point
-`publish(rset=None)` method.
-
-A handful of helpers are also provided there:
-
-* process_rql builds a result set from an rql query typically issued
- from the browser (and available through _cw.form['rql'])
-
-* validate_cache will force cache validation handling with respect to
- the HTTP Cache directives (that were typically originally issued
- from a previous server -> client response); concrete Controller
- implementations dealing with HTTP (thus, for instance, not the
- SendMail controller) may very well call this in their publication
- process.
--- a/doc/book/en/devweb/css.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-CSS Stylesheet
----------------
-Conventions
-~~~~~~~~~~~
-
-.. XXX external_resources variable
-.. naming convention
-.. request.add_css
-
-
-Extending / overriding existing styles
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We cannot modify the order in which the application is reading the CSS. In
-the case we want to create new CSS style, the best is to define it a in a new
-CSS located under ``myapp/data/`` and use those new styles while writing
-customized views and templates.
-
-If you want to modify an existing CSS styling property, you will have to use
-``!important`` declaration to override the existing property. The application
-apply a higher priority on the default CSS and you can not change that.
-Customized CSS will not be read first.
-
-
-CubicWeb stylesheets
-~~~~~~~~~~~~~~~~~~~~
-
-.. XXX explain diffenrent files and main classes
--- a/doc/book/en/devweb/edition/dissection.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,338 +0,0 @@
-
-.. _form_dissection:
-
-Dissection of an entity form
-----------------------------
-
-This is done (again) with a vanilla instance of the `tracker`_
-cube. We will populate the database with a bunch of entities and see
-what kind of job the automatic entity form does.
-
-.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
-
-Populating the database
-~~~~~~~~~~~~~~~~~~~~~~~
-
-We should start by setting up a bit of context: a project with two
-unpublished versions, and a ticket linked to the project and the first
-version.
-
-.. sourcecode:: python
-
- >>> p = rql('INSERT Project P: P name "cubicweb"')
- >>> for num in ('0.1.0', '0.2.0'):
- ... rql('INSERT Version V: V num "%s", V version_of P WHERE P eid %%(p)s' % num, {'p': p[0][0]})
- ...
- <resultset 'INSERT Version V: V num "0.1.0", V version_of P WHERE P eid %(p)s' (1 rows): [765L] (('Version',))>
- <resultset 'INSERT Version V: V num "0.2.0", V version_of P WHERE P eid %(p)s' (1 rows): [766L] (('Version',))>
- >>> t = rql('INSERT Ticket T: T title "let us write more doc", T done_in V, '
- 'T concerns P WHERE V num "0.1.0"', P eid %(p)s', {'p': p[0][0]})
- >>> commit()
-
-Now let's see what the edition form builds for us.
-
-.. sourcecode:: python
-
- >>> cnx.use_web_compatible_requests('http://fakeurl.com')
- >>> req = cnx.request()
- >>> form = req.vreg['forms'].select('edition', req, rset=rql('Ticket T'))
- >>> html = form.render()
-
-.. note::
-
- In order to play interactively with web side application objects, we have to
- cheat a bit to have request object that will looks like HTTP request object, by
- calling :meth:`use_web_compatible_requests()` on the connection.
-
-This creates an automatic entity form. The ``.render()`` call yields
-an html (unicode) string. The html output is shown below (with
-internal fieldset omitted).
-
-Looking at the html output
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The form enveloppe
-''''''''''''''''''
-
-.. sourcecode:: html
-
- <div class="iformTitle"><span>main informations</span></div>
- <div class="formBody">
- <form action="http://crater:9999/validateform" method="post" enctype="application/x-www-form-urlencoded"
- id="entityForm" onsubmit="return freezeFormButtons('entityForm');"
- class="entityForm" cubicweb:target="eformframe">
- <div id="progress">validating...</div>
- <fieldset>
- <input name="__form_id" type="hidden" value="edition" />
- <input name="__errorurl" type="hidden" value="http://perdu.com#entityForm" />
- <input name="__domid" type="hidden" value="entityForm" />
- <input name="__type:763" type="hidden" value="Ticket" />
- <input name="eid" type="hidden" value="763" />
- <input name="__maineid" type="hidden" value="763" />
- <input name="_cw_edited_fields:763" type="hidden"
- value="concerns-subject,done_in-subject,priority-subject,type-subject,title-subject,description-subject,__type,_cw_generic_field" />
- ...
- </fieldset>
- </form>
- </div>
-
-The main fieldset encloses a set of hidden fields containing various
-metadata, that will be used by the `edit controller` to process it
-back correctly.
-
-The `freezeFormButtons(...)` javascript callback defined on the
-``onlick`` event of the form element prevents accidental multiple
-clicks in a row.
-
-The ``action`` of the form is mapped to the ``validateform`` controller
-(situated in :mod:`cubicweb.web.views.basecontrollers`).
-
-A full explanation of the validation loop is given in
-:ref:`validation_process`.
-
-.. _attributes_section:
-
-The attributes section
-''''''''''''''''''''''
-
-We can have a look at some of the inner nodes of the form. Some fields
-are omitted as they are redundant for our purposes.
-
-.. sourcecode:: html
-
- <fieldset class="default">
- <table class="attributeForm">
- <tr class="title_subject_row">
- <th class="labelCol"><label class="required" for="title-subject:763">title</label></th>
- <td>
- <input id="title-subject:763" maxlength="128" name="title-subject:763" size="45"
- tabindex="1" type="text" value="let us write more doc" />
- </td>
- </tr>
- ... (description field omitted) ...
- <tr class="priority_subject_row">
- <th class="labelCol"><label class="required" for="priority-subject:763">priority</label></th>
- <td>
- <select id="priority-subject:763" name="priority-subject:763" size="1" tabindex="4">
- <option value="important">important</option>
- <option selected="selected" value="normal">normal</option>
- <option value="minor">minor</option>
- </select>
- <div class="helper">importance</div>
- </td>
- </tr>
- ... (type field omitted) ...
- <tr class="concerns_subject_row">
- <th class="labelCol"><label class="required" for="concerns-subject:763">concerns</label></th>
- <td>
- <select id="concerns-subject:763" name="concerns-subject:763" size="1" tabindex="6">
- <option selected="selected" value="760">Foo</option>
- </select>
- </td>
- </tr>
- <tr class="done_in_subject_row">
- <th class="labelCol"><label for="done_in-subject:763">done in</label></th>
- <td>
- <select id="done_in-subject:763" name="done_in-subject:763" size="1" tabindex="7">
- <option value="__cubicweb_internal_field__"></option>
- <option selected="selected" value="761">Foo 0.1.0</option>
- <option value="762">Foo 0.2.0</option>
- </select>
- <div class="helper">version in which this ticket will be / has been done</div>
- </td>
- </tr>
- </table>
- </fieldset>
-
-
-Note that the whole form layout has been computed by the form
-renderer. It is the renderer which produces the table
-structure. Otherwise, the fields html structure is emitted by their
-associated widget.
-
-While it is called the `attributes` section of the form, it actually
-contains attributes and *mandatory relations*. For each field, we
-observe:
-
-* a dedicated row with a specific class, such as ``title_subject_row``
- (responsability of the form renderer)
-
-* an html widget (input, select, ...) with:
-
- * an id built from the ``rtype-role:eid`` pattern
-
- * a name built from the same pattern
-
- * possible values or preselected options
-
-The relations section
-'''''''''''''''''''''
-
-.. sourcecode:: html
-
- <fieldset class="This ticket :">
- <legend>This ticket :</legend>
- <table class="attributeForm">
- <tr class="_cw_generic_field_None_row">
- <td colspan="2">
- <table id="relatedEntities">
- <tr><th> </th><td> </td></tr>
- <tr id="relationSelectorRow_763" class="separator">
- <th class="labelCol">
- <select id="relationSelector_763" tabindex="8"
- onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,763);">
- <option value="">select a relation</option>
- <option value="appeared_in_subject">appeared in</option>
- <option value="custom_workflow_subject">custom workflow</option>
- <option value="depends_on_object">dependency of</option>
- <option value="depends_on_subject">depends on</option>
- <option value="identical_to_subject">identical to</option>
- <option value="see_also_subject">see also</option>
- </select>
- </th>
- <td id="unrelatedDivs_763"></td>
- </tr>
- </table>
- </td>
- </tr>
- </table>
- </fieldset>
-
-The optional relations are grouped into a drop-down combo
-box. Selection of an item triggers a javascript function which will:
-
-* show already related entities in the div of id `relatedentities`
- using a two-colown layout, with an action to allow deletion of
- individual relations (there are none in this example)
-
-* provide a relation selector in the div of id `relationSelector_EID`
- to allow the user to set up relations and trigger dynamic action on
- the last div
-
-* fill the div of id `unrelatedDivs_EID` with a dynamically computed
- selection widget allowing direct selection of an unrelated (but
- relatable) entity or a switch towards the `search mode` of
- |cubicweb| which allows full browsing and selection of an entity
- using a dedicated action situated in the left column boxes.
-
-
-The buttons zone
-''''''''''''''''
-
-Finally comes the buttons zone.
-
-.. sourcecode:: html
-
- <table width="100%">
- <tbody>
- <tr>
- <td align="center">
- <button class="validateButton" tabindex="9" type="submit" value="validate">
- <img alt="OK_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/ok.png" />
- validate
- </button>
- </td>
- <td style="align: right; width: 50%;">
- <button class="validateButton"
- onclick="postForm('__action_apply', 'button_apply', 'entityForm')"
- tabindex="10" type="button" value="apply">
- <img alt="APPLY_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/plus.png" />
- apply
- </button>
- <button class="validateButton"
- onclick="postForm('__action_cancel', 'button_cancel', 'entityForm')"
- tabindex="11" type="button" value="cancel">
- <img alt="CANCEL_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/cancel.png" />
- cancel
- </button>
- </td>
- </tr>
- </tbody>
- </table>
-
-The most notable artifacts here are the ``postForm(...)`` calls
-defined on click events on these buttons. This function basically
-submits the form.
-
-.. _validation_process:
-
-The form validation process
----------------------------
-
-Preparation
-~~~~~~~~~~~
-
-After the (html) document is loaded, the ``setFormsTarget`` javascript
-function dynamically transforms the DOM as follows. For all forms of
-the DOM, it:
-
-* sets the ``target`` attribute where there is a ``cubicweb:target``
- attribute (with the same value)
-
-* appends an empty `IFRAME` element at the end
-
-Let us have a look again at the form element. We have omitted some
-irrelevant attributes.
-
-.. sourcecode::html
-
- <form action="http://crater:9999/validateform" method="post"
- enctype="application/x-www-form-urlencoded"
- id="entityForm" cubicweb:target="eformframe"
- target="eformframe">
- ...
- </form>
-
-Validation loop
-~~~~~~~~~~~~~~~
-
-On form submission, the form.action is invoked. Basically, the
-``validateform`` controller is called and its output lands in the
-specified ``target``, the iframe that was previously prepared.
-
-Hence, the main page is not replaced, only the iframe contents. The
-``validateform`` controller only outputs a tiny javascript fragment
-which is then immediately executed.
-
-.. sourcecode:: html
-
- <iframe width="0px" height="0px" name="eformframe" id="eformframe" src="javascript: void(0)">
- <script type="text/javascript">
- window.parent.handleFormValidationResponse('entityForm', null, null,
- [false, [2164, {"name-subject": "required field"}], null],
- null);
- </script>
- </iframe>
-
-The ``window.parent`` part ensures the javascript function is called
-on the right context (that is: the form element). We will describe its
-parameters:
-
-* first comes the form id (`entityForm`)
-
-* then two optional callbacks for the success and failure case
-
-* an array containing:
-
- * a boolean which indicates status (success or failure), and then, on error:
-
- * an array structured as ``[eid, {'rtype-role': 'error msg'}, ...]``
-
- * on success:
-
- * an url (string) representing the next thing to jump to
-
-Given the array structure described above, it is quite simple to
-manipulate the DOM to show the errors at appropriate places.
-
-Explanation
-~~~~~~~~~~~
-
-This mecanism may seem a bit overcomplicated but we have to deal with
-two realities:
-
-* in the (strict) XHTML world, there are no iframes (hence the dynamic
- inclusion, tolerated by Firefox)
-
-* no (or not all) browser(s) support file input field handling through
- ajax.
--- a/doc/book/en/devweb/edition/editcontroller.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-.. _edit_controller:
-
-The `edit controller`
----------------------
-
-It can be found in (:mod:`cubicweb.web.views.editcontroller`). This
-controller processes data received from an html form to create or
-update entities.
-
-Edition handling
-~~~~~~~~~~~~~~~~
-
-The parameters related to entities to edit are specified as follows
-(first seen in :ref:`attributes_section`)::
-
- <rtype-role>:<entity eid>
-
-where entity eid could be a letter in case of an entity to create. We
-name those parameters as *qualified*.
-
-* Retrieval of entities to edit is done by using the forms parameters
- `eid` and `__type`
-
-* For all the attributes and the relations of an entity to edit
- (attributes and relations are handled a bit differently but these
- details are not much relevant here) :
-
- * using the ``rtype``, ``role`` and ``__type`` information, fetch
- an appropriate field instance
-
- * check if the field has been modified (if not, proceed to the next
- relation)
-
- * build an rql expression to update the entity
-
-At the end, all rql expressions are executed.
-
-* For each entity to edit:
-
- * if a qualified parameter `__linkto` is specified, its value has
- to be a string (or a list of strings) such as: ::
-
- <relation type>:<eids>:<target>
-
- where <target> is either `subject` or `object` and each eid could
- be separated from the others by a `_`. Target specifies if the
- *edited entity* is subject or object of the relation and each
- relation specified will be inserted.
-
- * if a qualified parameter `__clone_eid` is specified for an entity, the
- relations of the specified entity passed as value of this parameter are
- copied on the edited entity.
-
- * if a qualified parameter `__delete` is specified, its value must be
- a string or a list of string such as follows: ::
-
- <subjects eids>:<relation type>:<objects eids>
-
- where each eid subject or object can be seperated from the other
- by `_`. Each specified relation will be deleted.
-
-
-* If no entity is edited but the form contains the parameters `__linkto`
- and `eid`, this one is interpreted by using the value specified for `eid`
- to designate the entity on which to add the relations.
-
-.. note::
-
- * if the parameter `__action_delete` is found, all the entities specified
- as to be edited will be deleted.
-
- * if the parameter `__action_cancel` is found, no action is completed.
-
- * if the parameter `__action_apply` is found, the editing is
- applied normally but the redirection is done on the form (see
- :ref:`RedirectionControl`).
-
- * if no entity is found to be edited and if there is no parameter
- `__action_delete`, `__action_cancel`, `__linkto`, `__delete` or
- `__insert`, an error is raised.
-
- * using the parameter `__message` in the form will allow to use its value
- as a message to provide the user once the editing is completed.
-
-
-.. _RedirectionControl:
-
-Redirection control
-~~~~~~~~~~~~~~~~~~~
-Once editing is completed, there is still an issue left: where should we go
-now? If nothing is specified, the controller will do his job but it does not
-mean we will be happy with the result. We can control that by using the
-following parameters:
-
-* `__redirectpath`: path of the URL (relative to the root URL of the site,
- no form parameters
-
-* `__redirectparams`: forms parameters to add to the path
-
-* `__redirectrql`: redirection RQL request
-
-* `__redirectvid`: redirection view identifier
-
-* `__errorurl`: initial form URL, used for redirecting in case a validation
- error is raised during editing. If this one is not specified, an error page
- is displayed instead of going back to the form (which is, if necessary,
- responsible for displaying the errors)
-
-* `__form_id`: initial view form identifier, used if `__action_apply` is
- found
-
-In general we use either `__redirectpath` and `__redirectparams` or
-`__redirectrql` and `__redirectvid`.
--- a/doc/book/en/devweb/edition/examples.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,232 +0,0 @@
-Examples
---------
-
-(Automatic) Entity form
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Looking at some cubes available on the `cubicweb forge`_ we find some
-with form manipulation. The following example comes from the the
-`conference`_ cube. It extends the change state form for the case
-where a ``Talk`` entity is getting into ``submitted`` state. The goal
-is to select reviewers for the submitted talk.
-
-.. _`cubicweb forge`: http://www.cubicweb.org/view?rql=Any+P+ORDERBY+N+WHERE+P+name+LIKE+%22cubicweb-%25%22%2C+P+is+Project%2C+P+name+N
-.. _`conference`: http://www.cubicweb.org/project/cubicweb-conference
-
-.. sourcecode:: python
-
- from cubicweb.web import formfields as ff, formwidgets as fwdgs
- class SendToReviewerStatusChangeView(ChangeStateFormView):
- __select__ = (ChangeStateFormView.__select__ &
- is_instance('Talk') &
- rql_condition('X in_state S, S name "submitted"'))
-
- def get_form(self, entity, transition, **kwargs):
- form = super(SendToReviewerStatusChangeView, self).get_form(entity, transition, **kwargs)
- relation = ff.RelationField(name='reviews', role='object',
- eidparam=True,
- label=_('select reviewers'),
- widget=fwdgs.Select(multiple=True))
- form.append_field(relation)
- return form
-
-Simple extension of a form can be done from within the `FormView`
-wrapping the form. FormView instances have a handy ``get_form`` method
-that returns the form to be rendered. Here we add a ``RelationField``
-to the base state change form.
-
-One notable point is the ``eidparam`` argument: it tells both the
-field and the ``edit controller`` that the field is linked to a
-specific entity.
-
-It is hence entirely possible to add ad-hoc fields that will be
-processed by some specialized instance of the edit controller.
-
-
-Ad-hoc fields form
-~~~~~~~~~~~~~~~~~~
-
-We want to define a form doing something else than editing an entity. The idea is
-to propose a form to send an email to entities in a resultset which implements
-:class:`IEmailable`. Let's take a simplified version of what you'll find in
-:mod:`cubicweb.web.views.massmailing`.
-
-Here is the source code:
-
-.. sourcecode:: python
-
- def sender_value(form, field):
- return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
-
- def recipient_choices(form, field):
- return [(e.get_email(), e.eid)
- for e in form.cw_rset.entities()
- if e.get_email()]
-
- def recipient_value(form, field):
- return [e.eid for e in form.cw_rset.entities()
- if e.get_email()]
-
- class MassMailingForm(forms.FieldsForm):
- __regid__ = 'massmailing'
-
- needs_js = ('cubicweb.widgets.js',)
- domid = 'sendmail'
- action = 'sendmail'
-
- sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
- label=_('From:'),
- value=sender_value)
-
- recipient = ff.StringField(widget=CheckBox(),
- label=_('Recipients:'),
- choices=recipient_choices,
- value=recipients_value)
-
- subject = ff.StringField(label=_('Subject:'), max_length=256)
-
- mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
- inputid='mailbody'))
-
- form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
- _('send email'), 'SEND_EMAIL_ICON'),
- ImgButton('cancelbutton', "javascript: history.back()",
- stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
-
-Let's detail what's going on up there. Our form will hold four fields:
-
-* a sender field, which is disabled and will simply contains the user's name and
- email
-
-* a recipients field, which will be displayed as a list of users in the context
- result set with checkboxes so user can still choose who will receive his mailing
- by checking or not the checkboxes. By default all of them will be checked since
- field's value return a list containing same eids as those returned by the
- vocabulary function.
-
-* a subject field, limited to 256 characters (hence we know a
- :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in
- :class:`~cubicweb.web.formfields.StringField`)
-
-* a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`,
- and whose definition won't be shown here. Notice though that we tell this form
- need this javascript file by using `needs_js`
-
-Last but not least, we add two buttons control: one to post the form using
-javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id
-set to 'sendmail', which is our form DOM id as specified by its `domid`
-attribute), another to cancel the form which will go back to the previous page
-using another javascript call. Also we specify an image to use as button icon as a
-resource identifier (see :ref:`uiprops`) given as last argument to
-:class:`cubicweb.web.formwidgets.ImgButton`.
-
-To see this form, we still have to wrap it in a view. This is pretty simple:
-
-.. sourcecode:: python
-
- class MassMailingFormView(form.FormViewMixIn, EntityView):
- __regid__ = 'massmailing'
- __select__ = is_instance(IEmailable) & authenticated_user()
-
- def call(self):
- form = self._cw.vreg['forms'].select('massmailing', self._cw,
- rset=self.cw_rset)
- form.render(w=self.w)
-
-As you see, we simply define a view with proper selector so it only apply to a
-result set containing :class:`IEmailable` entities, and so that only users in the
-managers or users group can use it. Then in the `call()` method for this view we
-simply select the above form and call its `.render()` method with our output
-stream as argument.
-
-When this form is submitted, a controller with id 'sendmail' will be called (as
-specified using `action`). This controller will be responsible to actually send
-the mail to specified recipients.
-
-Here is what it looks like:
-
-.. sourcecode:: python
-
- class SendMailController(Controller):
- __regid__ = 'sendmail'
- __select__ = (authenticated_user() &
- match_form_params('recipient', 'mailbody', 'subject'))
-
- def publish(self, rset=None):
- body = self._cw.form['mailbody']
- subject = self._cw.form['subject']
- eids = self._cw.form['recipient']
- # eids may be a string if only one recipient was specified
- if isinstance(eids, basestring):
- rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
- else:
- rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
- recipients = list(rset.entities())
- msg = format_mail({'email' : self._cw.user.get_email(),
- 'name' : self._cw.user.dc_title()},
- recipients, body, subject)
- if not self._cw.vreg.config.sendmails([(msg, recipients)]):
- msg = self._cw._('could not connect to the SMTP server')
- else:
- msg = self._cw._('emails successfully sent')
- raise Redirect(self._cw.build_url(__message=msg))
-
-
-The entry point of a controller is the publish method. In that case we simply get
-back post values in request's `form` attribute, get user instances according
-to eids found in the 'recipient' form value, and send email after calling
-:func:`format_mail` to get a proper email message. If we can't send email or
-if we successfully sent email, we redirect to the index page with proper message
-to inform the user.
-
-Also notice that our controller has a selector that deny access to it
-to anonymous users (we don't want our instance to be used as a spam
-relay), but also checks if the expected parameters are specified in
-forms. That avoids later defensive programming (though it's not enough
-to handle all possible error cases).
-
-To conclude our example, suppose we wish a different form layout and that existent
-renderers are not satisfying (we would check that first of course :). We would then
-have to define our own renderer:
-
-.. sourcecode:: python
-
- class MassMailingFormRenderer(formrenderers.FormRenderer):
- __regid__ = 'massmailing'
-
- def _render_fields(self, fields, w, form):
- w(u'<table class="headersform">')
- for field in fields:
- if field.name == 'mailbody':
- w(u'</table>')
- w(u'<div id="toolbar">')
- w(u'<ul>')
- for button in form.form_buttons:
- w(u'<li>%s</li>' % button.render(form))
- w(u'</ul>')
- w(u'</div>')
- w(u'<div>')
- w(field.render(form, self))
- w(u'</div>')
- else:
- w(u'<tr>')
- w(u'<td class="hlabel">%s</td>' %
- self.render_label(form, field))
- w(u'<td class="hvalue">')
- w(field.render(form, self))
- w(u'</td></tr>')
-
- def render_buttons(self, w, form):
- pass
-
-We simply override the `_render_fields` and `render_buttons` method of the base form renderer
-to arrange fields as we desire it: here we'll have first a two columns table with label and
-value of the sender, recipients and subject field (form order respected), then form controls,
-then a div containing the textarea for the email's content.
-
-To bind this renderer to our form, we should add to our form definition above:
-
-.. sourcecode:: python
-
- form_renderer_id = 'massmailing'
-
--- a/doc/book/en/devweb/edition/form.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,377 +0,0 @@
-.. _webform:
-
-HTML form construction
-----------------------
-
-CubicWeb provides the somewhat usual form / field / widget / renderer abstraction
-to provide generic building blocks which will greatly help you in building forms
-properly integrated with CubicWeb (coherent display, error handling, etc...),
-while keeping things as flexible as possible.
-
-A ``form`` basically only holds a set of ``fields``, and has te be bound to a
-``renderer`` which is responsible to layout them. Each field is bound to a
-``widget`` that will be used to fill in value(s) for that field (at form
-generation time) and 'decode' (fetch and give a proper Python type to) values
-sent back by the browser.
-
-The ``field`` should be used according to the type of what you want to edit.
-E.g. if you want to edit some date, you'll have to use the
-:class:`cubicweb.web.formfields.DateField`. Then you can choose among multiple
-widgets to edit it, for instance :class:`cubicweb.web.formwidgets.TextInput` (a
-bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple
-calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery
-calendar). You can of course also write your own widget.
-
-Exploring the available forms
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A small excursion into a |cubicweb| shell is the quickest way to
-discover available forms (or application objects in general).
-
-.. sourcecode:: python
-
- >>> from pprint import pprint
- >>> pprint( session.vreg['forms'] )
- {'base': [<class 'cubicweb.web.views.forms.FieldsForm'>,
- <class 'cubicweb.web.views.forms.EntityFieldsForm'>],
- 'changestate': [<class 'cubicweb.web.views.workflow.ChangeStateForm'>,
- <class 'cubes.tracker.views.forms.VersionChangeStateForm'>],
- 'composite': [<class 'cubicweb.web.views.forms.CompositeForm'>,
- <class 'cubicweb.web.views.forms.CompositeEntityForm'>],
- 'deleteconf': [<class 'cubicweb.web.views.editforms.DeleteConfForm'>],
- 'edition': [<class 'cubicweb.web.views.autoform.AutomaticEntityForm'>,
- <class 'cubicweb.web.views.workflow.TransitionEditionForm'>,
- <class 'cubicweb.web.views.workflow.StateEditionForm'>],
- 'logform': [<class 'cubicweb.web.views.basetemplates.LogForm'>],
- 'massmailing': [<class 'cubicweb.web.views.massmailing.MassMailingForm'>],
- 'muledit': [<class 'cubicweb.web.views.editforms.TableEditForm'>],
- 'sparql': [<class 'cubicweb.web.views.sparql.SparqlForm'>]}
-
-
-The two most important form families here (for all practical purposes) are `base`
-and `edition`. Most of the time one wants alterations of the
-:class:`AutomaticEntityForm` to generate custom forms to handle edition of an
-entity.
-
-The Automatic Entity Form
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: cubicweb.web.views.autoform
-
-Anatomy of a choices function
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Let's have a look at the `ticket_done_in_choices` function given to
-the `choices` parameter of the relation tag that is applied to the
-('Ticket', 'done_in', '*') relation definition, as it is both typical
-and sophisticated enough. This is a code snippet from the `tracker`_
-cube.
-
-.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
-
-The ``Ticket`` entity type can be related to a ``Project`` and a
-``Version``, respectively through the ``concerns`` and ``done_in``
-relations. When a user is about to edit a ticket, we want to fill the
-combo box for the ``done_in`` relation with values pertinent with
-respect to the context. The important context here is:
-
-* creation or modification (we cannot fetch values the same way in
- either case)
-
-* ``__linkto`` url parameter given in a creation context
-
-.. sourcecode:: python
-
- from cubicweb.web import formfields
-
- def ticket_done_in_choices(form, field):
- entity = form.edited_entity
- # first see if its specified by __linkto form parameters
- linkedto = form.linked_to[('done_in', 'subject')]
- if linkedto:
- return linkedto
- # it isn't, get initial values
- vocab = field.relvoc_init(form)
- veid = None
- # try to fetch the (already or pending) related version and project
- if not entity.has_eid():
- peids = form.linked_to[('concerns', 'subject')]
- peid = peids and peids[0]
- else:
- peid = entity.project.eid
- veid = entity.done_in and entity.done_in[0].eid
- if peid:
- # we can complete the vocabulary with relevant values
- rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
- rset = form._cw.execute(
- 'Any V, VN ORDERBY version_sort_value(VN) '
- 'WHERE V version_of P, P eid %(p)s, V num VN, '
- 'V in_state ST, NOT ST name "published"', {'p': peid}, 'p')
- vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
- if rschema.has_perm(form._cw, 'add', toeid=v.eid)
- and v.eid != veid]
- return vocab
-
-The first thing we have to do is fetch potential values from the ``__linkto`` url
-parameter that is often found in entity creation contexts (the creation action
-provides such a parameter with a predetermined value; for instance in this case,
-ticket creation could occur in the context of a `Version` entity). The
-:class:`~cubicweb.web.formfields.RelationField` field class provides a
-:meth:`~cubicweb.web.formfields.RelationField.relvoc_linkedto` method that gets a
-list suitably filled with vocabulary values.
-
-.. sourcecode:: python
-
- linkedto = field.relvoc_linkedto(form)
- if linkedto:
- return linkedto
-
-Then, if no ``__linkto`` argument was given, we must prepare the vocabulary with
-an initial empty value (because `done_in` is not mandatory, we must allow the
-user to not select a verson) and already linked values. This is done with the
-:meth:`~cubicweb.web.formfields.RelationField.relvoc_init` method.
-
-.. sourcecode:: python
-
- vocab = field.relvoc_init(form)
-
-But then, we have to give more: if the ticket is related to a project,
-we should provide all the non published versions of this project
-(`Version` and `Project` can be related through the `version_of`
-relation). Conversely, if we do not know yet the project, it would not
-make sense to propose all existing versions as it could potentially
-lead to incoherences. Even if these will be caught by some
-RQLConstraint, it is wise not to tempt the user with error-inducing
-candidate values.
-
-The "ticket is related to a project" part must be decomposed as:
-
-* this is a new ticket which is created is the context of a project
-
-* this is an already existing ticket, linked to a project (through the
- `concerns` relation)
-
-* there is no related project (quite unlikely given the cardinality of
- the `concerns` relation, so it can only mean that we are creating a
- new ticket, and a project is about to be selected but there is no
- ``__linkto`` argument)
-
-.. note::
-
- the last situation could happen in several ways, but of course in a
- polished application, the paths to ticket creation should be
- controlled so as to avoid a suboptimal end-user experience
-
-Hence, we try to fetch the related project.
-
-.. sourcecode:: python
-
- veid = None
- if not entity.has_eid():
- peids = form.linked_to[('concerns', 'subject')]
- peid = peids and peids[0]
- else:
- peid = entity.project.eid
- veid = entity.done_in and entity.done_in[0].eid
-
-We distinguish between entity creation and entity modification using
-the ``Entity.has_eid()`` method, which returns `False` on creation. At
-creation time the only way to get a project is through the
-``__linkto`` parameter. Notice that we fetch the version in which the
-ticket is `done_in` if any, for later.
-
-.. note::
-
- the implementation above assumes that if there is a ``__linkto``
- parameter, it is only about a project. While it makes sense most of
- the time, it is not an absolute. Depending on how an entity creation
- action action url is built, several outcomes could be possible
- there
-
-If the ticket is already linked to a project, fetching it is
-trivial. Then we add the relevant version to the initial vocabulary.
-
-.. sourcecode:: python
-
- if peid:
- rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
- rset = form._cw.execute(
- 'Any V, VN ORDERBY version_sort_value(VN) '
- 'WHERE V version_of P, P eid %(p)s, V num VN, '
- 'V in_state ST, NOT ST name "published"', {'p': peid})
- vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
- if rschema.has_perm(form._cw, 'add', toeid=v.eid)
- and v.eid != veid]
-
-.. warning::
-
- we have to defend ourselves against lack of a project eid. Given
- the cardinality of the `concerns` relation, there *must* be a
- project, but this rule can only be enforced at validation time,
- which will happen of course only after form subsmission
-
-Here, given a project eid, we complete the vocabulary with all
-unpublished versions defined in the project (sorted by number) for
-which the current user is allowed to establish the relation.
-
-
-Building self-posted form with custom fields/widgets
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Sometimes you want a form that is not related to entity edition. For those,
-you'll have to handle form posting by yourself. Here is a complete example on how
-to achieve this (and more).
-
-Imagine you want a form that selects a month period. There are no proper
-field/widget to handle this in CubicWeb, so let's start by defining them:
-
-.. sourcecode:: python
-
- # let's have the whole import list at the beginning, even those necessary for
- # subsequent snippets
- from logilab.common import date
- from logilab.mtconverter import xml_escape
- from cubicweb.view import View
- from cubicweb.predicates import match_kwargs
- from cubicweb.web import RequestError, ProcessFormError
- from cubicweb.web import formfields as fields, formwidgets as wdgs
- from cubicweb.web.views import forms, calendar
-
- class MonthSelect(wdgs.Select):
- """Custom widget to display month and year. Expect value to be given as a
- date instance.
- """
-
- def format_value(self, form, field, value):
- return u'%s/%s' % (value.year, value.month)
-
- def process_field_data(self, form, field):
- val = super(MonthSelect, self).process_field_data(form, field)
- try:
- year, month = val.split('/')
- year = int(year)
- month = int(month)
- return date.date(year, month, 1)
- except ValueError:
- raise ProcessFormError(
- form._cw._('badly formated date string %s') % val)
-
-
- class MonthPeriodField(fields.CompoundField):
- """custom field composed of two subfields, 'begin_month' and 'end_month'.
-
- It expects to be used on form that has 'mindate' and 'maxdate' in its
- extra arguments, telling the range of month to display.
- """
-
- def __init__(self, *args, **kwargs):
- kwargs.setdefault('widget', wdgs.IntervalWidget())
- super(MonthPeriodField, self).__init__(
- [fields.StringField(name='begin_month',
- choices=self.get_range, sort=False,
- value=self.get_mindate,
- widget=MonthSelect()),
- fields.StringField(name='end_month',
- choices=self.get_range, sort=False,
- value=self.get_maxdate,
- widget=MonthSelect())], *args, **kwargs)
-
- @staticmethod
- def get_range(form, field):
- mindate = date.todate(form.cw_extra_kwargs['mindate'])
- maxdate = date.todate(form.cw_extra_kwargs['maxdate'])
- assert mindate <= maxdate
- _ = form._cw._
- months = []
- while mindate <= maxdate:
- label = '%s %s' % (_(calendar.MONTHNAMES[mindate.month - 1]),
- mindate.year)
- value = field.widget.format_value(form, field, mindate)
- months.append( (label, value) )
- mindate = date.next_month(mindate)
- return months
-
- @staticmethod
- def get_mindate(form, field):
- return form.cw_extra_kwargs['mindate']
-
- @staticmethod
- def get_maxdate(form, field):
- return form.cw_extra_kwargs['maxdate']
-
- def process_posted(self, form):
- for field, value in super(MonthPeriodField, self).process_posted(form):
- if field.name == 'end_month':
- value = date.last_day(value)
- yield field, value
-
-
-Here we first define a widget that will be used to select the beginning and the
-end of the period, displaying months like '<month> YYYY' but using 'YYYY/mm' as
-actual value.
-
-We then define a field that will actually hold two fields, one for the beginning
-and another for the end of the period. Each subfield uses the widget we defined
-earlier, and the outer field itself uses the standard
-:class:`IntervalWidget`. The field adds some logic:
-
-* a vocabulary generation function `get_range`, used to populate each sub-field
-
-* two 'value' functions `get_mindate` and `get_maxdate`, used to tell to
- subfields which value they should consider on form initialization
-
-* overriding of `process_posted`, called when the form is being posted, so that
- the end of the period is properly set to the last day of the month.
-
-Now, we can define a very simple form:
-
-.. sourcecode:: python
-
- class MonthPeriodSelectorForm(forms.FieldsForm):
- __regid__ = 'myform'
- __select__ = match_kwargs('mindate', 'maxdate')
-
- form_buttons = [wdgs.SubmitButton()]
- form_renderer_id = 'onerowtable'
- period = MonthPeriodField()
-
-
-where we simply add our field, set a submit button and use a very simple renderer
-(try others!). Also we specify a selector that ensures form will have arguments
-necessary to our field.
-
-Now, we need a view that will wrap the form and handle post when it occurs,
-simply displaying posted values in the page:
-
-.. sourcecode:: python
-
- class SelfPostingForm(View):
- __regid__ = 'myformview'
-
- def call(self):
- mindate, maxdate = date.date(2010, 1, 1), date.date(2012, 1, 1)
- form = self._cw.vreg['forms'].select(
- 'myform', self._cw, mindate=mindate, maxdate=maxdate, action='')
- try:
- posted = form.process_posted()
- self.w(u'<p>posted values %s</p>' % xml_escape(repr(posted)))
- except RequestError: # no specified period asked
- pass
- form.render(w=self.w, formvalues=self._cw.form)
-
-
-Notice usage of the :meth:`process_posted` method, that will return a dictionary
-of typed values (because they have been processed by the field). In our case, when
-the form is posted you should see a dictionary with 'begin_month' and 'end_month'
-as keys with the selected dates as value (as a python `date` object).
-
-
-APIs
-~~~~
-
-.. automodule:: cubicweb.web.formfields
-.. automodule:: cubicweb.web.formwidgets
-.. automodule:: cubicweb.web.views.forms
-.. automodule:: cubicweb.web.views.formrenderers
-
-
--- a/doc/book/en/devweb/edition/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-Edition control
-===============
-
-This chapter covers the editing capabilities of |cubicweb|. It
-explains html Form construction, the Edit Controller and their
-interactions.
-
-
-.. toctree::
- :maxdepth: 2
-
- form
- dissection
- editcontroller
- examples
--- a/doc/book/en/devweb/facets.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-The facets system
------------------
-
-Facets allow to restrict searches according to some user friendly criterias.
-CubicWeb has a builtin `facet`_ system to define restrictions `filters`_ really
-as easily as possible.
-
-Here is an exemple of the facets rendering picked from our
-http://www.cubicweb.org web site:
-
-.. image:: ../images/facet_overview.png
-
-Facets will appear on each page presenting more than one entity that may be
-filtered according to some known criteria.
-
-Base classes for facets
-~~~~~~~~~~~~~~~~~~~~~~~
-.. automodule:: cubicweb.web.facet
-
-
-.. _facet: http://en.wikipedia.org/wiki/Faceted_browser
-.. _filters: http://www.cubicweb.org/blogentry/154152
-
--- a/doc/book/en/devweb/httpcaching.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-HTTP cache management
-=====================
-
-.. automodule:: cubicweb.web.httpcache
-
-Cache policies
---------------
-.. autoclass:: cubicweb.web.httpcache.NoHTTPCacheManager
-.. autoclass:: cubicweb.web.httpcache.MaxAgeHTTPCacheManager
-.. autoclass:: cubicweb.web.httpcache.EtagHTTPCacheManager
-.. autoclass:: cubicweb.web.httpcache.EntityHTTPCacheManager
-
-Exception
----------
-.. autoexception:: cubicweb.web.httpcache.NoEtag
-
-Helper functions
-----------------
-.. autofunction:: cubicweb.web.httpcache.set_http_cache_headers
-
-.. NOT YET AVAILABLE IN STABLE autofunction:: cubicweb.web.httpcache.lastmodified
--- a/doc/book/en/devweb/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-Web side development
-====================
-
-In this chapter, we will describe the core APIs for web development in
-the *CubicWeb* framework.
-
-.. toctree::
- :maxdepth: 2
-
- publisher
- controllers
- request
- searchbar
- views/index
- rtags
- ajax
- js
- css
- edition/index
- facets
- internationalization
- property
- httpcaching
- resource
--- a/doc/book/en/devweb/internationalization.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,229 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _internationalization:
-
-Internationalization
----------------------
-
-Cubicweb fully supports the internalization of its content and interface.
-
-Cubicweb's interface internationalization is based on the translation project `GNU gettext`_.
-
-.. _`GNU gettext`: http://www.gnu.org/software/gettext/
-
-Cubicweb' internalization involves two steps:
-
-* in your Python code and cubicweb-tal templates : mark translatable strings
-
-* in your instance : handle the translation catalog, edit translations
-
-String internationalization
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-User defined string
-```````````````````
-
-In the Python code and cubicweb-tal templates translatable strings can be
-marked in one of the following ways :
-
- * by using the *built-in* function `_`:
-
- .. sourcecode:: python
-
- class PrimaryView(EntityView):
- """the full view of an non final entity"""
- __regid__ = 'primary'
- title = _('primary')
-
- OR
-
- * by using the equivalent request's method:
-
- .. sourcecode:: python
-
- class NoResultView(View):
- """default view when no result has been found"""
- __regid__ = 'noresult'
-
- def call(self, **kwargs):
- self.w(u'<div class="searchMessage"><strong>%s</strong></div>\n'
- % self._cw._('No result matching query'))
-
-The goal of the *built-in* function `_` is only **to mark the
-translatable strings**, it will only return the string to translate
-itself, but not its translation (it's actually another name for the
-`unicode` builtin).
-
-In the other hand the request's method `self._cw._` is also meant to
-retrieve the proper translation of translation strings in the
-requested language.
-
-Finally you can also use the `__` attribute of request object to get a
-translation for a string *which should not itself added to the catalog*,
-usually in case where the actual msgid is created by string interpolation ::
-
- self._cw.__('This %s' % etype)
-
-In this example ._cw.__` is used instead of ._cw._` so we don't have 'This %s' in
-messages catalogs.
-
-Translations in cubicweb-tal template can also be done with TAL tags
-`i18n:content` and `i18n:replace`.
-
-If you need to add messages on top of those that can be found in the source,
-you can create a file named `i18n/static-messages.pot`.
-
-You could put there messages not found in the python sources or
-overrides for some messages of used cubes.
-
-Generated string
-````````````````
-
-We do not need to mark the translation strings of entities/relations used by a
-particular instance's schema as they are generated automatically. String for
-various actions are also generated.
-
-For exemple the following schema:
-
-.. sourcecode:: python
-
-
- class EntityA(EntityType):
- relation_a2b = SubjectRelation('EntityB')
-
- class EntityB(EntityType):
- pass
-
-May generate the following message ::
-
- add EntityA relation_a2b EntityB subject
-
-This message will be used in views of ``EntityA`` for creation of a new
-``EntityB`` with a preset relation ``relation_a2b`` between the current
-``EntityA`` and the new ``EntityB``. The opposite message ::
-
- add EntityA relation_a2b EntityB object
-
-Is used for similar creation of an ``EntityA`` from a view of ``EntityB``. The
-title of they respective creation form will be ::
-
- creating EntityB (EntityA %(linkto)s relation_a2b EntityB)
-
- creating EntityA (EntityA relation_a2b %(linkto)s EntityA)
-
-In the translated string you can use ``%(linkto)s`` for reference to the source
-``entity``.
-
-Handling the translation catalog
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Once the internationalization is done in your code, you need to populate and
-update the translation catalog. Cubicweb provides the following commands for this
-purpose:
-
-
-* `i18ncubicweb` updates Cubicweb framework's translation
- catalogs. Unless you actually work on the framework itself, you
- don't need to use this command.
-
-* `i18ncube` updates the translation catalogs of *one particular cube*
- (or of all cubes). After this command is executed you must update
- the translation files *.po* in the "i18n" directory of your
- cube. This command will of course not remove existing translations
- still in use. It will mark unused translation but not remove them.
-
-* `i18ninstance` recompiles the translation catalogs of *one particular
- instance* (or of all instances) after the translation catalogs of
- its cubes have been updated. This command is automatically
- called every time you create or update your instance. The compiled
- catalogs (*.mo*) are stored in the i18n/<lang>/LC_MESSAGES of
- instance where `lang` is the language identifier ('en' or 'fr'
- for exemple).
-
-
-Example
-```````
-
-You have added and/or modified some translation strings in your cube
-(after creating a new view or modifying the cube's schema for exemple).
-To update the translation catalogs you need to do:
-
-1. `cubicweb-ctl i18ncube <cube>`
-2. Edit the <cube>/i18n/xxx.po files and add missing translations (empty `msgstr`)
-3. `hg ci -m "updated i18n catalogs"`
-4. `cubicweb-ctl i18ninstance <myinstance>`
-
-Editing po files
-~~~~~~~~~~~~~~~~
-
-Using a PO aware editor
-````````````````````````
-
-Many tools exist to help maintain .po (PO) files. Common editors or
-development environment provides modes for these. One can also find
-dedicated PO files editor, such as `poedit`_.
-
-.. _`poedit`: http://www.poedit.net/
-
-While usage of such a tool is commendable, PO files are perfectly
-editable with a (unicode aware) plain text editor. It is also useful
-to know their structure for troubleshooting purposes.
-
-Structure of a PO file
-``````````````````````
-
-In this section, we selectively quote passages of the `GNU gettext`_
-manual chapter on PO files, available there::
-
- http://www.gnu.org/software/hello/manual/gettext/PO-Files.html
-
-One PO file entry has the following schematic structure::
-
- white-space
- # translator-comments
- #. extracted-comments
- #: reference...
- #, flag...
- #| msgid previous-untranslated-string
- msgid untranslated-string
- msgstr translated-string
-
-
-A simple entry can look like this::
-
- #: lib/error.c:116
- msgid "Unknown system error"
- msgstr "Error desconegut del sistema"
-
-It is also possible to have entries with a context specifier. They
-look like this::
-
- white-space
- # translator-comments
- #. extracted-comments
- #: reference...
- #, flag...
- #| msgctxt previous-context
- #| msgid previous-untranslated-string
- msgctxt context
- msgid untranslated-string
- msgstr translated-string
-
-
-The context serves to disambiguate messages with the same
-untranslated-string. It is possible to have several entries with the
-same untranslated-string in a PO file, provided that they each have a
-different context. Note that an empty context string and an absent
-msgctxt line do not mean the same thing.
-
-Contexts and CubicWeb
-`````````````````````
-
-CubicWeb PO files have both non-contextual and contextual msgids.
-
-Contextual entries are automatically used in some cases. For instance,
-entity.dc_type(), eschema.display_name(req) or display_name(etype,
-req, form, context) methods/function calls will use them.
-
-It is also possible to explicitly use the with _cw.pgettext(context,
-msgid).
--- a/doc/book/en/devweb/js.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,394 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Javascript
-----------
-
-*CubicWeb* uses quite a bit of javascript in its user interface and
-ships with jquery (1.3.x) and parts of the jquery UI library, plus a
-number of homegrown files and also other third party libraries.
-
-All javascript files are stored in cubicweb/web/data/. There are
-around thirty js files there. In a cube it goes to data/.
-
-Obviously one does not want javascript pieces to be loaded all at
-once, hence the framework provides a number of mechanisms and
-conventions to deal with javascript resources.
-
-Conventions
-~~~~~~~~~~~
-
-It is good practice to name cube specific js files after the name of
-the cube, like this : 'cube.mycube.js', so as to avoid name clashes.
-
-.. XXX external_resources variable (which needs love)
-
-Server-side Javascript API
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Javascript resources are typically loaded on demand, from views. The
-request object (available as self._cw from most application objects,
-for instance views and entities objects) has a few methods to do that:
-
-* `add_js(self, jsfiles, localfile=True)` which takes a sequence of
- javascript files and writes proper entries into the HTML header
- section. The localfile parameter allows to declare resources which
- are not from web/data (for instance, residing on a content delivery
- network).
-
-* `add_onload(self, jscode)` which adds one raw javascript code
- snippet inline in the html headers. This is quite useful for setting
- up early jQuery(document).ready(...) initialisations.
-
-Javascript events
-~~~~~~~~~~~~~~~~~
-
-* ``server-response``: this event is triggered on HTTP responses (both
- standard and ajax). The two following extra parameters are passed
- to callbacks :
-
- - ``ajax``: a boolean that says if the reponse was issued by an
- ajax request
-
- - ``node``: the DOM node returned by the server in case of an
- ajax request, otherwise the document itself for standard HTTP
- requests.
-
-Important javascript AJAX APIS
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* `asyncRemoteExec` and `remoteExec` are the base building blocks for
- doing arbitrary async (resp. sync) communications with the server
-
-* `reloadComponent` is a convenience function to replace a DOM node
- with server supplied content coming from a specific registry (this
- is quite handy to refresh the content of some boxes for instances)
-
-* `jQuery.fn.loadxhtml` is an important extension to jQuery which
- allows proper loading and in-place DOM update of xhtml views. It is
- suitably augmented to trigger necessary events, and process CubicWeb
- specific elements such as the facet system, fckeditor, etc.
-
-
-A simple example with asyncRemoteExec
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-On the python side, we have to define an
-:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The
-simplest way to do that is to use the
-:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more
-details on this, refer to :ref:`ajax`).
-
-.. sourcecode: python
-
- from cubicweb.web.views.ajaxcontroller import ajaxfunc
-
- # serialize output to json to get it back easily on the javascript side
- @ajaxfunc(output_type='json')
- def js_say_hello(self, name):
- return u'hello %s' % name
-
-On the javascript side, we do the asynchronous call. Notice how it
-creates a `deferred` object. Proper treatment of the return value or
-error handling has to be done through the addCallback and addErrback
-methods.
-
-.. sourcecode: javascript
-
- function asyncHello(name) {
- var deferred = asyncRemoteExec('say_hello', name);
- deferred.addCallback(function (response) {
- alert(response);
- });
- deferred.addErrback(function (error) {
- alert('something fishy happened');
- });
- }
-
- function syncHello(name) {
- alert( remoteExec('say_hello', name) );
- }
-
-Anatomy of a reloadComponent call
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-`reloadComponent` allows to dynamically replace some DOM node with new
-elements. It has the following signature:
-
-* `compid` (mandatory) is the name of the component to be reloaded
-
-* `rql` (optional) will be used to generate a result set given as
- argument to the selected component
-
-* `registry` (optional) defaults to 'components' but can be any other
- valid registry name
-
-* `nodeid` (optional) defaults to compid + 'Component' but can be any
- explicitly specified DOM node id
-
-* `extraargs` (optional) should be a dictionary of values that will be
- given to the cell_call method of the component
-
-A simple reloadComponent example
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The server side implementation of `reloadComponent` is the
-:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject.
-
-The following function implements a two-steps method to delete a
-standard bookmark and refresh the UI, while keeping the UI responsive.
-
-.. sourcecode:: javascript
-
- function removeBookmark(beid) {
- d = asyncRemoteExec('delete_bookmark', beid);
- d.addCallback(function(boxcontent) {
- reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
- document.location.hash = '#header';
- updateMessage(_("bookmark has been removed"));
- });
- }
-
-`reloadComponent` is called with the id of the bookmark box as
-argument, no rql expression (because the bookmarks display is actually
-independant of any dataset context), a reference to the 'boxes'
-registry (which hosts all left, right and contextual boxes) and
-finally an explicit 'bookmarks_box' nodeid argument that stipulates
-the target DOM node.
-
-Anatomy of a loadxhtml call
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-`jQuery.fn.loadxhtml` is an important extension to jQuery which allows
-proper loading and in-place DOM update of xhtml views. The existing
-`jQuery.load`_ function does not handle xhtml, hence the addition. The
-API of loadxhtml is roughly similar to that of `jQuery.load`_.
-
-.. _`jQuery.load`: http://api.jquery.com/load/
-
-
-* `url` (mandatory) should be a complete url (typically referencing
- the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`,
- but this is not strictly mandatory)
-
-* `data` (optional) is a dictionary of values given to the
- controller specified through an `url` argument; some keys may have a
- special meaning depending on the choosen controller (such as `fname`
- for the JSonController); the `callback` key, if present, must refer
- to a function to be called at the end of loadxhtml (more on this
- below)
-
-* `reqtype` (optional) specifies the request method to be used (get or
- post); if the argument is 'post', then the post method is used,
- otherwise the get method is used
-
-* `mode` (optional) is one of `replace` (the default) which means the
- loaded node will replace the current node content, `swap` to replace
- the current node with the loaded node, and `append` which will
- append the loaded node to the current node content
-
-About the `callback` option:
-
-* it is called with two parameters: the current node, and a list
- containing the loaded (and post-processed node)
-
-* whenever it returns another function, this function is called in
- turn with the same parameters as above
-
-This mechanism allows callback chaining.
-
-
-A simple example with loadxhtml
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Here we are concerned with the retrieval of a specific view to be
-injected in the live DOM. The view will be of course selected
-server-side using an entity eid provided by the client side.
-
-.. sourcecode:: python
-
- from cubicweb.web.views.ajaxcontroller import ajaxfunc
-
- @ajaxfunc(output_type='xhtml')
- def frob_status(self, eid, frobname):
- entity = self._cw.entity_from_eid(eid)
- return entity.view('frob', name=frobname)
-
-.. sourcecode:: javascript
-
- function updateSomeDiv(divid, eid, frobname) {
- var params = {fname:'frob_status', eid: eid, frobname:frobname};
- jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
- }
-
-In this example, the url argument is the base json url of a cube
-instance (it should contain something like
-`http://myinstance/ajax?`). The actual AjaxController method name is
-encoded in the `params` dictionary using the `fname` key.
-
-A more real-life example
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-A frequent need of Web 2 applications is the delayed (or demand
-driven) loading of pieces of the DOM. This is typically achieved using
-some preparation of the initial DOM nodes, jQuery event handling and
-proper use of loadxhtml.
-
-We present here a skeletal version of the mecanism used in CubicWeb
-and available in web/views/tabs.py, in the `LazyViewMixin` class.
-
-.. sourcecode:: python
-
- def lazyview(self, vid, rql=None):
- """ a lazy version of wview """
- w = self.w
- self._cw.add_js('cubicweb.lazy.js')
- urlparams = {'vid' : vid, 'fname' : 'view'}
- if rql is not None:
- urlparams['rql'] = rql
- w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
- vid, xml_escape(self._cw.build_url('json', **urlparams))))
- w(u'</div>')
- self._cw.add_onload(u"""
- jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
- loadNow('#lazy-%(vid)s');});"""
- % {'event': 'load_%s' % vid, 'vid': vid})
-
-This creates a `div` with a specific event associated to it.
-
-The full version deals with:
-
-* optional parameters such as an entity eid, an rset
-
-* the ability to further reload the fragment
-
-* the ability to display a spinning wheel while the fragment is still
- not loaded
-
-* handling of browsers that do not support ajax (search engines,
- text-based browsers such as lynx, etc.)
-
-The javascript side is quite simple, due to loadxhtml awesomeness.
-
-.. sourcecode:: javascript
-
- function loadNow(eltsel) {
- var lazydiv = jQuery(eltsel);
- lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
- }
-
-This is all significantly different of the previous `simple example`
-(albeit this example actually comes from real-life code).
-
-Notice how the `cubicweb:loadurl` is used to convey the url
-information. The base of this url is similar to the global javascript
-JSON_BASE_URL. According to the pattern described earlier,
-the `fname` parameter refers to the standard `js_view` method of the
-JSonController. This method renders an arbitrary view provided a view
-id (or `vid`) is provided, and most likely an rql expression yielding
-a result set against which a proper view instance will be selected.
-
-The `cubicweb:loadurl` is one of the 29 attributes extensions to XHTML
-in a specific cubicweb namespace. It is a means to pass information
-without breaking HTML nor XHTML compliance and without resorting to
-ungodly hacks.
-
-Given all this, it is easy to add a small nevertheless useful feature
-to force the loading of a lazy view (for instance, a very
-computation-intensive web page could be scinded into one fast-loading
-part and a delayed part).
-
-On the server side, a simple call to a javascript function is
-sufficient.
-
-.. sourcecode:: python
-
- def forceview(self, vid):
- """trigger an event that will force immediate loading of the view
- on dom readyness
- """
- self._cw.add_onload("triggerLoad('%s');" % vid)
-
-The browser-side definition follows.
-
-.. sourcecode:: javascript
-
- function triggerLoad(divid) {
- jQuery('#lazy-' + divd).trigger('load_' + divid);
- }
-
-
-Javascript library: overview
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* jquery.* : jquery and jquery UI library
-
-* cubicweb.ajax.js : concentrates all ajax related facilities (it
- extends jQuery with the loahxhtml function, provides a handfull of
- high-level ajaxy operations like asyncRemoteExec, reloadComponent,
- replacePageChunk, getDomFromResponse)
-
-* cubicweb.python.js : adds a number of practical extension to stdanrd
- javascript objects (on Date, Array, String, some list and dictionary
- operations), and a pythonesque way to build classes. Defines a
- CubicWeb namespace.
-
-* cubicweb.htmlhelpers.js : a small bag of convenience functions used
- in various other cubicweb javascript resources (baseuri, progress
- cursor handling, popup login box, html2dom function, etc.)
-
-* cubicweb.widgets.js : provides a widget namespace and constructors
- and helpers for various widgets (mainly facets and timeline)
-
-* cubicweb.edition.js : used by edition forms
-
-* cubicweb.preferences.js : used by the preference form
-
-* cubicweb.facets.js : used by the facets mechanism
-
-There is also javascript support for massmailing, gmap (google maps),
-fckcwconfig (fck editor), timeline, calendar, goa (CubicWeb over
-AppEngine), flot (charts drawing), tabs and bookmarks.
-
-API
-~~~
-
-.. toctree::
- :maxdepth: 1
-
- js_api/index
-
-
-Testing javascript
-~~~~~~~~~~~~~~~~~~
-
-You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests
-inside the python unittest run . You simply have to define a new class that
-inherit from ``QUnitTestCase`` and register your javascript test file in the
-``all_js_tests`` lclass attribut. This ``all_js_tests`` is a sequence a
-3-tuple (<test_file, [<dependencies> ,] [<data_files>]):
-
-The <test_file> should contains the qunit test. <dependencies> defines the list
-of javascript file that must be imported before the test script. Dependencies
-are included their definition order. <data_files> are additional files copied in the
-test directory. both <dependencies> and <data_files> are optionnal.
-``jquery.js`` is preincluded in for all test.
-
-.. sourcecode:: python
-
- from cubicweb.qunit import QUnitTestCase
-
- class MyQUnitTest(QUnitTestCase):
-
- all_js_tests = (
- ("relative/path/to/my_simple_testcase.js",)
- ("relative/path/to/my_qunit_testcase.js",(
- "rel/path/to/dependency_1.js",
- "rel/path/to/dependency_2.js",)),
- ("relative/path/to/my_complexe_qunit_testcase.js",(
- "rel/path/to/dependency_1.js",
- "rel/path/to/dependency_2.js",
- ),(
- "rel/path/file_dependency.html",
- "path/file_dependency.json")
- ),
- )
--- a/doc/book/en/devweb/property.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-.. _cwprops:
-
-The property mecanism
----------------------
-
-.. XXX CWProperty and co
-
-
-Property API
-~~~~~~~~~~~~
-.. XXX feed me
-
-Registering and using your own property
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. XXX feed me
--- a/doc/book/en/devweb/publisher.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-.. _publisher:
-
-Publisher
----------
-
-What happens when an HTTP request is issued ?
-
-The story begins with the ``CubicWebPublisher.main_publish``
-method. We do not get upper in the bootstrap process because it is
-dependant on the used HTTP library. With `twisted`_ however,
-``cubicweb.etwist.server.CubicWebRootResource.render_request`` is the
-real entry point.
-
-.. _`twisted`: http://twistedmatrix.com/trac/
-
-What main_publish does:
-
-* get a controller id and a result set from the path (this is actually
- delegated to the `urlpublisher` component)
-
-* the controller is then selected (if not, this is considered an
- authorization failure and signaled as such) and called
-
-* then either a proper result is returned, in which case the
- request/connection object issues a ``commit`` and returns the result
-
-* or error handling must happen:
-
- * ``ValidationErrors`` pop up there and may lead to a redirect to a
- previously arranged url or standard error handling applies
- * an HTTP 500 error (`Internal Server Error`) is issued
-
-
-Now, let's turn to the controller. There are many of them in
-:mod:`cubicweb.web.views.basecontrollers`. We can just follow the
-default `view` controller that is selected on a `view` path. See the
-:ref:`controllers` chapter for more information on controllers.
-
-The `View` controller's entry point is the `publish` method. It does
-the following:
-
-* compute the `main` view to be applied, using either the given result
- set or building one from a user provided rql string (`rql` and `vid`
- can be forced from the url GET parameters), that is:
-
- * compute the `vid` using the result set and the schema (see
- `cubicweb.web.views.vid_from_rset`)
- * handle all error cases that could happen in this phase
-
-* do some cache management chores
-
-* select a main template (typically `TheMainTemplate`, see chapter
- :ref:`templates`)
-
-* call it with the result set and the computed view.
-
-What happens next actually depends on the template and the view, but
-in general this is the rendering phase.
-
-
-CubicWebPublisher API
-`````````````````````
-
-.. autoclass:: cubicweb.web.application.CubicWebPublisher
- :members:
--- a/doc/book/en/devweb/request.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-The `Request` class (`cubicweb.web.request`)
---------------------------------------------
-
-Overview
-````````
-
-A request instance is created when an HTTP request is sent to the web
-server. It contains informations such as form parameters,
-authenticated user, etc. It is a very prevalent object and is used
-throughout all of the framework and applications, as you'll access to
-almost every resources through it.
-
-**A request represents a user query, either through HTTP or not (we
-also talk about RQL queries on the server side for example).**
-
-Here is a non-exhaustive list of attributes and methods available on
-request objects (grouped by category):
-
-* `Browser control`:
-
- * `ie_browser`: tells if the browser belong to the Internet Explorer
- family
-
-* `User and identification`:
-
- * `user`, instance of `cubicweb.entities.authobjs.CWUser` corresponding to the
- authenticated user
-
-* `Session data handling`
-
- * `session.data` is the dictionary of the session data; it can be
- manipulated like an ordinary Python dictionary
-
-* `Edition` (utilities for edition control):
-
- * `cancel_edition`: resets error url and cleans up pending operations
- * `create_entity`: utility to create an entity (from an etype,
- attributes and relation values)
- * `datadir_url`: returns the url to the merged external resources
- (|cubicweb|'s `web/data` directory plus all `data` directories of
- used cubes)
- * `edited_eids`: returns the list of eids of entities that are
- edited under the current http request
- * `eid_rset(eid)`: utility which returns a result set from an eid
- * `entity_from_eid(eid)`: returns an entity instance from the given eid
- * `encoding`: returns the encoding of the current HTTP request
- * `ensure_ro_rql(rql)`: ensure some rql query is a data request
- * etype_rset
- * `form`, dictionary containing the values of a web form
- * `encoding`, character encoding to use in the response
- * `next_tabindex()`: returns a monotonically growing integer used to
- build the html tab index of forms
-
-* `HTTP`
-
- * `authmode`: returns a string describing the authentication mode
- (http, cookie, ...)
- * `lang`: returns the user agents/browser's language as carried by
- the http request
- * `demote_to_html()`: in the context of an XHTML compliant browser,
- this will force emission of the response as an HTML document
- (using the http content negociation)
-
-* `Cookies handling`
-
- * `get_cookie()`, returns a dictionary containing the value of the header
- HTTP 'Cookie'
- * `set_cookie(cookie, key, maxage=300)`, adds a header HTTP `Set-Cookie`,
- with a minimal 5 minutes length of duration by default (`maxage` = None
- returns a *session* cookie which will expire when the user closes the browser
- window)
- * `remove_cookie(cookie, key)`, forces a value to expire
-
-* `URL handling`
-
- * `build_url(__vid, *args, **kwargs)`: return an absolute URL using
- params dictionary key/values as URL parameters. Values are
- automatically URL quoted, and the publishing method to use may be
- specified or will be guessed.
- * `build_url_params(**kwargs)`: returns a properly prepared (quoted,
- separators, ...) string from the given parameters
- * `url()`, returns the full URL of the HTTP request
- * `base_url()`, returns the root URL of the web application
- * `relative_path()`, returns the relative path of the request
-
-* `Web resource (.css, .js files, etc.) handling`:
-
- * `add_css(cssfiles)`: adds the given list of css resources to the current
- html headers
- * `add_js(jsfiles)`: adds the given list of javascript resources to the
- current html headers
- * `add_onload(jscode)`: inject the given jscode fragment (an unicode
- string) into the current html headers, wrapped inside a
- document.ready(...) or another ajax-friendly one-time trigger event
- * `add_header(header, values)`: adds the header/value pair to the
- current html headers
- * `status_out`: control the HTTP status of the response
-
-* `And more...`
-
- * `set_content_type(content_type, filename=None)`, adds the header HTTP
- 'Content-Type'
- * `get_header(header)`, returns the value associated to an arbitrary header
- of the HTTP request
- * `set_header(header, value)`, adds an arbitrary header in the response
- * `execute(*args, **kwargs)`, executes an RQL query and return the result set
- * `property_value(key)`, properties management (`CWProperty`)
- * dictionary `data` to store data to share informations between components
- *while a request is executed*
-
-Please note that this class is abstract and that a concrete implementation
-will be provided by the *frontend* web used (in particular *twisted* as of
-today). For the views or others that are executed on the server side,
-most of the interface of `Request` is defined in the session associated
-to the client.
-
-API
-```
-
-The elements we gave in overview for above are built in three layers,
-from ``cubicweb.req.RequestSessionBase``, ``cubicweb.repoapi.ClientConnection`` and
-``cubicweb.web.ConnectionCubicWebRequestBase``.
-
-.. autoclass:: cubicweb.req.RequestSessionBase
- :members:
-
-.. autoclass:: cubicweb.repoapi.ClientConnection
- :members:
-
-.. autoclass:: cubicweb.web.request.ConnectionCubicWebRequestBase
- :members:
--- a/doc/book/en/devweb/resource.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-.. _resources:
-
-Locate resources
-----------------
-
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_resource
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_doc_file
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.locate_all_files
-
-Static files handling
----------------------
-
-.. autoattribute:: cubicweb.web.webconfig.WebConfiguration.static_directory
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_exists
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_open
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_add
-.. automethod:: cubicweb.web.webconfig.WebConfiguration.static_file_del
-
--- a/doc/book/en/devweb/rtags.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-Configuring the user interface
-------------------------------
-
-.. _relation_tags:
-
-Relation tags
-~~~~~~~~~~~~~
-.. automodule:: cubicweb.rtags
-
-.. _uicfg:
-
-The uicfg module
-~~~~~~~~~~~~~~~~
-
-.. note::
-
- The part of uicfg that deals with primary views is in the
- :ref:`primary_view_configuration` chapter.
-
-.. automodule:: cubicweb.web.views.uicfg
-
-
-The uihelper module
-~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: cubicweb.web.uihelper
-
--- a/doc/book/en/devweb/searchbar.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-.. _searchbar:
-
-RQL search bar
---------------
-
-The RQL search bar is a visual component, hidden by default, the tiny *search*
-input being enough for common use cases.
-
-An autocompletion helper is provided to help you type valid queries, both
-in terms of syntax and in terms of schema validity.
-
-.. autoclass:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder
-
-
-How search is performed
-+++++++++++++++++++++++
-
-You can use the *rql search bar* to either type RQL queries, plain text queries
-or standard shortcuts such as *<EntityType>* or *<EntityType> <attrname> <value>*.
-
-Ultimately, all queries are translated to rql since it's the only
-language understood on the server (data) side. To transform the user
-query into RQL, CubicWeb uses the so-called *magicsearch component*,
-defined in :mod:`cubicweb.web.views.magicsearch`, which in turn
-delegates to a number of query preprocessor that are responsible of
-interpreting the user query and generating corresponding RQL.
-
-The code of the main processor loop is easy to understand:
-
-.. sourcecode:: python
-
- for proc in self.processors:
- try:
- return proc.process_query(uquery, req)
- except (RQLSyntaxError, BadRQLQuery):
- pass
-
-The idea is simple: for each query processor, try to translate the
-query. If it fails, try with the next processor, if it succeeds,
-we're done and the RQL query will be executed.
-
--- a/doc/book/en/devweb/views/basetemplates.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _templates:
-
-Templates
-=========
-
-Templates are the entry point for the |cubicweb| view system. As seen
-in :ref:`views_base_class`, there are two kinds of views: the
-templatable and non-templatable.
-
-
-Non-templatable views
----------------------
-
-Non-templatable views are standalone. They are responsible for all the details
-such as setting a proper content type (or mime type), the proper document
-headers, namespaces, etc. Examples are pure xml views such as RSS or Semantic Web
-views (`SIOC`_, `DOAP`_, `FOAF`_, `Linked Data`_, etc.), and views which generate
-binary files (pdf, excel files, etc.)
-
-.. _`SIOC`: http://sioc-project.org/
-.. _`DOAP`: http://trac.usefulinc.com/doap
-.. _`FOAF`: http://www.foaf-project.org/
-.. _`Linked Data`: http://linkeddata.org/
-
-
-To notice that a view is not templatable, you just have to set the
-view's class attribute `templatable` to `False`. In this case, it
-should set the `content_type` class attribute to the correct MIME
-type. By default, it is text/xhtml. Additionally, if your view
-generate a binary file, you have to set the view's class attribute
-`binary` to `True` too.
-
-
-Templatable views
------------------
-
-Templatable views are not concerned with such pesky details. They
-leave it to the template. Conversely, the template's main job is to:
-
-* set up the proper document header and content type
-* define the general layout of a document
-* invoke adequate views in the various sections of the document
-
-
-Look at :mod:`cubicweb.web.views.basetemplates` and you will find the base
-templates used to generate (X)HTML for your application. The most important
-template there is :class:`~cubicweb.web.views.basetemplates.TheMainTemplate`.
-
-.. _the_main_template_layout:
-
-TheMainTemplate
-~~~~~~~~~~~~~~~
-
-.. _the_main_template_sections:
-
-Layout and sections
-```````````````````
-
-A page is composed as indicated on the schema below :
-
-.. image:: ../../images/main_template.png
-
-The sections dispatches specific views:
-
-* `header`: the rendering of the header is delegated to the
- `htmlheader` view, whose default implementation can be found in
- ``basetemplates.py`` and which does the following things:
-
- * inject the favicon if there is one
- * inject the global style sheets and javascript resources
- * call and display a link to an rss component if there is one available
-
- it also sets up the page title, and fills the actual
- `header` section with top-level components, using the `header` view, which:
-
- * tries to display a logo, the name of the application and the `breadcrumbs`
- * provides a login status area
- * provides a login box (hiden by default)
-
-* `left column`: this is filled with all selectable boxes matching the
- `left` context (there is also a right column but nowadays it is
- seldom used due to bad usability)
-
-* `contentcol`: this is the central column; it is filled with:
-
- * the `rqlinput` view (hidden by default)
- * the `applmessages` component
- * the `contentheader` view which in turns dispatches all available
- content navigation components having the `navtop` context (this
- is used to navigate through entities implementing the IPrevNext
- interface)
- * the view that was given as input to the template's `call`
- method, also dealing with pagination concerns
- * the `contentfooter`
-
-* `footer`: adds all footer actions
-
-.. note::
-
- How and why a view object is given to the main template is explained
- in the :ref:`publisher` chapter.
-
-Configure the main template
-```````````````````````````
-
-You can overload some methods of the
-:class:`~cubicweb.web.views.basetemplates.TheMainTemplate`, in order to fulfil
-your needs. There are also some attributes and methods which can be defined on a
-view to modify the base template behaviour:
-
-* `paginable`: if the result set is bigger than a configurable size, your result
- page will be paginated by default. You can set this attribute to `False` to
- avoid this.
-
-* `binary`: boolean flag telling if the view generates some text or a binary
- stream. Default to False. When view generates text argument given to `self.w`
- **must be an unicode string**, encoded string otherwise.
-
-* `content_type`, view's content type, default to 'text/xhtml'
-
-* `templatable`, boolean flag telling if the view's content should be returned
- directly (when `False`) or included in the main template layout (including
- header, boxes and so on).
-
-* `page_title()`, method that should return a title that will be set as page
- title in the html headers.
-
-* `html_headers()`, method that should return a list of HTML headers to be
- included the html headers.
-
-
-You can also modify certain aspects of the main template of a page
-when building an url or setting these parameters in the req.form:
-
-* `__notemplate`, if present (whatever the value assigned), only the content view
- is returned
-
-* `__force_display`, if present and its value is not null, no pagination whatever
- the number of entities to display (e.g. similar effect as view's `paginable`
- attribute described above.
-
-* `__method`, if the result set to render contains only one entity and this
- parameter is set, it refers to a method to call on the entity by passing it the
- dictionary of the forms parameters, before going the classic way (through step
- 1 and 2 described juste above)
-
-* `vtitle`, a title to be set as <h1> of the content
-
-Other templates
-~~~~~~~~~~~~~~~
-
-There are also the following other standard templates:
-
-* :class:`cubicweb.web.views.basetemplates.LogInTemplate`
-* :class:`cubicweb.web.views.basetemplates.LogOutTemplate`
-* :class:`cubicweb.web.views.basetemplates.ErrorTemplate` specializes
- :class:`~cubicweb.web.views.basetemplates.TheMainTemplate` to do
- proper end-user output if an error occurs during the computation of
- TheMainTemplate (it is a fallback view).
--- a/doc/book/en/devweb/views/baseviews.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-Base views
-----------
-
-|cubicweb| provides a lot of standard views, that can be found in
-:mod:`cubicweb.web.views` sub-modules.
-
-A certain number of views are used to build the web interface, which apply to one
-or more entities. As other appobjects, their identifier is what distinguish them
-from each others. The most generic ones, found in
-:mod:`cubicweb.web.views.baseviews`, are described below.
-
-You'll probably want to customize one or more of the described views which are
-default, generic, implementations.
-
-
-.. automodule:: cubicweb.web.views.baseviews
-
-You will also find modules providing some specific services:
-
-.. automodule:: cubicweb.web.views.navigation
-
--- a/doc/book/en/devweb/views/boxes.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-Boxes
------
-
-(:mod:`cubicweb.web.views.boxes`)
-
-*sidebox*
- This view displays usually a side box of some related entities
- in a primary view.
-
-The action box
-~~~~~~~~~~~~~~~
-
-The ``add_related`` is an automatic menu in the action box that allows to create
-an entity automatically related to the initial entity (context in
-which the box is displayed). By default, the links generated in this
-box are computed from the schema properties of the displayed entity,
-but it is possible to explicitly specify them thanks to the
-`cubicweb.web.views.uicfg.rmode` *relation tag*:
-
-* `link`, indicates that a relation is in general created pointing
- to an existing entity and that we should not to display a link
- for this relation
-
-* `create`, indicates that a relation is in general created pointing
- to new entities and that we should display a link to create a new
- entity and link to it automatically
-
-
-If necessary, it is possible to overwrite the method
-`relation_mode(rtype, targettype, x='subject')` to dynamically
-compute a relation creation category.
-
-Please note that if at least one action belongs to the `addrelated` category,
-the automatic behavior is desactivated in favor of an explicit behavior
-(e.g. display of `addrelated` category actions only).
-
--- a/doc/book/en/devweb/views/breadcrumbs.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-Breadcrumbs
------------
-
-Breadcrumbs are a navigation component to help the user locate himself
-along a path of entities.
-
-Display
-~~~~~~~
-
-Breadcrumbs are displayed by default in the header section (see
-:ref:`the_main_template_sections`). With the default main template,
-the header section is composed by the logo, the application name,
-breadcrumbs and, at the most right, the login box. Breadcrumbs are
-displayed just next to the application name, thus they begin with a
-separator.
-
-Here is the header section of the CubicWeb's forge:
-
-.. image:: ../../images/breadcrumbs_header.png
-
-There are three breadcrumbs components defined in
-:mod:`cubicweb.web.views.ibreadcrumbs`:
-
-- `BreadCrumbEntityVComponent`: displayed for a result set with one line
- if the entity is adaptable to ``IBreadCrumbsAdapter``.
-- `BreadCrumbETypeVComponent`: displayed for a result set with more than
- one line, but with all entities of the same type which can adapt to
- ``IBreadCrumbsAdapter``.
-- `BreadCrumbAnyRSetVComponent`: displayed for any other result set.
-
-Building breadcrumbs
-~~~~~~~~~~~~~~~~~~~~
-
-The ``IBreadCrumbsAdapter`` adapter is defined in the
-:mod:`cubicweb.web.views.ibreadcrumbs` module. It specifies that an
-entity which implements this interface must have a ``breadcrumbs`` and
-a ``parent_entity`` method. A default implementation for each is
-provided. This implementation expoits the ITreeAdapter.
-
-.. note::
-
- Redefining the breadcrumbs is the hammer way to do it. Another way
- is to define an `ITreeAdapter` adapter on an entity type. If
- available, it will be used to compute breadcrumbs.
-
-Here is the API of the ``IBreadCrumbsAdapter`` class:
-
-.. automethod:: cubicweb.web.views.ibreadcrumbs.IBreadCrumbsAdapter.parent_entity
-.. automethod:: cubicweb.web.views.ibreadcrumbs.IBreadCrumbsAdapter.breadcrumbs
-
-If the breadcrumbs method return a list of entities, the
-``cubicweb.web.views.ibreadcrumbs.BreadCrumbView`` is used to display
-the elements.
-
-By default, for any entity, if recurs=True, breadcrumbs method returns
-a list of entities, else a list of a simple string.
-
-In order to see a hierarchical breadcrumbs, entities must have a
-``parent`` method which returns the parent entity. By default this
-method doesn't exist on entity, given that it can not be guessed.
--- a/doc/book/en/devweb/views/idownloadable.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-The 'download' views
-====================
-
-.. automodule:: cubicweb.web.views.idownloadable
-
-Components
-----------
-
-.. autoclass:: cubicweb.web.views.idownloadable.DownloadBox
-
-Download views
---------------
-
-.. autoclass:: cubicweb.web.views.idownloadable.DownloadView
-.. autoclass:: cubicweb.web.views.idownloadable.DownloadLinkView
-.. autoclass:: cubicweb.web.views.idownloadable.IDownloadablePrimaryView
-
-Embedded views
---------------
-
-.. autoclass:: cubicweb.web.views.idownloadable.ImageView
-.. autoclass:: cubicweb.web.views.idownloadable.EHTMLView
--- a/doc/book/en/devweb/views/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-The View system
-===============
-
-This chapter aims to describe the concept of a `view` used all along
-the development of a web application and how it has been implemented
-in |cubicweb|.
-
-
-.. toctree::
- :maxdepth: 3
-
- views
- basetemplates
- primary
- reledit
- baseviews
- startup
- boxes
- table
- xmlrss
- urlpublish
- breadcrumbs
- idownloadable
- wdoc
-
-.. editforms
-.. embedding
-
--- a/doc/book/en/devweb/views/primary.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,276 +0,0 @@
-.. _primary_view:
-
-The Primary View
------------------
-
-By default, *CubicWeb* provides a view that fits every available
-entity type. This is the first view you might be interested in
-modifying. It is also one of the richest and most complex.
-
-It is automatically selected on a one line result set containing an
-entity.
-
-It lives in the :mod:`cubicweb.web.views.primary` module.
-
-The *primary* view is supposed to render a maximum of informations about the
-entity.
-
-.. _primary_view_layout:
-
-Layout
-``````
-
-The primary view has the following layout.
-
-.. image:: ../../images/primaryview_template.png
-
-.. _primary_view_configuration:
-
-Primary view configuration
-``````````````````````````
-
-If you want to customize the primary view of an entity, overriding the primary
-view class may not be necessary. For simple adjustments (attributes or relations
-display locations and styles), a much simpler way is to use uicfg.
-
-Attributes/relations display location
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-In the primary view, there are three sections where attributes and
-relations can be displayed (represented in pink in the image above):
-
-* 'attributes'
-* 'relations'
-* 'sideboxes'
-
-**Attributes** can only be displayed in the attributes section (default
- behavior). They can also be hidden. By default, attributes of type `Password`
- and `Bytes` are hidden.
-
-For instance, to hide the ``title`` attribute of the ``Blog`` entity:
-
-.. sourcecode:: python
-
- from cubicweb.web.views import uicfg
- uicfg.primaryview_section.tag_attribute(('Blog', 'title'), 'hidden')
-
-**Relations** can be either displayed in one of the three sections or hidden.
-
-For relations, there are two methods:
-
-* ``tag_object_of`` for modifying the primary view of the object
-* ``tag_subject_of`` for modifying the primary view of the subject
-
-These two methods take two arguments:
-
-* a triplet ``(subject, relation_name, object)``, where subject or object can be replaced with ``'*'``
-* the section name or ``hidden``
-
-.. sourcecode:: python
-
- pv_section = uicfg.primaryview_section
- # hide every relation `entry_of` in the `Blog` primary view
- pv_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
-
- # display `entry_of` relations in the `relations`
- # section in the `BlogEntry` primary view
- pv_section.tag_subject_of(('BlogEntry', 'entry_of', '*'), 'relations')
-
-
-Display content
-^^^^^^^^^^^^^^^
-
-You can use ``primaryview_display_ctrl`` to customize the display of attributes
-or relations. Values of ``primaryview_display_ctrl`` are dictionaries.
-
-
-Common keys for attributes and relations are:
-
-* ``vid``: specifies the regid of the view for displaying the attribute or the relation.
-
- If ``vid`` is not specified, the default value depends on the section:
- * ``attributes`` section: 'reledit' view
- * ``relations`` section: 'autolimited' view
- * ``sideboxes`` section: 'sidebox' view
-
-* ``order``: int used to control order within a section. When not specified,
- automatically set according to order in which tags are added.
-
-* ``label``: label for the relations section or side box
-
-* ``showlabel``: boolean telling whether the label is displayed
-
-.. sourcecode:: python
-
- # let us remind the schema of a blog entry
- class BlogEntry(EntityType):
- title = String(required=True, fulltextindexed=True, maxsize=256)
- publish_date = Date(default='TODAY')
- content = String(required=True, fulltextindexed=True)
- entry_of = SubjectRelation('Blog', cardinality='?*')
-
- # now, we want to show attributes
- # with an order different from that in the schema definition
- view_ctrl = uicfg.primaryview_display_ctrl
- for index, attr in enumerate('title', 'content', 'publish_date'):
- view_ctrl.tag_attribute(('BlogEntry', attr), {'order': index})
-
-By default, relations displayed in the 'relations' section are being displayed by
-the 'autolimited' view. This view will use comma separated values, or list view
-and/or limit your rset if there is too much items in it (and generate the "view
-all" link in this case).
-
-You can control this view by setting the following values in the
-`primaryview_display_ctrl` relation tag:
-
-* `limit`, maximum number of entities to display. The value of the
- 'navigation.related-limit' cwproperty is used by default (which is 8 by default).
- If None, no limit.
-
-* `use_list_limit`, number of entities until which they should be display as a list
- (eg using the 'list' view). Below that limit, the 'csv' view is used. If None,
- display using 'csv' anyway.
-
-* `subvid`, the subview identifier (eg view that should be used of each item in the
- list)
-
-Notice you can also use the `filter` key to set up a callback taking the related
-result set as argument and returning it filtered, to do some arbitrary filtering
-that can't be done using rql for instance.
-
-
-.. sourcecode:: python
-
- pv_section = uicfg.primaryview_section
- # in `CWUser` primary view, display `created_by`
- # relations in relations section
- pv_section.tag_object_of(('*', 'created_by', 'CWUser'), 'relations')
-
- # display this relation as a list, sets the label,
- # limit the number of results and filters on comments
- def filter_comment(rset):
- return rset.filtered_rset(lambda x: x.e_schema == 'Comment')
- pv_ctrl = uicfg.primaryview_display_ctrl
- pv_ctrl.tag_object_of(('*', 'created_by', 'CWUser'),
- {'vid': 'list', 'label': _('latest comment(s):'),
- 'limit': True,
- 'filter': filter_comment})
-
-.. warning:: with the ``primaryview_display_ctrl`` rtag, the subject or the
- object of the relation is ignored for respectively ``tag_object_of`` or
- ``tag_subject_of``. To avoid warnings during execution, they should be set to
- ``'*'``.
-
-
-.. automodule:: cubicweb.web.views.primary
-
-
-Example of customization and creation
-`````````````````````````````````````
-
-We'll show you now an example of a ``primary`` view and how to customize it.
-
-If you want to change the way a ``BlogEntry`` is displayed, just
-override the method ``cell_call()`` of the view ``primary`` in
-``BlogDemo/views.py``.
-
-.. sourcecode:: python
-
- from cubicweb.predicates import is_instance
- from cubicweb.web.views.primary import Primaryview
-
- class BlogEntryPrimaryView(PrimaryView):
- __select__ = PrimaryView.__select__ & is_instance('BlogEntry')
-
- def render_entity_attributes(self, entity):
- self.w(u'<p>published on %s</p>' %
- entity.publish_date.strftime('%Y-%m-%d'))
- super(BlogEntryPrimaryView, self).render_entity_attributes(entity)
-
-
-The above source code defines a new primary view for
-``BlogEntry``. The `__reid__` class attribute is not repeated there since it
-is inherited through the `primary.PrimaryView` class.
-
-The selector for this view chains the selector of the inherited class
-with its own specific criterion.
-
-The view method ``self.w()`` is used to output data. Here `lines
-08-09` output HTML for the publication date of the entry.
-
-.. image:: ../../images/lax-book_09-new-view-blogentry_en.png
- :alt: blog entries now look much nicer
-
-Let us now improve the primary view of a blog
-
-.. sourcecode:: python
-
- from logilab.mtconverter import xml_escape
- from cubicweb.predicates import is_instance, one_line_rset
- from cubicweb.web.views.primary import Primaryview
-
- class BlogPrimaryView(PrimaryView):
- __regid__ = 'primary'
- __select__ = PrimaryView.__select__ & is_instance('Blog')
- rql = 'Any BE ORDERBY D DESC WHERE BE entry_of B, BE publish_date D, B eid %(b)s'
-
- def render_entity_relations(self, entity):
- rset = self._cw.execute(self.rql, {'b' : entity.eid})
- for entry in rset.entities():
- self.w(u'<p>%s</p>' % entry.view('inblogcontext'))
-
- class BlogEntryInBlogView(EntityView):
- __regid__ = 'inblogcontext'
- __select__ = is_instance('BlogEntry')
-
- def cell_call(self, row, col):
- entity = self.cw_rset.get_entity(row, col)
- self.w(u'<a href="%s" title="%s">%s</a>' %
- entity.absolute_url(),
- xml_escape(entity.content[:50]),
- xml_escape(entity.description))
-
-This happens in two places. First we override the
-render_entity_relations method of a Blog's primary view. Here we want
-to display our blog entries in a custom way.
-
-At `line 10`, a simple request is made to build a result set with all
-the entities linked to the current ``Blog`` entity by the relationship
-``entry_of``. The part of the framework handling the request knows
-about the schema and infers that such entities have to be of the
-``BlogEntry`` kind and retrieves them (in the prescribed publish_date
-order).
-
-The request returns a selection of data called a result set. Result
-set objects have an .entities() method returning a generator on
-requested entities (going transparently through the `ORM` layer).
-
-At `line 13` the view 'inblogcontext' is applied to each blog entry to
-output HTML. (Note that the 'inblogcontext' view is not defined
-whatsoever in *CubicWeb*. You are absolutely free to define whole view
-families.) We juste arrange to wrap each blogentry output in a 'p'
-html element.
-
-Next, we define the 'inblogcontext' view. This is NOT a primary view,
-with its well-defined sections (title, metadata, attribtues,
-relations/boxes). All a basic view has to define is cell_call.
-
-Since views are applied to result sets which can be tables of data, we
-have to recover the entity from its (row,col)-coordinates (`line
-20`). Then we can spit some HTML.
-
-.. warning::
-
- Be careful: all strings manipulated in *CubicWeb* are actually
- unicode strings. While web browsers are usually tolerant to
- incoherent encodings they are being served, we should not abuse
- it. Hence we have to properly escape our data. The xml_escape()
- function has to be used to safely fill (X)HTML elements from Python
- unicode strings.
-
-Assuming we added entries to the blog titled `MyLife`, displaying it
-now allows to read its description and all its entries.
-
-.. image:: ../../images/lax-book_10-blog-with-two-entries_en.png
- :alt: a blog and all its entries
-
--- a/doc/book/en/devweb/views/reledit.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-.. _reledit:
-
-The "Click and Edit" (also `reledit`) View
-------------------------------------------
-
-The principal way to update data through the Web UI is through the
-`modify` action on entities, which brings a full form. This is
-described in the :ref:`webform` chapter.
-
-There is however another way to perform piecewise edition of entities
-and relations, using a specific `reledit` (for *relation edition*)
-view from the :mod:`cubicweb.web.views.reledit` module.
-
-This is typically applied from the default Primary View (see
-:ref:`primary_view`) on the attributes and relation section. It makes
-small editions more convenient.
-
-Of course, this can be used customely in any other view. Here come
-some explanation about its capabilities and instructions on the way to
-use it.
-
-Using `reledit`
-***************
-
-Let's start again with a simple example:
-
-.. sourcecode:: python
-
- class Company(EntityType):
- name = String(required=True, unique=True)
- boss = SubjectRelation('Person', cardinality='1*')
- status = SubjectRelation('File', cardinality='?*', composite='subject')
-
-In some view code we might want to show these attributes/relations and
-allow the user to edit each of them in turn without having to leave
-the current page. We would write code as below:
-
-.. sourcecode:: python
-
- company.view('reledit', rtype='name', default_value='<name>') # editable name attribute
- company.view('reledit', rtype='boss') # editable boss relation
- company.view('reledit', rtype='status') # editable attribute-like relation
-
-If one wanted to edit the company from a boss's point of view, one
-would have to indicate the proper relation's role. By default the role
-is `subject`.
-
-.. sourcecode:: python
-
- person.view('reledit', rtype='boss', role='object')
-
-Each of these will provide with a different editing widget. The `name`
-attribute will obviously get a text input field. The `boss` relation
-will be edited through a selection box, allowing to pick another
-`Person` as boss. The `status` relation, given that it defines Company
-as a composite entity with one file inside, will provide additional actions
-
-* to `add` a `File` when there is one
-* to `delete` the `File` (if the cardinality allows it)
-
-Moreover, editing the relation or using the `add` action leads to an
-embedded edition/creation form allowing edition of the target entity
-(which is `File` in our example) instead of merely allowing to choose
-amongst existing files.
-
-The `reledit_ctrl` rtag
-***********************
-
-The behaviour of reledited attributes/relations can be finely
-controlled using the reledit_ctrl rtag, defined in
-:mod:`cubicweb.web.views.uicfg`.
-
-This rtag provides four control variables:
-
-* ``default_value``: alternative default value
- The default value is what is shown when there is no value.
-* ``reload``: boolean, eid (to reload to) or function taking subject
- and returning bool/eid This is useful when editing a relation (or
- attribute) that impacts the url or another parts of the current
- displayed page. Defaults to false.
-* ``rvid``: alternative view id (as str) for relation or composite
- edition Default is 'incontext' or 'csv' depending on the
- cardinality. They can also be statically changed by subclassing
- ClickAndEditFormView and redefining _one_rvid (resp. _many_rvid).
-* ``edit_target``: 'rtype' (to edit the relation) or 'related' (to
- edit the related entity) This controls whether to edit the relation
- or the target entity of the relation. Currently only one-to-one
- relations support target entity edition. By default, the 'related'
- option is taken whenever the relation is composite and one-to-one.
-
-Let's see how to use these controls.
-
-.. sourcecode:: python
-
- from logilab.mtconverter import xml_escape
- from cubicweb.web.views.uicfg import reledit_ctrl
- reledit_ctrl.tag_attribute(('Company', 'name'),
- {'reload': lambda x:x.eid,
- 'default_value': xml_escape(u'<logilab tastes better>')})
- reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'edit_target': 'related'})
-
-The `default_value` needs to be an xml escaped unicode string.
-
-The `edit_target` tag on the `boss` relation being set to `related` will
-ensure edition of the `Person` entity instead (using a standard
-automatic form) of the association of Company and Person.
-
-Finally, the `reload` key accepts either a boolean, an eid or an
-unicode string representing an url. If an eid is provided, it will be
-internally transformed into an url. The eid/url case helps when one
-needs to reload and the current url is inappropriate. A common case is
-edition of a key attribute, which is part of the current url. If one
-user changed the Company's name from `lozilab` to `logilab`, reloading
-on http://myapp/company/lozilab would fail. Providing the entity's
-eid, then, forces to reload on something like http://myapp/company/42,
-which always work.
-
-
-Disable `reledit`
-*****************
-
-By default, `reledit` is available on attributes and relations displayed in
-the 'attribute' section of the default primary view. If you want to disable
-it for some attribute or relation, you have use `uicfg`:
-
-.. sourcecode:: python
-
- from cubicweb.web.views.uicfg import primaryview_display_ctrl as _pvdc
- _pvdc.tag_attribute(('Company', 'name'), {'vid': 'incontext'})
-
-To deactivate it everywhere it's used automatically, you may use the code snippet
-below somewhere in your cube's views:
-
-.. sourcecode:: python
-
- from cubicweb.web.views import reledit
-
- class DeactivatedAutoClickAndEditFormView(reledit.AutoClickAndEditFormView):
- def _should_edit_attribute(self, rschema):
- return False
-
- def _should_edit_attribute(self, rschema, role):
- return False
-
- def registration_callback(vreg):
- vreg.register_and_replace(DeactivatedAutoClickAndEditFormView,
- reledit.AutoClickAndEditFormView)
-
-
--- a/doc/book/en/devweb/views/startup.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-Startup views
--------------
-
-Startup views are views requiring no context, from which you usually start
-browsing (for instance the index page). The usual selectors are
-:class:`~cubicweb.predicates.none_rset` or :class:`~logilab.common.registry.yes`.
-
-You'll find here a description of startup views provided by the framework.
-
-.. automodule:: cubicweb.web.views.startup
-
-
-Other startup views:
-
-*schema*
- A view dedicated to the display of the schema of the instance
-
-.. XXX to be continued
\ No newline at end of file
--- a/doc/book/en/devweb/views/table.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-Table views
------------
-
-.. automodule:: cubicweb.web.views.tableview
-
-Example
-```````
-
-Let us take an example from the timesheet cube:
-
-.. sourcecode:: python
-
- class ActivityResourcesTable(EntityView):
- __regid__ = 'activity.resources.table'
- __select__ = is_instance('Activity')
-
- def call(self, showresource=True):
- eids = ','.join(str(row[0]) for row in self.cw_rset)
- rql = ('Any R,D,DUR,WO,DESCR,S,A, SN,RT,WT ORDERBY D DESC '
- 'WHERE '
- ' A is Activity, A done_by R, R title RT, '
- ' A diem D, A duration DUR, '
- ' A done_for WO, WO title WT, '
- ' A description DESCR, A in_state S, S name SN, '
- ' A eid IN (%s)' % eids)
- rset = self._cw.execute(rql)
- self.wview('resource.table', rset, 'null')
-
- class ResourcesTable(RsetTableView):
- __regid__ = 'resource.table'
- # notice you may wish a stricter selector to check rql's shape
- __select__ = is_instance('Resource')
- # my table headers
- headers = ['Resource', 'diem', 'duration', 'workpackage', 'description', 'state']
- # I want a table where attributes are editable (reledit inside)
- finalvid = 'editable-final'
-
- cellvids = {3: 'editable-final'}
- # display facets and actions with a menu
- layout_args = {'display_filter': 'top',
- 'add_view_actions': None}
-
-To obtain an editable table, you may specify the 'editable-table' view identifier
-using some of `cellvids`, `finalvid` or `nonfinalvid`.
-
-The previous example results in:
-
-.. image:: ../../images/views-table-shadow.png
-
-In order to activate table filter mechanism, the `display_filter` option is given
-as a layout argument. A small arrow will be displayed at the table's top right
-corner. Clicking on `show filter form` action, will display the filter form as
-below:
-
-.. image:: ../../images/views-table-filter-shadow.png
-
-By the same way, you can display additional actions for the selected entities
-by setting `add_view_actions` layout option to `True`. This will add actions
-returned by the view's :meth:`~cubicweb.web.views.tableview.TableMixIn.table_actions`.
-
-You can notice that all columns of the result set are not displayed. This is
-because of given `headers`, implying to display only columns from 0 to
-len(headers).
-
-Also Notice that the `ResourcesTable` view relies on a particular rql shape
-(which is not ensured by the way, the only checked thing is that the result set
-contains instance of the `Resource` type). That usually implies that you can't
-use this view for user specific queries (e.g. generated by facets or typed
-manually).
-
-
-So another option would be to write this view using
-:class:`~cubicweb.web.views.tableview.EntityTableView`, as below.
-
-.. sourcecode:: python
-
- class ResourcesTable(EntityTableView):
- __regid__ = 'resource.table'
- __select__ = is_instance('Resource')
- # table columns definition
- columns = ['resource', 'diem', 'duration', 'workpackage', 'description', 'in_state']
- # I want a table where attributes are editable (reledit inside)
- finalvid = 'editable-final'
- # display facets and actions with a menu
- layout_args = {'display_filter': 'top',
- 'add_view_actions': None}
-
- def workpackage_cell(entity):
- activity = entity.reverse_done_in[0]
- activity.view('reledit', rtype='done_for', role='subject', w=w)
- def workpackage_sortvalue(entity):
- activity = entity.reverse_done_in[0]
- return activity.done_for[0].sortvalue()
-
- column_renderers = {
- 'resource': MainEntityColRenderer(),
- 'workpackage': EntityTableColRenderer(
- header='Workpackage',
- renderfunc=worpackage_cell,
- sortfunc=worpackage_sortvalue,),
- 'in_state': EntityTableColRenderer(
- renderfunc=lambda w,x: w(x.cw_adapt_to('IWorkflowable').printable_state),
- sortfunc=lambda x: x.cw_adapt_to('IWorkflowable').printable_state),
- }
-
-Notice the following point:
-
-* `cell_<column>(w, entity)` will be searched for rendering the content of a
- cell. If not found, `column` is expected to be an attribute of `entity`.
-
-* `cell_sortvalue_<column>(entity)` should return a typed value to use for
- javascript sorting or None for not sortable columns (the default).
-
-* The :func:`etable_entity_sortvalue` decorator will set a 'sortvalue' function
- for the column containing the main entity (the one given as argument to all
- methods), which will call `entity.sortvalue()`.
-
-* You can set a column header using the :func:`etable_header_title` decorator.
- This header will be translated. If it's not an already existing msgid, think
- to mark it using `_()` (the example supposes headers are schema defined msgid).
-
-
-Pro/cons of each approach
-`````````````````````````
-:class:`EntityTableView` and :class:`RsetableView` provides basically the same
-set of features, though they don't share the same properties. Let's try to sum
-up pro and cons of each class.
-
-* `EntityTableView` view is:
-
- - more verbose, but usually easier to understand
-
- - easily extended (easy to add/remove columns for instance)
-
- - doesn't rely on a particular rset shape. Simply give it a title and will be
- listed in the 'possible views' box if any.
-
-* `RsetTableView` view is:
-
- - hard to beat to display barely a result set, or for cases where some of
- `headers`, `displaycols` or `cellvids` could be defined to enhance the table
- while you don't care about e.g. pagination or facets.
-
- - hardly extensible, as you usually have to change places where the view is
- called to modify the RQL (hence the view's result set shape).
--- a/doc/book/en/devweb/views/urlpublish.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,137 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-URL publishing
---------------
-
-(:mod:`cubicweb.web.views.urlpublishing`)
-
-.. automodule:: cubicweb.web.views.urlpublishing
-
-.. autoclass:: cubicweb.web.views.urlpublishing.URLPublisherComponent
- :members:
-
-
-You can write your own *URLPathEvaluator* class to handle custom paths.
-For instance, if you want */my-card-id* to redirect to the corresponding
-card's primary view, you would write:
-
-.. sourcecode:: python
-
- class CardWikiidEvaluator(URLPathEvaluator):
- priority = 3 # make it be evaluated *before* RestPathEvaluator
-
- def evaluate_path(self, req, segments):
- if len(segments) != 1:
- raise PathDontMatch()
- rset = req.execute('Any C WHERE C wikiid %(w)s',
- {'w': segments[0]})
- if len(rset) == 0:
- # Raise NotFound if no card is found
- raise PathDontMatch()
- return None, rset
-
-On the other hand, you can also deactivate some of the standard
-evaluators in your final application. The only thing you have to
-do is to unregister them, for instance in a *registration_callback*
-in your cube:
-
-.. sourcecode:: python
-
- def registration_callback(vreg):
- vreg.unregister(RestPathEvaluator)
-
-You can even replace the :class:`cubicweb.web.views.urlpublishing.URLPublisherComponent`
-class if you want to customize the whole toolchain process or if you want
-to plug into an early enough extension point to control your request
-parameters:
-
-.. sourcecode:: python
-
- class SanitizerPublisherComponent(URLPublisherComponent):
- """override default publisher component to explicitly ignore
- unauthorized request parameters in anonymous mode.
- """
- unauthorized_form_params = ('rql', 'vid', '__login', '__password')
-
- def process(self, req, path):
- if req.session.anonymous_session:
- self._remove_unauthorized_params(req)
- return super(SanitizerPublisherComponent, self).process(req, path)
-
- def _remove_unauthorized_params(self, req):
- for param in req.form.keys():
- if param in self.unauthorized_form_params:
- req.form.pop(param)
-
-
- def registration_callback(vreg):
- vreg.register_and_replace(SanitizerPublisherComponent, URLPublisherComponent)
-
-
-.. autoclass:: cubicweb.web.views.urlpublishing.RawPathEvaluator
-.. autoclass:: cubicweb.web.views.urlpublishing.EidPathEvaluator
-.. autoclass:: cubicweb.web.views.urlpublishing.URLRewriteEvaluator
-.. autoclass:: cubicweb.web.views.urlpublishing.RestPathEvaluator
-.. autoclass:: cubicweb.web.views.urlpublishing.ActionPathEvaluator
-
-URL rewriting
--------------
-
-(:mod:`cubicweb.web.views.urlrewrite`)
-
-.. autoclass:: cubicweb.web.views.urlrewrite.URLRewriter
- :members:
-
-.. autoclass:: cubicweb.web.views.urlrewrite.SimpleReqRewriter
- :members:
-
-.. autoclass:: cubicweb.web.views.urlrewrite.SchemaBasedRewriter
- :members:
-
-
-``SimpleReqRewriter`` is enough for a certain number of simple cases. If it is not sufficient, ``SchemaBasedRewriter`` allows to do more elaborate things.
-
-Here is an example of ``SimpleReqRewriter`` usage with plain string:
-
-.. sourcecode:: python
-
- from cubicweb.web.views.urlrewrite import SimpleReqRewriter
- class TrackerSimpleReqRewriter(SimpleReqRewriter):
- rules = [
- ('/versions', dict(vid='versionsinfo')),
- ]
-
-When the url is `<base_url>/versions`, the view with the __regid__ `versionsinfo` is displayed.
-
-Here is an example of ``SimpleReqRewriter`` usage with regular expressions:
-
-.. sourcecode:: python
-
- from cubicweb.web.views.urlrewrite import (
- SimpleReqRewriter, rgx)
-
- class BlogReqRewriter(SimpleReqRewriter):
- rules = [
- (rgx('/blogentry/([a-z_]+)\.rss'),
- dict(rql=('Any X ORDERBY CD DESC LIMIT 20 WHERE X is BlogEntry,'
- 'X creation_date CD, X created_by U, '
- 'U login "%(user)s"'
- % {'user': r'\1'}), vid='rss'))
- ]
-
-When a url matches the regular expression, the view with the __regid__
-`rss` which match the result set is displayed.
-
-Here is an example of ``SchemaBasedRewriter`` usage:
-
-.. sourcecode:: python
-
- from cubicweb.web.views.urlrewrite import (
- SchemaBasedRewriter, rgx, build_rset)
-
- class TrackerURLRewriter(SchemaBasedRewriter):
- rules = [
- (rgx('/project/([^/]+)/([^/]+)/tests'),
- build_rset(rql='Version X WHERE X version_of P, P name %(project)s, X num %(num)s',
- rgxgroups=[('project', 1), ('num', 2)], vid='versiontests')),
- ]
--- a/doc/book/en/devweb/views/views.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-
-.. _Views:
-
-Principles
-----------
-
-We'll start with a description of the interface providing a basic
-understanding of the available classes and methods, then detail the
-view selection principle.
-
-A `View` is an object responsible for the rendering of data from the
-model into an end-user consummable form. They typically churn out an
-XHTML stream, but there are views concerned with email other non-html
-outputs.
-
-.. _views_base_class:
-
-Discovering possible views
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-It is possible to configure the web user interface to have a left box
-showing all the views than can be applied to the current result set.
-
-To enable this, click on your login at the top right corner. Chose
-"user preferences", then "boxes", then "possible views box" and check
-"visible = yes" before validating your changes.
-
-The views listed there we either not selected because of a lower
-score, or they were deliberately excluded by the main template logic.
-
-
-Basic class for views
-~~~~~~~~~~~~~~~~~~~~~
-
-Class :class:`~cubicweb.view.View`
-``````````````````````````````````
-
-.. autoclass:: cubicweb.view.View
-
-The basic interface for views is as follows (remember that the result
-set has a tabular structure with rows and columns, hence cells):
-
-* `render(**context)`, render the view by calling `call` or
- `cell_call` depending on the context
-
-* `call(**kwargs)`, call the view for a complete result set or null
- (the default implementation calls `cell_call()` on each cell of the
- result set)
-
-* `cell_call(row, col, **kwargs)`, call the view for a given cell of a
- result set (`row` and `col` being integers used to access the cell)
-
-* `url()`, returns the URL enabling us to get the view with the current
- result set
-
-* `wview(__vid, rset, __fallback_vid=None, **kwargs)`, call the view of
- identifier `__vid` on the given result set. It is possible to give a
- fallback view identifier that will be used if the requested view is
- not applicable to the result set.
-
-* `html_headers()`, returns a list of HTML headers to be set by the
- main template
-
-* `page_title()`, returns the title to use in the HTML header `title`
-
-Other basic view classes
-````````````````````````
-Here are some of the subclasses of :class:`~cubicweb.view.View` defined in :mod:`cubicweb.view`
-that are more concrete as they relate to data rendering within the application:
-
-.. autoclass:: cubicweb.view.EntityView
-.. autoclass:: cubicweb.view.StartupView
-.. autoclass:: cubicweb.view.EntityStartupView
-.. autoclass:: cubicweb.view.AnyRsetView
-
-Examples of views class
-```````````````````````
-
-- Using `templatable`, `content_type` and HTTP cache configuration
-
-.. sourcecode:: python
-
- class RSSView(XMLView):
- __regid__ = 'rss'
- title = _('rss')
- templatable = False
- content_type = 'text/xml'
- http_cache_manager = MaxAgeHTTPCacheManager
- cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
-
-
-- Using a custom selector
-
-.. sourcecode:: python
-
- class SearchForAssociationView(EntityView):
- """view called by the edition view when the user asks
- to search for something to link to the edited eid
- """
- __regid__ = 'search-associate'
- title = _('search for association')
- __select__ = one_line_rset() & match_search_state('linksearch') & is_instance('Any')
-
-
-XML views, binaries views...
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-For views generating other formats than HTML (an image generated dynamically
-for example), and which can not simply be included in the HTML page generated
-by the main template (see above), you have to:
-
-* set the attribute `templatable` of the class to `False`
-* set, through the attribute `content_type` of the class, the MIME
- type generated by the view to `application/octet-stream` or any
- relevant and more specialised mime type
-
-For views dedicated to binary content creation (like dynamically generated
-images), we have to set the attribute `binary` of the class to `True` (which
-implies that `templatable == False`, so that the attribute `w` of the view could be
-replaced by a binary flow instead of unicode).
--- a/doc/book/en/devweb/views/wdoc.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Online documentation system
-===========================
-
-.. automodule:: cubicweb.web.views.wdoc
-
-Help views
-----------
-.. autoclass:: cubicweb.web.views.wdoc.InlineHelpView
-
-Actions
--------
-.. autoclass:: cubicweb.web.views.wdoc.HelpAction
-.. autoclass:: cubicweb.web.views.wdoc.AboutAction
--- a/doc/book/en/devweb/views/xmlrss.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-.. _XmlAndRss:
-
-XML and RSS views
------------------
-
-(:mod:`cubicweb.web.views.xmlrss`)
-
-Overview
-+++++++++
-
-*rss*
- Creates a RSS/XML view and call the view `rssitem` for each entity of
- the result set.
-
-*rssitem*
- Create a RSS/XML view for each entity based on the results of the dublin core
- methods of the entity (`dc_*`)
-
-RSS Channel Example
-++++++++++++++++++++
-
-Assuming you have several blog entries, click on the title of the
-search box in the left column. A larger search box should appear. Enter:
-
-.. sourcecode:: sql
-
- Any X ORDERBY D WHERE X is BlogEntry, X creation_date D
-
-and you get a list of blog entries.
-
-Click on your login at the top right corner. Chose "user preferences",
-then "boxes", then "possible views box" and check "visible = yes"
-before validating your changes.
-
-Enter the same query in the search box and you will see the same list,
-plus a box titled "possible views" in the left column. Click on
-"entityview", then "RSS".
-
-You just applied the "RSS" view to the RQL selection you requested.
-
-That's it, you have a RSS channel for your blog.
-
-Try again with:
-
-.. sourcecode:: sql
-
- Any X ORDERBY D WHERE X is BlogEntry, X creation_date D,
- X entry_of B, B title "MyLife"
-
-Another RSS channel, but a bit more focused.
-
-A last one for the road:
-
-.. sourcecode:: sql
-
- Any C ORDERBY D WHERE C is Comment, C creation_date D LIMIT 15
-
-displayed with the RSS view, that's a channel for the last fifteen
-comments posted.
-
-[WRITE ME]
-
-* show that the RSS view can be used to display an ordered selection
- of blog entries, thus providing a RSS channel
-
-* show that a different selection (by category) means a different channel
Binary file doc/book/en/images/03-transitions-view_en.png has changed
Binary file doc/book/en/images/archi_globale.png has changed
Binary file doc/book/en/images/archi_globale_en.png has changed
Binary file doc/book/en/images/breadcrumbs_header.png has changed
Binary file doc/book/en/images/facet_date_range.png has changed
Binary file doc/book/en/images/facet_has_image.png has changed
Binary file doc/book/en/images/facet_overview.png has changed
Binary file doc/book/en/images/facet_range.png has changed
Binary file doc/book/en/images/lax-book_00-login_en.png has changed
Binary file doc/book/en/images/lax-book_01-start_en.png has changed
Binary file doc/book/en/images/lax-book_02-cookie-values_en.png has changed
Binary file doc/book/en/images/lax-book_02-create-blog_en.png has changed
Binary file doc/book/en/images/lax-book_03-list-one-blog_en.png has changed
Binary file doc/book/en/images/lax-book_03-site-config-panel_en.png has changed
Binary file doc/book/en/images/lax-book_03-state-submitted_en.png has changed
Binary file doc/book/en/images/lax-book_03-transitions-view_en.png has changed
Binary file doc/book/en/images/lax-book_04-detail-one-blog_en.png has changed
Binary file doc/book/en/images/lax-book_05-list-two-blog_en.png has changed
Binary file doc/book/en/images/lax-book_06-add-relation-entryof_en.png has changed
Binary file doc/book/en/images/lax-book_06-main-template-logo_en.png has changed
Binary file doc/book/en/images/lax-book_07-detail-one-blogentry_en.png has changed
Binary file doc/book/en/images/lax-book_08-schema_en.png has changed
Binary file doc/book/en/images/lax-book_09-new-view-blogentry_en.png has changed
Binary file doc/book/en/images/lax-book_10-blog-with-two-entries_en.png has changed
Binary file doc/book/en/images/main_template.png has changed
--- a/doc/book/en/images/main_template.svg Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,207 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="1036.6421"
- height="845.07812"
- id="svg2"
- sodipodi:version="0.32"
- inkscape:version="0.46"
- sodipodi:docname="main_template.svg"
- inkscape:output_extension="org.inkscape.output.svg.inkscape"
- version="1.0"
- inkscape:export-filename="/home/auc/cw/doc/book/en/images/main_template.png"
- inkscape:export-xdpi="60.659016"
- inkscape:export-ydpi="60.659016">
- <defs
- id="defs4">
- <inkscape:perspective
- sodipodi:type="inkscape:persp3d"
- inkscape:vp_x="0 : 526.18109 : 1"
- inkscape:vp_y="0 : 1000 : 0"
- inkscape:vp_z="744.09448 : 526.18109 : 1"
- inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
- id="perspective10" />
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0.0"
- inkscape:pageshadow="2"
- inkscape:zoom="0.80355603"
- inkscape:cx="510.91495"
- inkscape:cy="422.53906"
- inkscape:document-units="px"
- inkscape:current-layer="layer1"
- showgrid="false"
- inkscape:window-width="925"
- inkscape:window-height="1168"
- inkscape:window-x="0"
- inkscape:window-y="0"
- inkscape:snap-bbox="true" />
- <metadata
- id="metadata7">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="Calque 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(162.2968,90.697922)">
- <rect
- style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect2439"
- width="854.37006"
- height="698.2019"
- x="20.307629"
- y="-20.575344" />
- <rect
- style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3301"
- width="816.3457"
- height="508.15628"
- x="31.751091"
- y="96.33345" />
- <g
- id="g3220"
- transform="matrix(1.0035394,0,0,1,0.5745006,0)">
- <rect
- y="-89.447922"
- x="-161.0468"
- height="55.714287"
- width="1031.1713"
- id="rect3240"
- style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.50000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
- <text
- id="text3264"
- y="-51.771908"
- x="757.85767"
- style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
- xml:space="preserve"><tspan
- id="tspan3266"
- y="-51.771908"
- x="757.85767"
- sodipodi:role="line">header</tspan></text>
- </g>
- <rect
- style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3270"
- width="167.87744"
- height="707.71222"
- x="-160.02441"
- y="-24.671618" />
- <g
- id="g2434"
- transform="matrix(0.975467,0,0,1,0.6942419,-3.6587365)">
- <rect
- y="35.365849"
- x="29.548275"
- height="55.714287"
- width="842.59979"
- id="rect3279"
- style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
- <text
- id="text3281"
- y="72.885193"
- x="681.65283"
- style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
- xml:space="preserve"><tspan
- id="tspan3283"
- y="72.885193"
- x="681.65283"
- sodipodi:role="line">contentheader</tspan></text>
- </g>
- <g
- id="g3170"
- transform="matrix(1.0023324,0,0,1,-2.0421673,-10.976211)">
- <rect
- y="698.6355"
- x="-158.28485"
- height="55.714287"
- width="1032.5997"
- id="rect3285"
- style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
- <text
- id="text3287"
- y="736.52045"
- x="770.28204"
- style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
- xml:space="preserve"><tspan
- id="tspan3289"
- y="736.52045"
- x="770.28204"
- sodipodi:role="line">footer</tspan></text>
- </g>
- <g
- id="g3211" />
- <g
- id="g3215"
- transform="matrix(0.9712065,0,0,1,0.7659296,-17.074106)">
- <rect
- style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3291"
- width="844.62012"
- height="55.714287"
- x="27.850754"
- y="629.88562" />
- <text
- id="text3293"
- y="666.60339"
- x="692.85773"
- style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
- xml:space="preserve"><tspan
- id="tspan3295"
- y="666.60339"
- x="692.85773"
- sodipodi:role="line">contentfooter</tspan></text>
- </g>
- <text
- xml:space="preserve"
- style="font-size:23.38711166px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
- x="-143.67273"
- y="20.58094"
- id="text3297"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan2432"
- x="-143.67273"
- y="20.58094">left column</tspan></text>
- <text
- transform="scale(0.9876573,1.0124969)"
- id="text3175"
- y="12.071429"
- x="721.0575"
- style="font-size:23.09845161px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
- xml:space="preserve"><tspan
- id="tspan3177"
- y="12.071429"
- x="721.0575"
- sodipodi:role="line">contentcol</tspan></text>
- <text
- transform="scale(0.9876573,1.0124969)"
- id="text3179"
- y="126.27104"
- x="701.45959"
- style="font-size:23.09845161px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
- xml:space="preserve"><tspan
- id="tspan3181"
- y="126.27104"
- x="701.45959"
- sodipodi:role="line">contentmain</tspan></text>
- </g>
-</svg>
Binary file doc/book/en/images/main_template_layout.png has changed
Binary file doc/book/en/images/primaryview_template.png has changed
--- a/doc/book/en/images/primaryview_template.svg Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,285 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="1036.6421"
- height="845.07812"
- id="svg2"
- sodipodi:version="0.32"
- inkscape:version="0.46"
- sodipodi:docname="primaryview_template.svg"
- inkscape:output_extension="org.inkscape.output.svg.inkscape"
- version="1.0"
- inkscape:export-filename="/home/steph/local/fcubicweb/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="43.451603"
- inkscape:export-ydpi="43.451603">
- <defs
- id="defs4">
- <inkscape:perspective
- sodipodi:type="inkscape:persp3d"
- inkscape:vp_x="0 : 526.18109 : 1"
- inkscape:vp_y="0 : 1000 : 0"
- inkscape:vp_z="744.09448 : 526.18109 : 1"
- inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
- id="perspective10" />
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0.0"
- inkscape:pageshadow="2"
- inkscape:zoom="0.9357135"
- inkscape:cx="518.32104"
- inkscape:cy="337.0428"
- inkscape:document-units="px"
- inkscape:current-layer="layer1"
- showgrid="false"
- inkscape:window-width="1307"
- inkscape:window-height="1168"
- inkscape:window-x="0"
- inkscape:window-y="0" />
- <metadata
- id="metadata7">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="Calque 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(162.2968,90.697922)">
- <g
- id="g3869"
- transform="matrix(1,0,0,1.0373644,0,-72.039777)"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449">
- <rect
- y="-15.840891"
- x="-159.08963"
- height="770.11017"
- width="1033.0049"
- id="rect3301"
- style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.90144825;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
- <text
- id="text3865"
- y="19.784882"
- x="-150.07172"
- style="font-size:28.67479324px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
- xml:space="preserve"><tspan
- id="tspan3867"
- y="19.784882"
- x="-150.07172"
- sodipodi:role="line">contentmain</tspan></text>
- </g>
- <rect
- style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.45654476;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect2383"
- width="772.32111"
- height="43.888428"
- x="-131.1837"
- y="86.559296"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449" />
- <text
- xml:space="preserve"
- style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%"
- x="-122.69418"
- y="115.50363"
- id="text2385"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- x="-122.69418"
- y="115.50363"
- id="tspan3163">navcontenttop</tspan></text>
- <rect
- style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:3.06523442;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3167"
- width="770.26868"
- height="203.16078"
- x="-125.88269"
- y="172.90417"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449" />
- <text
- xml:space="preserve"
- style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
- x="348.26724"
- y="205.34305"
- id="text3169"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- x="348.26724"
- y="205.34305"
- id="tspan3171">render_entity_attributes()</tspan></text>
- <rect
- style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:3.06523442;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3173"
- width="769.93549"
- height="237.84663"
- x="-125.03326"
- y="391.32156"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449" />
- <text
- xml:space="preserve"
- style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
- x="360.99954"
- y="428.38055"
- id="text3175"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- x="360.99954"
- y="428.38055"
- id="tspan3177">render_entity_relations()</tspan></text>
- <rect
- style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:2.15903592;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3185"
- width="178.93939"
- height="612.36584"
- x="667.10443"
- y="84.64225"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449" />
- <text
- xml:space="preserve"
- style="font-size:22px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%"
- x="105.32364"
- y="-810.65997"
- id="text3187"
- transform="matrix(0,1,-1,0,0,0)"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- id="tspan2408">render_side_boxes()</tspan></text>
- <rect
- style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:3.0652349;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3191"
- width="771.97766"
- height="55.647793"
- x="-127.80586"
- y="642.0293"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449" />
- <text
- xml:space="preserve"
- style="font-size:16px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
- x="-121.22153"
- y="674.1748"
- id="text3181"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- x="-121.22153"
- y="674.1748"
- id="tspan3183">navcontentbottom</tspan></text>
- <rect
- style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.68198514;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3881"
- width="986.90503"
- height="45.800392"
- x="-128.34428"
- y="-31.574066"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449" />
- <text
- xml:space="preserve"
- style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
- x="355.60541"
- y="-2.7424495"
- id="text3883"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- x="355.60541"
- y="-2.7424495"
- id="tspan3885">render_entity_toolbox(), render_entity_title()</tspan></text>
- <rect
- style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.68198514;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect3890"
- width="986.90503"
- height="45.800392"
- x="-128.87863"
- y="19.723684"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449" />
- <text
- xml:space="preserve"
- style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
- x="565.71027"
- y="50.135612"
- id="text3892"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- x="565.71027"
- y="50.135612"
- id="tspan3894">render_entity_summary()</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
- x="87.154541"
- y="114.2578"
- id="text3899"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- id="tspan3903"
- x="87.154541"
- y="114.2578">content_navigation_components('navcontenttop')</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
- x="88.46772"
- y="675.71582"
- id="text2410"
- sodipodi:linespacing="125%"
- inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
- inkscape:export-xdpi="60.912449"
- inkscape:export-ydpi="60.912449"><tspan
- sodipodi:role="line"
- id="tspan2412"
- x="88.46772"
- y="675.71582">content_navigation_components('navcontenttop')</tspan></text>
- </g>
-</svg>
Binary file doc/book/en/images/request_session.png has changed
--- a/doc/book/en/images/request_session.svg Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,206 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="85.960938"
- height="12.382812"
- id="svg2"
- version="1.1"
- inkscape:version="0.48.3.1 r9886"
- sodipodi:docname="request_session.svg">
- <defs
- id="defs4">
- <marker
- inkscape:stockid="Arrow1Lend"
- orient="auto"
- refY="0.0"
- refX="0.0"
- id="Arrow1Lend"
- style="overflow:visible;">
- <path
- id="path3822"
- d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
- style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;marker-start:none;"
- transform="scale(0.8) rotate(180) translate(12.5,0)" />
- </marker>
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0.0"
- inkscape:pageshadow="2"
- inkscape:zoom="0.98994949"
- inkscape:cx="25.928992"
- inkscape:cy="-185.87004"
- inkscape:document-units="px"
- inkscape:current-layer="layer1"
- showgrid="false"
- fit-margin-top="0"
- fit-margin-left="0"
- fit-margin-right="0"
- fit-margin-bottom="0"
- inkscape:window-width="958"
- inkscape:window-height="1160"
- inkscape:window-x="0"
- inkscape:window-y="38"
- inkscape:window-maximized="0"
- inkscape:snap-global="true" />
- <metadata
- id="metadata7">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(-263.52249,-495.73373)">
- <rect
- style="fill:#ffffff;stroke:#000000;stroke-width:0.92460138;stroke-opacity:1"
- id="rect3773"
- width="214.15233"
- height="184.80336"
- x="57.578697"
- y="366.01306" />
- <rect
- id="rect2985"
- width="216.86372"
- height="183.54575"
- x="348.50262"
- y="367.78079"
- style="fill:#ffffff;stroke:#000000;stroke-width:0.55298227;stroke-opacity:1" />
- <text
- xml:space="preserve"
- style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
- x="376.7869"
- y="399.80365"
- id="text3755"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan3757"
- x="376.7869"
- y="399.80365">Repository</tspan></text>
- <rect
- style="fill:#ffffff;stroke:#000000;stroke-opacity:1"
- id="rect3759"
- width="144.45181"
- height="104.04572"
- x="237.38585"
- y="423.03714" />
- <text
- xml:space="preserve"
- style="font-size:24px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
- x="262.63968"
- y="470.51431"
- id="text3761"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan3763"
- x="262.63968"
- y="470.51431">REPOAPI</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
- x="262.63968"
- y="507.88998"
- id="text3765"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan3767"
- x="262.63968"
- y="507.88998">connection</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
- x="419.21332"
- y="509.91025"
- id="text3769"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan3771"
- x="419.21332"
- y="509.91025">session</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
- x="102.02541"
- y="397.78333"
- id="text3775"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan3777"
- x="102.02541"
- y="397.78333">Client</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
- x="116.16754"
- y="507.88995"
- id="text3779"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan3781"
- x="116.16754"
- y="507.88995">request</tspan></text>
- <text
- xml:space="preserve"
- style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
- x="361.50729"
- y="585.89832"
- id="text3802"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan3804"
- x="361.50729"
- y="585.89832">database </tspan><tspan
- sodipodi:role="line"
- x="361.50729"
- y="605.89832"
- id="tspan3806">connection</tspan></text>
- <rect
- style="fill:#ffffff;stroke:#000000;stroke-width:1.48014534;stroke-opacity:1"
- id="rect3808"
- width="192.09367"
- height="58.095726"
- x="365.79443"
- y="621.50018" />
- <text
- xml:space="preserve"
- style="font-size:36px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
- x="369.5885"
- y="662.66992"
- id="text3810"
- sodipodi:linespacing="125%"><tspan
- sodipodi:role="line"
- id="tspan3812"
- x="369.5885"
- y="662.66992">Database</tspan></text>
- <path
- style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:none"
- d="M 197.57252,125.76645 195.76971,55.592808"
- id="path4260"
- inkscape:connector-type="polyline"
- inkscape:connector-curvature="3"
- inkscape:connection-start="#rect3808"
- inkscape:connection-start-point="d4"
- inkscape:connection-end="#rect2985"
- inkscape:connection-end-point="d4"
- transform="translate(263.52249,495.73373)" />
- </g>
-</svg>
Binary file doc/book/en/images/server-class-diagram.png has changed
Binary file doc/book/en/images/tutos-base_blog-form_en.png has changed
Binary file doc/book/en/images/tutos-base_blog-primary-after-post-creation_en.png has changed
Binary file doc/book/en/images/tutos-base_blog-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_blogs-list_en.png has changed
Binary file doc/book/en/images/tutos-base_form-generic-relations_en.png has changed
Binary file doc/book/en/images/tutos-base_index_en.png has changed
Binary file doc/book/en/images/tutos-base_login-form_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-community-custom-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-community-default-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-community-taggable-primary_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-custom-footer_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-schema_en.png has changed
Binary file doc/book/en/images/tutos-base_myblog-siteinfo_en.png has changed
Binary file doc/book/en/images/tutos-base_schema_en.png has changed
Binary file doc/book/en/images/tutos-base_siteconfig_en.png has changed
Binary file doc/book/en/images/tutos-base_user-menu_en.png has changed
Binary file doc/book/en/images/tutos-photowebsite_background-image.png has changed
Binary file doc/book/en/images/tutos-photowebsite_boxes.png has changed
Binary file doc/book/en/images/tutos-photowebsite_breadcrumbs.png has changed
Binary file doc/book/en/images/tutos-photowebsite_facets.png has changed
Binary file doc/book/en/images/tutos-photowebsite_grey-box.png has changed
Binary file doc/book/en/images/tutos-photowebsite_index-after.png has changed
Binary file doc/book/en/images/tutos-photowebsite_index-before.png has changed
Binary file doc/book/en/images/tutos-photowebsite_login-box.png has changed
Binary file doc/book/en/images/tutos-photowebsite_prevnext.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui1.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui2.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui3.png has changed
Binary file doc/book/en/images/undo_history-view_w600.png has changed
Binary file doc/book/en/images/undo_mesage_w600.png has changed
Binary file doc/book/en/images/undo_startup-link_w600.png has changed
Binary file doc/book/en/images/views-table-filter-shadow.png has changed
Binary file doc/book/en/images/views-table-filter.png has changed
Binary file doc/book/en/images/views-table-shadow.png has changed
Binary file doc/book/en/images/views-table.png has changed
--- a/doc/book/en/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _contents:
-
-=====================================================
-|cubicweb| - The Semantic Web is a construction game!
-=====================================================
-
-|cubicweb| is a semantic web application framework, licensed under the LGPL,
-that empowers developers to efficiently build web applications by reusing
-components (called `cubes`) and following the well known object-oriented design
-principles.
-
-Its main features are:
-
-* an engine driven by the explicit :ref:`data model
- <TutosBaseCustomizingTheApplicationDataModel>` of the application,
-
-* a query language named :ref:`RQL <RQL>` similar to W3C's SPARQL,
-
-* a :ref:`selection+view <TutosBaseCustomizingTheApplicationCustomViews>`
- mechanism for semi-automatic XHTML/XML/JSON/text generation,
-
-* a library of reusable :ref:`components <Cube>` (data model and views) that
- fulfill common needs,
-
-* the power and flexibility of the Python_ programming language,
-
-* the reliability of SQL databases, LDAP directories, Subversion and Mercurial
- for storage backends.
-
-Built since 2000 from an R&D effort still continued, supporting 100,000s of
-daily visits at some production sites, |cubicweb| is a proven end to end solution
-for semantic web application development that promotes quality, reusability and
-efficiency.
-
-The unbeliever will read the :ref:`Tutorials`.
-
-The hacker will join development at the forge_.
-
-The impatient developer will move right away to :ref:`SetUpEnv` then to :ref:`ConfigEnv`.
-
-The chatter lover will join the `jabber forum`_, the `mailing-list`_ and the blog_.
-
-.. _Logilab: http://www.logilab.fr/
-.. _forge: http://www.cubicweb.org/project/
-.. _Python: http://www.python.org/
-.. _`jabber forum`: http://www.logilab.org/blogentry/6718
-.. _`mailing-list`: http://lists.cubicweb.org/mailman/listinfo/cubicweb
-.. _blog: http://www.cubicweb.org/blog/1238
-
-.. toctree::
- :maxdepth: 2
-
- intro/index
- tutorials/index
-
-.. toctree::
- :maxdepth: 3
-
- devrepo/index
- devweb/index
-
-.. toctree::
- :maxdepth: 2
-
- admin/index
- additionnal_services/index
- annexes/index
-
-See also:
-
-* the :ref:`genindex`,
-* the :ref:`modindex`,
--- a/doc/book/en/intro/concepts.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,306 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Concepts:
-
-The Core Concepts of |cubicweb|
-===============================
-
-This section defines some terms and core concepts of the |cubicweb| framework. To
-avoid confusion while reading this book, take time to go through the following
-definitions and use this section as a reference during your reading.
-
-
-.. _Cube:
-
-Cubes
------
-
-A cube is a software component made of three parts: its data model
-(:mod:`schema`), its logic (:mod:`entities`) and its user interface
-(:mod:`views`).
-
-A cube can use other cubes as building blocks and assemble them to provide a
-whole with richer functionnalities than its parts. The cubes `cubicweb-blog`_ and
-`cubicweb-comment`_ could be used to make a cube named *myblog* with commentable
-blog entries.
-
-The `CubicWeb.org Forge`_ offers a large number of cubes developed by the community
-and available under a free software license.
-
-.. note::
-
- The command :command:`cubicweb-ctl list` displays the list of available cubes.
-
-.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
-.. _`cubicweb-blog`: http://www.cubicweb.org/project/cubicweb-blog
-.. _`cubicweb-comment`: http://www.cubicweb.org/project/cubicweb-comment
-
-
-.. _Instance:
-
-Instances
----------
-
-An instance is a runnable application installed on a computer and based on a
-cube.
-
-The instance directory contains the configuration files. Several instances can be
-created and based on the same cube. For exemple, several software forges can be
-set up on one computer system based on the `cubicweb-forge`_ cube.
-
-.. _`cubicweb-forge`: http://www.cubicweb.org/project/cubicweb-forge
-
-Instances can be of three different types: all-in-one, web engine or data
-repository. For applications that support high traffic, several web (front-end)
-and data (back-end) instances can be set-up to share the load.
-
-.. image:: ../images/archi_globale_en.png
-
-The command :command:`cubicweb-ctl list` also displays the list of instances
-installed on your system.
-
-.. note::
-
- The term application is used to refer to "something that should do something as
- a whole", eg more like a project and so can refer to an instance or to a cube,
- depending on the context. This book will try to use *application*, *cube* and
- *instance* as appropriate.
-
-
-.. _RepositoryIntro:
-
-Data Repository
----------------
-
-The data repository [1]_ encapsulates and groups an access to one or
-more data sources (including SQL databases, LDAP repositories, other
-|cubicweb| instance repositories, filesystems, Google AppEngine's
-DataStore, etc).
-
-All interactions with the repository are done using the `Relation Query Language`
-(:ref:`RQL`). The repository federates the data sources and hides them from the
-querier, which does not realize when a query spans several data sources
-and requires running sub-queries and merges to complete.
-
-Application logic can be mapped to data events happenning within the
-repository, like creation of entities, deletion of relations,
-etc. This is used for example to send email notifications when the
-state of an object changes. See :ref:`HookIntro` below.
-
-.. [1] not to be confused with a Mercurial repository or a Debian repository.
-.. _`Python Remote Objects`: http://pythonhosted.org/Pyro4/
-
-.. _WebEngineIntro:
-
-Web Engine
-----------
-
-The web engine replies to http requests and runs the user interface.
-
-By default the web engine provides a `CRUD`_ user interface based on
-the data model of the instance. Entities can be created, displayed,
-updated and deleted. As the default user interface is not very fancy,
-it is usually necessary to develop your own.
-
-It is common to run the web engine and the repository in the same
-process (see instances of type all-in-one above), but this is not a
-requirement. A repository can be set up to be accessed remotely using
-Pyro (`Python Remote Objects`_) and act as a standalone server, which
-can be directly accessed or also through a standalone web engine.
-
-.. _`CRUD`: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
-
-.. _SchemaIntro:
-
-Schema (Data Model)
--------------------
-
-The data model of a cube is described as an entity-relationship schema using a
-comprehensive language made of Python classes imported from the yams_ library.
-
-.. _yams: http://www.logilab.org/project/yams/
-
-An `entity type` defines a sequence of attributes. Attributes may be
-of the following types: `String`, `Int`, `Float`, `Boolean`, `Date`,
-`Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`.
-
-A `relation type` is used to define an oriented binary relation
-between entity types. The left-hand part of a relation is named the
-`subject` and the right-hand part is named the `object`.
-
-A `relation definition` is a triple (*subject entity type*, *relation type*, *object
-entity type*) associated with a set of properties such as cardinality,
-constraints, etc.
-
-Permissions can be set on entity types or relation definition to control who
-will be able to create, read, update or delete entities and relations. Permissions
-are granted to groups (to which users may belong) or using rql expressions (if the
-rql expression returns some results, the permission is granted).
-
-Some meta-data necessary to the system are added to the data model. That includes
-entities like users and groups, the entities used to store the data model
-itself and attributes like unique identifier, creation date, creator, etc.
-
-When you create a new |cubicweb| instance, the schema is stored in the database.
-When the cubes the instance is based on evolve, they may change their data model
-and provide migration scripts that will be executed when the administrator will
-run the upgrade process for the instance.
-
-
-.. _VRegistryIntro:
-
-Registries and application objects
-----------------------------------
-
-Application objects
-~~~~~~~~~~~~~~~~~~~
-
-Besides a few core functionalities, almost every feature of the framework is
-achieved by dynamic objects (`application objects` or `appobjects`) stored in a
-two-levels registry. Each object is affected to a registry with
-an identifier in this registry. You may have more than one object sharing an
-identifier in the same registry:
-
- object's `__registry__` : object's `__regid__` : [list of app objects]
-
-In other words, the `registry` contains several (sub-)registries which hold a
-list of appobjects associated to an identifier.
-
-The base class of appobjects is :class:`cubicweb.appobject.AppObject`.
-
-Selectors
-~~~~~~~~~
-
-At runtime, appobjects can be selected in a registry according to some
-contextual information. Selection is done by comparing the *score*
-returned by each appobject's *selector*.
-
-The better the object fits the context, the higher the score. Scores
-are the glue that ties appobjects to the data model. Using them
-appropriately is an essential part of the construction of well behaved
-cubes.
-
-|cubicweb| provides a set of basic selectors that may be parametrized. Also,
-selectors can be combined with the `~` unary operator (negation) and the binary
-operators `&` and `|` (respectivly 'and' and 'or') to build more complex
-selectors. Of course complex selectors may be combined too. Last but not least, you
-can write your own selectors.
-
-The `registry`
-~~~~~~~~~~~~~~~
-
-At startup, the `registry` inspects a number of directories looking
-for compatible class definitions. After a recording process, the
-objects are assigned to registries and become available through the
-selection process.
-
-In a cube, application object classes are looked in the following modules or
-packages:
-
-- `entities`
-- `views`
-- `hooks`
-- `sobjects`
-
-There are three common ways to look up some application object from a
-registry:
-
-* get the most appropriate object by specifying an identifier and
- context objects. The object with the greatest score is
- selected. There should always be a single appobject with a greater
- score than others for a particular context.
-
-* get all objects applying to a context by specifying a registry. A
- list of objects will be returned containing the object with the
- highest score (> 0) for each identifier in that registry.
-
-* get the object within a particular registry/identifier. No selection
- process is involved: the registry will expect to find a single
- object in that cell.
-
-
-.. _RQLIntro:
-
-The RQL query language
-----------------------
-
-No need for a complicated ORM when you have a powerful data
-manipulation language.
-
-All the persistent data in a |cubicweb| instance is retrieved and
-modified using RQL (see :ref:`rql_intro`).
-
-This query language is inspired by SQL but is on a higher level in order to
-emphasize browsing relations.
-
-
-Result set
-~~~~~~~~~~
-
-Every request made (using RQL) to the data repository returns an object we call a
-Result Set. It enables easy use of the retrieved data, providing a translation
-layer between the backend's native datatypes and |cubicweb| schema's EntityTypes.
-
-Result sets provide access to the raw data, yielding either basic Python data
-types, or schema-defined high-level entities, in a straightforward way.
-
-
-.. _ViewIntro:
-
-Views
------
-
-**CubicWeb is data driven**
-
-The view system is loosely coupled to data through the selection system explained
-above. Views are application objects with a dedicated interface to 'render'
-something, eg producing some html, text, xml, pdf, or whatsover that can be
-displayed to a user.
-
-Views actually are partitioned into different kind of objects such as
-`templates`, `boxes`, `components` and proper `views`, which are more
-high-level abstraction useful to build the user interface in an object
-oriented way.
-
-
-.. _HookIntro:
-
-Hooks and operations
---------------------
-
-**CubicWeb provides an extensible data repository**
-
-The data model defined using Yams types allows to express the data
-model in a comfortable way. However several aspects of the data model
-can not be expressed there. For instance:
-
-* managing computed attributes
-
-* enforcing complicated business rules
-
-* real-world side-effects linked to data events (email notification
- being a prime example)
-
-The hook system is much like the triggers of an SQL database engine,
-except that:
-
-* it is not limited to one specific SQL backend (every one of them
- having an idiomatic way to encode triggers), nor to SQL backends at
- all (think about LDAP or a Subversion repository)
-
-* it is well-coupled to the rest of the framework
-
-Hooks are also application objects (in the `hooks` registry) and
-selected on events such as after/before add/update/delete on
-entities/relations, server startup or shutdown, etc.
-
-`Operations` may be instantiated by hooks to do further processing at different
-steps of the transaction's commit / rollback, which usually can not be done
-safely at the hook execution time.
-
-Hooks and operation are an essential building block of any moderately complicated
-cubicweb application.
-
-.. note::
- RQL queries executed in hooks and operations are *unsafe* by default, i.e. the
- read and write security is deactivated unless explicitly asked.
--- a/doc/book/en/intro/history.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-A little history...
-===================
-
-*CubicWeb* is a semantic web application framework that Logilab_ started
-developing in 2001 as an offspring of its Narval_ research project. *CubicWeb*
-is written in Python and includes a data server and a web engine.
-
-Its data server publishes data federated from different sources like
-SQL databases, LDAP directories, `VCS`_ repositories or even from other
-CubicWeb data servers.
-
-.. _`VCS`: http://en.wikipedia.org/wiki/Revision_control
-
-Its web engine was designed to let the final user control what content to select
-and how to display it. It allows one to browse the federated data sources and
-display the results with the rendering that best fits the context. This
-flexibility of the user interface gives back to the user some capabilities
-usually only accessible to application developers.
-
-*CubicWeb* has been developed by Logilab_ and used in-house for many years
-before it was first installed for its clients in 2006 as version 2.
-
-In 2008, *CubicWeb* version 3 became downloadable for free under the
-terms of the LGPL license. Its community is now steadily growing
-without hampering the fast-paced stream of changes thanks to the time
-and energy originally put in the design of the framework.
-
-
-.. _Narval: http://www.logilab.org/project/narval-moved
-.. _Logilab: http://www.logilab.fr/
--- a/doc/book/en/intro/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Part1:
-
---------------------------
-Introduction to *CubicWeb*
---------------------------
-
-This first part of the book offers different reading path to
-discover the *CubicWeb* framework, provides a tutorial to get a quick
-overview of its features and lists its key concepts.
-
-
-.. toctree::
- :maxdepth: 2
- :numbered:
-
- history
- concepts.rst
--- a/doc/book/en/makefile Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-SRC=.
-
-# You can set these sphinx variables from the command line.
-SPHINXOPTS =
-SPHINXBUILD = sphinx-build
-PAPER =
-#BUILDDIR = build
-BUILDDIR = ../..
-CWDIR = ../../..
-JSDIR = ${CWDIR}/web/data
-JSTORST = ${CWDIR}/doc/tools/pyjsrest.py
-BUILDJS = devweb/js_api
-
-# Internal variables for sphinx
-PAPEROPT_a4 = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d ${BUILDDIR}/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-
-
-.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
-
-help:
- @echo "Please use \`make <target>' where <target> is one of"
- @echo " all to make standalone HTML files, developer manual and API doc"
- @echo " html to make standalone HTML files"
- @echo "--- "
- @echo " pickle to make pickle files (usable by e.g. sphinx-web)"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " changes to make an overview over all changed/added/deprecated items"
- @echo " linkcheck to check all external links for integrity"
-
-clean:
- rm -f *.html
- -rm -rf ${BUILDDIR}/html ${BUILDDIR}/doctrees
- -rm -rf ${BUILDJS}
-
-all: html
-
-# run sphinx ###
-html: js
- mkdir -p ${BUILDDIR}/html ${BUILDDIR}/doctrees
- $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) ${BUILDDIR}/html
- @echo
- @echo "Build finished. The HTML pages are in ${BUILDDIR}/html."
-
-js:
- mkdir -p ${BUILDJS}
- $(JSTORST) -p ${JSDIR} -o ${BUILDJS}
-
-pickle:
- mkdir -p ${BUILDDIR}/pickle ${BUILDDIR}/doctrees
- $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) ${BUILDDIR}/pickle
- @echo
- @echo "Build finished; now you can process the pickle files or run"
- @echo " sphinx-web ${BUILDDIR}/pickle"
- @echo "to start the sphinx-web server."
-
-web: pickle
-
-htmlhelp:
- mkdir -p ${BUILDDIR}/htmlhelp ${BUILDDIR}/doctrees
- $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) ${BUILDDIR}/htmlhelp
- @echo
- @echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in ${BUILDDIR}/htmlhelp."
-
-latex:
- mkdir -p ${BUILDDIR}/latex ${BUILDDIR}/doctrees
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) ${BUILDDIR}/latex
- @echo
- @echo "Build finished; the LaTeX files are in ${BUILDDIR}/latex."
- @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
- "run these through (pdf)latex."
-
-changes:
- mkdir -p ${BUILDDIR}/changes ${BUILDDIR}/doctrees
- $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) ${BUILDDIR}/changes
- @echo
- @echo "The overview file is in ${BUILDDIR}/changes."
-
-linkcheck:
- mkdir -p ${BUILDDIR}/linkcheck ${BUILDDIR}/doctrees
- $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) ${BUILDDIR}/linkcheck
- @echo
- @echo "Link check complete; look for any errors in the above output " \
- "or in ${BUILDDIR}/linkcheck/output.txt."
--- a/doc/book/en/tutorials/advanced/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-
-.. _TutosPhotoWebSite:
-
-Building a photo gallery with |cubicweb|
-========================================
-
-Desired features
-----------------
-
-* basically a photo gallery
-
-* photo stored on the file system and displayed dynamically through a web interface
-
-* navigation through folder (album), tags, geographical zone, people on the
- picture... using facets
-
-* advanced security (not everyone can see everything). More on this later.
-
-
-.. toctree::
- :maxdepth: 2
-
- part01_create-cube
- part02_security
- part03_bfss
- part04_ui-base
- part05_ui-advanced
-
-
--- a/doc/book/en/tutorials/advanced/part01_create-cube.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-.. _TutosPhotoWebSiteCubeCreation:
-
-Cube creation and schema definition
------------------------------------
-
-.. _adv_tuto_create_new_cube:
-
-Step 1: creating a new cube for my web site
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-One note about my development environment: I wanted to use the packaged
-version of CubicWeb and cubes while keeping my cube in my user
-directory, let's say `~src/cubes`. I achieve this by setting the
-following environment variables::
-
- CW_CUBES_PATH=~/src/cubes
- CW_MODE=user
-
-I can now create the cube which will hold custom code for this web
-site using::
-
- cubicweb-ctl newcube --directory=~/src/cubes sytweb
-
-
-.. _adv_tuto_assemble_cubes:
-
-Step 2: pick building blocks into existing cubes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Almost everything I want to handle in my web-site is somehow already modelized in
-existing cubes that I'll extend for my need. So I'll pick the following cubes:
-
-* `folder`, containing the `Folder` entity type, which will be used as
- both 'album' and a way to map file system folders. Entities are
- added to a given folder using the `filed_under` relation.
-
-* `file`, containing `File` entity type, gallery view, and a file system import
- utility.
-
-* `zone`, containing the `Zone` entity type for hierarchical geographical
- zones. Entities (including sub-zones) are added to a given zone using the
- `situated_in` relation.
-
-* `person`, containing the `Person` entity type plus some basic views.
-
-* `comment`, providing a full commenting system allowing one to comment entity types
- supporting the `comments` relation by adding a `Comment` entity.
-
-* `tag`, providing a full tagging system as an easy and powerful way to classify
- entities supporting the `tags` relation by linking the to `Tag` entities. This
- will allows navigation into a large number of picture.
-
-Ok, now I'll tell my cube requires all this by editing :file:`cubes/sytweb/__pkginfo__.py`:
-
- .. sourcecode:: python
-
- __depends__ = {'cubicweb': '>= 3.10.0',
- 'cubicweb-file': '>= 1.9.0',
- 'cubicweb-folder': '>= 1.1.0',
- 'cubicweb-person': '>= 1.2.0',
- 'cubicweb-comment': '>= 1.2.0',
- 'cubicweb-tag': '>= 1.2.0',
- 'cubicweb-zone': None}
-
-Notice that you can express minimal version of the cube that should be used,
-`None` meaning whatever version available. All packages starting with 'cubicweb-'
-will be recognized as being cube, not bare python packages. You can still specify
-this explicitly using instead the `__depends_cubes__` dictionary which should
-contains cube's name without the prefix. So the example below would be written
-as:
-
- .. sourcecode:: python
-
- __depends__ = {'cubicweb': '>= 3.10.0'}
- __depends_cubes__ = {'file': '>= 1.9.0',
- 'folder': '>= 1.1.0',
- 'person': '>= 1.2.0',
- 'comment': '>= 1.2.0',
- 'tag': '>= 1.2.0',
- 'zone': None}
-
-If your cube is packaged for debian, it's a good idea to update the
-`debian/control` file at the same time, so you won't forget it.
-
-
-Step 3: glue everything together in my cube's schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. sourcecode:: python
-
- from yams.buildobjs import RelationDefinition
-
- class comments(RelationDefinition):
- subject = 'Comment'
- object = 'File'
- cardinality = '1*'
- composite = 'object'
-
- class tags(RelationDefinition):
- subject = 'Tag'
- object = 'File'
-
- class filed_under(RelationDefinition):
- subject = 'File'
- object = 'Folder'
-
- class situated_in(RelationDefinition):
- subject = 'File'
- object = 'Zone'
-
- class displayed_on(RelationDefinition):
- subject = 'Person'
- object = 'File'
-
-
-This schema:
-
-* allows to comment and tag on `File` entity type by adding the `comments` and
- `tags` relations. This should be all we've to do for this feature since the
- related cubes provide 'pluggable section' which are automatically displayed on
- the primary view of entity types supporting the relation.
-
-* adds a `situated_in` relation definition so that image entities can be
- geolocalized.
-
-* add a new relation `displayed_on` relation telling who can be seen on a
- picture.
-
-This schema will probably have to evolve as time goes (for security handling at
-least), but since the possibility to let a schema evolve is one of CubicWeb's
-features (and goals), we won't worry about it for now and see that later when needed.
-
-
-Step 4: creating the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Now that I have a schema, I want to create an instance. To
-do so using this new 'sytweb' cube, I run::
-
- cubicweb-ctl create sytweb sytweb_instance
-
-Hint: if you get an error while the database is initialized, you can
-avoid having to answer the questions again by running::
-
- cubicweb-ctl db-create sytweb_instance
-
-This will use your already configured instance and start directly from the create
-database step, thus skipping questions asked by the 'create' command.
-
-Once the instance and database are fully initialized, run ::
-
- cubicweb-ctl start sytweb_instance
-
-to start the instance, check you can connect on it, etc...
-
--- a/doc/book/en/tutorials/advanced/part02_security.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,440 +0,0 @@
-.. _TutosPhotoWebSiteSecurity:
-
-Security, testing and migration
--------------------------------
-
-This part will cover various topics:
-
-* configuring security
-* migrating existing instance
-* writing some unit tests
-
-Here is the ``read`` security model I want:
-
-* folders, files, images and comments should have one of the following visibility:
-
- - ``public``, everyone can see it
- - ``authenticated``, only authenticated users can see it
- - ``restricted``, only a subset of authenticated users can see it
-
-* managers (e.g. me) can see everything
-* only authenticated users can see people
-* everyone can see classifier entities, such as tag and zone
-
-Also, unless explicitly specified, the visibility of an image should be the same as
-its parent folder, as well as visibility of a comment should be the same as the
-commented entity. If there is no parent entity, the default visibility is
-``authenticated``.
-
-Regarding write security, that's much easier:
-* anonymous can't write anything
-* authenticated users can only add comment
-* managers will add the remaining stuff
-
-Now, let's implement that!
-
-Proper security in CubicWeb is done at the schema level, so you don't have to
-bother with it in views: users will only see what they can see automatically.
-
-.. _adv_tuto_security:
-
-Step 1: configuring security into the schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In schema, you can grant access according to groups, or to some RQL expressions:
-users get access if the expression returns some results. To implement the read
-security defined earlier, groups are not enough, we'll need some RQL expression. Here
-is the idea:
-
-* add a `visibility` attribute on Folder, File and Comment, which may be one of
- the value explained above
-
-* add a `may_be_read_by` relation from Folder, File and Comment to users,
- which will define who can see the entity
-
-* security propagation will be done in hook.
-
-So the first thing to do is to modify my cube's schema.py to define those
-relations:
-
-.. sourcecode:: python
-
- from yams.constraints import StaticVocabularyConstraint
-
- class visibility(RelationDefinition):
- subject = ('Folder', 'File', 'Comment')
- object = 'String'
- constraints = [StaticVocabularyConstraint(('public', 'authenticated',
- 'restricted', 'parent'))]
- default = 'parent'
- cardinality = '11' # required
-
- class may_be_read_by(RelationDefinition):
- __permissions__ = {
- 'read': ('managers', 'users'),
- 'add': ('managers',),
- 'delete': ('managers',),
- }
-
- subject = ('Folder', 'File', 'Comment',)
- object = 'CWUser'
-
-We can note the following points:
-
-* we've added a new `visibility` attribute to folder, file, image and comment
- using a `RelationDefinition`
-
-* `cardinality = '11'` means this attribute is required. This is usually hidden
- under the `required` argument given to the `String` constructor, but we can
- rely on this here (same thing for StaticVocabularyConstraint, which is usually
- hidden by the `vocabulary` argument)
-
-* the `parent` possible value will be used for visibility propagation
-
-* think to secure the `may_be_read_by` permissions, else any user can add/delete it
- by default, which somewhat breaks our security model...
-
-Now, we should be able to define security rules in the schema, based on these new
-attribute and relation. Here is the code to add to *schema.py*:
-
-.. sourcecode:: python
-
- from cubicweb.schema import ERQLExpression
-
- VISIBILITY_PERMISSIONS = {
- 'read': ('managers',
- ERQLExpression('X visibility "public"'),
- ERQLExpression('X may_be_read_by U')),
- 'add': ('managers',),
- 'update': ('managers', 'owners',),
- 'delete': ('managers', 'owners'),
- }
- AUTH_ONLY_PERMISSIONS = {
- 'read': ('managers', 'users'),
- 'add': ('managers',),
- 'update': ('managers', 'owners',),
- 'delete': ('managers', 'owners'),
- }
- CLASSIFIERS_PERMISSIONS = {
- 'read': ('managers', 'users', 'guests'),
- 'add': ('managers',),
- 'update': ('managers', 'owners',),
- 'delete': ('managers', 'owners'),
- }
-
- from cubes.folder.schema import Folder
- from cubes.file.schema import File
- from cubes.comment.schema import Comment
- from cubes.person.schema import Person
- from cubes.zone.schema import Zone
- from cubes.tag.schema import Tag
-
- Folder.__permissions__ = VISIBILITY_PERMISSIONS
- File.__permissions__ = VISIBILITY_PERMISSIONS
- Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
- Comment.__permissions__['add'] = ('managers', 'users',)
- Person.__permissions__ = AUTH_ONLY_PERMISSIONS
- Zone.__permissions__ = CLASSIFIERS_PERMISSIONS
- Tag.__permissions__ = CLASSIFIERS_PERMISSIONS
-
-What's important in there:
-
-* `VISIBILITY_PERMISSIONS` provides read access to managers group, if
- `visibility` attribute's value is 'public', or if user (designed by the 'U'
- variable in the expression) is linked to the entity (the 'X' variable) through
- the `may_be_read_by` permission
-
-* we modify permissions of the entity types we use by importing them and
- modifying their `__permissions__` attribute
-
-* notice the `.copy()`: we only want to modify 'add' permission for `Comment`,
- not for all entity types using `VISIBILITY_PERMISSIONS`!
-
-* the remaining part of the security model is done using regular groups:
-
- - `users` is the group to which all authenticated users will belong
- - `guests` is the group of anonymous users
-
-
-.. _adv_tuto_security_propagation:
-
-Step 2: security propagation in hooks
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To fullfill the requirements, we have to implement::
-
- Also, unless explicity specified, visibility of an image should be the same as
- its parent folder, as well as visibility of a comment should be the same as the
- commented entity.
-
-This kind of `active` rule will be done using CubicWeb's hook
-system. Hooks are triggered on database events such as addition of a new
-entity or relation.
-
-The tricky part of the requirement is in *unless explicitly specified*, notably
-because when the entity is added, we don't know yet its 'parent'
-entity (e.g. Folder of an File, File commented by a Comment). To handle such things,
-CubicWeb provides `Operation`, which allow to schedule things to do at commit time.
-
-In our case we will:
-
-* on entity creation, schedule an operation that will set default visibility
-
-* when a "parent" relation is added, propagate parent's visibility unless the
- child already has a visibility set
-
-Here is the code in cube's *hooks.py*:
-
-.. sourcecode:: python
-
- from cubicweb.predicates import is_instance
- from cubicweb.server import hook
-
- class SetVisibilityOp(hook.DataOperationMixIn, hook.Operation):
-
- def precommit_event(self):
- for eid in self.get_data():
- entity = self.session.entity_from_eid(eid)
- if entity.visibility == 'parent':
- entity.cw_set(visibility=u'authenticated')
-
- class SetVisibilityHook(hook.Hook):
- __regid__ = 'sytweb.setvisibility'
- __select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Comment')
- events = ('after_add_entity',)
-
- def __call__(self):
- SetVisibilityOp.get_instance(self._cw).add_data(self.entity.eid)
-
- class SetParentVisibilityHook(hook.Hook):
- __regid__ = 'sytweb.setparentvisibility'
- __select__ = hook.Hook.__select__ & hook.match_rtype('filed_under', 'comments')
- events = ('after_add_relation',)
-
- def __call__(self):
- parent = self._cw.entity_from_eid(self.eidto)
- child = self._cw.entity_from_eid(self.eidfrom)
- if child.visibility == 'parent':
- child.cw_set(visibility=parent.visibility)
-
-Notice:
-
-* hooks are application objects, hence have selectors that should match entity or
- relation types to which the hook applies. To match a relation type, we use the
- hook specific `match_rtype` selector.
-
-* usage of `DataOperationMixIn`: instead of adding an operation for each added entity,
- DataOperationMixIn allows to create a single one and to store entity's eids to be
- processed in the transaction data. This is a good pratice to avoid heavy
- operations manipulation cost when creating a lot of entities in the same
- transaction.
-
-* the `precommit_event` method of the operation will be called at transaction's
- commit time.
-
-* in a hook, `self._cw` is the repository session, not a web request as usually
- in views
-
-* according to hook's event, you have access to different attributes on the hook
- instance. Here:
-
- - `self.entity` is the newly added entity on 'after_add_entity' events
-
- - `self.eidfrom` / `self.eidto` are the eid of the subject / object entity on
- 'after_add_relation' events (you may also get the relation type using
- `self.rtype`)
-
-The `parent` visibility value is used to tell "propagate using parent security"
-because we want that attribute to be required, so we can't use None value else
-we'll get an error before we get any chance to propagate...
-
-Now, we also want to propagate the `may_be_read_by` relation. Fortunately,
-CubicWeb provides some base hook classes for such things, so we only have to add
-the following code to *hooks.py*:
-
-.. sourcecode:: python
-
- # relations where the "parent" entity is the subject
- S_RELS = set()
- # relations where the "parent" entity is the object
- O_RELS = set(('filed_under', 'comments',))
-
- class AddEntitySecurityPropagationHook(hook.PropagateRelationHook):
- """propagate permissions when new entity are added"""
- __regid__ = 'sytweb.addentity_security_propagation'
- __select__ = (hook.PropagateRelationHook.__select__
- & hook.match_rtype_sets(S_RELS, O_RELS))
- main_rtype = 'may_be_read_by'
- subject_relations = S_RELS
- object_relations = O_RELS
-
- class AddPermissionSecurityPropagationHook(hook.PropagateRelationAddHook):
- """propagate permissions when new entity are added"""
- __regid__ = 'sytweb.addperm_security_propagation'
- __select__ = (hook.PropagateRelationAddHook.__select__
- & hook.match_rtype('may_be_read_by',))
- subject_relations = S_RELS
- object_relations = O_RELS
-
- class DelPermissionSecurityPropagationHook(hook.PropagateRelationDelHook):
- __regid__ = 'sytweb.delperm_security_propagation'
- __select__ = (hook.PropagateRelationDelHook.__select__
- & hook.match_rtype('may_be_read_by',))
- subject_relations = S_RELS
- object_relations = O_RELS
-
-* the `AddEntitySecurityPropagationHook` will propagate the relation
- when `filed_under` or `comments` relations are added
-
- - the `S_RELS` and `O_RELS` set as well as the `match_rtype_sets` selector are
- used here so that if my cube is used by another one, it'll be able to
- configure security propagation by simply adding relation to one of the two
- sets.
-
-* the two others will propagate permissions changes on parent entities to
- children entities
-
-
-.. _adv_tuto_tesing_security:
-
-Step 3: testing our security
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Security is tricky. Writing some tests for it is a very good idea. You should
-even write them first, as Test Driven Development recommends!
-
-Here is a small test case that will check the basis of our security
-model, in *test/unittest_sytweb.py*:
-
-.. sourcecode:: python
-
- from cubicweb.devtools.testlib import CubicWebTC
- from cubicweb import Binary
-
- class SecurityTC(CubicWebTC):
-
- def test_visibility_propagation(self):
- with self.admin_access.repo_cnx() as cnx:
- # create a user for later security checks
- toto = self.create_user(cnx, 'toto')
- cnx.commit()
- # init some data using the default manager connection
- folder = cnx.create_entity('Folder',
- name=u'restricted',
- visibility=u'restricted')
- photo1 = cnx.create_entity('File',
- data_name=u'photo1.jpg',
- data=Binary('xxx'),
- filed_under=folder)
- cnx.commit()
- # visibility propagation
- self.assertEquals(photo1.visibility, 'restricted')
- # unless explicitly specified
- photo2 = cnx.create_entity('File',
- data_name=u'photo2.jpg',
- data=Binary('xxx'),
- visibility=u'public',
- filed_under=folder)
- cnx.commit()
- self.assertEquals(photo2.visibility, 'public')
- with self.new_access('toto').repo_cnx() as cnx:
- # test security
- self.assertEqual(1, len(cnx.execute('File X'))) # only the public one
- self.assertEqual(0, len(cnx.execute('Folder X'))) # restricted...
- with self.admin_access.repo_cnx() as cnx:
- # may_be_read_by propagation
- folder = cnx.entity_from_eid(folder.eid)
- folder.cw_set(may_be_read_by=toto)
- cnx.commit()
- with self.new_access('toto').repo_cnx() as cnx:
- photo1 = cnx.entity_from_eid(photo1.eid)
- self.failUnless(photo1.may_be_read_by)
- # test security with permissions
- self.assertEquals(2, len(cnx.execute('File X'))) # now toto has access to photo2
- self.assertEquals(1, len(cnx.execute('Folder X'))) # and to restricted folder
-
- if __name__ == '__main__':
- from logilab.common.testlib import unittest_main
- unittest_main()
-
-It's not complete, but shows most things you'll want to do in tests: adding some
-content, creating users and connecting as them in the test, etc...
-
-To run it type:
-
-.. sourcecode:: bash
-
- $ pytest unittest_sytweb.py
- ======================== unittest_sytweb.py ========================
- -> creating tables [....................]
- -> inserting default user and default groups.
- -> storing the schema in the database [....................]
- -> database for instance data initialized.
- .
- ----------------------------------------------------------------------
- Ran 1 test in 22.547s
-
- OK
-
-
-The first execution is taking time, since it creates a sqlite database for the
-test instance. The second one will be much quicker:
-
-.. sourcecode:: bash
-
- $ pytest unittest_sytweb.py
- ======================== unittest_sytweb.py ========================
- .
- ----------------------------------------------------------------------
- Ran 1 test in 2.662s
-
- OK
-
-If you do some changes in your schema, you'll have to force regeneration of that
-database. You do that by removing the tmpdb files before running the test: ::
-
- $ rm data/database/tmpdb*
-
-
-.. Note::
- pytest is a very convenient utility used to control test execution. It is available from the `logilab-common`_ package.
-
-.. _`logilab-common`: http://www.logilab.org/project/logilab-common
-
-.. _adv_tuto_migration_script:
-
-Step 4: writing the migration script and migrating the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Prior to those changes, I created an instance, fed it with some data, so I
-don't want to create a new one, but to migrate the existing one. Let's see how to
-do that.
-
-Migration commands should be put in the cube's *migration* directory, in a
-file named file:`<X.Y.Z>_Any.py` ('Any' being there mostly for historical reasons).
-
-Here I'll create a *migration/0.2.0_Any.py* file containing the following
-instructions:
-
-.. sourcecode:: python
-
- add_relation_type('may_be_read_by')
- add_relation_type('visibility')
- sync_schema_props_perms()
-
-Then I update the version number in the cube's *__pkginfo__.py* to 0.2.0. And
-that's it! Those instructions will:
-
-* update the instance's schema by adding our two new relations and update the
- underlying database tables accordingly (the first two instructions)
-
-* update schema's permissions definition (the last instruction)
-
-
-To migrate my instance I simply type::
-
- cubicweb-ctl upgrade sytweb_instance
-
-You'll then be asked some questions to do the migration step by step. You should say
-YES when it asks if a backup of your database should be done, so you can get back
-to initial state if anything goes wrong...
--- a/doc/book/en/tutorials/advanced/part03_bfss.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-Storing images on the file-system
----------------------------------
-
-Step 1: configuring the BytesFileSystem storage
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To avoid cluttering my database, and to ease file manipulation, I don't want them
-to be stored in the database. I want to be able create File entities for some
-files on the server file system, where those file will be accessed to get
-entities data. To do so I've to set a custom :class:`BytesFileSystemStorage`
-storage for the File 'data' attribute, which hold the actual file's content.
-
-Since the function to register a custom storage needs to have a repository
-instance as first argument, we've to call it in a server startup hook. So I added
-in `cubes/sytweb/hooks.py` :
-
-.. sourcecode:: python
-
- from os import makedirs
- from os.path import join, exists
-
- from cubicweb.server import hook
- from cubicweb.server.sources import storages
-
- class ServerStartupHook(hook.Hook):
- __regid__ = 'sytweb.serverstartup'
- events = ('server_startup', 'server_maintenance')
-
- def __call__(self):
- bfssdir = join(self.repo.config.appdatahome, 'bfss')
- if not exists(bfssdir):
- makedirs(bfssdir)
- print 'created', bfssdir
- storage = storages.BytesFileSystemStorage(bfssdir)
- storages.set_attribute_storage(self.repo, 'File', 'data', storage)
-
-.. Note::
-
- * how we built the hook's registry identifier (`__regid__`): you can introduce
- 'namespaces' by using there python module like naming identifiers. This is
- especially important for hooks where you usually want a new custom hook, not
- overriding / specializing an existant one, but the concept may be applied to
- any application objects
-
- * we catch two events here: "server_startup" and "server_maintenance". The first
- is called on regular repository startup (eg, as a server), the other for
- maintenance task such as shell or upgrade. In both cases, we need to have
- the storage set, else we'll be in trouble...
-
- * the path given to the storage is the place where file added through the ui
- (or in the database before migration) will be located
-
- * beware that by doing this, you can't anymore write queries that will try to
- restrict on File `data` attribute. Hopefuly we don't do that usually
- on file's content or more generally on attributes for the Bytes type
-
-Now, if you've already added some photos through the web ui, you'll have to
-migrate existing data so file's content will be stored on the file-system instead
-of the database. There is a migration command to do so, let's run it in the
-cubicweb shell (in real life, you would have to put it in a migration script as we
-have seen last time):
-
-::
-
- $ cubicweb-ctl shell sytweb_instance
- entering the migration python shell
- just type migration commands or arbitrary python code and type ENTER to execute it
- type "exit" or Ctrl-D to quit the shell and resume operation
- >>> storage_changed('File', 'data')
- [........................]
-
-
-That's it. Now, files added through the web ui will have their content stored on
-the file-system, and you'll also be able to import files from the file-system as
-explained in the next part.
-
-Step 2: importing some data into the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Hey, we start to have some nice features, let us give a try to this new web
-site. For instance if I have a 'photos/201005WePyrenees' containing pictures for
-a particular event, I can import it to my web site by typing ::
-
- $ cubicweb-ctl fsimport -F sytweb_instance photos/201005WePyrenees/
- ** importing directory /home/syt/photos/201005WePyrenees
- importing IMG_8314.JPG
- importing IMG_8274.JPG
- importing IMG_8286.JPG
- importing IMG_8308.JPG
- importing IMG_8304.JPG
-
-.. Note::
- The -F option means that folders should be mapped, hence my photos will be
- linked to a Folder entity corresponding to the file-system folder.
-
-Let's take a look at the web ui:
-
-.. image:: ../../images/tutos-photowebsite_ui1.png
-
-Nothing different, I can't see the new folder... But remember our security model!
-By default, files are only accessible to authenticated users, and I'm looking at
-the site as anonymous, e.g. not authenticated. If I login, I can now see:
-
-.. image:: ../../images/tutos-photowebsite_ui2.png
-
-Yeah, it's there! You will notice that I can see some entities as well as
-folders and images the anonymous user can't. It just works **everywhere in the
-ui** since it's handled at the repository level, thanks to our security model.
-
-Now if I click on the recently inserted folder, I can see
-
-.. image:: ../../images/tutos-photowebsite_ui3.png
-
-Great! There is even my pictures in the folder. I can know give to this folder a
-nicer name (provided I don't intend to import from it anymore, else already
-imported photos will be reimported), change permissions, title for some pictures,
-etc... Having a good content is much more difficult than having a good web site
-;)
-
-
-Conclusion
-~~~~~~~~~~
-
-We started to see here an advanced feature of our repository: the ability
-to store some parts of our data-model into a custom storage, outside the
-database. There is currently only the :class:`BytesFileSystemStorage` available,
-but you can expect to see more coming in a near future (or write your own!).
-
-Also, we can know start to feed our web-site with some nice pictures!
-The site isn't perfect (far from it actually) but it's usable, and we can
-now start using it and improve it on the way. The Incremental Cubic Way :)
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,361 +0,0 @@
-Let's make it more user friendly
-================================
-
-
-Step 1: let's improve site's usability for our visitors
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The first thing I've noticed is that people to whom I send links to photos with
-some login/password authentication get lost, because they don't grasp they have
-to login by clicking on the 'authenticate' link. That's much probably because
-they only get a 404 when trying to access an unauthorized folder, and the site
-doesn't make clear that 1. you're not authenticated, 2. you could get more
-content by authenticating yourself.
-
-So, to improve this situation, I decided that I should:
-
-* make a login box appears for anonymous, so they see at a first glance a place
- to put the login / password information I provided
-
-* customize the 404 page, proposing to login to anonymous.
-
-Here is the code, samples from my cube's `views.py` file:
-
-.. sourcecode:: python
-
- from cubicweb.predicates import is_instance
- from cubicweb.web import component
- from cubicweb.web.views import error
- from cubicweb.predicates import anonymous_user
-
- class FourOhFour(error.FourOhFour):
- __select__ = error.FourOhFour.__select__ & anonymous_user()
-
- def call(self):
- self.w(u"<h1>%s</h1>" % self._cw._('this resource does not exist'))
- self.w(u"<p>%s</p>" % self._cw._('have you tried to login?'))
-
-
- class LoginBox(component.CtxComponent):
- """display a box containing links to all startup views"""
- __regid__ = 'sytweb.loginbox'
- __select__ = component.CtxComponent.__select__ & anonymous_user()
-
- title = _('Authenticate yourself')
- order = 70
-
- def render_body(self, w):
- cw = self._cw
- form = cw.vreg['forms'].select('logform', cw)
- form.render(w=w, table_class='', display_progress_div=False)
-
-The first class provides a new specific implementation of the default page you
-get on 404 error, to display an adapted message to anonymous user.
-
-.. Note::
-
- Thanks to the selection mecanism, it will be selected for anoymous user,
- since the additional `anonymous_user()` selector gives it a higher score than
- the default, and not for authenticated since this selector will return 0 in
- such case (hence the object won't be selectable)
-
-The second class defines a simple box, that will be displayed by default with
-boxes in the left column, thanks to default :class:`component.CtxComponent`
-selector. The HTML is written to match default CubicWeb boxes style. The code
-fetch the actual login form and render it.
-
-
-.. figure:: ../../images/tutos-photowebsite_login-box.png
- :alt: login box / 404 screenshot
-
- The login box and the custom 404 page for an anonymous visitor (translated in french)
-
-
-Step 2: providing a custom index page
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Another thing we can easily do to improve the site is... A nicer index page
-(e.g. the first page you get when accessing the web site)! The default one is
-quite intimidating (that should change in a near future). I will provide a much
-simpler index page that simply list available folders (e.g. photo albums in that
-site).
-
-.. sourcecode:: python
-
- from cubicweb.web.views import startup
-
- class IndexView(startup.IndexView):
- def call(self, **kwargs):
- self.w(u'<div>\n')
- if self._cw.cnx.anonymous_connection:
- self.w(u'<h4>%s</h4>\n' % self._cw._('Public Albums'))
- else:
- self.w(u'<h4>%s</h4>\n' % self._cw._('Albums for %s') % self._cw.user.login)
- self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
- self.w(u'</div>\n')
-
- def registration_callback(vreg):
- vreg.register_all(globals().values(), __name__, (IndexView,))
- vreg.register_and_replace(IndexView, startup.IndexView)
-
-As you can see, we override the default index view found in
-`cubicweb.web.views.startup`, geting back nothing but its identifier and selector
-since we override the top level view's `call` method.
-
-.. Note::
-
- in that case, we want our index view to **replace** the existing one. To do so
- we've to implements the `registration_callback` function, in which we tell to
- register everything in the module *but* our IndexView, then we register it
- instead of the former index view.
-
-Also, we added a title that tries to make it more evident that the visitor is
-authenticated, or not. Hopefuly people will get it now!
-
-
-.. figure:: ../../images/tutos-photowebsite_index-before.png
- :alt: default index page screenshot
-
- The default index page
-
-.. figure:: ../../images/tutos-photowebsite_index-after.png
- :alt: new index page screenshot
-
- Our simpler, less intimidating, index page (still translated in french)
-
-
-Step 3: more navigation improvments
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-There are still a few problems I want to solve...
-
-* Images in a folder are displayed in a somewhat random order. I would like to
- have them ordered by file's name (which will usually, inside a given folder,
- also result ordering photo by their date and time)
-
-* When clicking a photo from an album view, you've to get back to the gallery
- view to go to the next photo. This is pretty annoying...
-
-* Also, when viewing an image, there is no clue about the folder to which this
- image belongs to.
-
-I will first try to explain the ordering problem. By default, when accessing
-related entities by using the ORM's API, you should get them ordered according to
-the target's class `cw_fetch_order`. If we take a look at the file cube'schema,
-we can see:
-
-.. sourcecode:: python
-
- class File(AnyEntity):
- """customized class for File entities"""
- __regid__ = 'File'
- fetch_attrs, cw_fetch_order = fetch_config(['data_name', 'title'])
-
-
-By default, `fetch_config` will return a `cw_fetch_order` method that will order
-on the first attribute in the list. So, we could expect to get files ordered by
-their name. But we don't. What's up doc ?
-
-The problem is that files are related to folder using the `filed_under` relation.
-And that relation is ambiguous, eg it can lead to `File` entities, but also to
-`Folder` entities. In such case, since both entity types doesn't share the
-attribute on which we want to sort, we'll get linked entities sorted on a common
-attribute (usually `modification_date`).
-
-To fix this, we've to help the ORM. We'll do this in the method from the `ITree`
-folder's adapter, used in the folder's primary view to display the folder's
-content. Here's the code, that I've put in our cube's `entities.py` file, since
-it's more logical stuff than view stuff:
-
-.. sourcecode:: python
-
- from cubes.folder import entities as folder
-
- class FolderITreeAdapter(folder.FolderITreeAdapter):
-
- def different_type_children(self, entities=True):
- rql = self.entity.cw_related_rql(self.tree_relation,
- self.parent_role, ('File',))
- rset = self._cw.execute(rql, {'x': self.entity.eid})
- if entities:
- return list(rset.entities())
- return rset
-
- def registration_callback(vreg):
- vreg.register_and_replace(FolderITreeAdapter, folder.FolderITreeAdapter)
-
-As you can see, we simple inherit from the adapter defined in the `folder` cube,
-then we override the `different_type_children` method to give a clue to the ORM's
-`cw_related_rql` method, that is responsible to generate the rql to get entities
-related to the folder by the `filed_under` relation (the value of the
-`tree_relation` attribute). The clue is that we only want to consider the `File`
-target entity type. By doing this, we remove the ambiguity and get back a RQL
-query that correctly order files by their `data_name` attribute.
-
-
-.. Note::
-
- * As seen earlier, we want to **replace** the folder's `ITree` adapter by our
- implementation, hence the custom `registration_callback` method.
-
-
-Ouf. That one was tricky...
-
-Now the easier parts. Let's start by adding some links on the file's primary view
-to see the previous / next image in the same folder. CubicWeb's provide a
-component that do exactly that. To make it appears, one have to be adaptable to
-the `IPrevNext` interface. Here is the related code sample, extracted from our
-cube's `views.py` file:
-
-.. sourcecode:: python
-
- from cubicweb.predicates import is_instance
- from cubicweb.web.views import navigation
-
-
- class FileIPrevNextAdapter(navigation.IPrevNextAdapter):
- __select__ = is_instance('File')
-
- def previous_entity(self):
- rset = self._cw.execute('File F ORDERBY FDN DESC LIMIT 1 WHERE '
- 'X filed_under FOLDER, F filed_under FOLDER, '
- 'F data_name FDN, X data_name > FDN, X eid %(x)s',
- {'x': self.entity.eid})
- if rset:
- return rset.get_entity(0, 0)
-
- def next_entity(self):
- rset = self._cw.execute('File F ORDERBY FDN ASC LIMIT 1 WHERE '
- 'X filed_under FOLDER, F filed_under FOLDER, '
- 'F data_name FDN, X data_name < FDN, X eid %(x)s',
- {'x': self.entity.eid})
- if rset:
- return rset.get_entity(0, 0)
-
-
-The `IPrevNext` interface implemented by the adapter simply consist in the
-`previous_entity` / `next_entity` methods, that should respectivly return the
-previous / next entity or `None`. We make an RQL query to get files in the same
-folder, ordered similarly (eg by their `data_name` attribute). We set
-ascendant/descendant ordering and a strict comparison with current file's name
-(the "X" variable representing the current file).
-
-Notice that this query supposes we wont have two files of the same name in the
-same folder, else things may go wrong. Fixing this is out of the scope of this
-blog. And as I would like to have at some point a smarter, context sensitive
-previous/next entity, I'll probably never fix this query (though if I had to, I
-would probably choosing to add a constraint in the schema so that we can't add
-two files of the same name in a folder).
-
-One more thing: by default, the component will be displayed below the content
-zone (the one with the white background). You can change this in the site's
-properties through the ui, but you can also change the default value in the code
-by modifying the `context` attribute of the component:
-
-.. sourcecode:: python
-
- navigation.NextPrevNavigationComponent.context = 'navcontentbottom'
-
-.. Note::
-
- `context` may be one of 'navtop', 'navbottom', 'navcontenttop' or
- 'navcontentbottom'; the first two being outside the main content zone, the two
- others inside it.
-
-.. figure:: ../../images/tutos-photowebsite_prevnext.png
- :alt: screenshot of the previous/next entity component
-
- The previous/next entity component, at the bottom of the main content zone.
-
-Now, the only remaining stuff in my todo list is to see the file's folder. I'll use
-the standard breadcrumb component to do so. Similarly as what we've seen before, this
-component is controled by the :class:`IBreadCrumbs` interface, so we'll have to provide a custom
-adapter for `File` entity, telling the a file's parent entity is its folder:
-
-.. sourcecode:: python
-
- from cubicweb.web.views import ibreadcrumbs
-
- class FileIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
- __select__ = is_instance('File')
-
- def parent_entity(self):
- if self.entity.filed_under:
- return self.entity.filed_under[0]
-
-In that case, we simply use attribute notation provided by the ORM to get the
-folder in which the current file (e.g. `self.entity`) is located.
-
-.. Note::
-
- The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default
- :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look
- at the value returned by its `parent_entity` method. It also provides a
- default implementation for this method for entities adapting to the `ITree`
- interface, but as our `File` doesn't, we've to provide a custom adapter.
-
-.. figure:: ../../images/tutos-photowebsite_breadcrumbs.png
- :alt: screenshot of the breadcrumb component
-
- The breadcrumb component when on a file entity, now displaying parent folder.
-
-
-Step 4: preparing the release and migrating the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Now that greatly enhanced our cube, it's time to release it to upgrade production site.
-I'll probably detail that process later, but I currently simply transfer the new code
-to the server running the web site.
-
-However, I've still today some step to respect to get things done properly...
-
-First, as I've added some translatable string, I've to run: ::
-
- $ cubicweb-ctl i18ncube sytweb
-
-To update the cube's gettext catalogs (the '.po' files under the cube's `i18n`
-directory). Once the above command is executed, I'll then update translations.
-
-To see if everything is ok on my test instance, I do: ::
-
- $ cubicweb-ctl i18ninstance sytweb
- $ cubicweb-ctl start -D sytweb
-
-The first command compile i18n catalogs (e.g. generates '.mo' files) for my test
-instance. The second command start it in debug mode, so I can open my browser and
-navigate through the web site to see if everything is ok...
-
-.. Note::
-
- In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while
- in the two other, it refers to the **instance** (if you can't see the
- difference, reread CubicWeb's concept chapter !).
-
-
-Once I've checked it's ok, I simply have to bump the version number in the
-`__pkginfo__` module to trigger a migration once I'll have updated the code on
-the production site. I can check then check the migration is also going fine, by
-first restoring a dump from the production site, then upgrading my test instance.
-
-To generate a dump from the production site: ::
-
- $ cubicweb-ctl db-dump sytweb
- pg_dump -Fc --username=syt --no-owner --file /home/syt/etc/cubicweb.d/sytweb/backup/tmpYIN0YI/system sytweb
- -> backup file /home/syt/etc/cubicweb.d/sytweb/backup/sytweb-2010-07-13_10-22-40.tar.gz
-
-I can now get back the dump file ('sytweb-2010-07-13_10-22-40.tar.gz') to my test
-machine (using `scp` for instance) to restore it and start migration: ::
-
- $ cubicweb-ctl db-restore sytweb sytweb-2010-07-13_10-22-40.tar.gz
- $ cubicweb-ctl upgrade sytweb
-
-You'll have to answer some questions, as we've seen in `an earlier post`_.
-
-Now that everything is tested, I can transfer the new code to the production
-server, `apt-get upgrade` cubicweb and its dependencies, and eventually
-upgrade the production instance.
-
-
-.. _`several improvments`: http://www.cubicweb.org/blogentry/1179899
-.. _`3.8`: http://www.cubicweb.org/blogentry/917107
-.. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642
-.. _`an earlier post`: http://www.cubicweb.org/867464
--- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,374 +0,0 @@
-Building my photos web site with |cubicweb| part V: let's make it even more user friendly
-=========================================================================================
-
-.. _uiprops:
-
-Step 1: tired of the default look?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-OK... Now our site has its most desired features. But... I would like to make it look
-somewhat like *my* website. It is not www.cubicweb.org after all. Let's tackle this
-first!
-
-The first thing we can to is to change the logo. There are various way to achieve
-this. The easiest way is to put a :file:`logo.png` file into the cube's :file:`data`
-directory. As data files are looked at according to cubes order (CubicWeb
-resources coming last), that file will be selected instead of CubicWeb's one.
-
-.. Note::
- As the location for static resources are cached, you'll have to restart
- your instance for this to be taken into account.
-
-Though there are some cases where you don't want to use a :file:`logo.png` file.
-For instance if it's a JPEG file. You can still change the logo by defining in
-the cube's :file:`uiprops.py` file:
-
-.. sourcecode:: python
-
- LOGO = data('logo.jpg')
-
-The uiprops machinery is used to define some static file resources,
-such as the logo, default Javascript / CSS files, as well as CSS
-properties (we'll see that later).
-
-.. Note::
- This file is imported specifically by |cubicweb|, with a predefined name space,
- containing for instance the `data` function, telling the file is somewhere
- in a cube or CubicWeb's data directory.
-
- One side effect of this is that it can't be imported as a regular python
- module.
-
-The nice thing is that in debug mode, change to a :file:`uiprops.py` file are detected
-and then automatically reloaded.
-
-Now, as it's a photos web-site, I would like to have a photo of mine as background...
-After some trials I won't detail here, I've found a working recipe explained `here`_.
-All I've to do is to override some stuff of the default CubicWeb user interface to
-apply it as explained.
-
-The first thing to to get the ``<img/>`` tag as first element after the
-``<body>`` tag. If you know a way to avoid this by simply specifying the image
-in the CSS, tell me! The easiest way to do so is to override the
-:class:`HTMLPageHeader` view, since that's the one that is directly called once
-the ``<body>`` has been written. How did I find this? By looking in the
-:mod:`cubiweb.web.views.basetemplates` module, since I know that global page
-layouts sits there. I could also have grep the "body" tag in
-:mod:`cubicweb.web.views`... Finding this was the hardest part. Now all I need is
-to customize it to write that ``img`` tag, as below:
-
-.. sourcecode:: python
-
- class HTMLPageHeader(basetemplates.HTMLPageHeader):
- # override this since it's the easier way to have our bg image
- # as the first element following <body>
- def call(self, **kwargs):
- self.w(u'<img id="bg-image" src="%sbackground.jpg" alt="background image"/>'
- % self._cw.datadir_url)
- super(HTMLPageHeader, self).call(**kwargs)
-
-
- def registration_callback(vreg):
- vreg.register_all(globals().values(), __name__, (HTMLPageHeader))
- vreg.register_and_replace(HTMLPageHeader, basetemplates.HTMLPageHeader)
-
-
-As you may have guessed, my background image is in a :file:`background.jpg` file
-in the cube's :file:`data` directory, but there are still some things to explain
-to newcomers here:
-
-* The :meth:`call` method is there the main access point of the view. It's called by
- the view's :meth:`render` method. It is not the only access point for a view, but
- this will be detailed later.
-
-* Calling `self.w` writes something to the output stream. Except for binary views
- (which do not generate text), it *must* be passed an Unicode string.
-
-* The proper way to get a file in :file:`data` directory is to use the `datadir_url`
- attribute of the incoming request (e.g. `self._cw`).
-
-I won't explain again the :func:`registration_callback` stuff, you should understand it
-now! If not, go back to previous posts in the series :)
-
-Fine. Now all I've to do is to add a bit of CSS to get it to behave nicely (which
-is not the case at all for now). I'll put all this in a :file:`cubes.sytweb.css`
-file, stored as usual in our :file:`data` directory:
-
-.. sourcecode:: css
-
-
- /* fixed full screen background image
- * as explained on http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
- *
- * syt update: set z-index=0 on the img instead of z-index=1 on div#page & co to
- * avoid pb with the user actions menu
- */
- img#bg-image {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 0;
- }
-
- div#page, table#header, div#footer {
- background: transparent;
- position: relative;
- }
-
- /* add some space around the logo
- */
- img#logo {
- padding: 5px 15px 0px 15px;
- }
-
- /* more dark font for metadata to have a chance to see them with the background
- * image
- */
- div.metadata {
- color: black;
- }
-
-You can see here stuff explained in the cited page, with only a slight modification
-explained in the comments, plus some additional rules to make things somewhat cleaner:
-
-* a bit of padding around the logo
-
-* darker metadata which appears by default below the content (the white frame in the page)
-
-To get this CSS file used everywhere in the site, I have to modify the :file:`uiprops.py` file
-introduced above:
-
-.. sourcecode:: python
-
- STYLESHEETS = sheet['STYLESHEETS'] + [data('cubes.sytweb.css')]
-
-.. Note::
- `sheet` is another predefined variable containing values defined by
- already process `:file:`uiprops.py`` file, notably the CubicWeb's one.
-
-Here we simply want our CSS in addition to CubicWeb's base CSS files, so we
-redefine the `STYLESHEETS` variable to existing CSS (accessed through the `sheet`
-variable) with our one added. I could also have done:
-
-.. sourcecode:: python
-
- sheet['STYLESHEETS'].append(data('cubes.sytweb.css'))
-
-But this is less interesting since we don't see the overriding mechanism...
-
-At this point, the site should start looking good, the background image being
-resized to fit the screen.
-
-.. image:: ../../images/tutos-photowebsite_background-image.png
-
-The final touch: let's customize CubicWeb's CSS to get less orange... By simply adding
-
-.. sourcecode:: python
-
- contextualBoxTitleBg = incontextBoxTitleBg = '#AAAAAA'
-
-and reloading the page we've just seen, we know have a nice greyed box instead of
-the orange one:
-
-.. image:: ../../images/tutos-photowebsite_grey-box.png
-
-This is because CubicWeb's CSS include some variables which are
-expanded by values defined in uiprops file. In our case we controlled the
-properties of the CSS `background` property of boxes with CSS class
-`contextualBoxTitleBg` and `incontextBoxTitleBg`.
-
-
-Step 2: configuring boxes
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Boxes present to the user some ways to use the application. Let's first do a few
-user interface tweaks in our :file:`views.py` file:
-
-.. sourcecode:: python
-
- from cubicweb.predicates import none_rset
- from cubicweb.web.views import bookmark
- from cubes.zone import views as zone
- from cubes.tag import views as tag
-
- # change bookmarks box selector so it's only displayed on startup views
- bookmark.BookmarksBox.__select__ = bookmark.BookmarksBox.__select__ & none_rset()
- # move zone box to the left instead of in the context frame and tweak its order
- zone.ZoneBox.context = 'left'
- zone.ZoneBox.order = 100
- # move tags box to the left instead of in the context frame and tweak its order
- tag.TagsBox.context = 'left'
- tag.TagsBox.order = 102
- # hide similarity box, not interested
- tag.SimilarityBox.visible = False
-
-The idea is to move all boxes in the left column, so we get more space for the
-photos. Now, serious things: I want a box similar to the tags box but to handle
-the `Person displayed_on File` relation. We can do this simply by adding a
-:class:`AjaxEditRelationCtxComponent` subclass to our views, as below:
-
-.. sourcecode:: python
-
- from logilab.common.decorators import monkeypatch
- from cubicweb import ValidationError
- from cubicweb.web.views import uicfg, component
- from cubicweb.web.views import basecontrollers
-
- # hide displayed_on relation using uicfg since it will be displayed by the box below
- uicfg.primaryview_section.tag_object_of(('*', 'displayed_on', '*'), 'hidden')
-
- class PersonBox(component.AjaxEditRelationCtxComponent):
- __regid__ = 'sytweb.displayed-on-box'
- # box position
- order = 101
- context = 'left'
- # define relation to be handled
- rtype = 'displayed_on'
- role = 'object'
- target_etype = 'Person'
- # messages
- added_msg = _('person has been added')
- removed_msg = _('person has been removed')
- # bind to js_* methods of the json controller
- fname_vocabulary = 'unrelated_persons'
- fname_validate = 'link_to_person'
- fname_remove = 'unlink_person'
-
-
- @monkeypatch(basecontrollers.JSonController)
- @basecontrollers.jsonize
- def js_unrelated_persons(self, eid):
- """return tag unrelated to an entity"""
- rql = "Any F + ' ' + S WHERE P surname S, P firstname F, X eid %(x)s, NOT P displayed_on X"
- return [name for (name,) in self._cw.execute(rql, {'x' : eid})]
-
-
- @monkeypatch(basecontrollers.JSonController)
- def js_link_to_person(self, eid, people):
- req = self._cw
- for name in people:
- name = name.strip().title()
- if not name:
- continue
- try:
- firstname, surname = name.split(None, 1)
- except:
- raise ValidationError(eid, {('displayed_on', 'object'): 'provide <first name> <surname>'})
- rset = req.execute('Person P WHERE '
- 'P firstname %(firstname)s, P surname %(surname)s',
- locals())
- if rset:
- person = rset.get_entity(0, 0)
- else:
- person = req.create_entity('Person', firstname=firstname,
- surname=surname)
- req.execute('SET P displayed_on X WHERE '
- 'P eid %(p)s, X eid %(x)s, NOT P displayed_on X',
- {'p': person.eid, 'x' : eid})
-
- @monkeypatch(basecontrollers.JSonController)
- def js_unlink_person(self, eid, personeid):
- self._cw.execute('DELETE P displayed_on X WHERE P eid %(p)s, X eid %(x)s',
- {'p': personeid, 'x': eid})
-
-
-You basically subclass to configure with some class attributes. The `fname_*`
-attributes give the name of methods that should be defined on the json control to
-make the AJAX part of the widget work: one to get the vocabulary, one to add a
-relation and another to delete a relation. These methods must start by a `js_`
-prefix and are added to the controller using the `@monkeypatch` decorator. In my
-case, the most complicated method is the one which adds a relation, since it
-tries to see if the person already exists, and else automatically create it,
-assuming the user entered "firstname surname".
-
-Let's see how it looks like on a file primary view:
-
-.. image:: ../../images/tutos-photowebsite_boxes.png
-
-Great, it's now as easy for me to link my pictures to people than to tag them.
-Also, visitors get a consistent display of these two pieces of information.
-
-.. Note::
- The ui component system has been refactored in `CubicWeb 3.10`_, which also
- introduced the :class:`AjaxEditRelationCtxComponent` class.
-
-
-Step 3: configuring facets
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The last feature we'll add today is facet configuration. If you access to the
-'/file' url, you'll see a set of 'facets' appearing in the left column. Facets
-provide an intuitive way to build a query incrementally, by proposing to the user
-various way to restrict the result set. For instance CubicWeb proposes a facet to
-restrict based on who created an entity; the tag cube proposes a facet to
-restrict based on tags; the zoe cube a facet to restrict based on geographical
-location, and so on. In that gist, I want to propose a facet to restrict based on
-the people displayed on the picture. To do so, there are various classes in the
-:mod:`cubicweb.web.facet` module which simply have to be configured using class
-attributes as we've done for the box. In our case, we'll define a subclass of
-:class:`RelationFacet`.
-
-.. Note::
-
- Since that's ui stuff, we'll continue to add code below to our
- :file:`views.py` file. Though we begin to have a lot of various code their, so
- it's may be a good time to split our views module into submodules of a `view`
- package. In our case of a simple application (glue) cube, we could start using
- for instance the layout below: ::
-
- views/__init__.py # uicfg configuration, facets
- views/layout.py # header/footer/background stuff
- views/components.py # boxes, adapters
- views/pages.py # index view, 404 view
-
-.. sourcecode:: python
-
- from cubicweb.web import facet
-
- class DisplayedOnFacet(facet.RelationFacet):
- __regid__ = 'displayed_on-facet'
- # relation to be displayed
- rtype = 'displayed_on'
- role = 'object'
- # view to use to display persons
- label_vid = 'combobox'
-
-Let's say we also want to filter according to the `visibility` attribute. This is
-even simpler as we just have to derive from the :class:`AttributeFacet` class:
-
-.. sourcecode:: python
-
- class VisibilityFacet(facet.AttributeFacet):
- __regid__ = 'visibility-facet'
- rtype = 'visibility'
-
-Now if I search for some pictures on my site, I get the following facets available:
-
-.. image:: ../../images/tutos-photowebsite_facets.png
-
-.. Note::
-
- By default a facet must be applyable to every entity in the result set and
- provide at leat two elements of vocabulary to be displayed (for instance you
- won't see the `created_by` facet if the same user has created all
- entities). This may explain why you don't see yours...
-
-
-Conclusion
-~~~~~~~~~~
-
-We started to see the power behind the infrastructure provided by the
-framework, both on the pure ui (CSS, Javascript) side and on the Python side
-(high level generic classes for components, including boxes and facets). We now
-have, with a few lines of code, a full-featured web site with a personalized look.
-
-Of course we'll probably want more as time goes, but we can now
-concentrate on making good pictures, publishing albums and sharing them with
-friends...
-
-
-
-.. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518
-.. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
--- a/doc/book/en/tutorials/base/blog-in-five-minutes.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _TutosBaseBlogFiveMinutes:
-
-Get a blog running in five minutes!
------------------------------------
-
-For Debian or Ubuntu users, first install the following packages
-(:ref:`DebianInstallation`)::
-
- cubicweb, cubicweb-dev, cubicweb-blog
-
-Windows or Mac OS X users must install |cubicweb| from source (see
-:ref:`SourceInstallation` and :ref:`WindowsInstallation`).
-
-Then create and initialize your instance::
-
- cubicweb-ctl create blog myblog
-
-You'll be asked a few questions, and you can keep the default answer for most of
-them. The one question you'll have to think about is the database you'll want to
-use for that instance. For a quick test, if you don't have `postgresql` installed
-and configured (see :ref:`PostgresqlConfiguration`), it's highly recommended to
-choose `sqlite` when asked for which database driver to use, since it has a much
-simple setup (no database server needed).
-
-One the process is completed (including database initialisation), you can start
-your instance by using: ::
-
- cubicweb-ctl start -D myblog
-
-The `-D` option activates the debugging mode. Removing it will launch the instance
-as a daemon in the background, and ``cubicweb-ctl stop myblog`` will stop
-it in that case.
-
-
-About file system permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Unless you installed from sources, the above commands assume that you have root
-access to the :file:`/etc/` directory. In order to initialize your instance as a
-regular user, within your home directory, you can use the :envvar:`CW_MODE`
-environment variable: ::
-
- export CW_MODE=user
-
-then create a :file:`~/etc/cubicweb.d` directory that will hold your instances.
-
-More information about how to configure your own environment is
-available in :ref:`ResourceMode`.
-
-
-Instance parameters
-~~~~~~~~~~~~~~~~~~~
-
-If you would like to change database parameters such as the database host or the
-user name used to connect to the database, edit the `sources` file located in the
-:file:`/etc/cubicweb.d/myblog` directory.
-
-Then relaunch the database creation::
-
- cubicweb-ctl db-create myblog
-
-Other parameters, like web server or emails parameters, can be modified in the
-:file:`/etc/cubicweb.d/myblog/all-in-one.conf` file.
-
-You'll have to restart the instance after modification in one of those files.
-
-This is it. Your blog is functional and running. Visit http://localhost:8080 and enjoy it!
-
--- a/doc/book/en/tutorials/base/conclusion.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-What's next?
-------------
-
-In this tutorial, we have seen that you can, right after the installation of
-|cubicweb|, build a web application in a few minutes by defining a data model as
-assembling cubes. You get a working application that you can then customize there
-and there while keeping something that works. This is important in agile
-development practices, you can right from the start of the project show things
-to customer and so take the right decision early in the process.
-
-The next steps will be to discover hooks, security, data sources, digging deeper
-into view writing and interface customisation... Yet a lot of fun stuff to
-discover! You will find more `tutorials and howtos`_ in the blog published on the
-CubicWeb.org website.
-
-.. _`tutorials and howtos`: http://www.cubicweb.org/view?rql=Any+X+ORDERBY+D+DESC+WHERE+X+is+BlogEntry%2C+T+tags+X%2C+T+name+IN+%28%22tutorial%22%2C+%22howto%22%29%2C+X+creation_date+D
--- a/doc/book/en/tutorials/base/customizing-the-application.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,539 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _TutosBaseCustomizingTheApplication:
-
-Customizing your application
-----------------------------
-
-So far so good. The point is that usually, you won't get enough by assembling
-cubes out-of-the-box. You will want to customize them, have a personal look and
-feel, add your own data model and so on. Or maybe start from scratch?
-
-So let's get a bit deeper and start coding our own cube. In our case, we want
-to customize the blog we created to add more features to it.
-
-
-Create your own cube
-~~~~~~~~~~~~~~~~~~~~
-
-First, notice that if you've installed |cubicweb| using Debian packages, you will
-need the additional ``cubicweb-dev`` package to get the commands necessary to
-|cubicweb| development. All `cubicweb-ctl` commands are described in details in
-:ref:`cubicweb-ctl`.
-
-Once your |cubicweb| development environment is set up, you can create a new
-cube::
-
- cubicweb-ctl newcube myblog
-
-This will create in the cubes directory (:file:`/path/to/grshell/cubes` for source
-installation, :file:`/usr/share/cubicweb/cubes` for Debian packages installation)
-a directory named :file:`blog` reflecting the structure described in
-:ref:`cubelayout`.
-
-For packages installation, you can still create new cubes in your home directory
-using the following configuration. Let's say you want to develop your new cubes
-in `~src/cubes`, then set the following environment variables: ::
-
- CW_CUBES_PATH=~/src/cubes
-
-and then create your new cube using: ::
-
- cubicweb-ctl newcube --directory=~/src/cubes myblog
-
-.. Note::
-
- We previously used `myblog` as the name of our *instance*. We're now creating
- a *cube* with the same name. Both are different things. We'll now try to
- specify when we talk about one or another, but keep in mind this difference.
-
-
-Cube metadata
-~~~~~~~~~~~~~
-
-A simple set of metadata about your cube are stored in the :file:`__pkginfo__.py`
-file. In our case, we want to extend the blog cube, so we have to tell that our
-cube depends on this cube, by modifying the ``__depends__`` dictionary in that
-file:
-
-.. sourcecode:: python
-
- __depends__ = {'cubicweb': '>= 3.10.7',
- 'cubicweb-blog': None}
-
-where the ``None`` means we do not depends on a particular version of the cube.
-
-.. _TutosBaseCustomizingTheApplicationDataModel:
-
-Extending the data model
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-The data model or schema is the core of your |cubicweb| application. It defines
-the type of content your application will handle. It is defined in the file
-:file:`schema.py` of the cube.
-
-
-Defining our model
-******************
-
-For the sake of example, let's say we want a new entity type named `Community`
-with a name, a description. A `Community` will hold several blogs.
-
-.. sourcecode:: python
-
- from yams.buildobjs import EntityType, RelationDefinition, String, RichString
-
- class Community(EntityType):
- name = String(maxsize=50, required=True)
- description = RichString()
-
- class community_blog(RelationDefinition):
- subject = 'Community'
- object = 'Blog'
- cardinality = '*?'
- composite = 'subject'
-
-The first step is the import from the :mod:`yams` package necessary classes to build
-the schema.
-
-This file defines the following:
-
-* a `Community` has a title and a description as attributes
-
- - the name is a string that is required and can't be longer than 50 characters
-
- - the description is a string that is not constrained and may contains rich
- content such as HTML or Restructured text.
-
-* a `Community` may be linked to a `Blog` using the `community_blog` relation
-
- - ``*`` means a community may be linked to 0 to N blog, ``?`` means a blog may
- be linked to 0 to 1 community. For completeness, remember that you can also
- use ``+`` for 1 to N, and ``1`` for single, mandatory relation (e.g. one to one);
-
- - this is a composite relation where `Community` (e.g. the subject of the
- relation) is the composite. That means that if you delete a community, its
- blog will be deleted as well.
-
-Of course, there are a lot of other data types and things such as constraints,
-permissions, etc, that may be defined in the schema, but those won't be covered
-in this tutorial.
-
-Notice that our schema refers to the `Blog` entity type which is not defined
-here. But we know this type is available since we depend on the `blog` cube
-which is defining it.
-
-
-Applying changes to the model into our instance
-***********************************************
-
-Now the problem is that we created an instance using the `blog` cube, not our
-`myblog` cube, so if we don't do anything there is no way that we'll see anything
-changing in the instance.
-
-One easy way, as we've no really valuable data in the instance would be to trash and recreated it::
-
- cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
- cubicweb-ctl delete myblog
- cubicweb-ctl create myblog
- cubicweb-ctl start -D myblog
-
-Another way is to add our cube to the instance using the cubicweb-ctl shell
-facility. It's a python shell connected to the instance with some special
-commands available to manipulate it (the same as you'll have in migration
-scripts, which are not covered in this tutorial). In that case, we're interested
-in the `add_cube` command: ::
-
- $ cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
- $ cubicweb-ctl shell myblog
- entering the migration python shell
- just type migration commands or arbitrary python code and type ENTER to execute it
- type "exit" or Ctrl-D to quit the shell and resume operation
- >>> add_cube('myblog')
- >>>
- $ cubicweb-ctl start -D myblog
-
-The `add_cube` command is enough since it automatically updates our
-application to the cube's schema. There are plenty of other migration
-commands of a more finer grain. They are described in :ref:`migration`
-
-As explained, leave the shell by typing Ctrl-D. If you restart the instance and
-take another look at the schema, you'll see that changes to the data model have
-actually been applied (meaning database schema updates and all necessary stuff
-has been done).
-
-.. image:: ../../images/tutos-base_myblog-schema_en.png
- :alt: the instance schema after adding our cube
-
-If you follow the 'info' link in the user pop-up menu, you'll also see that the
-instance is using blog and myblog cubes.
-
-.. image:: ../../images/tutos-base_myblog-siteinfo_en.png
- :alt: the instance schema after adding our cube
-
-You can now add some communities, link them to blog, etc... You'll see that the
-framework provides default views for this entity type (we have not yet defined any
-view for it!), and also that the blog primary view will show the community it's
-linked to if any. All this thanks to the model driven interface provided by the
-framework.
-
-You'll then be able to redefine each of them according to your needs
-and preferences. We'll now see how to do such thing.
-
-.. _TutosBaseCustomizingTheApplicationCustomViews:
-
-Defining your views
-~~~~~~~~~~~~~~~~~~~
-
-|cubicweb| provides a lot of standard views in directory
-:file:`cubicweb/web/views/`. We already talked about 'primary' and 'list' views,
-which are views which apply to one ore more entities.
-
-A view is defined by a python class which includes:
-
- - an identifier: all objects used to build the user interface in |cubicweb| are
- recorded in a registry and this identifier will be used as a key in that
- registry. There may be multiple views for the same identifier.
-
- - a *selector*, which is a kind of filter telling how well a view suit to a
- particular context. When looking for a particular view (e.g. given an
- identifier), |cubicweb| computes for each available view with that identifier
- a score which is returned by the selector. Then the view with the highest
- score is used. The standard library of predicates is in
- :mod:`cubicweb.predicates`.
-
-A view has a set of methods inherited from the :class:`cubicweb.view.View` class,
-though you usually don't derive directly from this class but from one of its more
-specific child class.
-
-Last but not least, |cubicweb| provides a set of default views accepting any kind
-of entities.
-
-Want a proof? Create a community as you've already done for other entity types
-through the index page, you'll then see something like that:
-
-.. image:: ../../images/tutos-base_myblog-community-default-primary_en.png
- :alt: the default primary view for our community entity type
-
-
-If you notice the weird messages that appear in the page: those are messages
-generated for the new data model, which have no translation yet. To fix that,
-we'll have to use dedicated `cubicweb-ctl` commands:
-
-.. sourcecode: bash
-
- cubicweb-ctl i18ncube myblog # build/update cube's message catalogs
- # then add translation into .po file into the cube's i18n directory
- cubicweb-ctl i18ninstance myblog # recompile instance's message catalogs
- cubicweb-ctl restart -D myblog # instance has to be restarted to consider new catalogs
-
-You'll then be able to redefine each of them according to your needs and
-preferences. So let's see how to do such thing.
-
-Changing the layout of the application
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The layout is the general organization of the pages in the site. Views that generate
-the layout are sometimes referred to as 'templates'. They are implemented in the
-framework in the module :mod:`cubicweb.web.views.basetemplates`. By overriding
-classes in this module, you can customize whatever part you wish of the default
-layout.
-
-But notice that |cubicweb| provides many other ways to customize the
-interface, thanks to actions and components (which you can individually
-(de)activate, control their location, customize their look...) as well as
-"simple" CSS customization. You should first try to achieve your goal using such
-fine grained parametrization rather then overriding a whole template, which usually
-embeds customisation access points that you may loose in the process.
-
-But for the sake of example, let's say we want to change the generic page
-footer... We can simply add to the module ``views`` of our cube,
-e.g. :file:`cubes/myblog/views.py`, the code below:
-
-.. sourcecode:: python
-
- from cubicweb.web.views import basetemplates
-
- class MyHTMLPageFooter(basetemplates.HTMLPageFooter):
-
- def footer_content(self):
- self.w(u'This website has been created with <a href="http://cubicweb.org">CubicWeb</a>.')
-
- def registration_callback(vreg):
- vreg.register_all(globals().values(), __name__, (MyHTMLPageFooter,))
- vreg.register_and_replace(MyHTMLPageFooter, basetemplates.HTMLPageFooter)
-
-
-* Our class inherits from the default page footer to ease getting things right,
- but this is not mandatory.
-
-* When we want to write something to the output stream, we simply call `self.w`,
- with *must be passed an unicode string*.
-
-* The latest function is the most exotic stuff. The point is that without it, you
- would get an error at display time because the framework wouldn't be able to
- choose which footer to use between :class:`HTMLPageFooter` and
- :class:`MyHTMLPageFooter`, since both have the same selector, hence the same
- score... In this case, we want our footer to replace the default one, so we have
- to define a :func:`registration_callback` function to control object
- registration: the first instruction tells to register everything in the module
- but the :class:`MyHTMLPageFooter` class, then the second to register it instead
- of :class:`HTMLPageFooter`. Without this function, everything in the module is
- registered blindly.
-
-.. Note::
-
- When a view is modified while running in debug mode, it is not required to
- restart the instance server. Save the Python file and reload the page in your
- web browser to view the changes.
-
-We will now have this simple footer on every page of the site.
-
-
-Primary view customization
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The 'primary' view (i.e. any view with the identifier set to 'primary') is the one used to
-display all the information about a single entity. The standard primary view is one
-of the most sophisticated views of all. It has several customisation points, but
-its power comes with `uicfg`, allowing you to control it without having to
-subclass it.
-
-However this is a bit off-topic for this first tutorial. Let's say we simply want a
-custom primary view for my `Community` entity type, using directly the view
-interface without trying to benefit from the default implementation (you should
-do that though if you're rewriting reusable cubes; everything is described in more
-details in :ref:`primary_view`).
-
-
-So... Some code! That we'll put again in the module ``views`` of our cube.
-
-.. sourcecode:: python
-
- from cubicweb.predicates import is_instance
- from cubicweb.web.views import primary
-
- class CommunityPrimaryView(primary.PrimaryView):
- __select__ = is_instance('Community')
-
- def cell_call(self, row, col):
- entity = self.cw_rset.get_entity(row, col)
- self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
- if entity.description:
- self.w(u'<p>%s</p>' % entity.printable_value('description'))
-
-What's going on here?
-
-* Our class inherits from the default primary view, here mainly to get the correct
- view identifier, since we don't use any of its features.
-
-* We set on it a selector telling that it only applies when trying to display
- some entity of the `Community` type. This is enough to get an higher score than
- the default view for entities of this type.
-
-* View applying to entities usually have to define `cell_call` as entry point,
- and are given `row` and `col` arguments tell to which entity in the result set
- the view is applied. We can then get this entity from the result set
- (`self.cw_rset`) by using the `get_entity` method.
-
-* To ease thing, we access our entity's attribute for display using its
- printable_value method, which will handle formatting and escaping when
- necessary. As you can see, you can also access attributes by their name on the
- entity to get the raw value.
-
-
-You can now reload the page of the community we just created and see the changes.
-
-.. image:: ../../images/tutos-base_myblog-community-custom-primary_en.png
- :alt: the custom primary view for our community entity type
-
-We've seen here a lot of thing you'll have to deal with to write views in
-|cubicweb|. The good news is that this is almost everything that is used to
-build higher level layers.
-
-.. Note::
-
- As things get complicated and the volume of code in your cube increases, you can
- of course still split your views module into a python package with subpackages.
-
-You can find more details about views and selectors in :ref:`Views`.
-
-
-Write entities to add logic in your data
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-|cubicweb| provides an ORM to easily programmaticaly manipulate
-entities (just like the one we have fetched earlier by calling
-`get_entity` on a result set). By default, entity
-types are instances of the :class:`AnyEntity` class, which holds a set of
-predefined methods as well as property automatically generated for
-attributes/relations of the type it represents.
-
-You can redefine each entity to provide additional methods or whatever you want
-to help you write your application. Customizing an entity requires that your
-entity:
-
-- inherits from :class:`cubicweb.entities.AnyEntity` or any subclass
-
-- defines a :attr:`__regid__` linked to the corresponding data type of your schema
-
-You may then want to add your own methods, override default implementation of some
-method, etc...
-
-.. sourcecode:: python
-
- from cubicweb.entities import AnyEntity, fetch_config
-
-
- class Community(AnyEntity):
- """customized class for Community entities"""
- __regid__ = 'Community'
-
- fetch_attrs, cw_fetch_order = fetch_config(['name'])
-
- def dc_title(self):
- return self.name
-
- def display_cw_logo(self):
- return 'CubicWeb' in self.description
-
-In this example:
-
-* we used convenience :func:`fetch_config` function to tell which attributes
- should be prefetched by the ORM when looking for some related entities of this
- type, and how they should be ordered
-
-* we overrode the standard `dc_title` method, used in various place in the interface
- to display the entity (though in this case the default implementation would
- have had the same result)
-
-* we implemented here a method :meth:`display_cw_logo` which tests if the blog
- entry title contains 'CW'. It can then be used when you're writing code
- involving 'Community' entities in your views, hooks, etc. For instance, you can
- modify your previous views as follows:
-
-.. sourcecode:: python
-
-
- class CommunityPrimaryView(primary.PrimaryView):
- __select__ = is_instance('Community')
-
- def cell_call(self, row, col):
- entity = self.cw_rset.get_entity(row, col)
- self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
- if entity.display_cw_logo():
- self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
- if entity.description:
- self.w(u'<p>%s</p>' % entity.printable_value('description'))
-
-Then each community whose description contains 'CW' is shown with the |cubicweb|
-logo in front of it.
-
-.. Note::
-
- As for view, you don't have to restart your instance when modifying some entity
- classes while your server is running in debug mode, the code will be
- automatically reloaded.
-
-
-Extending the application by using more cubes!
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-One of the goal of the |cubicweb| framework was to have truly reusable
-components. To do so, they must both behave nicely when plugged into the
-application and be easily customisable, from the data model to the user
-interface. And I think the result is pretty successful, thanks to system such as
-the selection mechanism and the choice to write views as python code which allows
-to build our page using true object oriented programming techniques, that no
-template language provides.
-
-
-A library of standard cubes is available from `CubicWeb Forge`_, to address a
-lot of common concerns such has manipulating people, files, things to do, etc. In
-our community blog case, we could be interested for instance in functionalities
-provided by the `comment` and `tag` cubes. The former provides threaded
-discussion functionalities, the latter a simple tag mechanism to classify content.
-Let's say we want to try those. We will first modify our cube's :file:`__pkginfo__.py`
-file:
-
-.. sourcecode:: python
-
- __depends__ = {'cubicweb': '>= 3.10.7',
- 'cubicweb-blog': None,
- 'cubicweb-comment': None,
- 'cubicweb-tag': None}
-
-Now, we'll simply tell on which entity types we want to activate the 'comment'
-and 'tag' facilities by adding respectively the 'comments' and 'tags' relations on
-them in our schema (:file:`schema.py`).
-
-.. sourcecode:: python
-
- class comments(RelationDefinition):
- subject = 'Comment'
- object = 'BlogEntry'
- cardinality = '1*'
- composite = 'object'
-
- class tags(RelationDefinition):
- subject = 'Tag'
- object = ('Community', 'BlogEntry')
-
-
-So in the case above we activated comments on `BlogEntry` entities and tags on
-both `Community` and `BlogEntry`. Various views from both `comment` and `tag`
-cubes will then be automatically displayed when one of those relations is
-supported.
-
-Let's synchronize the data model as we've done earlier: ::
-
-
- $ cubicweb-ctl stop myblog
- $ cubicweb-ctl shell myblog
- entering the migration python shell
- just type migration commands or arbitrary python code and type ENTER to execute it
- type "exit" or Ctrl-D to quit the shell and resume operation
- >>> add_cubes(('comment', 'tag'))
- >>>
-
-Then restart the instance. Let's look at a blog entry:
-
-.. image:: ../../images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png
- :alt: the primary view for a blog entry with comments and tags activated
-
-As you can see, we now have a box displaying tags and a section proposing to add
-a comment and displaying existing one below the post. All this without changing
-anything in our views, thanks to the design of generic views provided by the
-framework. Though if we take a look at a community, we won't see the tags box!
-That's because by default this box try to locate itself in the left column within
-the white frame, and this column is handled by the primary view we
-hijacked. Let's change our view to make it more extensible, by keeping both our
-custom rendering but also extension points provided by the default
-implementation.
-
-
-.. sourcecode:: python
-
- class CommunityPrimaryView(primary.PrimaryView):
- __select__ = is_instance('Community')
-
- def render_entity_title(self, entity):
- self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
-
- def render_entity_attributes(self, entity):
- if entity.display_cw_logo():
- self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
- if entity.description:
- self.w(u'<p>%s</p>' % entity.printable_value('description'))
-
-It appears now properly:
-
-.. image:: ../../images/tutos-base_myblog-community-taggable-primary_en.png
- :alt: the custom primary view for a community entry with tags activated
-
-You can control part of the interface independently from each others, piece by
-piece. Really.
-
-
-
-.. _`CubicWeb Forge`: http://www.cubicweb.org/project
--- a/doc/book/en/tutorials/base/discovering-the-ui.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-
-.. _TutosBaseDiscoveringTheUI:
-
-Discovering the web interface
------------------------------
-
-You can now access your web instance to create blogs and post messages
-by visiting the URL http://localhost:8080/.
-
-By default, anonymous access is disabled, so a login form will appear. If you
-asked to allow anonymous access when initializing the instance, click on the
-'login' link in the top right hand corner. To login, you need then use the admin
-account you specified at the time you initialized the database with
-``cubicweb-ctl create``.
-
-.. image:: ../../images/tutos-base_login-form_en.png
- :alt: the login form
-
-
-Once authenticated, you can start playing with your instance. The default index
-page looks like the following:
-
-.. image:: ../../images/tutos-base_index_en.png
- :alt: the index page
-
-
-Minimal configuration
-~~~~~~~~~~~~~~~~~~~~~
-
-Before creating entities, let's change that 'unset title' thing that appears
-here and there. This comes from a |cubicweb| system properties. To set it,
-click on the 'site configuration link' in the pop-up menu behind your login name
-in the upper left-hand corner
-
-.. image:: ../../images/tutos-base_user-menu_en.png
- :alt: the user pop-up menu
-
-The site title is in the 'Ui' section. Simply set it to the desired value and
-click the 'validate' button.
-
-.. image:: ../../images/tutos-base_siteconfig_en.png
- :alt: the site configuration form
-
-You should see a 'changes applied' message. You can now go back to the
-index page by clicking on the |cubicweb| logo in the upper left-hand corner.
-
-You will much likely still see 'unset title' at this point. This is because by
-default the index page is cached. Force a refresh of the page (by typing Ctrl-R
-in Firefox for instance) and you should now see the title you entered.
-
-
-Adding entities
-~~~~~~~~~~~~~~~
-
-The ``blog`` cube defines several entity types, among them ``Blog`` which is a
-container for ``BlogEntry`` (i.e. posts) on a particular topic. We can get a
-graphical view of the schema by clicking on the 'site schema' link in the user
-pop-up menu we've already seen:
-
-.. image:: ../../images/tutos-base_schema_en.png
- :alt: graphical view of the schema (aka data-model)
-
-Nice isn't it? Notice that this, as most other stuff we'll see in this tutorial,
-is generated by the framework according to the model of the application. In our
-case, the model defined by the ``blog`` cube.
-
-Now let us create a few of these entities.
-
-
-Add a blog
-**********
-
-Clicking on the `[+]` at the left of the 'Blog' link on the index page will lead
-you to an HTML form to create a blog.
-
-.. image:: ../../images/tutos-base_blog-form_en.png
- :alt: the blog creation form
-
-For instance, call this new blog 'Tech-blog' and type in 'everything about
-technology' as the description , then validate the form by clicking on
-'Validate'. You will be redirected to the `primary` view of the newly created blog.
-
-.. image:: ../../images/tutos-base_blog-primary_en.png
- :alt: the blog primary view
-
-
-Add a blog post
-***************
-
-There are several ways to add a blog entry. The simplest is to click on the 'add
-blog entry' link in the actions box on viewing the blog you have just created.
-You will then see a form to create a post, with a 'blog entry of' field preset
-to the blog we're coming from. Enter a title, some content, click the 'validate'
-button and you're done. You will be redirected to the blog primary view, though you
-now see that it contains the blog post you've just created.
-
-.. image:: ../../images/tutos-base_blog-primary-after-post-creation_en.png
- :alt: the blog primary view after creation of a post
-
-Notice there are some new boxes that appears in the left column.
-
-You can achieve the same thing by following the same path as we did for the blog
-creation, e.g. by clicking on the `[+]` at the left of the 'Blog entry' link on
-the index page. The diffidence being that since there is no context information,
-the 'blog entry of' selector won't be preset to the blog.
-
-
-If you click on the 'modify' link of the action box, you are back to
-the form to edit the entity you just created, except that the form now
-has another section with a combo-box entitled 'add relation'. It
-provisos a generic way to edit relations which don't appears in the
-above form. Choose the relation you want to add and a second combo box
-appears where you can pick existing entities. If there are too many
-of them, you will be offered to navigate to the target entity, that is
-go away from the form and go back to it later, once you've selected
-the entity you want to link with.
-
-.. image:: ../../images/tutos-base_form-generic-relations_en.png
- :alt: the generic relations combo box
-
-This combo box can't appear until the entity is actually created. That's why you
-haven't seen it at creation time. You could also have hit 'Apply' instead of
-'validate' and it would have showed up.
-
-
-About ui auto-adaptation
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-One of the things that make |cubicweb| different of other frameworks is
-its automatic user interface that adapts itself according to the data being
-displayed. Let's see an example.
-
-If you go back to the home page an click on the 'Blog' link, you will be redirected
-to the primary view of the blog, the same we've seen earlier. Now, add another
-blog, go back to the index page, and click again on this link. You will see
-a very different view (namely the 'list' view).
-
-.. image:: ../../images/tutos-base_blogs-list_en.png
- :alt: the list view when there are more than one blog to display
-
-This is because in the first case, the framework chose to use the 'primary'
-view since there was only one entity in the data to be displayed. Now that there
-are two entities, the 'list' view is more appropriate and hence is being used.
-
-There are various other places where |cubicweb| adapts to display data in the best
-way, the main being provided by the view *selection* mechanism that will be detailed
-later.
-
-
-Digging deeper
-~~~~~~~~~~~~~~
-
-By following principles explained below, you should now be able to
-create new users for your application, to configure with a finer
-grain, etc... You will notice that the index page lists a lot of types
-you don't know about. Most are built-in types provided by the framework
-to make the whole system work. You may ignore them in a first time and
-discover them as time goes.
-
-One thing that is worth playing with is the search box. It may be used in various
-way, from simple full text search to advanced queries using the :ref:`RQL` .
--- a/doc/book/en/tutorials/base/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _TutosBase:
-
-Building a simple blog with |cubicweb|
-======================================
-
-|cubicweb| is a semantic web application framework that favors reuse and
-object-oriented design.
-
-
-This tutorial is designed to help in your very first steps to start with
-|cubicweb|. We will tour through basic concepts such as:
-
-* getting an application running by using existing components
-* discovering the default user interface
-* basic extending and customizing the look and feel of that application
-
-More advanced concepts are covered in :ref:`TutosPhotoWebSite`.
-
-
-.. _TutosBaseVocab:
-
-Some vocabulary
----------------
-
-|cubicweb| comes with a few words of vocabulary that you should know to
-understand what we're talking about. To follow this tutorial, you should at least
-know that:
-
-* a `cube` is a component that usually includes a model defining some data types
- and a set of views to display them. A cube can be built by assembling other
- cubes;
-
-* an `instance` is a specific installation of one or more cubes and includes
- configuration files, a web server and a database.
-
-Reading :ref:`Concepts` for more vocabulary will be required at some point.
-
-Now, let's start the hot stuff!
-
-.. toctree::
- :maxdepth: 2
-
- blog-in-five-minutes
- discovering-the-ui
- customizing-the-application
- conclusion
--- a/doc/book/en/tutorials/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-.. _Tutorials:
-
----------
-Tutorials
----------
-
-We present two tutorials of different levels. The blog building
-tutorial introduces one smoothly to the basic concepts.
-
-Then there is a photo gallery construction tutorial which highlights
-more advanced concepts such as unit tests, security settings,
-migration scripts.
-
-.. toctree::
- :maxdepth: 1
- :numbered:
-
- base/index
- advanced/index
- tools/windmill.rst
- textreports/index
--- a/doc/book/en/tutorials/textreports/index.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Writing text reports with RestructuredText
-==========================================
-
-|cubicweb| offers several text formats for the RichString type used in schemas,
-including restructuredtext.
-
-Three additional restructuredtext roles are defined by |cubicweb|:
-
-.. autofunction:: cubicweb.ext.rest.eid_reference_role
-.. autofunction:: cubicweb.ext.rest.rql_role
-.. autofunction:: cubicweb.ext.rest.bookmark_role
--- a/doc/book/en/tutorials/tools/windmill.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,229 +0,0 @@
-==========================
-Use Windmill with CubicWeb
-==========================
-
-Windmill_ implements cross browser testing, in-browser recording and playback,
-and functionality for fast accurate debugging and test environment integration.
-
-.. _Windmill: http://www.getwindmill.com/
-
-`Online features list <http://www.getwindmill.com/features>`_ is available.
-
-
-Installation
-============
-
-Windmill
---------
-
-You have to install Windmill manually for now. If you're using Debian, there is
-no binary package (`yet <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=579109>`_).
-
-The simplest solution is to use a *setuptools/pip* command (for a clean
-environment, take a look to the `virtualenv
-<http://pypi.python.org/pypi/virtualenv>`_ project as well)::
-
- $ pip install windmill
- $ curl -O http://github.com/windmill/windmill/tarball/master
-
-However, the Windmill project doesn't release frequently. Our recommandation is
-to used the last snapshot of the Git repository::
-
- $ git clone git://github.com/windmill/windmill.git HEAD
- $ cd windmill
- $ python setup.py develop
-
-Install instructions are `available <http://wiki.github.com/windmill/windmill/installing>`_.
-
-Be sure to have the windmill module in your PYTHONPATH afterwards::
-
- $ python -c "import windmill"
-
-X dummy
--------
-
-In order to reduce unecessary system load from your test machines, It's
-recommended to use X dummy server for testing the Unix web clients, you need a
-dummy video X driver (as xserver-xorg-video-dummy package in Debian) coupled
-with a light X server as `Xvfb <http://en.wikipedia.org/wiki/Xvfb>`_.
-
- The dummy driver is a special driver available with the XFree86 DDX. To use
- the dummy driver, simply substitue it for your normal card driver in the
- Device section of your xorg.conf configuration file. For example, if you
- normally uses an ati driver, then you will have a Device section with
- Driver "ati" to let the X server know that you want it to load and use the
- ati driver; however, for these conformance tests, you would change that
- line to Driver "dummy" and remove any other ati specific options from the
- Device section.
-
- *From: http://www.x.org/wiki/XorgTesting*
-
-Then, you can run the X server with the following command ::
-
- $ /usr/bin/X11/Xvfb :1 -ac -screen 0 1280x1024x8 -fbdir /tmp
-
-
-Windmill usage
-==============
-
-Record your use case
---------------------
-
-- start your instance manually
-- start Windmill_ with url site as last argument (read Usage_ or use *'-h'*
- option to find required command line arguments)
-- use the record button
-- click on save to obtain python code of your use case
-- copy the content to a new file in a *windmill* directory
-
-.. _Usage: http://wiki.github.com/windmill/windmill/running-tests
-
-If you are using firefox as client, consider the "firebug" option.
-
-If you have a running instance, you can refine the test by the *loadtest* windmill option::
-
- $ windmill -m firebug loadtest=<test_file.py> <instance url>
-
-Or use the internal windmill shell to explore available commands::
-
- $ windmill -m firebug shell <instance url>
-
-And enter python commands:
-
-.. sourcecode:: python
-
- >>> load_test(<your test file>)
- >>> run_test(<your test file>)
-
-
-
-Integrate Windmill tests into CubicWeb
-======================================
-
-Set environment
----------------
-
-You have to create a new unit test file and a `windmill` directory and copy all
-your windmill use case into it.
-
-.. sourcecode:: python
-
- # test_windmill.py
-
- # Run all scenarii found in windmill directory
- from cubicweb.devtools.cwwindmill import (CubicWebWindmillUseCase,
- unittest_main)
-
- if __name__ == '__main__':
- unittest_main()
-
-Run your tests
---------------
-
-You can easily run your windmill test suite through `pytest` or :mod:`unittest`.
-You have to copy a *test_windmill.py* file from :mod:`web.test`.
-
-To run your test series::
-
- $ pytest test/test_windmill.py
-
-By default, CubicWeb will use **firefox** as the default browser and will try
-to run test instance server on localhost. In the general case, You've no need
-to change anything.
-
-Check :class:`cubicweb.devtools.cwwindmill.CubicWebWindmillUseCase` for
-Windmill configuration. You can edit windmill settings with following class attributes:
-
-* browser
- identification string (firefox|ie|safari|chrome) (firefox by default)
-* test_dir
- testing file path or directory (windmill directory under your unit case
- file by default)
-* edit_test
- load and edit test for debugging (False by default)
-
-Examples:
-
-.. sourcecode:: python
-
- browser = 'firefox'
- test_dir = osp.join(__file__, 'windmill')
- edit_test = False
-
-If you want to change cubicweb test server parameters, you can check class
-variables from :class:`CubicWebServerConfig` or inherit it with overriding the
-:attr:`configcls` attribute in :class:`CubicWebServerTC` ::
-
-.. sourcecode:: python
-
- class OtherCubicWebServerConfig(CubicWebServerConfig):
- port = 9999
-
- class NewCubicWebServerTC(CubicWebServerTC):
- configcls = OtherCubicWebServerConfig
-
-For instance, CubicWeb framework windmill tests can be manually run by::
-
- $ pytest web/test/test_windmill.py
-
-Edit your tests
----------------
-
-You can toggle the `edit_test` variable to enable test edition.
-
-But if you are using `pytest` as test runner, use the `-i` option directly.
-The test series will be loaded and you can run assertions step-by-step::
-
- $ pytest -i test/test_windmill.py
-
-In this case, the `firebug` extension will be loaded automatically for you.
-
-Afterwards, don't forget to save your edited test into the right file (no autosave feature).
-
-Best practises
---------------
-
-Don't run another instance on the same port. You risk to silence some
-regressions (test runner will automatically fail in further versions).
-
-Start your use case by using an assert on the expected primary url page.
-Otherwise all your tests could fail without clear explanation of the used
-navigation.
-
-In the same location of the *test_windmill.py*, create a *windmill/* with your
-windmill recorded use cases.
-
-
-Caveats
-=======
-
-File Upload
------------
-
-Windmill can't do file uploads. This is a limitation of browser Javascript
-support / sandboxing, not of Windmill per se. It would be nice if there were
-some command that would prime the Windmill HTTP proxy to add a particular file
-to the next HTTP request that comes through, so that uploads could at least be
-faked.
-
-.. http://groups.google.com/group/windmill-dev/browse_thread/thread/cf9dc969722bd6bb/01aa18fdd652f7ff?lnk=gst&q=input+type+file#01aa18fdd652f7ff
-
-.. http://davisagli.com/blog/in-browser-integration-testing-with-windmill
-
-.. http://groups.google.com/group/windmill-dev/browse_thread/thread/b7bebcc38ed30dc7
-
-
-Preferences
-===========
-
-A *.windmill/prefs.py* could be used to redefine default configuration values.
-
-.. define CubicWeb preferences in the parent test case instead with a dedicated firefox profile
-
-For managing browser extensions, read `advanced topic chapter
-<http://wiki.github.com/windmill/windmill/advanced-topics>`_.
-
-More configuration examples could be seen in *windmill/conf/global_settings.py*
-as template.
-
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/intro/concepts.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,297 @@
+.. -*- coding: utf-8 -*-
+
+.. _Concepts:
+
+The Core Concepts of |cubicweb|
+===============================
+
+This section defines some terms and core concepts of the |cubicweb| framework. To
+avoid confusion while reading this book, take time to go through the following
+definitions and use this section as a reference during your reading.
+
+
+.. _Cube:
+
+Cubes
+-----
+
+A cube is a software component made of three parts:
+
+- its data model (:mod:`schema`),
+- its logic (:mod:`entities`) and
+- its user interface (:mod:`views`).
+
+A cube can use other cubes as building blocks and assemble them to provide a
+whole with richer functionnalities than its parts. The cubes `cubicweb-blog`_ and
+`cubicweb-comment`_ could be used to make a cube named *myblog* with commentable
+blog entries.
+
+The `CubicWeb.org Forge`_ offers a large number of cubes developed by the community
+and available under a free software license.
+
+.. note::
+
+ The command :command:`cubicweb-ctl list` displays the list of available cubes.
+
+.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
+.. _`cubicweb-blog`: http://www.cubicweb.org/project/cubicweb-blog
+.. _`cubicweb-comment`: http://www.cubicweb.org/project/cubicweb-comment
+
+
+.. _Instance:
+
+Instances
+---------
+
+An instance is a runnable application installed on a computer and
+based on one or more cubes.
+
+The instance directory contains the configuration files. Several
+instances can be created and based on the same cube. For example,
+several software forges can be set up on one computer system based on
+the `cubicweb-forge`_ cube.
+
+.. _`cubicweb-forge`: http://www.cubicweb.org/project/cubicweb-forge
+
+The command :command:`cubicweb-ctl list` also displays the list of instances
+installed on your system.
+
+.. note::
+
+ The term application is used to refer to "something that should do something as
+ a whole", eg more like a project and so can refer to an instance or to a cube,
+ depending on the context. This book will try to use *application*, *cube* and
+ *instance* as appropriate.
+
+
+.. _RepositoryIntro:
+
+Data Repository
+---------------
+
+The data repository [1]_ encapsulates and groups an access to one or
+more data sources (including SQL databases, LDAP repositories, other
+|cubicweb| instance repositories, filesystems, Google AppEngine's
+DataStore, etc).
+
+All interactions with the repository are done using the `Relation Query Language`
+(:ref:`RQL`). The repository federates the data sources and hides them from the
+querier, which does not realize when a query spans several data sources
+and requires running sub-queries and merges to complete.
+
+Application logic can be mapped to data events happenning within the
+repository, like creation of entities, deletion of relations,
+etc. This is used for example to send email notifications when the
+state of an object changes. See :ref:`HookIntro` below.
+
+.. [1] not to be confused with a Mercurial repository or a Debian repository.
+.. _`Python Remote Objects`: http://pythonhosted.org/Pyro4/
+
+.. _WebEngineIntro:
+
+Web Engine
+----------
+
+The web engine replies to http requests and runs the user interface.
+
+By default the web engine provides a `CRUD`_ user interface based on
+the data model of the instance. Entities can be created, displayed,
+updated and deleted. As the default user interface is not very fancy,
+it is usually necessary to develop your own.
+
+.. _`CRUD`: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
+
+.. _SchemaIntro:
+
+Schema (Data Model)
+-------------------
+
+The data model of a cube is described as an entity-relationship schema using a
+comprehensive language made of Python classes imported from the yams_ library.
+
+.. _yams: http://www.logilab.org/project/yams/
+
+An `entity type` defines a sequence of attributes. Attributes may be
+of the following types: `String`, `Int`, `Float`, `Boolean`, `Date`,
+`Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`.
+
+A `relation type` is used to define an oriented binary relation
+between entity types. The left-hand part of a relation is named the
+`subject` and the right-hand part is named the `object`.
+
+A `relation definition` is a triple (*subject entity type*, *relation type*, *object
+entity type*) associated with a set of properties such as cardinality,
+constraints, etc.
+
+Permissions can be set on entity types or relation definition to control who
+will be able to create, read, update or delete entities and relations. Permissions
+are granted to groups (to which users may belong) or using rql expressions (if the
+rql expression returns some results, the permission is granted).
+
+Some meta-data necessary to the system are added to the data model. That includes
+entities like users and groups, the entities used to store the data model
+itself and attributes like unique identifier, creation date, creator, etc.
+
+When you create a new |cubicweb| instance, the schema is stored in the database.
+When the cubes the instance is based on evolve, they may change their data model
+and provide migration scripts that will be executed when the administrator will
+run the upgrade process for the instance.
+
+
+.. _VRegistryIntro:
+
+Registries and application objects
+----------------------------------
+
+Application objects
+~~~~~~~~~~~~~~~~~~~
+
+Besides a few core functionalities, almost every feature of the framework is
+achieved by dynamic objects (`application objects` or `appobjects`) stored in a
+two-levels registry. Each object is affected to a registry with
+an identifier in this registry. You may have more than one object sharing an
+identifier in the same registry:
+
+ object's `__registry__` : object's `__regid__` : [list of app objects]
+
+In other words, the `registry` contains several (sub-)registries which hold a
+list of appobjects associated to an identifier.
+
+The base class of appobjects is :class:`cubicweb.appobject.AppObject`.
+
+Selectors
+~~~~~~~~~
+
+At runtime, appobjects can be selected in a registry according to some
+contextual information. Selection is done by comparing the *score*
+returned by each appobject's *selector*.
+
+The better the object fits the context, the higher the score. Scores
+are the glue that ties appobjects to the data model. Using them
+appropriately is an essential part of the construction of well behaved
+cubes.
+
+|cubicweb| provides a set of basic selectors that may be parametrized. Also,
+selectors can be combined with the `~` unary operator (negation) and the binary
+operators `&` and `|` (respectivly 'and' and 'or') to build more complex
+selectors. Of course complex selectors may be combined too. Last but not least, you
+can write your own selectors.
+
+The `registry`
+~~~~~~~~~~~~~~~
+
+At startup, the `registry` inspects a number of directories looking
+for compatible class definitions. After a recording process, the
+objects are assigned to registries and become available through the
+selection process.
+
+In a cube, application object classes are looked in the following modules or
+packages:
+
+- `entities`
+- `views`
+- `hooks`
+- `sobjects`
+
+There are three common ways to look up some application object from a
+registry:
+
+* get the most appropriate object by specifying an identifier and
+ context objects. The object with the greatest score is
+ selected. There should always be a single appobject with a greater
+ score than others for a particular context.
+
+* get all objects applying to a context by specifying a registry. A
+ list of objects will be returned containing the object with the
+ highest score (> 0) for each identifier in that registry.
+
+* get the object within a particular registry/identifier. No selection
+ process is involved: the registry will expect to find a single
+ object in that cell.
+
+
+.. _RQLIntro:
+
+The RQL query language
+----------------------
+
+No need for a complicated ORM when you have a powerful data
+manipulation language.
+
+All the persistent data in a |cubicweb| instance is retrieved and
+modified using RQL (see :ref:`rql_intro`).
+
+This query language is inspired by SQL but is on a higher level in order to
+emphasize browsing relations.
+
+
+Result set
+~~~~~~~~~~
+
+Every request made (using RQL) to the data repository returns an object we call a
+Result Set. It enables easy use of the retrieved data, providing a translation
+layer between the backend's native datatypes and |cubicweb| schema's EntityTypes.
+
+Result sets provide access to the raw data, yielding either basic Python data
+types, or schema-defined high-level entities, in a straightforward way.
+
+
+.. _ViewIntro:
+
+Views
+-----
+
+**CubicWeb is data driven**
+
+The view system is loosely coupled to data through the selection system explained
+above. Views are application objects with a dedicated interface to 'render'
+something, eg producing some html, text, xml, pdf, or whatsover that can be
+displayed to a user.
+
+Views actually are partitioned into different kind of objects such as
+`templates`, `boxes`, `components` and proper `views`, which are more
+high-level abstraction useful to build the user interface in an object
+oriented way.
+
+
+.. _HookIntro:
+
+Hooks and operations
+--------------------
+
+**CubicWeb provides an extensible data repository**
+
+The data model defined using Yams types allows to express the data
+model in a comfortable way. However several aspects of the data model
+can not be expressed there. For instance:
+
+* managing computed attributes
+
+* enforcing complicated business rules
+
+* real-world side-effects linked to data events (email notification
+ being a prime example)
+
+The hook system is much like the triggers of an SQL database engine,
+except that:
+
+* it is not limited to one specific SQL backend (every one of them
+ having an idiomatic way to encode triggers), nor to SQL backends at
+ all (think about LDAP or a Subversion repository)
+
+* it is well-coupled to the rest of the framework
+
+Hooks are also application objects (in the `hooks` registry) and
+selected on events such as after/before add/update/delete on
+entities/relations, server startup or shutdown, etc.
+
+`Operations` may be instantiated by hooks to do further processing at different
+steps of the transaction's commit / rollback, which usually can not be done
+safely at the hook execution time.
+
+Hooks and operation are an essential building block of any moderately complicated
+cubicweb application.
+
+.. note::
+ RQL queries executed in hooks and operations are *unsafe* by default, i.e. the
+ read and write security is deactivated unless explicitly asked.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/intro/history.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,32 @@
+.. -*- coding: utf-8 -*-
+
+A little history...
+===================
+
+*CubicWeb* is a semantic web application framework that Logilab_ started
+developing in 2001 as an offspring of its Narval_ research project. *CubicWeb*
+is written in Python and includes a data server and a web engine.
+
+Its data server publishes data federated from different sources like
+SQL databases, LDAP directories, `VCS`_ repositories or even from other
+CubicWeb data servers.
+
+.. _`VCS`: http://en.wikipedia.org/wiki/Revision_control
+
+Its web engine was designed to let the final user control what content to select
+and how to display it. It allows one to browse the federated data sources and
+display the results with the rendering that best fits the context. This
+flexibility of the user interface gives back to the user some capabilities
+usually only accessible to application developers.
+
+*CubicWeb* has been developed by Logilab_ and used in-house for many years
+before it was first installed for its clients in 2006 as version 2.
+
+In 2008, *CubicWeb* version 3 became downloadable for free under the
+terms of the LGPL license. Its community is now steadily growing
+without hampering the fast-paced stream of changes thanks to the time
+and energy originally put in the design of the framework.
+
+
+.. _Narval: http://www.logilab.org/project/narval-moved
+.. _Logilab: http://www.logilab.fr/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/intro/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,19 @@
+.. -*- coding: utf-8 -*-
+
+.. _Part1:
+
+--------------------------
+Introduction to *CubicWeb*
+--------------------------
+
+This first part of the book offers different reading path to
+discover the *CubicWeb* framework, provides a tutorial to get a quick
+overview of its features and lists its key concepts.
+
+
+.. toctree::
+ :maxdepth: 2
+ :numbered:
+
+ history
+ concepts.rst
--- a/doc/book/mode_plan.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +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/>.
-"""
->>> from mode_plan import *
->>> ls()
-<list of directory content>
->>> ren('A01','A03')
-rename A010-joe.en.txt to A030-joe.en.txt
-accept [y/N]?
-"""
-
-def ren(a,b):
- names = glob.glob('%s*'%a)
- for name in names :
- print 'rename %s to %s' % (name, name.replace(a,b))
- if raw_input('accept [y/N]?').lower() =='y':
- for name in names:
- os.system('hg mv %s %s' % (name, name.replace(a,b)))
-
-
-def ls(): print '\n'.join(sorted(os.listdir('.')))
-
-def move():
- filenames = []
- for name in sorted(os.listdir('.')):
- num = name[:2]
- if num.isdigit():
- filenames.append( (int(num), name) )
-
-
- #print filenames
-
- for num, name in filenames:
- if num >= start:
- print 'hg mv %s %2i%s' %(name,num+1,name[2:])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.14.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,165 @@
+3.14 (09/11/2011)
+=================
+
+First notice CW 3.14 depends on yams 0.34 (which is incompatible with prior
+cubicweb releases regarding instance re-creation).
+
+
+API changes
+-----------
+
+* `Entity.fetch_rql` `restriction` argument has been deprecated and should be
+ replaced with a call to the new `Entity.fetch_rqlst` method, get the returned
+ value (a rql `Select` node) and use the RQL syntax tree API to include the
+ above-mentionned restrictions.
+
+ Backward compat is kept with proper warning.
+
+* `Entity.fetch_order` and `Entity.fetch_unrelated_order` class methods have been
+ replaced by `Entity.cw_fetch_order` and `Entity.cw_fetch_unrelated_order` with
+ a different prototype:
+
+ - instead of taking (attr, var) as two string argument, they now take (select,
+ attr, var) where select is the rql syntax tree beinx constructed and var the
+ variable *node*.
+
+ - instead of returning some string to be inserted in the ORDERBY clause, it has
+ to modify the syntax tree
+
+ Backward compat is kept with proper warning, BESIDE cases below:
+
+ - custom order method return **something else the a variable name with or
+ without the sorting order** (e.g. cases where you sort on the value of a
+ registered procedure as it was done in the tracker for instance). In such
+ case, an error is logged telling that this sorting is ignored until API
+ upgrade.
+
+ - client code use direct access to one of those methods on an entity (no code
+ known to do that).
+
+* `Entity._rest_attr_info` class method has been renamed to
+ `Entity.cw_rest_attr_info`
+
+ No backward compat yet since this is a protected method an no code is known to
+ use it outside cubicweb itself.
+
+* `AnyEntity.linked_to` has been removed as part of a refactoring of this
+ functionality (link a entity to another one at creation step). It was replaced
+ by a `EntityFieldsForm.linked_to` property.
+
+ In the same refactoring, `cubicweb.web.formfield.relvoc_linkedto`,
+ `cubicweb.web.formfield.relvoc_init` and
+ `cubicweb.web.formfield.relvoc_unrelated` were removed and replaced by
+ RelationField methods with the same names, that take a form as a parameter.
+
+ **No backward compatibility yet**. It's still time to cry for it.
+ Cubes known to be affected: tracker, vcsfile, vcreview.
+
+* `CWPermission` entity type and its associated require_permission relation type
+ (abstract) and require_group relation definitions have been moved to a new
+ `localperms` cube. With this have gone some functions from the
+ `cubicweb.schemas` package as well as some views. This makes cubicweb itself
+ smaller while you get all the local permissions stuff into a single,
+ documented, place.
+
+ Backward compat is kept for existing instances, **though you should have
+ installed the localperms cubes**. A proper error should be displayed when
+ trying to migrate to 3.14 an instance the use `CWPermission` without the new
+ cube installed. For new instances / test, you should add a dependancy on the
+ new cube in cubes using this feature, along with a dependancy on cubicweb >=
+ 3.14.
+
+* jQuery has been updated to 1.6.4 and jquery-tablesorter to 2.0.5. No backward
+ compat issue known.
+
+* Table views refactoring : new `RsetTableView` and `EntityTableView`, as well as
+ rewritten an enhanced version of `PyValTableView` on the same bases, with logic
+ moved to some column renderers and a layout. Those should be well documented
+ and deprecates former `TableView`, `EntityAttributesTableView` and `CellView`,
+ which are however kept for backward compat, with some warnings that may not be
+ very clear unfortunatly (you may see your own table view subclass name here,
+ which doesn't make the problem that clear). Notice that `_cw.view('table',
+ rset, *kwargs)` will be routed to the new `RsetTableView` or to the old
+ `TableView` depending on given extra arguments. See #1986413.
+
+* `display_name` don't call .lower() anymore. This may leads to changes in your
+ user interface. Different msgid for upper/lower cases version of entity type
+ names, as this is the only proper way to handle this with some languages.
+
+* `IEditControlAdapter` has been deprecated in favor of `EditController`
+ overloading, which was made easier by adding dedicated selectors called
+ `match_edited_type` and `match_form_id`.
+
+* Pre 3.6 API backward compat has been dropped, though *data* migration
+ compatibility has been kept. You may have to fix errors due to old API usage
+ for your instance before to be able to run migration, but then you should be
+ able to upgrade even a pre 3.6 database.
+
+* Deprecated `cubicweb.web.views.iprogress` in favor of new `iprogress` cube.
+
+* Deprecated `cubicweb.web.views.flot` in favor of new `jqplot` cube.
+
+
+Unintrusive API changes
+-----------------------
+
+* Refactored properties forms (eg user preferences and site wide properties) as
+ well as pagination components to ease overridding.
+
+* New `cubicweb.web.uihelper` module with high-level helpers for uicfg.
+
+* New `anonymized_request` decorator to temporary run stuff as an anonymous
+ user, whatever the currently logged in user.
+
+* New 'verbatimattr' attribute view.
+
+* New facet and form widget for Integer used to store binary mask.
+
+* New `js_href` function to generated proper javascript href.
+
+* `match_kwargs` and `match_form_params` selectors both accept a new
+ `once_is_enough` argument.
+
+* `printable_value` is now a method of request, and may be given dict of
+ formatters to use.
+
+* `[Rset]TableView` allows to set None in 'headers', meaning the label should be
+ fetched from the result set as done by default.
+
+* Field vocabulary computation on entity creation now takes `__linkto`
+ information into accounet.
+
+* Started a `cubicweb.pylintext` pylint plugin to help pylint analyzing cubes.
+
+
+RQL
+---
+
+* Support for HAVING in 'SET' and 'DELETE' queries.
+
+* new `AT_TZ` function to get back a timestamp at a given time-zone.
+
+* new `WEEKDAY` date extraction function
+
+
+User interface changes
+----------------------
+
+* Datafeed source now present an history of the latest import's log, including
+ global status and debug/info/warning/error messages issued during
+ imports. Import logs older than a configurable amount of time are automatically
+ deleted.
+
+* Breadcrumbs component is properly kept when creating an entity with '__linkto'.
+
+* users and groups management now really lead to that (i.e. includes *groups*
+ management).
+
+* New 'jsonp' controller with 'jsonexport' and 'ejsonexport' views.
+
+
+Configuration
+-------------
+
+* Added option 'resources-concat' to make javascript/css files concatenation
+ optional.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.15.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,98 @@
+3.15 (12/04/2012)
+=================
+
+New functionnalities
+--------------------
+
+* Add Zmq server, based on the cutting edge ZMQ (http://www.zeromq.org/) socket
+ library. This allows to access distant instance, in a similar way as Pyro.
+
+* Publish/subscribe mechanism using ZMQ for communication among cubicweb
+ instances. The new zmq-address-sub and zmq-address-pub configuration variables
+ define where this communication occurs. As of this release this mechanism is
+ used for entity cache invalidation.
+
+* Improved WSGI support. While there is still some caveats, most of the code
+ which was twisted only is now generic and allows related functionalities to work
+ with a WSGI front-end.
+
+* Full undo/transaction support : undo of modification has eventually been
+ implemented, and the configuration simplified (basically you activate it or not
+ on an instance basis).
+
+* Controlling HTTP status code used is not much more easier :
+
+ - `WebRequest` now has a `status_out` attribut to control the response status ;
+
+ - most web-side exceptions take an optional ``status`` argument.
+
+
+API changes
+-----------
+
+* The base registry implementation has been moved to a new
+ `logilab.common.registry` module (see #1916014). This includes code from :
+
+ * `cubicweb.vreg` (the whole things that was in there)
+ * `cw.appobject` (base selectors and all).
+
+ In the process, some renaming was done:
+
+ * the top level registry is now `RegistryStore` (was `VRegistry`), but that
+ should not impact cubicweb client code ;
+
+ * former selectors functions are now known as "predicate", though you still use
+ predicates to build an object'selector ;
+
+ * for consistency, the `objectify_selector` decoraror has hence be renamed to
+ `objectify_predicate` ;
+
+ * on the CubicWeb side, the `selectors` module has been renamed to
+ `predicates`.
+
+ Debugging refactoring dropped the more need for the `lltrace` decorator. There
+ should be full backward compat with proper deprecation warnings. Notice the
+ `yes` predicate and `objectify_predicate` decorator, as well as the
+ `traced_selection` function should now be imported from the
+ `logilab.common.registry` module.
+
+* All login forms are now submitted to <app_root>/login. Redirection to requested
+ page is now handled by the login controller (it was previously handle by the
+ session manager).
+
+* `Publisher.publish` has been renamed to `Publisher.handle_request`. This
+ method now contains generic version of logic previously handled by
+ Twisted. `Controller.publish` is **not** affected.
+
+
+Unintrusive API changes
+-----------------------
+
+* New 'ldapfeed' source type, designed to replace 'ldapuser' source with
+ data-feed (i.e. copy based) source ideas.
+
+* New 'zmqrql' source type, similar to 'pyrorql' but using ømq instead of Pyro.
+
+* A new registry called `services` has appeared, where you can register
+ server-side `cubicweb.server.Service` child classes. Their `call` method can be
+ invoked from a web-side AppObject instance using new `self._cw.call_service`
+ method or a server-side one using `self.session.call_service`. This is a new
+ way to call server-side methods, much cleaner than monkey patching the
+ Repository class, which becomes a deprecated way to perform similar tasks.
+
+* a new `ajax-func` registry now hosts all remote functions (i.e. functions
+ callable through the `asyncRemoteExec` JS api). A convenience `ajaxfunc`
+ decorator will let you expose your python function easily without all the
+ appobject standard boilerplate. Backward compatibility is preserved.
+
+* the 'json' controller is now deprecated in favor of the 'ajax' one.
+
+* `WebRequest.build_url` can now take a __secure__ argument. When True cubicweb
+ try to generate an https url.
+
+
+User interface changes
+----------------------
+
+A new 'undohistory' view expose the undoable transactions and give access to undo
+some of them.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.16.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,97 @@
+3.16 (25/01/2013)
+=================
+
+New functionalities
+-------------------
+
+* Add a new dataimport store (`SQLGenObjectStore`). This store enables a fast
+ import of data (entity creation, link creation) in CubicWeb, by directly
+ flushing information in SQL. This may only be used with PostgreSQL, as it
+ requires the 'COPY FROM' command.
+
+
+API changes
+-----------
+
+* Orm: `set_attributes` and `set_relations` are unified (and
+ deprecated) in favor of `cw_set` that works in all cases.
+
+* db-api/configuration: all the external repository connection information is
+ now in an URL (see `#2521848 <http://www.cubicweb.org/2521848>`_),
+ allowing to drop specific options of pyro nameserver host, group, etc and fix
+ broken `ZMQ <http://www.zeromq.org/>`_ source. Configuration related changes:
+
+ * Dropped 'pyro-ns-host', 'pyro-instance-id', 'pyro-ns-group' from the client side
+ configuration, in favor of 'repository-uri'. **NO MIGRATION IS DONE**,
+ supposing there is no web-only configuration in the wild.
+
+ * Stop discovering the connection method through `repo_method` class attribute
+ of the configuration, varying according to the configuration class. This is
+ a first step on the way to a simpler configuration handling.
+
+ DB-API related changes:
+
+ * Stop indicating the connection method using `ConnectionProperties`.
+
+ * Drop `_cnxtype` attribute from `Connection` and `cnxtype` from
+ `Session`. The former is replaced by a `is_repo_in_memory` property
+ and the later is totaly useless.
+
+ * Turn `repo_connect` into `_repo_connect` to mark it as a private function.
+
+ * Deprecate `in_memory_cnx` which becomes useless, use `_repo_connect` instead
+ if necessary.
+
+* the "tcp://" uri scheme used for `ZMQ <http://www.zeromq.org/>`_
+ communications (in a way reminiscent of Pyro) is now named
+ "zmqpickle-tcp://", so as to make room for future zmq-based lightweight
+ communications (without python objects pickling).
+
+* Request.base_url gets a `secure=True` optional parameter that yields
+ an https url if possible, allowing hook-generated content to send
+ secure urls (e.g. when sending mail notifications)
+
+* Dataimport ucsvreader gets a new boolean `ignore_errors`
+ parameter.
+
+
+Unintrusive API changes
+-----------------------
+
+* Drop of `cubicweb.web.uicfg.AutoformSectionRelationTags.bw_tag_map`,
+ deprecated since 3.6.
+
+
+User interface changes
+----------------------
+
+* The RQL search bar has now some auto-completion support. It means
+ relation types or entity types can be suggested while typing. It is
+ an awesome improvement over the current behaviour !
+
+* The `action box` associated with `table` views (from `tableview.py`)
+ has been transformed into a nice-looking series of small tabs; it
+ means that the possible actions are immediately visible and need not
+ be discovered by clicking on an almost invisible icon on the upper
+ right.
+
+* The `uicfg` module has moved to web/views/ and ui configuration
+ objects are now selectable. This will reduce the amount of
+ subclassing and whole methods replacement usually needed to
+ customize the ui behaviour in many cases.
+
+* Remove changelog view, as neither cubicweb nor known
+ cubes/applications were properly feeding related files.
+
+
+Other changes
+-------------
+
+* 'pyrorql' sources will be automatically updated to use an URL to locate the source
+ rather than configuration option. 'zmqrql' sources were broken before this change,
+ so no upgrade is needed...
+
+* Debugging filters for Hooks and Operations have been added.
+
+* Some cubicweb-ctl commands used to show the output of `msgcat` and
+ `msgfmt`; they don't anymore.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.17.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,61 @@
+3.17 (02/05/2013)
+=================
+
+New functionalities
+-------------------
+
+* add a command to compare db schema and file system schema
+ (see `#464991 <http://www.cubicweb.org/464991>`_)
+
+* Add CubicWebRequestBase.content with the content of the HTTP request (see #2742453)
+ (see `#2742453 <http://www.cubicweb.org/2742453>`_)
+
+* Add directive bookmark to ReST rendering
+ (see `#2545595 <http://www.cubicweb.org/ticket/2545595>`_)
+
+* Allow user defined final type
+ (see `#124342 <https://www.logilab.org/ticket/124342>`_)
+
+
+API changes
+-----------
+
+* drop typed_eid() in favour of int() (see `#2742462 <http://www.cubicweb.org/2742462>`_)
+
+* The SIOC views and adapters have been removed from CubicWeb and moved to the
+ `sioc` cube.
+
+* The web page embedding views and adapters have been removed from CubicWeb and
+ moved to the `embed` cube.
+
+* The email sending views and controllers have been removed from CubicWeb and
+ moved to the `massmailing` cube.
+
+* ``RenderAndSendNotificationView`` is deprecated in favor of
+ ``ActualNotificationOp`` the new operation use the more efficient *data*
+ idiom.
+
+* Looping task can now have a interval <= ``0``. Negative interval disable the
+ looping task entirely.
+
+* We now serve html instead of xhtml.
+ (see `#2065651 <http://www.cubicweb.org/ticket/2065651>`_)
+
+
+Deprecation
+-----------
+
+* ``ldapuser`` have been deprecated. It'll be fully dropped in the next
+ version. If you are still using ldapuser switch to ``ldapfeed`` **NOW**!
+
+* ``hijack_user`` have been deprecated. It will be dropped soon.
+
+
+Deprecated Code Drops
+---------------------
+
+* The progress views and adapters have been removed from CubicWeb. These
+ classes were deprecated since 3.14.0. They are still available in the
+ `iprogress` cube.
+
+* API deprecated since 3.7 have been dropped.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.18.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,101 @@
+3.18 (10/01/2014)
+=================
+
+The migration script does not handle sqlite nor mysql instances.
+
+
+New functionalities
+-------------------
+
+* add a security debugging tool
+ (see `#2920304 <http://www.cubicweb.org/2920304>`_)
+
+* introduce an `add` permission on attributes, to be interpreted at
+ entity creation time only and allow the implementation of complex
+ `update` rules that don't block entity creation (before that the
+ `update` attribute permission was interpreted at entity creation and
+ update time)
+
+* the primary view display controller (uicfg) now has a
+ `set_fields_order` method similar to the one available for forms
+
+* new method `ResultSet.one(col=0)` to retrive a single entity and enforce the
+ result has only one row (see `#3352314 <https://www.cubicweb.org/ticket/3352314>`_)
+
+* new method `RequestSessionBase.find` to look for entities
+ (see `#3361290 <https://www.cubicweb.org/ticket/3361290>`_)
+
+* the embedded jQuery copy has been updated to version 1.10.2, and jQuery UI to
+ version 1.10.3.
+
+* initial support for wsgi for the debug mode, available through the new
+ ``wsgi`` cubicweb-ctl command, which can use either python's builtin
+ wsgi server or the werkzeug module if present.
+
+* a ``rql-table`` directive is now available in ReST fields
+
+* cubicweb-ctl upgrade can now generate the static data resource directory
+ directly, without a manual call to gen-static-datadir.
+
+API changes
+-----------
+
+* not really an API change, but the entity permission checks are now
+ systematically deferred to an operation, instead of a) trying in a
+ hook and b) if it failed, retrying later in an operation
+
+* The default value storage for attributes is no longer String, but
+ Bytes. This opens the road to storing arbitrary python objects, e.g.
+ numpy arrays, and fixes a bug where default values whose truth value
+ was False were not properly migrated.
+
+* `symmetric` relations are no more handled by an rql rewrite but are
+ now handled with hooks (from the `activeintegrity` category); this
+ may have some consequences for applications that do low-level database
+ manipulations or at times disable (some) hooks.
+
+* `unique together` constraints (multi-columns unicity constraints)
+ get a `name` attribute that maps the CubicWeb contraint entities to
+ corresponding backend index.
+
+* BreadCrumbEntityVComponent's open_breadcrumbs method now includes
+ the first breadcrumbs separator
+
+* entities can be compared for equality and hashed
+
+* the ``on_fire_transition`` predicate accepts a sequence of possible
+ transition names
+
+* the GROUP_CONCAT rql aggregate function no longer repeats duplicate
+ values, on the sqlite and postgresql backends
+
+Deprecation
+-----------
+
+* ``pyrorql`` sources have been deprecated. Multisource will be fully dropped
+ in the next version. If you are still using pyrorql, switch to ``datafeed``
+ **NOW**!
+
+* the old multi-source system
+
+* `find_one_entity` and `find_entities` in favor of `find`
+ (see `#3361290 <https://www.cubicweb.org/ticket/3361290>`_)
+
+* the `TmpFileViewMixin` and `TmpPngView` classes (see
+ `#3400448 <https://www.cubicweb.org/ticket/3400448>`_)
+
+Deprecated Code Drops
+---------------------
+
+* ``ldapuser`` have been dropped; use ``ldapfeed`` now
+ (see `#2936496 <http://www.cubicweb.org/2936496>`_)
+
+* action ``GotRhythm`` was removed, make sure you do not
+ import it in your cubes (even to unregister it)
+ (see `#3093362 <http://www.cubicweb.org/3093362>`_)
+
+* all 3.8 backward compat is gone
+
+* all 3.9 backward compat (including the javascript side) is gone
+
+* the ``twisted`` (web-only) instance type has been removed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.19.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,180 @@
+3.19 (28/04/2015)
+=================
+
+New functionalities
+-------------------
+
+* implement Cross Origin Resource Sharing (CORS)
+ (see `#2491768 <http://www.cubicweb.org/2491768>`_)
+
+* system_source.create_eid can get a range of IDs, to reduce overhead of batch
+ entity creation
+
+Behaviour Changes
+-----------------
+
+* The anonymous property of Session and Connection are now computed from the
+ related user login. If it matches the ``anonymous-user`` in the config the
+ connection is anonymous. Beware that the ``anonymous-user`` config is web
+ specific. Therefore, no session may be anonymous in a repository only setup.
+
+
+New Repository Access API
+-------------------------
+
+Connection replaces Session
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A new explicit Connection object replaces Session as the main repository entry
+point. Connection holds all the necessary methods to be used server-side
+(``execute``, ``commit``, ``rollback``, ``call_service``, ``entity_from_eid``,
+etc...). One obtains a new Connection object using ``session.new_cnx()``.
+Connection objects need to have an explicit begin and end. Use them as a context
+manager to never miss an end::
+
+ with session.new_cnx() as cnx:
+ cnx.execute('INSERT Elephant E, E name "Babar"')
+ cnx.commit()
+ cnx.execute('INSERT Elephant E, E name "Celeste"')
+ cnx.commit()
+ # Once you get out of the "with" clause, the connection is closed.
+
+Using the same Connection object in multiple threads will give you access to the
+same Transaction. However, Connection objects are not thread safe (hence at your
+own risks).
+
+``repository.internal_session`` is deprecated in favor of
+``repository.internal_cnx``. Note that internal connections are now `safe` by default,
+i.e. the integrity hooks are enabled.
+
+Backward compatibility is preserved on Session.
+
+
+dbapi vs repoapi
+~~~~~~~~~~~~~~~~
+
+A new API has been introduced to replace the dbapi. It is called `repoapi`.
+
+There are three relevant functions for now:
+
+* ``repoapi.get_repository`` returns a Repository object either from an
+ URI when used as ``repoapi.get_repository(uri)`` or from a config
+ when used as ``repoapi.get_repository(config=config)``.
+
+* ``repoapi.connect(repo, login, **credentials)`` returns a ClientConnection
+ associated with the user identified by the credentials. The
+ ClientConnection is associated with its own Session that is closed
+ when the ClientConnection is closed. A ClientConnection is a
+ Connection-like object to be used client side.
+
+* ``repoapi.anonymous_cnx(repo)`` returns a ClientConnection associated
+ with the anonymous user if described in the config.
+
+
+repoapi.ClientConnection replace dbapi.Connection and company
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On the client/web side, the Request is now using a ``repoapi.ClientConnection``
+instead of a ``dbapi.connection``. The ``ClientConnection`` has multiple backward
+compatible methods to make it look like a ``dbapi.Cursor`` and ``dbapi.Connection``.
+
+Session used on the Web side are now the same than the one used Server side.
+Some backward compatibility methods have been installed on the server side Session
+to ease the transition.
+
+The authentication stack has been altered to use the ``repoapi`` instead of
+the ``dbapi``. Cubes adding new element to this stack are likely to break.
+
+Session data can be accessed using the cnx.data dictionary, while
+transaction data is available through cnx.transaction_data. These
+replace the [gs]et_shared_data methods with optional txid kwarg.
+
+New API in tests
+~~~~~~~~~~~~~~~~
+
+All current methods and attributes used to access the repo on ``CubicWebTC`` are
+deprecated. You may now use a ``RepoAccess`` object. A ``RepoAccess`` object is
+linked to a new ``Session`` for a specified user. It is able to create
+``Connection``, ``ClientConnection`` and web side requests linked to this
+session::
+
+ access = self.new_access('babar') # create a new RepoAccess for user babar
+ with access.repo_cnx() as cnx:
+ # some work with server side cnx
+ cnx.execute(...)
+ cnx.commit()
+ cnx.execute(...)
+ cnx.commit()
+
+ with access.client_cnx() as cnx:
+ # some work with client side cnx
+ cnx.execute(...)
+ cnx.commit()
+
+ with access.web_request(elephant='babar') as req:
+ # some work with client side cnx
+ elephant_name = req.form['elephant']
+ req.execute(...)
+ req.cnx.commit()
+
+By default ``testcase.admin_access`` contains a ``RepoAccess`` object for the
+default admin session.
+
+
+API changes
+-----------
+
+* ``RepositorySessionManager.postlogin`` is now called with two arguments,
+ request and session. And this now happens before the session is linked to the
+ request.
+
+* ``SessionManager`` and ``AuthenticationManager`` now take a repo object at
+ initialization time instead of a vreg.
+
+* The ``async`` argument of ``_cw.call_service`` has been dropped. All calls are
+ now synchronous. The zmq notification bus looks like a good replacement for
+ most async use cases.
+
+* ``repo.stats()`` is now deprecated. The same information is available through
+ a service (``_cw.call_service('repo_stats')``).
+
+* ``repo.gc_stats()`` is now deprecated. The same information is available through
+ a service (``_cw.call_service('repo_gc_stats')``).
+
+* ``repo.register_user()`` is now deprecated. The functionality is now
+ available through a service (``_cw.call_service('register_user')``).
+
+* ``request.set_session`` no longer takes an optional ``user`` argument.
+
+* CubicwebTC does not have repo and cnx as class attributes anymore. They are
+ standard instance attributes. ``set_cnx`` and ``_init_repo`` class methods
+ become instance methods.
+
+* ``set_cnxset`` and ``free_cnxset`` are deprecated. cnxset are now
+ automatically managed.
+
+* The implementation of cascading deletion when deleting `composite`
+ entities has changed. There comes a semantic change: merely deleting
+ a composite relation does not entail any more the deletion of the
+ component side of the relation.
+
+* ``_cw.user_callback`` and ``_cw.user_rql_callback`` are deprecated. Users
+ are encouraged to write an actual controller (e.g. using ``ajaxfunc``)
+ instead of storing a closure in the session data.
+
+* A new ``entity.cw_linkable_rql`` method provides the rql to fetch all entities
+ that are already or may be related to the current entity using the given
+ relation.
+
+
+Deprecated Code Drops
+---------------------
+
+* session.hijack_user mechanism has been dropped.
+
+* EtypeRestrictionComponent has been removed, its functionality has been
+ replaced by facets a while ago.
+
+* the old multi-source support has been removed. Only copy-based sources
+ remain, such as datafeed or ldapfeed.
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.20.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,100 @@
+3.20 (06/01/2015)
+=================
+
+New features
+------------
+
+* virtual relations: a new ComputedRelation class can be used in
+ schema.py; its `rule` attribute is an RQL snippet that defines the new
+ relation.
+
+* computed attributes: an attribute can now be defined with a `formula`
+ argument (also an RQL snippet); it will be read-only, and updated
+ automatically.
+
+ Both of these features are described in `CWEP-002`_, and the updated
+ "Data model" chapter of the CubicWeb book.
+
+* cubicweb-ctl plugins can use the ``cubicweb.utils.admincnx`` function
+ to get a Connection object from an instance name.
+
+* new 'tornado' wsgi backend
+
+* session cookies have the HttpOnly flag, so they're no longer exposed to
+ javascript
+
+* rich text fields can be formatted as markdown
+
+* the edit controller detects concurrent editions, and raises a ValidationError
+ if an entity was modified between form generation and submission
+
+* cubicweb can use a postgresql "schema" (namespace) for its tables
+
+* "cubicweb-ctl configure" can be used to set values of the admin user
+ credentials in the sources configuration file
+
+* in debug mode, setting the _cwtracehtml parameter on a request allows tracing
+ where each bit of output is produced
+
+.. _CWEP-002: http://hg.logilab.org/review/cwep/file/tip/CWEP-002.rst
+
+
+API Changes
+-----------
+
+* ``ucsvreader()`` and ``ucsvreader_pb()`` from the ``dataimport`` module have
+ 2 new keyword arguments ``delimiter`` and ``quotechar`` to replace the
+ ``separator`` and ``quote`` arguments respectively. This makes the API match
+ that of Python's ``csv.reader()``. The old arguments are still supported
+ though deprecated.
+
+* the migration environment's ``remove_cube`` function is now called ``drop_cube``.
+
+* cubicweb.old.css is now cubicweb.css. The previous "new"
+ cubicweb.css, along with its cubicweb.reset.css companion, have been
+ removed.
+
+* the jquery-treeview plugin was updated to its latest version
+
+
+Deprecated Code Drops
+----------------------
+
+* most of 3.10 and 3.11 backward compat is gone; this includes:
+
+ - CtxComponent.box_action() and CtxComponent.build_link()
+
+ - cubicweb.devtools.htmlparser.XMLDemotingValidator
+
+ - various methods and properties on Entities, replaced by cw_edited
+ and cw_attr_cache
+
+ - 'commit_event' method on hooks, replaced by 'postcommit_event'
+
+ - server.hook.set_operation(), replaced by
+ Operation.get_instance(...).add_data()
+
+ - View.div_id(), View.div_class() and View.create_url()
+
+ - `*VComponent` classes
+
+ - in forms, Field.value() and Field.help() must take the form and
+ the field itself as arguments
+
+ - form.render() must get `w` as a named argument, and
+ renderer.render() must take `w` as first argument
+
+ - in breadcrumbs, the optional `recurs` argument must be a set, not
+ False
+
+ - cubicweb.web.views.idownloadable.{download_box,IDownloadableLineView}
+
+ - primary views no longer have `render_entity_summary` and `summary`
+ methods
+
+ - WFHistoryVComponent's `cell_call` method is replaced by
+ `render_body`
+
+ - cubicweb.dataimport.ObjectStore.add(), replaced by create_entity
+
+ - ManageView.{folders,display_folders}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.21.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,77 @@
+3.21
+====
+
+New features
+------------
+
+* the datadir-url configuration option lets one choose where static data files
+ are served (instead of the default ${base-url}/data/)
+
+* some integrity checking that was previously implemented in Python was
+ moved to the SQL backend. This includes some constraints, and
+ referential integrity. Some consequences are that:
+
+ - disabling integrity hooks no longer disables those checks
+ - upgrades that modify constraints will fail when running on sqlite
+ (but upgrades aren't supported on sqlite anyway)
+
+ Note: as of 3.21.0, the upgrade script only works on PostgreSQL. The
+ migration for SQLServer will be added in a future bugfix release.
+
+* for easier instance monitoring, cubicweb can regularly dump some statistics
+ (basically those exposed by the 'info' and 'gc' views) in json format to a file
+
+User-visible changes
+--------------------
+
+* the use of fckeditor for text form fields is disabled by default
+
+* the 'https-deny-anonymous' configuration setting no longer exists
+
+Code movement
+-------------
+
+The cubicweb.web.views.timeline module (providing the timeline-json, timeline
+and static-timeline views) has moved to a standalone cube_
+
+.. _cube: https://www.cubicweb.org/project/cubicweb-timeline
+
+API changes
+-----------
+
+* req.set_cookie's "expires" argument, if not None, is expected to be a
+ date or a datetime in UTC. It was previously interpreted as localtime
+ with the UTC offset the server started in, which was inconsistent (we
+ are not aware of any users of that API).
+
+* the way to run tests on a postgresql backend has changed slightly, use
+ cubicweb.devtools.{start,stop}pgcluster in setUpModule and tearDownModule
+
+* the Connection and ClientConnection objects introduced in CubicWeb 3.19 have
+ been unified. To connect to a repository, use::
+
+ session = repo.new_session(login, password=...)
+ with session.new_cnx() as cnx:
+ cnx.execute(...)
+
+ In tests, the 'repo_cnx' and 'client_cnx' methods of RepoAccess are now
+ aliases to 'cnx'.
+
+Deprecated code drops
+---------------------
+
+* the user_callback api has been removed; people should use plain
+ ajax functions instead
+
+* the `Pyro` and `Zmq-pickle` remote repository access methods have
+ been entirely removed (emerging alternatives such as rqlcontroller
+ and cwclientlib should be used instead). Note that as a side effect,
+ "repository-only" instances (i.e. without a http component) are no
+ longer possible. If you have any such instances, you will need to
+ rename the configuration file from repository.conf to all-in-one.conf
+ and run ``cubicweb-ctl upgrade`` to update it. Likewise, remote cubicweb-ctl
+ shell is no longer available.
+
+* the old (deprecated since 3.19) `DBAPI` api is completely removed
+
+* cubicweb.toolsutils.config_connect() has been removed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/changelog.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,12 @@
+===================
+ Changelog history
+===================
+
+.. include:: 3.21.rst
+.. include:: 3.20.rst
+.. include:: 3.19.rst
+.. include:: 3.18.rst
+.. include:: 3.17.rst
+.. include:: 3.16.rst
+.. include:: 3.15.rst
+.. include:: 3.14.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,14 @@
+Release notes
+-------------
+
+.. toctree::
+ :maxdepth: 1
+
+ 3.21
+ 3.20
+ 3.19
+ 3.18
+ 3.17
+ 3.16
+ 3.15
+ 3.14
--- a/doc/coding_standards_css.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-CSS Coding Standards
---------------------
-
-(Draft, to be continued)
-
-:Naming: camelCase
-
-Indentation rules
-~~~~~~~~~~~~~~~~~
-- 2 espaces avant les propriétés
-
-- pas d'espace avant les ":", un espace après
-
-- 1 seul espace entre les différentes valeurs pour une même propriété
-
-
-Documentation
-~~~~~~~~~~~~~
-Please keep rules semantically linked grouped together, with a comment about
-what they are for.
-
-Recommendation
-~~~~~~~~~~~~~~
-- Try to use existing classes rather than introduce new ones
-
-- Keep things as simple as possible while in the framework
-
-- Think about later customization by application
-
-- Avoid introducing a new CSS file for a few lines of CSS, at least while the
- framework doesn't include packing functionalities
-
-
--- a/doc/coding_standards_js.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-Javascript Coding Standards
----------------------------
-
-(Draft, to be continued)
-
-:Naming: camelCase, except for CONSTANTS
-
-Indentation rules
-~~~~~~~~~~~~~~~~~
-- espace avant accolade ouvrante
-
-- retour à la ligne après accolade ouvrante (éventuellement pas
- de retour à la ligne s'il y a tout sur la même ligne, mais ce n'est
- pas le cas ici.
-
-- no tabs
-
-
-Documentation
-~~~~~~~~~~~~~
-XXX explain comment format for documentation generation
-
-
-Coding
-~~~~~~
-- Don't forget 'var' before variable definition, and semi-colon (';') after **each** statement.
-- Check the firebug console for deprecation warnings
-
-
-API usage
-~~~~~~~~~
-- unless intended, use jQuery('container') rather than jqNode('container')
-
-
-See also
-~~~~~~~~
-http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/conf.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,225 @@
+# -*- coding: utf-8 -*-
+# copyright 2003-2015 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/>.
+"""
+
+"""
+#
+# Cubicweb documentation build configuration file, created by
+# sphinx-quickstart on Fri Oct 31 09:10:36 2008.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# The contents of this file are pickled, so don't put values in the namespace
+# that aren't pickleable (module imports are okay, they're removed automatically).
+#
+# All configuration values have a default value; values that are commented out
+# serve to show the default value.
+
+from os import path as osp
+
+path = __file__
+path = osp.dirname(path) # ./doc
+path = osp.dirname(path) # ./
+path = osp.join(path, '__pkginfo__.py') # ./__pkginfo__.py
+cw = {}
+execfile(path, {}, cw)
+
+# If your extensions are in another directory, add it here. If the directory
+# is relative to the documentation root, use os.path.abspath to make it
+# absolute, like shown here.
+#sys.path.append(os.path.abspath('some/directory'))
+
+# General configuration
+# ---------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.viewcode',
+ 'logilab.common.sphinx_ext',
+ ]
+
+autoclass_content = 'both'
+
+# Add any paths that contain templates here, relative to this directory.
+#templates_path = []
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General substitutions.
+project = 'CubicWeb'
+copyright = '2001-2015, Logilab'
+
+# The default replacements for |version| and |release|, also used in various
+# other places throughout the built documents.
+#
+# The short X.Y version.
+version = '.'.join(str(n) for n in cw['numversion'][:2])
+# The full version, including alpha/beta/rc tags.
+release = cw['version']
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+unused_docs = []
+
+# A list of glob-style patterns that should be excluded when looking
+# for source files. [1] They are matched against the source file names
+# relative to the source directory, using slashes as directory
+# separators on all platforms.
+exclude_patterns = ['book/_maybe_to_integrate']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+
+# Options for HTML output
+# -----------------------
+
+# The style sheet to use for HTML and HTML Help pages. A file of that name
+# must exist either in Sphinx' static/ path, or in one of the custom paths
+# given in html_static_path.
+#html_style = 'sphinx-default.css'
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+html_title = '%s %s' % (project, release)
+
+html_theme_path = ['_themes']
+html_theme = 'cubicweb'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (within the static path) to place at the top of
+# the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, the reST sources are included in the HTML build as _sources/<name>.
+#html_copy_source = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+html_file_suffix = '.html'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Cubicwebdoc'
+
+
+# Options for LaTeX output
+# ------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, document class [howto/manual]).
+latex_documents = [
+ ('index', 'Cubicweb.tex', 'Cubicweb Documentation',
+ 'Logilab', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+#aafig_format = dict(latex='pdf', html='svg', text=None)
+
+rst_epilog = """
+.. |cubicweb| replace:: *CubicWeb*
+.. |yams| replace:: *Yams*
+.. |rql| replace:: *RQL*
+"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/dev/coding_standards_css.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,33 @@
+CSS Coding Standards
+--------------------
+
+(Draft, to be continued)
+
+:Naming: camelCase
+
+Indentation rules
+~~~~~~~~~~~~~~~~~
+- 2 espaces avant les propriétés
+
+- pas d'espace avant les ":", un espace après
+
+- 1 seul espace entre les différentes valeurs pour une même propriété
+
+
+Documentation
+~~~~~~~~~~~~~
+Please keep rules semantically linked grouped together, with a comment about
+what they are for.
+
+Recommendation
+~~~~~~~~~~~~~~
+- Try to use existing classes rather than introduce new ones
+
+- Keep things as simple as possible while in the framework
+
+- Think about later customization by application
+
+- Avoid introducing a new CSS file for a few lines of CSS, at least while the
+ framework doesn't include packing functionalities
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/dev/coding_standards_js.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,37 @@
+Javascript Coding Standards
+---------------------------
+
+(Draft, to be continued)
+
+:Naming: camelCase, except for CONSTANTS
+
+Indentation rules
+~~~~~~~~~~~~~~~~~
+- espace avant accolade ouvrante
+
+- retour à la ligne après accolade ouvrante (éventuellement pas
+ de retour à la ligne s'il y a tout sur la même ligne, mais ce n'est
+ pas le cas ici.
+
+- no tabs
+
+
+Documentation
+~~~~~~~~~~~~~
+XXX explain comment format for documentation generation
+
+
+Coding
+~~~~~~
+- Don't forget 'var' before variable definition, and semi-colon (';') after **each** statement.
+- Check the firebug console for deprecation warnings
+
+
+API usage
+~~~~~~~~~
+- unless intended, use jQuery('container') rather than jqNode('container')
+
+
+See also
+~~~~~~~~
+http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/dev/documenting.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,85 @@
+====
+Book
+====
+
+----
+Part
+----
+
+Chapter
+=======
+
+.. _Level1AnchorForLaterReference:
+
+Level 1 section
+---------------
+
+Level 2 section
+~~~~~~~~~~~~~~~
+
+Level 3 section
+```````````````
+
+
+
+*CubicWeb*
+
+
+inline directives:
+ :file:`directory/file`
+ :envvar:`AN_ENV_VARIABLE`
+ :command:`command --option arguments`
+
+ :ref:, :mod:
+
+
+.. sourcecode:: python
+
+ class SomePythonCode:
+ ...
+
+.. XXX a comment, wont be rendered
+
+
+a [foot note]_
+
+.. [foot note] the foot note content
+
+
+Boxes
+=====
+
+- warning box:
+ .. warning::
+
+ Warning content
+- note box:
+ .. note::
+
+ Note content
+
+
+
+Cross references
+================
+
+To arbitrary section
+--------------------
+
+:ref:`identifier` ou :ref:`label <identifier>`
+
+Label required of referencing node which as no title, else the node's title will be used.
+
+
+To API objects
+--------------
+See the autodoc sphinx extension documentation. Quick overview:
+
+* ref to a class: :class:`cubicweb.devtools.testlib.AutomaticWebTest`
+
+* if you can to see only the class name in the generated documentation, add a ~:
+ :class:`~cubicweb.devtools.testlib.AutomaticWebTest`
+
+* you can also use :mod: (module), :exc: (exception), :func: (function), :meth: (method)...
+
+* syntax explained above to specify label explicitly may also be used
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/dev/features_list.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,223 @@
+=================
+CubicWeb features
+=================
+
+This page tries to resume features found in the bare cubicweb framework,
+how mature and documented they are.
+
+:code maturity (CM):
+
+ - 0: experimental, not ready at all for production, may be killed
+
+ - 1: draft / unsatisfying, api may change in a near future, much probably in long
+ term
+
+ - 2: good enough, api sounds good but will probably evolve a bit with more
+ hindsight
+
+ - 3: mature, backward incompatible changes unexpected (may still evolve though,
+ of course)
+
+
+:documentation level (DL):
+
+ - 0: no documentation
+
+ - 1: poor documentation
+
+ - 2: some valuable documentation but some parts keep uncovered
+
+ - 3: good / complete documentation
+
+
+Instance configuration and maintainance
+=======================================
+
++====================================================================+====+====+
+| FEATURE | CM | DL |
++====================================================================+====+====+
+| setup - installation | 2 | 3 |
+| setup - environment variables | 3 | 2 |
+| setup - running modes | 2 | 2 |
+| setup - administration tasks | 2 | 2 |
+| setup - configuration file | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+| configuration - user / groups handling | 3 | 1 |
+| configuration - site configuration | 3 | 1 |
+| configuration - distributed configuration | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+| multi-sources - capabilities | NA | 0 |
+| multi-sources - configuration | 2 | 0 |
+| multi-sources - ldap integration | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+| usage - custom ReST markup | 2 | 0 |
+| usage - personal preferences | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+
+
+Core development
+================
+
++====================================================================+====+====+
+| FEATURE | CM | DL |
++====================================================================+====+====+
+| base - concepts | NA | 3 |
+| base - security model | NA | 2 |
+| base - database initialization | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+| rql - base | 2 | 2 |
+| rql - write | 2 | 2 |
+| rql - function | 2 | 0 |
+| rql - outer joins | 2 | 1 |
+| rql - aggregates | 2 | 1 |
+| rql - subqueries | 2 | 0 |
++--------------------------------------------------------------------+----+----+
+| schema - base | 2 | 3 |
+| schema - constraints | 3 | 2 |
+| schema - security | 2 | 2 |
+| schema - inheritance | 1 | 1 |
+| schema - customization | 1 | 1 |
+| schema - introspection | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+| vregistry - appobject | 2 | 2 |
+| vregistry - registration | 2 | 2 |
+| vregistry - selection | 3 | 2 |
+| vregistry - core selectors | 3 | 3 |
+| vregistry - custom selectors | 2 | 1 |
+| vregistry - debugging selection | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+| entities - interfaces | 2 | ? |
+| entities - customization (`dc_`, ...) | 2 | ? |
+| entities - app logic | 2 | 2 |
+| entities - orm configuration | 2 | 1 |
+| entities - pluggable mixins | 1 | 0 |
+| entities - workflow | 3 | 2 |
++--------------------------------------------------------------------+----+----+
+| dbapi - connection | 3 | 1 |
+| dbapi - data management | 1 | 1 |
+| dbapi - result set | 3 | 1 |
+| dbapi - transaction, undo | 2 | 0 |
++--------------------------------------------------------------------+----+----+
+| cube - layout | 2 | 3 |
+| cube - new cube | 2 | 2 |
++--------------------------------------------------------------------+----+----+
+| migration - context | 2 | 1 |
+| migration - commands | 2 | 2 |
++--------------------------------------------------------------------+----+----+
+| testlib - CubicWebTC | 2 | 1 |
+| testlib - automatic tests | 2 | 2 |
++--------------------------------------------------------------------+----+----+
+| i18n - mark string | 3 | 2 |
+| i18n - customize strings from other cubes / cubicweb | 3 | 1 |
+| i18n - update catalog | 3 | 2 |
++--------------------------------------------------------------------+----+----+
+| more - reloading tips | NA | 0 |
+| more - site_cubicweb | 2 | ? |
+| more - adding options in configuration file | 3 | 0 |
+| more - adding options in site configuration / preferences | 3 | ? |
+| more - optimizing / profiling | 2 | 1 |
+| more - c-c plugins | 3 | 0 |
+| more - crypto services | 0 | 0 |
+| more - massive import | 2 | 0 |
+| more - mime type based conversion | 2 | 0 |
+| more - CWCache | 1 | 0 |
++--------------------------------------------------------------------+----+----+
+
+
+Web UI development
+==================
+
++====================================================================+====+====+
+| FEATURE | CM | DL |
++====================================================================+====+====+
+| base - web request | 2 | 2 |
+| base - exceptions | 2 | 0 |
+| base - session, authentication | 1 | 0 |
+| base - http caching | 2 | 1 |
+| base - external resources | 2 | 2 |
+| base - static files | 2 | ? |
+| base - data sharing | 2 | 2 |
+| base - graphical chart customization | 1 | 1 |
++--------------------------------------------------------------------+----+----+
+| publishing - cycle | 2 | 2 |
+| publishing - error handling | 2 | 1 |
+| publishing - transactions | NA | ? |
++--------------------------------------------------------------------+----+----+
+| controller - base | 2 | 2 |
+| controller - view | 2 | 1 |
+| controller - edit | 2 | 1 |
+| controller - json | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+| views - base | 2 | 2 |
+| views - templates | 2 | 2 |
+| views - boxes | 2 | 1 |
+| views - components | 2 | 1 |
+| views - primary | 2 | 1 |
+| views - tabs | 2 | 1 |
+| views - xml | 2 | 0 |
+| views - text | 2 | 1 |
+| views - table | 2 | 1 |
+| views - plot | 2 | 0 |
+| views - navigation | 2 | 0 |
+| views - calendar, timeline | 2 | 0 |
+| views - index | 2 | 2 |
+| views - breadcrumbs | 2 | 1 |
+| views - actions | 2 | 1 |
+| views - debugging | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+| form - base | 2 | 1 |
+| form - fields | 2 | 1 |
+| form - widgets | 2 | 1 |
+| form - captcha | 2 | 0 |
+| form - renderers | 2 | 0 |
+| form - validation error handling | 2 | 0 |
+| form - autoform | 2 | 2 |
+| form - reledit | 2 | 0 |
++--------------------------------------------------------------------+----+----+
+| facets - base | 2 | ? |
+| facets - configuration | 2 | 1 |
+| facets - custom facets | 2 | 0 |
++--------------------------------------------------------------------+----+----+
+| css - base | 1 | 1 |
+| css - customization | 1 | 1 |
++--------------------------------------------------------------------+----+----+
+| js - base | 1 | 1 |
+| js - jquery | 1 | 1 |
+| js - base functions | 1 | 0 |
+| js - ajax | 1 | 0 |
+| js - widgets | 1 | 1 |
++--------------------------------------------------------------------+----+----+
+| other - page template | 0 | 0 |
+| other - inline doc (wdoc) | 2 | 0 |
+| other - magic search | 2 | 0 |
+| other - url mapping | 1 | 1 |
+| other - apache style url rewrite | 1 | 1 |
+| other - sparql | 1 | 0 |
+| other - bookmarks | 2 | 1 |
++--------------------------------------------------------------------+----+----+
+
+
+Repository development
+======================
+
++====================================================================+====+====+
+| FEATURE | CM | DL |
++====================================================================+====+====+
+| base - session | 2 | 2 |
+| base - more security control | 2 | 0 |
+| base - debugging | 2 | 0 |
++--------------------------------------------------------------------+----+----+
+| hooks - development | 2 | 2 |
+| hooks - abstract hooks | 2 | 0 |
+| hooks - core hooks | 2 | 0 |
+| hooks - control | 2 | 0 |
+| hooks - operation | 2 | 2 |
++--------------------------------------------------------------------+----+----+
+| notification - sending email | 2 | ? |
+| notification - base views | 1 | ? |
+| notification - supervisions | 1 | 0 |
++--------------------------------------------------------------------+----+----+
+| source - storages | 2 | 0 |
+| source - authentication plugins | 2 | 0 |
+| source - custom sources | 2 | 0 |
++--------------------------------------------------------------------+----+----+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/dev/refactoring-the-css-with-uiprops.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,42 @@
+=========================================
+Refactoring the CSSs with UI properties
+=========================================
+
+Overview
+=========
+
+Managing styles progressively became difficult in CubicWeb. The
+introduction of uiprops is an attempt to fix this problem.
+
+The goal is to make it possible to use variables in our CSSs.
+
+These variables are defined or computed in the uiprops.py python file
+and inserted in the CSS using the Python string interpolation syntax.
+
+A quick example, put in ``uiprops.py``::
+
+ defaultBgColor = '#eee'
+
+and in your css::
+
+ body { background-color: %(defaultBgColor)s; }
+
+
+The good practices are:
+
+- define a variable in uiprops to avoid repetitions in the CSS
+ (colors, borders, fonts, etc.)
+
+- define a variable in uiprops when you need to compute values
+ (compute a color palette, etc.)
+
+The algorithm implemented in CubicWeb is the following:
+
+- read uiprops file while walk up the chain of cube dependencies: if
+ cube myblog depends on cube comment, the variables defined in myblog
+ will have precedence over the ones in comment
+
+- replace the %(varname)s in all the CSSs of all the cubes
+
+Keep in mind that the browser will then interpret the CSSs and apply
+the standard cascading mechanism.
--- a/doc/features_list.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,224 +0,0 @@
-=================
-CubicWeb features
-=================
-
-This page tries to resume features found in the bare cubicweb framework,
-how mature and documented they are.
-
-:code maturity (CM):
-
- - 0: experimental, not ready at all for production, may be killed
-
- - 1: draft / unsatisfying, api may change in a near future, much probably in long
- term
-
- - 2: good enough, api sounds good but will probably evolve a bit with more
- hindsight
-
- - 3: mature, backward incompatible changes unexpected (may still evolve though,
- of course)
-
-
-:documentation level (DL):
-
- - 0: no documentation
-
- - 1: poor documentation
-
- - 2: some valuable documentation but some parts keep uncovered
-
- - 3: good / complete documentation
-
-
-Instance configuration and maintainance
-=======================================
-
-+====================================================================+====+====+
-| FEATURE | CM | DL |
-+====================================================================+====+====+
-| setup - installation | 2 | 3 |
-| setup - environment variables | 3 | 2 |
-| setup - running modes | 2 | 2 |
-| setup - administration tasks | 2 | 2 |
-| setup - configuration file | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-| configuration - user / groups handling | 3 | 1 |
-| configuration - site configuration | 3 | 1 |
-| configuration - distributed configuration | 2 | 1 |
-| configuration - pyro | 2 | 2 |
-+--------------------------------------------------------------------+----+----+
-| multi-sources - capabilities | NA | 0 |
-| multi-sources - configuration | 2 | 0 |
-| multi-sources - ldap integration | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-| usage - custom ReST markup | 2 | 0 |
-| usage - personal preferences | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-
-
-Core development
-================
-
-+====================================================================+====+====+
-| FEATURE | CM | DL |
-+====================================================================+====+====+
-| base - concepts | NA | 3 |
-| base - security model | NA | 2 |
-| base - database initialization | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-| rql - base | 2 | 2 |
-| rql - write | 2 | 2 |
-| rql - function | 2 | 0 |
-| rql - outer joins | 2 | 1 |
-| rql - aggregates | 2 | 1 |
-| rql - subqueries | 2 | 0 |
-+--------------------------------------------------------------------+----+----+
-| schema - base | 2 | 3 |
-| schema - constraints | 3 | 2 |
-| schema - security | 2 | 2 |
-| schema - inheritance | 1 | 1 |
-| schema - customization | 1 | 1 |
-| schema - introspection | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-| vregistry - appobject | 2 | 2 |
-| vregistry - registration | 2 | 2 |
-| vregistry - selection | 3 | 2 |
-| vregistry - core selectors | 3 | 3 |
-| vregistry - custom selectors | 2 | 1 |
-| vregistry - debugging selection | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-| entities - interfaces | 2 | ? |
-| entities - customization (dc_,...) | 2 | ? |
-| entities - app logic | 2 | 2 |
-| entities - orm configuration | 2 | 1 |
-| entities - pluggable mixins | 1 | 0 |
-| entities - workflow | 3 | 2 |
-+--------------------------------------------------------------------+----+----+
-| dbapi - connection | 3 | 1 |
-| dbapi - data management | 1 | 1 |
-| dbapi - result set | 3 | 1 |
-| dbapi - transaction, undo | 2 | 0 |
-+--------------------------------------------------------------------+----+----+
-| cube - layout | 2 | 3 |
-| cube - new cube | 2 | 2 |
-+--------------------------------------------------------------------+----+----+
-| migration - context | 2 | 1 |
-| migration - commands | 2 | 2 |
-+--------------------------------------------------------------------+----+----+
-| testlib - CubicWebTC | 2 | 1 |
-| testlib - automatic tests | 2 | 2 |
-+--------------------------------------------------------------------+----+----+
-| i18n - mark string | 3 | 2 |
-| i18n - customize strings from other cubes / cubicweb | 3 | 1 |
-| i18n - update catalog | 3 | 2 |
-+--------------------------------------------------------------------+----+----+
-| more - reloading tips | NA | 0 |
-| more - site_cubicweb | 2 | ? |
-| more - adding options in configuration file | 3 | 0 |
-| more - adding options in site configuration / preferences | 3 | ? |
-| more - optimizing / profiling | 2 | 1 |
-| more - c-c plugins | 3 | 0 |
-| more - crypto services | 0 | 0 |
-| more - massive import | 2 | 0 |
-| more - mime type based conversion | 2 | 0 |
-| more - CWCache | 1 | 0 |
-+--------------------------------------------------------------------+----+----+
-
-
-Web UI development
-==================
-
-+====================================================================+====+====+
-| FEATURE | CM | DL |
-+====================================================================+====+====+
-| base - web request | 2 | 2 |
-| base - exceptions | 2 | 0 |
-| base - session, authentication | 1 | 0 |
-| base - http caching | 2 | 1 |
-| base - external resources | 2 | 2 |
-| base - static files | 2 | ? |
-| base - data sharing | 2 | 2 |
-| base - graphical chart customization | 1 | 1 |
-+--------------------------------------------------------------------+----+----+
-| publishing - cycle | 2 | 2 |
-| publishing - error handling | 2 | 1 |
-| publishing - transactions | NA | ? |
-+--------------------------------------------------------------------+----+----+
-| controller - base | 2 | 2 |
-| controller - view | 2 | 1 |
-| controller - edit | 2 | 1 |
-| controller - json | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-| views - base | 2 | 2 |
-| views - templates | 2 | 2 |
-| views - boxes | 2 | 1 |
-| views - components | 2 | 1 |
-| views - primary | 2 | 1 |
-| views - tabs | 2 | 1 |
-| views - xml | 2 | 0 |
-| views - text | 2 | 1 |
-| views - table | 2 | 1 |
-| views - plot | 2 | 0 |
-| views - navigation | 2 | 0 |
-| views - calendar, timeline | 2 | 0 |
-| views - index | 2 | 2 |
-| views - breadcrumbs | 2 | 1 |
-| views - actions | 2 | 1 |
-| views - debugging | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-| form - base | 2 | 1 |
-| form - fields | 2 | 1 |
-| form - widgets | 2 | 1 |
-| form - captcha | 2 | 0 |
-| form - renderers | 2 | 0 |
-| form - validation error handling | 2 | 0 |
-| form - autoform | 2 | 2 |
-| form - reledit | 2 | 0 |
-+--------------------------------------------------------------------+----+----+
-| facets - base | 2 | ? |
-| facets - configuration | 2 | 1 |
-| facets - custom facets | 2 | 0 |
-+--------------------------------------------------------------------+----+----+
-| css - base | 1 | 1 |
-| css - customization | 1 | 1 |
-+--------------------------------------------------------------------+----+----+
-| js - base | 1 | 1 |
-| js - jquery | 1 | 1 |
-| js - base functions | 1 | 0 |
-| js - ajax | 1 | 0 |
-| js - widgets | 1 | 1 |
-+--------------------------------------------------------------------+----+----+
-| other - page template | 0 | 0 |
-| other - inline doc (wdoc) | 2 | 0 |
-| other - magic search | 2 | 0 |
-| other - url mapping | 1 | 1 |
-| other - apache style url rewrite | 1 | 1 |
-| other - sparql | 1 | 0 |
-| other - bookmarks | 2 | 1 |
-+--------------------------------------------------------------------+----+----+
-
-
-Repository development
-======================
-
-+====================================================================+====+====+
-| FEATURE | CM | DL |
-+====================================================================+====+====+
-| base - session | 2 | 2 |
-| base - more security control | 2 | 0 |
-| base - debugging | 2 | 0 |
-+--------------------------------------------------------------------+----+----+
-| hooks - development | 2 | 2 |
-| hooks - abstract hooks | 2 | 0 |
-| hooks - core hooks | 2 | 0 |
-| hooks - control | 2 | 0 |
-| hooks - operation | 2 | 2 |
-+--------------------------------------------------------------------+----+----+
-| notification - sending email | 2 | ? |
-| notification - base views | 1 | ? |
-| notification - supervisions | 1 | 0 |
-+--------------------------------------------------------------------+----+----+
-| source - storages | 2 | 0 |
-| source - authentication plugins | 2 | 0 |
-| source - custom sources | 2 | 0 |
-+--------------------------------------------------------------------+----+----+
Binary file doc/images/03-transitions-view_en.png has changed
Binary file doc/images/breadcrumbs_header.png has changed
Binary file doc/images/facet_date_range.png has changed
Binary file doc/images/facet_has_image.png has changed
Binary file doc/images/facet_overview.png has changed
Binary file doc/images/facet_range.png has changed
Binary file doc/images/lax-book_00-login_en.png has changed
Binary file doc/images/lax-book_01-start_en.png has changed
Binary file doc/images/lax-book_02-cookie-values_en.png has changed
Binary file doc/images/lax-book_02-create-blog_en.png has changed
Binary file doc/images/lax-book_03-list-one-blog_en.png has changed
Binary file doc/images/lax-book_03-site-config-panel_en.png has changed
Binary file doc/images/lax-book_03-state-submitted_en.png has changed
Binary file doc/images/lax-book_03-transitions-view_en.png has changed
Binary file doc/images/lax-book_04-detail-one-blog_en.png has changed
Binary file doc/images/lax-book_05-list-two-blog_en.png has changed
Binary file doc/images/lax-book_06-add-relation-entryof_en.png has changed
Binary file doc/images/lax-book_06-main-template-logo_en.png has changed
Binary file doc/images/lax-book_07-detail-one-blogentry_en.png has changed
Binary file doc/images/lax-book_08-schema_en.png has changed
Binary file doc/images/lax-book_09-new-view-blogentry_en.png has changed
Binary file doc/images/lax-book_10-blog-with-two-entries_en.png has changed
Binary file doc/images/main_template.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/images/main_template.svg Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="1036.6421"
+ height="845.07812"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="main_template.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="/home/auc/cw/doc/book/en/images/main_template.png"
+ inkscape:export-xdpi="60.659016"
+ inkscape:export-ydpi="60.659016">
+ <defs
+ id="defs4">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 526.18109 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+ id="perspective10" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.80355603"
+ inkscape:cx="510.91495"
+ inkscape:cy="422.53906"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="925"
+ inkscape:window-height="1168"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:snap-bbox="true" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Calque 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(162.2968,90.697922)">
+ <rect
+ style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect2439"
+ width="854.37006"
+ height="698.2019"
+ x="20.307629"
+ y="-20.575344" />
+ <rect
+ style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3301"
+ width="816.3457"
+ height="508.15628"
+ x="31.751091"
+ y="96.33345" />
+ <g
+ id="g3220"
+ transform="matrix(1.0035394,0,0,1,0.5745006,0)">
+ <rect
+ y="-89.447922"
+ x="-161.0468"
+ height="55.714287"
+ width="1031.1713"
+ id="rect3240"
+ style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.50000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <text
+ id="text3264"
+ y="-51.771908"
+ x="757.85767"
+ style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan3266"
+ y="-51.771908"
+ x="757.85767"
+ sodipodi:role="line">header</tspan></text>
+ </g>
+ <rect
+ style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.775;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3270"
+ width="167.87744"
+ height="707.71222"
+ x="-160.02441"
+ y="-24.671618" />
+ <g
+ id="g2434"
+ transform="matrix(0.975467,0,0,1,0.6942419,-3.6587365)">
+ <rect
+ y="35.365849"
+ x="29.548275"
+ height="55.714287"
+ width="842.59979"
+ id="rect3279"
+ style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <text
+ id="text3281"
+ y="72.885193"
+ x="681.65283"
+ style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan3283"
+ y="72.885193"
+ x="681.65283"
+ sodipodi:role="line">contentheader</tspan></text>
+ </g>
+ <g
+ id="g3170"
+ transform="matrix(1.0023324,0,0,1,-2.0421673,-10.976211)">
+ <rect
+ y="698.6355"
+ x="-158.28485"
+ height="55.714287"
+ width="1032.5997"
+ id="rect3285"
+ style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <text
+ id="text3287"
+ y="736.52045"
+ x="770.28204"
+ style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan3289"
+ y="736.52045"
+ x="770.28204"
+ sodipodi:role="line">footer</tspan></text>
+ </g>
+ <g
+ id="g3211" />
+ <g
+ id="g3215"
+ transform="matrix(0.9712065,0,0,1,0.7659296,-17.074106)">
+ <rect
+ style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3291"
+ width="844.62012"
+ height="55.714287"
+ x="27.850754"
+ y="629.88562" />
+ <text
+ id="text3293"
+ y="666.60339"
+ x="692.85773"
+ style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan3295"
+ y="666.60339"
+ x="692.85773"
+ sodipodi:role="line">contentfooter</tspan></text>
+ </g>
+ <text
+ xml:space="preserve"
+ style="font-size:23.38711166px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="-143.67273"
+ y="20.58094"
+ id="text3297"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan2432"
+ x="-143.67273"
+ y="20.58094">left column</tspan></text>
+ <text
+ transform="scale(0.9876573,1.0124969)"
+ id="text3175"
+ y="12.071429"
+ x="721.0575"
+ style="font-size:23.09845161px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan3177"
+ y="12.071429"
+ x="721.0575"
+ sodipodi:role="line">contentcol</tspan></text>
+ <text
+ transform="scale(0.9876573,1.0124969)"
+ id="text3179"
+ y="126.27104"
+ x="701.45959"
+ style="font-size:23.09845161px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan3181"
+ y="126.27104"
+ x="701.45959"
+ sodipodi:role="line">contentmain</tspan></text>
+ </g>
+</svg>
Binary file doc/images/main_template_layout.png has changed
Binary file doc/images/primaryview_template.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/images/primaryview_template.svg Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="1036.6421"
+ height="845.07812"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="primaryview_template.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="/home/steph/local/fcubicweb/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="43.451603"
+ inkscape:export-ydpi="43.451603">
+ <defs
+ id="defs4">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 526.18109 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+ id="perspective10" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.9357135"
+ inkscape:cx="518.32104"
+ inkscape:cy="337.0428"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1307"
+ inkscape:window-height="1168"
+ inkscape:window-x="0"
+ inkscape:window-y="0" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Calque 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(162.2968,90.697922)">
+ <g
+ id="g3869"
+ transform="matrix(1,0,0,1.0373644,0,-72.039777)"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449">
+ <rect
+ y="-15.840891"
+ x="-159.08963"
+ height="770.11017"
+ width="1033.0049"
+ id="rect3301"
+ style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.90144825;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <text
+ id="text3865"
+ y="19.784882"
+ x="-150.07172"
+ style="font-size:28.67479324px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan3867"
+ y="19.784882"
+ x="-150.07172"
+ sodipodi:role="line">contentmain</tspan></text>
+ </g>
+ <rect
+ style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.45654476;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect2383"
+ width="772.32111"
+ height="43.888428"
+ x="-131.1837"
+ y="86.559296"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449" />
+ <text
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%"
+ x="-122.69418"
+ y="115.50363"
+ id="text2385"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ x="-122.69418"
+ y="115.50363"
+ id="tspan3163">navcontenttop</tspan></text>
+ <rect
+ style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:3.06523442;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3167"
+ width="770.26868"
+ height="203.16078"
+ x="-125.88269"
+ y="172.90417"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449" />
+ <text
+ xml:space="preserve"
+ style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="348.26724"
+ y="205.34305"
+ id="text3169"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ x="348.26724"
+ y="205.34305"
+ id="tspan3171">render_entity_attributes()</tspan></text>
+ <rect
+ style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:3.06523442;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3173"
+ width="769.93549"
+ height="237.84663"
+ x="-125.03326"
+ y="391.32156"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449" />
+ <text
+ xml:space="preserve"
+ style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="360.99954"
+ y="428.38055"
+ id="text3175"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ x="360.99954"
+ y="428.38055"
+ id="tspan3177">render_entity_relations()</tspan></text>
+ <rect
+ style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:2.15903592;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3185"
+ width="178.93939"
+ height="612.36584"
+ x="667.10443"
+ y="84.64225"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449" />
+ <text
+ xml:space="preserve"
+ style="font-size:22px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%"
+ x="105.32364"
+ y="-810.65997"
+ id="text3187"
+ transform="matrix(0,1,-1,0,0,0)"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ id="tspan2408">render_side_boxes()</tspan></text>
+ <rect
+ style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:3.0652349;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3191"
+ width="771.97766"
+ height="55.647793"
+ x="-127.80586"
+ y="642.0293"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449" />
+ <text
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="-121.22153"
+ y="674.1748"
+ id="text3181"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ x="-121.22153"
+ y="674.1748"
+ id="tspan3183">navcontentbottom</tspan></text>
+ <rect
+ style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.68198514;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3881"
+ width="986.90503"
+ height="45.800392"
+ x="-128.34428"
+ y="-31.574066"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449" />
+ <text
+ xml:space="preserve"
+ style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="355.60541"
+ y="-2.7424495"
+ id="text3883"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ x="355.60541"
+ y="-2.7424495"
+ id="tspan3885">render_entity_toolbox(), render_entity_title()</tspan></text>
+ <rect
+ style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1.68198514;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect3890"
+ width="986.90503"
+ height="45.800392"
+ x="-128.87863"
+ y="19.723684"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449" />
+ <text
+ xml:space="preserve"
+ style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="565.71027"
+ y="50.135612"
+ id="text3892"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ x="565.71027"
+ y="50.135612"
+ id="tspan3894">render_entity_summary()</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="87.154541"
+ y="114.2578"
+ id="text3899"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ id="tspan3903"
+ x="87.154541"
+ y="114.2578">content_navigation_components('navcontenttop')</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="88.46772"
+ y="675.71582"
+ id="text2410"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/home/auc/src/fcw/cubicweb/doc/book/en/images/primaryview_template.png"
+ inkscape:export-xdpi="60.912449"
+ inkscape:export-ydpi="60.912449"><tspan
+ sodipodi:role="line"
+ id="tspan2412"
+ x="88.46772"
+ y="675.71582">content_navigation_components('navcontenttop')</tspan></text>
+ </g>
+</svg>
Binary file doc/images/request_session.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/images/request_session.svg Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="85.960938"
+ height="12.382812"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="request_session.svg">
+ <defs
+ id="defs4">
+ <marker
+ inkscape:stockid="Arrow1Lend"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow1Lend"
+ style="overflow:visible;">
+ <path
+ id="path3822"
+ d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;marker-start:none;"
+ transform="scale(0.8) rotate(180) translate(12.5,0)" />
+ </marker>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.98994949"
+ inkscape:cx="25.928992"
+ inkscape:cy="-185.87004"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="958"
+ inkscape:window-height="1160"
+ inkscape:window-x="0"
+ inkscape:window-y="38"
+ inkscape:window-maximized="0"
+ inkscape:snap-global="true" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-263.52249,-495.73373)">
+ <rect
+ style="fill:#ffffff;stroke:#000000;stroke-width:0.92460138;stroke-opacity:1"
+ id="rect3773"
+ width="214.15233"
+ height="184.80336"
+ x="57.578697"
+ y="366.01306" />
+ <rect
+ id="rect2985"
+ width="216.86372"
+ height="183.54575"
+ x="348.50262"
+ y="367.78079"
+ style="fill:#ffffff;stroke:#000000;stroke-width:0.55298227;stroke-opacity:1" />
+ <text
+ xml:space="preserve"
+ style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="376.7869"
+ y="399.80365"
+ id="text3755"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3757"
+ x="376.7869"
+ y="399.80365">Repository</tspan></text>
+ <rect
+ style="fill:#ffffff;stroke:#000000;stroke-opacity:1"
+ id="rect3759"
+ width="144.45181"
+ height="104.04572"
+ x="237.38585"
+ y="423.03714" />
+ <text
+ xml:space="preserve"
+ style="font-size:24px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="262.63968"
+ y="470.51431"
+ id="text3761"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3763"
+ x="262.63968"
+ y="470.51431">REPOAPI</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="262.63968"
+ y="507.88998"
+ id="text3765"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3767"
+ x="262.63968"
+ y="507.88998">connection</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="419.21332"
+ y="509.91025"
+ id="text3769"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3771"
+ x="419.21332"
+ y="509.91025">session</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="102.02541"
+ y="397.78333"
+ id="text3775"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3777"
+ x="102.02541"
+ y="397.78333">Client</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="116.16754"
+ y="507.88995"
+ id="text3779"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3781"
+ x="116.16754"
+ y="507.88995">request</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="361.50729"
+ y="585.89832"
+ id="text3802"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3804"
+ x="361.50729"
+ y="585.89832">database </tspan><tspan
+ sodipodi:role="line"
+ x="361.50729"
+ y="605.89832"
+ id="tspan3806">connection</tspan></text>
+ <rect
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.48014534;stroke-opacity:1"
+ id="rect3808"
+ width="192.09367"
+ height="58.095726"
+ x="365.79443"
+ y="621.50018" />
+ <text
+ xml:space="preserve"
+ style="font-size:36px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="369.5885"
+ y="662.66992"
+ id="text3810"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3812"
+ x="369.5885"
+ y="662.66992">Database</tspan></text>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:none"
+ d="M 197.57252,125.76645 195.76971,55.592808"
+ id="path4260"
+ inkscape:connector-type="polyline"
+ inkscape:connector-curvature="3"
+ inkscape:connection-start="#rect3808"
+ inkscape:connection-start-point="d4"
+ inkscape:connection-end="#rect2985"
+ inkscape:connection-end-point="d4"
+ transform="translate(263.52249,495.73373)" />
+ </g>
+</svg>
Binary file doc/images/server-class-diagram.png has changed
Binary file doc/images/tutos-base_blog-form_en.png has changed
Binary file doc/images/tutos-base_blog-primary-after-post-creation_en.png has changed
Binary file doc/images/tutos-base_blog-primary_en.png has changed
Binary file doc/images/tutos-base_blogs-list_en.png has changed
Binary file doc/images/tutos-base_form-generic-relations_en.png has changed
Binary file doc/images/tutos-base_index_en.png has changed
Binary file doc/images/tutos-base_login-form_en.png has changed
Binary file doc/images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png has changed
Binary file doc/images/tutos-base_myblog-community-custom-primary_en.png has changed
Binary file doc/images/tutos-base_myblog-community-default-primary_en.png has changed
Binary file doc/images/tutos-base_myblog-community-taggable-primary_en.png has changed
Binary file doc/images/tutos-base_myblog-custom-footer_en.png has changed
Binary file doc/images/tutos-base_myblog-schema_en.png has changed
Binary file doc/images/tutos-base_myblog-siteinfo_en.png has changed
Binary file doc/images/tutos-base_schema_en.png has changed
Binary file doc/images/tutos-base_siteconfig_en.png has changed
Binary file doc/images/tutos-base_user-menu_en.png has changed
Binary file doc/images/tutos-photowebsite_background-image.png has changed
Binary file doc/images/tutos-photowebsite_boxes.png has changed
Binary file doc/images/tutos-photowebsite_breadcrumbs.png has changed
Binary file doc/images/tutos-photowebsite_facets.png has changed
Binary file doc/images/tutos-photowebsite_grey-box.png has changed
Binary file doc/images/tutos-photowebsite_index-after.png has changed
Binary file doc/images/tutos-photowebsite_index-before.png has changed
Binary file doc/images/tutos-photowebsite_login-box.png has changed
Binary file doc/images/tutos-photowebsite_prevnext.png has changed
Binary file doc/images/tutos-photowebsite_ui1.png has changed
Binary file doc/images/tutos-photowebsite_ui2.png has changed
Binary file doc/images/tutos-photowebsite_ui3.png has changed
Binary file doc/images/undo_history-view_w600.png has changed
Binary file doc/images/undo_mesage_w600.png has changed
Binary file doc/images/undo_startup-link_w600.png has changed
Binary file doc/images/views-table-filter-shadow.png has changed
Binary file doc/images/views-table-filter.png has changed
Binary file doc/images/views-table-shadow.png has changed
Binary file doc/images/views-table.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,125 @@
+=====================================================
+|cubicweb| - The Semantic Web is a construction game!
+=====================================================
+
+|cubicweb| is a semantic web application framework, licensed under the LGPL,
+that empowers developers to efficiently build web applications by reusing
+components (called `cubes`) and following the well known object-oriented design
+principles.
+
+Main Features
+~~~~~~~~~~~~~
+
+* an engine driven by the explicit :ref:`data model
+ <TutosBaseCustomizingTheApplicationDataModel>` of the application,
+
+* a query language named :ref:`RQL <RQL>` similar to W3C's SPARQL,
+
+* a :ref:`selection+view <TutosBaseCustomizingTheApplicationCustomViews>`
+ mechanism for semi-automatic XHTML/XML/JSON/text generation,
+
+* a library of reusable :ref:`components <Cube>` (data model and views) that
+ fulfill common needs,
+
+* the power and flexibility of the Python_ programming language,
+
+* the reliability of SQL databases, LDAP directories, Subversion and Mercurial
+ for storage backends.
+
+Built since 2000 from an R&D effort still continued, supporting 100,000s of
+daily visits at some production sites, |cubicweb| is a proven end to end solution
+for semantic web application development that promotes quality, reusability and
+efficiency.
+
+QuickStart
+~~~~~~~~~~
+
+The impatient developer will move right away to :ref:`SetUpEnv` then to :ref:`ConfigEnv`.
+
+Social
+~~~~~~
+
+* Chat on the `jabber forum`_
+* Discuss on the `mailing-list`_
+* Discover on the `blog`_
+* Contribute on the forge_
+
+
+.. _Logilab: http://www.logilab.fr/
+.. _forge: http://www.cubicweb.org/project/
+.. _Python: http://www.python.org/
+.. _`jabber forum`: http://www.logilab.org/blogentry/6718
+.. _`mailing-list`: http://lists.cubicweb.org/mailman/listinfo/cubicweb
+.. _blog: http://www.cubicweb.org/blog/1238
+
+
+Narrative Documentation
+~~~~~~~~~~~~~~~~~~~~~~~
+
+A.k.a. "The Book"
+
+.. toctree::
+ :maxdepth: 2
+
+ book/intro/index
+
+.. toctree::
+ :maxdepth: 2
+
+ tutorials/index
+
+.. toctree::
+ :maxdepth: 3
+
+ book/devrepo/index
+ book/devweb/index
+
+.. toctree::
+ :maxdepth: 2
+
+ book/admin/index
+ book/additionnal_services/index
+ book/annexes/index
+
+
+
+Changes
+~~~~~~~
+
+.. toctree::
+ :maxdepth: 2
+
+ changes/changelog
+
+
+Reference documentation
+~~~~~~~~~~~~~~~~~~~~~~~
+
+API
+'''
+
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ api/*
+
+.. toctree::
+ :maxdepth: 1
+
+ js_api/index
+
+Developpers
+~~~~~~~~~~~
+
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ dev/*
+
+Indexes
+~~~~~~~
+
+* the :ref:`genindex`,
+* the :ref:`modindex`,
--- a/doc/refactoring-the-css-with-uiprops.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-=========================================
-Refactoring the CSSs with UI properties
-=========================================
-
-Overview
-=========
-
-Managing styles progressively became difficult in CubicWeb. The
-introduction of uiprops is an attempt to fix this problem.
-
-The goal is to make it possible to use variables in our CSSs.
-
-These variables are defined or computed in the uiprops.py python file
-and inserted in the CSS using the Python string interpolation syntax.
-
-A quick example, put in ``uiprops.py``::
-
- defaultBgColor = '#eee'
-
-and in your css::
-
- body { background-color: %(defaultBgColor)s; }
-
-
-The good practices are:
-
-- define a variable in uiprops to avoid repetitions in the CSS
- (colors, borders, fonts, etc.)
-
-- define a variable in uiprops when you need to compute values
- (compute a color palette, etc.)
-
-The algorithm implemented in CubicWeb is the following:
-
-- read uiprops file while walk up the chain of cube dependencies: if
- cube myblog depends on cube comment, the variables defined in myblog
- will have precedence over the ones in comment
-
-- replace the %(varname)s in all the CSSs of all the cubes
-
-Keep in mind that the browser will then interpret the CSSs and apply
-the standard cascading mechanism.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tools/mode_plan.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,50 @@
+# 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/>.
+"""
+>>> from mode_plan import *
+>>> ls()
+<list of directory content>
+>>> ren('A01','A03')
+rename A010-joe.en.txt to A030-joe.en.txt
+accept [y/N]?
+"""
+
+def ren(a,b):
+ names = glob.glob('%s*'%a)
+ for name in names :
+ print 'rename %s to %s' % (name, name.replace(a,b))
+ if raw_input('accept [y/N]?').lower() =='y':
+ for name in names:
+ os.system('hg mv %s %s' % (name, name.replace(a,b)))
+
+
+def ls(): print '\n'.join(sorted(os.listdir('.')))
+
+def move():
+ filenames = []
+ for name in sorted(os.listdir('.')):
+ num = name[:2]
+ if num.isdigit():
+ filenames.append( (int(num), name) )
+
+
+ #print filenames
+
+ for num, name in filenames:
+ if num >= start:
+ print 'hg mv %s %2i%s' %(name,num+1,name[2:])
--- a/doc/tools/pyjsrest.py Tue Jul 19 15:59:02 2016 +0200
+++ b/doc/tools/pyjsrest.py Tue Jul 19 16:13:12 2016 +0200
@@ -92,6 +92,9 @@
f_rst.write(rst_content)
stream = open(osp.join(rst_dir, 'index.rst'), 'w')
stream.write('''
+Javascript API
+==============
+
.. toctree::
:maxdepth: 1
@@ -134,7 +137,6 @@
'cubicweb.preferences',
'cubicweb.edition',
'cubicweb.reledit',
- 'cubicweb.timeline-ext',
]
FILES_TO_IGNORE = set([
@@ -152,7 +154,6 @@
'cubicweb.fckcwconfig-full.js',
'cubicweb.goa.js',
'cubicweb.compat.js',
- 'cubicweb.timeline-bundle.js',
])
if __name__ == '__main__':
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,29 @@
+
+.. _TutosPhotoWebSite:
+
+Building a photo gallery with |cubicweb|
+========================================
+
+Desired features
+----------------
+
+* basically a photo gallery
+
+* photo stored on the file system and displayed dynamically through a web interface
+
+* navigation through folder (album), tags, geographical zone, people on the
+ picture... using facets
+
+* advanced security (not everyone can see everything). More on this later.
+
+
+.. toctree::
+ :maxdepth: 2
+
+ part01_create-cube
+ part02_security
+ part03_bfss
+ part04_ui-base
+ part05_ui-advanced
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part01_create-cube.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,155 @@
+.. _TutosPhotoWebSiteCubeCreation:
+
+Cube creation and schema definition
+-----------------------------------
+
+.. _adv_tuto_create_new_cube:
+
+Step 1: creating a new cube for my web site
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One note about my development environment: I wanted to use the packaged
+version of CubicWeb and cubes while keeping my cube in my user
+directory, let's say `~src/cubes`. I achieve this by setting the
+following environment variables::
+
+ CW_CUBES_PATH=~/src/cubes
+ CW_MODE=user
+
+I can now create the cube which will hold custom code for this web
+site using::
+
+ cubicweb-ctl newcube --directory=~/src/cubes sytweb
+
+
+.. _adv_tuto_assemble_cubes:
+
+Step 2: pick building blocks into existing cubes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Almost everything I want to handle in my web-site is somehow already modelized in
+existing cubes that I'll extend for my need. So I'll pick the following cubes:
+
+* `folder`, containing the `Folder` entity type, which will be used as
+ both 'album' and a way to map file system folders. Entities are
+ added to a given folder using the `filed_under` relation.
+
+* `file`, containing `File` entity type, gallery view, and a file system import
+ utility.
+
+* `zone`, containing the `Zone` entity type for hierarchical geographical
+ zones. Entities (including sub-zones) are added to a given zone using the
+ `situated_in` relation.
+
+* `person`, containing the `Person` entity type plus some basic views.
+
+* `comment`, providing a full commenting system allowing one to comment entity types
+ supporting the `comments` relation by adding a `Comment` entity.
+
+* `tag`, providing a full tagging system as an easy and powerful way to classify
+ entities supporting the `tags` relation by linking the to `Tag` entities. This
+ will allows navigation into a large number of picture.
+
+Ok, now I'll tell my cube requires all this by editing :file:`cubes/sytweb/__pkginfo__.py`:
+
+ .. sourcecode:: python
+
+ __depends__ = {'cubicweb': '>= 3.10.0',
+ 'cubicweb-file': '>= 1.9.0',
+ 'cubicweb-folder': '>= 1.1.0',
+ 'cubicweb-person': '>= 1.2.0',
+ 'cubicweb-comment': '>= 1.2.0',
+ 'cubicweb-tag': '>= 1.2.0',
+ 'cubicweb-zone': None}
+
+Notice that you can express minimal version of the cube that should be used,
+`None` meaning whatever version available. All packages starting with 'cubicweb-'
+will be recognized as being cube, not bare python packages. You can still specify
+this explicitly using instead the `__depends_cubes__` dictionary which should
+contains cube's name without the prefix. So the example below would be written
+as:
+
+ .. sourcecode:: python
+
+ __depends__ = {'cubicweb': '>= 3.10.0'}
+ __depends_cubes__ = {'file': '>= 1.9.0',
+ 'folder': '>= 1.1.0',
+ 'person': '>= 1.2.0',
+ 'comment': '>= 1.2.0',
+ 'tag': '>= 1.2.0',
+ 'zone': None}
+
+If your cube is packaged for debian, it's a good idea to update the
+`debian/control` file at the same time, so you won't forget it.
+
+
+Step 3: glue everything together in my cube's schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. sourcecode:: python
+
+ from yams.buildobjs import RelationDefinition
+
+ class comments(RelationDefinition):
+ subject = 'Comment'
+ object = 'File'
+ cardinality = '1*'
+ composite = 'object'
+
+ class tags(RelationDefinition):
+ subject = 'Tag'
+ object = 'File'
+
+ class filed_under(RelationDefinition):
+ subject = 'File'
+ object = 'Folder'
+
+ class situated_in(RelationDefinition):
+ subject = 'File'
+ object = 'Zone'
+
+ class displayed_on(RelationDefinition):
+ subject = 'Person'
+ object = 'File'
+
+
+This schema:
+
+* allows to comment and tag on `File` entity type by adding the `comments` and
+ `tags` relations. This should be all we've to do for this feature since the
+ related cubes provide 'pluggable section' which are automatically displayed on
+ the primary view of entity types supporting the relation.
+
+* adds a `situated_in` relation definition so that image entities can be
+ geolocalized.
+
+* add a new relation `displayed_on` relation telling who can be seen on a
+ picture.
+
+This schema will probably have to evolve as time goes (for security handling at
+least), but since the possibility to let a schema evolve is one of CubicWeb's
+features (and goals), we won't worry about it for now and see that later when needed.
+
+
+Step 4: creating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now that I have a schema, I want to create an instance. To
+do so using this new 'sytweb' cube, I run::
+
+ cubicweb-ctl create sytweb sytweb_instance
+
+Hint: if you get an error while the database is initialized, you can
+avoid having to answer the questions again by running::
+
+ cubicweb-ctl db-create sytweb_instance
+
+This will use your already configured instance and start directly from the create
+database step, thus skipping questions asked by the 'create' command.
+
+Once the instance and database are fully initialized, run ::
+
+ cubicweb-ctl start sytweb_instance
+
+to start the instance, check you can connect on it, etc...
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part02_security.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,440 @@
+.. _TutosPhotoWebSiteSecurity:
+
+Security, testing and migration
+-------------------------------
+
+This part will cover various topics:
+
+* configuring security
+* migrating existing instance
+* writing some unit tests
+
+Here is the ``read`` security model I want:
+
+* folders, files, images and comments should have one of the following visibility:
+
+ - ``public``, everyone can see it
+ - ``authenticated``, only authenticated users can see it
+ - ``restricted``, only a subset of authenticated users can see it
+
+* managers (e.g. me) can see everything
+* only authenticated users can see people
+* everyone can see classifier entities, such as tag and zone
+
+Also, unless explicitly specified, the visibility of an image should be the same as
+its parent folder, as well as visibility of a comment should be the same as the
+commented entity. If there is no parent entity, the default visibility is
+``authenticated``.
+
+Regarding write security, that's much easier:
+* anonymous can't write anything
+* authenticated users can only add comment
+* managers will add the remaining stuff
+
+Now, let's implement that!
+
+Proper security in CubicWeb is done at the schema level, so you don't have to
+bother with it in views: users will only see what they can see automatically.
+
+.. _adv_tuto_security:
+
+Step 1: configuring security into the schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In schema, you can grant access according to groups, or to some RQL expressions:
+users get access if the expression returns some results. To implement the read
+security defined earlier, groups are not enough, we'll need some RQL expression. Here
+is the idea:
+
+* add a `visibility` attribute on Folder, File and Comment, which may be one of
+ the value explained above
+
+* add a `may_be_read_by` relation from Folder, File and Comment to users,
+ which will define who can see the entity
+
+* security propagation will be done in hook.
+
+So the first thing to do is to modify my cube's schema.py to define those
+relations:
+
+.. sourcecode:: python
+
+ from yams.constraints import StaticVocabularyConstraint
+
+ class visibility(RelationDefinition):
+ subject = ('Folder', 'File', 'Comment')
+ object = 'String'
+ constraints = [StaticVocabularyConstraint(('public', 'authenticated',
+ 'restricted', 'parent'))]
+ default = 'parent'
+ cardinality = '11' # required
+
+ class may_be_read_by(RelationDefinition):
+ __permissions__ = {
+ 'read': ('managers', 'users'),
+ 'add': ('managers',),
+ 'delete': ('managers',),
+ }
+
+ subject = ('Folder', 'File', 'Comment',)
+ object = 'CWUser'
+
+We can note the following points:
+
+* we've added a new `visibility` attribute to folder, file, image and comment
+ using a `RelationDefinition`
+
+* `cardinality = '11'` means this attribute is required. This is usually hidden
+ under the `required` argument given to the `String` constructor, but we can
+ rely on this here (same thing for StaticVocabularyConstraint, which is usually
+ hidden by the `vocabulary` argument)
+
+* the `parent` possible value will be used for visibility propagation
+
+* think to secure the `may_be_read_by` permissions, else any user can add/delete it
+ by default, which somewhat breaks our security model...
+
+Now, we should be able to define security rules in the schema, based on these new
+attribute and relation. Here is the code to add to *schema.py*:
+
+.. sourcecode:: python
+
+ from cubicweb.schema import ERQLExpression
+
+ VISIBILITY_PERMISSIONS = {
+ 'read': ('managers',
+ ERQLExpression('X visibility "public"'),
+ ERQLExpression('X may_be_read_by U')),
+ 'add': ('managers',),
+ 'update': ('managers', 'owners',),
+ 'delete': ('managers', 'owners'),
+ }
+ AUTH_ONLY_PERMISSIONS = {
+ 'read': ('managers', 'users'),
+ 'add': ('managers',),
+ 'update': ('managers', 'owners',),
+ 'delete': ('managers', 'owners'),
+ }
+ CLASSIFIERS_PERMISSIONS = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers',),
+ 'update': ('managers', 'owners',),
+ 'delete': ('managers', 'owners'),
+ }
+
+ from cubes.folder.schema import Folder
+ from cubes.file.schema import File
+ from cubes.comment.schema import Comment
+ from cubes.person.schema import Person
+ from cubes.zone.schema import Zone
+ from cubes.tag.schema import Tag
+
+ Folder.__permissions__ = VISIBILITY_PERMISSIONS
+ File.__permissions__ = VISIBILITY_PERMISSIONS
+ Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
+ Comment.__permissions__['add'] = ('managers', 'users',)
+ Person.__permissions__ = AUTH_ONLY_PERMISSIONS
+ Zone.__permissions__ = CLASSIFIERS_PERMISSIONS
+ Tag.__permissions__ = CLASSIFIERS_PERMISSIONS
+
+What's important in there:
+
+* `VISIBILITY_PERMISSIONS` provides read access to managers group, if
+ `visibility` attribute's value is 'public', or if user (designed by the 'U'
+ variable in the expression) is linked to the entity (the 'X' variable) through
+ the `may_be_read_by` permission
+
+* we modify permissions of the entity types we use by importing them and
+ modifying their `__permissions__` attribute
+
+* notice the `.copy()`: we only want to modify 'add' permission for `Comment`,
+ not for all entity types using `VISIBILITY_PERMISSIONS`!
+
+* the remaining part of the security model is done using regular groups:
+
+ - `users` is the group to which all authenticated users will belong
+ - `guests` is the group of anonymous users
+
+
+.. _adv_tuto_security_propagation:
+
+Step 2: security propagation in hooks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To fullfill the requirements, we have to implement::
+
+ Also, unless explicity specified, visibility of an image should be the same as
+ its parent folder, as well as visibility of a comment should be the same as the
+ commented entity.
+
+This kind of `active` rule will be done using CubicWeb's hook
+system. Hooks are triggered on database events such as addition of a new
+entity or relation.
+
+The tricky part of the requirement is in *unless explicitly specified*, notably
+because when the entity is added, we don't know yet its 'parent'
+entity (e.g. Folder of an File, File commented by a Comment). To handle such things,
+CubicWeb provides `Operation`, which allow to schedule things to do at commit time.
+
+In our case we will:
+
+* on entity creation, schedule an operation that will set default visibility
+
+* when a "parent" relation is added, propagate parent's visibility unless the
+ child already has a visibility set
+
+Here is the code in cube's *hooks.py*:
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import is_instance
+ from cubicweb.server import hook
+
+ class SetVisibilityOp(hook.DataOperationMixIn, hook.Operation):
+
+ def precommit_event(self):
+ for eid in self.get_data():
+ entity = self.session.entity_from_eid(eid)
+ if entity.visibility == 'parent':
+ entity.cw_set(visibility=u'authenticated')
+
+ class SetVisibilityHook(hook.Hook):
+ __regid__ = 'sytweb.setvisibility'
+ __select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Comment')
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ SetVisibilityOp.get_instance(self._cw).add_data(self.entity.eid)
+
+ class SetParentVisibilityHook(hook.Hook):
+ __regid__ = 'sytweb.setparentvisibility'
+ __select__ = hook.Hook.__select__ & hook.match_rtype('filed_under', 'comments')
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ parent = self._cw.entity_from_eid(self.eidto)
+ child = self._cw.entity_from_eid(self.eidfrom)
+ if child.visibility == 'parent':
+ child.cw_set(visibility=parent.visibility)
+
+Notice:
+
+* hooks are application objects, hence have selectors that should match entity or
+ relation types to which the hook applies. To match a relation type, we use the
+ hook specific `match_rtype` selector.
+
+* usage of `DataOperationMixIn`: instead of adding an operation for each added entity,
+ DataOperationMixIn allows to create a single one and to store entity's eids to be
+ processed in the transaction data. This is a good pratice to avoid heavy
+ operations manipulation cost when creating a lot of entities in the same
+ transaction.
+
+* the `precommit_event` method of the operation will be called at transaction's
+ commit time.
+
+* in a hook, `self._cw` is the repository session, not a web request as usually
+ in views
+
+* according to hook's event, you have access to different attributes on the hook
+ instance. Here:
+
+ - `self.entity` is the newly added entity on 'after_add_entity' events
+
+ - `self.eidfrom` / `self.eidto` are the eid of the subject / object entity on
+ 'after_add_relation' events (you may also get the relation type using
+ `self.rtype`)
+
+The `parent` visibility value is used to tell "propagate using parent security"
+because we want that attribute to be required, so we can't use None value else
+we'll get an error before we get any chance to propagate...
+
+Now, we also want to propagate the `may_be_read_by` relation. Fortunately,
+CubicWeb provides some base hook classes for such things, so we only have to add
+the following code to *hooks.py*:
+
+.. sourcecode:: python
+
+ # relations where the "parent" entity is the subject
+ S_RELS = set()
+ # relations where the "parent" entity is the object
+ O_RELS = set(('filed_under', 'comments',))
+
+ class AddEntitySecurityPropagationHook(hook.PropagateRelationHook):
+ """propagate permissions when new entity are added"""
+ __regid__ = 'sytweb.addentity_security_propagation'
+ __select__ = (hook.PropagateRelationHook.__select__
+ & hook.match_rtype_sets(S_RELS, O_RELS))
+ main_rtype = 'may_be_read_by'
+ subject_relations = S_RELS
+ object_relations = O_RELS
+
+ class AddPermissionSecurityPropagationHook(hook.PropagateRelationAddHook):
+ """propagate permissions when new entity are added"""
+ __regid__ = 'sytweb.addperm_security_propagation'
+ __select__ = (hook.PropagateRelationAddHook.__select__
+ & hook.match_rtype('may_be_read_by',))
+ subject_relations = S_RELS
+ object_relations = O_RELS
+
+ class DelPermissionSecurityPropagationHook(hook.PropagateRelationDelHook):
+ __regid__ = 'sytweb.delperm_security_propagation'
+ __select__ = (hook.PropagateRelationDelHook.__select__
+ & hook.match_rtype('may_be_read_by',))
+ subject_relations = S_RELS
+ object_relations = O_RELS
+
+* the `AddEntitySecurityPropagationHook` will propagate the relation
+ when `filed_under` or `comments` relations are added
+
+ - the `S_RELS` and `O_RELS` set as well as the `match_rtype_sets` selector are
+ used here so that if my cube is used by another one, it'll be able to
+ configure security propagation by simply adding relation to one of the two
+ sets.
+
+* the two others will propagate permissions changes on parent entities to
+ children entities
+
+
+.. _adv_tuto_tesing_security:
+
+Step 3: testing our security
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Security is tricky. Writing some tests for it is a very good idea. You should
+even write them first, as Test Driven Development recommends!
+
+Here is a small test case that will check the basis of our security
+model, in *test/unittest_sytweb.py*:
+
+.. sourcecode:: python
+
+ from cubicweb.devtools.testlib import CubicWebTC
+ from cubicweb import Binary
+
+ class SecurityTC(CubicWebTC):
+
+ def test_visibility_propagation(self):
+ with self.admin_access.repo_cnx() as cnx:
+ # create a user for later security checks
+ toto = self.create_user(cnx, 'toto')
+ cnx.commit()
+ # init some data using the default manager connection
+ folder = cnx.create_entity('Folder',
+ name=u'restricted',
+ visibility=u'restricted')
+ photo1 = cnx.create_entity('File',
+ data_name=u'photo1.jpg',
+ data=Binary('xxx'),
+ filed_under=folder)
+ cnx.commit()
+ # visibility propagation
+ self.assertEquals(photo1.visibility, 'restricted')
+ # unless explicitly specified
+ photo2 = cnx.create_entity('File',
+ data_name=u'photo2.jpg',
+ data=Binary('xxx'),
+ visibility=u'public',
+ filed_under=folder)
+ cnx.commit()
+ self.assertEquals(photo2.visibility, 'public')
+ with self.new_access('toto').repo_cnx() as cnx:
+ # test security
+ self.assertEqual(1, len(cnx.execute('File X'))) # only the public one
+ self.assertEqual(0, len(cnx.execute('Folder X'))) # restricted...
+ with self.admin_access.repo_cnx() as cnx:
+ # may_be_read_by propagation
+ folder = cnx.entity_from_eid(folder.eid)
+ folder.cw_set(may_be_read_by=toto)
+ cnx.commit()
+ with self.new_access('toto').repo_cnx() as cnx:
+ photo1 = cnx.entity_from_eid(photo1.eid)
+ self.failUnless(photo1.may_be_read_by)
+ # test security with permissions
+ self.assertEquals(2, len(cnx.execute('File X'))) # now toto has access to photo2
+ self.assertEquals(1, len(cnx.execute('Folder X'))) # and to restricted folder
+
+ if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
+
+It's not complete, but shows most things you'll want to do in tests: adding some
+content, creating users and connecting as them in the test, etc...
+
+To run it type:
+
+.. sourcecode:: bash
+
+ $ pytest unittest_sytweb.py
+ ======================== unittest_sytweb.py ========================
+ -> creating tables [....................]
+ -> inserting default user and default groups.
+ -> storing the schema in the database [....................]
+ -> database for instance data initialized.
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 22.547s
+
+ OK
+
+
+The first execution is taking time, since it creates a sqlite database for the
+test instance. The second one will be much quicker:
+
+.. sourcecode:: bash
+
+ $ pytest unittest_sytweb.py
+ ======================== unittest_sytweb.py ========================
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 2.662s
+
+ OK
+
+If you do some changes in your schema, you'll have to force regeneration of that
+database. You do that by removing the tmpdb files before running the test: ::
+
+ $ rm data/database/tmpdb*
+
+
+.. Note::
+ pytest is a very convenient utility used to control test execution. It is available from the `logilab-common`_ package.
+
+.. _`logilab-common`: http://www.logilab.org/project/logilab-common
+
+.. _adv_tuto_migration_script:
+
+Step 4: writing the migration script and migrating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Prior to those changes, I created an instance, fed it with some data, so I
+don't want to create a new one, but to migrate the existing one. Let's see how to
+do that.
+
+Migration commands should be put in the cube's *migration* directory, in a
+file named file:`<X.Y.Z>_Any.py` ('Any' being there mostly for historical reasons).
+
+Here I'll create a *migration/0.2.0_Any.py* file containing the following
+instructions:
+
+.. sourcecode:: python
+
+ add_relation_type('may_be_read_by')
+ add_relation_type('visibility')
+ sync_schema_props_perms()
+
+Then I update the version number in the cube's *__pkginfo__.py* to 0.2.0. And
+that's it! Those instructions will:
+
+* update the instance's schema by adding our two new relations and update the
+ underlying database tables accordingly (the first two instructions)
+
+* update schema's permissions definition (the last instruction)
+
+
+To migrate my instance I simply type::
+
+ cubicweb-ctl upgrade sytweb_instance
+
+You'll then be asked some questions to do the migration step by step. You should say
+YES when it asks if a backup of your database should be done, so you can get back
+to initial state if anything goes wrong...
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part03_bfss.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,131 @@
+Storing images on the file-system
+---------------------------------
+
+Step 1: configuring the BytesFileSystem storage
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To avoid cluttering my database, and to ease file manipulation, I don't want them
+to be stored in the database. I want to be able create File entities for some
+files on the server file system, where those file will be accessed to get
+entities data. To do so I've to set a custom :class:`BytesFileSystemStorage`
+storage for the File 'data' attribute, which hold the actual file's content.
+
+Since the function to register a custom storage needs to have a repository
+instance as first argument, we've to call it in a server startup hook. So I added
+in `cubes/sytweb/hooks.py` :
+
+.. sourcecode:: python
+
+ from os import makedirs
+ from os.path import join, exists
+
+ from cubicweb.server import hook
+ from cubicweb.server.sources import storages
+
+ class ServerStartupHook(hook.Hook):
+ __regid__ = 'sytweb.serverstartup'
+ events = ('server_startup', 'server_maintenance')
+
+ def __call__(self):
+ bfssdir = join(self.repo.config.appdatahome, 'bfss')
+ if not exists(bfssdir):
+ makedirs(bfssdir)
+ print 'created', bfssdir
+ storage = storages.BytesFileSystemStorage(bfssdir)
+ storages.set_attribute_storage(self.repo, 'File', 'data', storage)
+
+.. Note::
+
+ * how we built the hook's registry identifier (`__regid__`): you can introduce
+ 'namespaces' by using there python module like naming identifiers. This is
+ especially important for hooks where you usually want a new custom hook, not
+ overriding / specializing an existant one, but the concept may be applied to
+ any application objects
+
+ * we catch two events here: "server_startup" and "server_maintenance". The first
+ is called on regular repository startup (eg, as a server), the other for
+ maintenance task such as shell or upgrade. In both cases, we need to have
+ the storage set, else we'll be in trouble...
+
+ * the path given to the storage is the place where file added through the ui
+ (or in the database before migration) will be located
+
+ * beware that by doing this, you can't anymore write queries that will try to
+ restrict on File `data` attribute. Hopefuly we don't do that usually
+ on file's content or more generally on attributes for the Bytes type
+
+Now, if you've already added some photos through the web ui, you'll have to
+migrate existing data so file's content will be stored on the file-system instead
+of the database. There is a migration command to do so, let's run it in the
+cubicweb shell (in real life, you would have to put it in a migration script as we
+have seen last time):
+
+::
+
+ $ cubicweb-ctl shell sytweb_instance
+ entering the migration python shell
+ just type migration commands or arbitrary python code and type ENTER to execute it
+ type "exit" or Ctrl-D to quit the shell and resume operation
+ >>> storage_changed('File', 'data')
+ [........................]
+
+
+That's it. Now, files added through the web ui will have their content stored on
+the file-system, and you'll also be able to import files from the file-system as
+explained in the next part.
+
+Step 2: importing some data into the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Hey, we start to have some nice features, let us give a try to this new web
+site. For instance if I have a 'photos/201005WePyrenees' containing pictures for
+a particular event, I can import it to my web site by typing ::
+
+ $ cubicweb-ctl fsimport -F sytweb_instance photos/201005WePyrenees/
+ ** importing directory /home/syt/photos/201005WePyrenees
+ importing IMG_8314.JPG
+ importing IMG_8274.JPG
+ importing IMG_8286.JPG
+ importing IMG_8308.JPG
+ importing IMG_8304.JPG
+
+.. Note::
+ The -F option means that folders should be mapped, hence my photos will be
+ linked to a Folder entity corresponding to the file-system folder.
+
+Let's take a look at the web ui:
+
+.. image:: ../../images/tutos-photowebsite_ui1.png
+
+Nothing different, I can't see the new folder... But remember our security model!
+By default, files are only accessible to authenticated users, and I'm looking at
+the site as anonymous, e.g. not authenticated. If I login, I can now see:
+
+.. image:: ../../images/tutos-photowebsite_ui2.png
+
+Yeah, it's there! You will notice that I can see some entities as well as
+folders and images the anonymous user can't. It just works **everywhere in the
+ui** since it's handled at the repository level, thanks to our security model.
+
+Now if I click on the recently inserted folder, I can see
+
+.. image:: ../../images/tutos-photowebsite_ui3.png
+
+Great! There is even my pictures in the folder. I can know give to this folder a
+nicer name (provided I don't intend to import from it anymore, else already
+imported photos will be reimported), change permissions, title for some pictures,
+etc... Having a good content is much more difficult than having a good web site
+;)
+
+
+Conclusion
+~~~~~~~~~~
+
+We started to see here an advanced feature of our repository: the ability
+to store some parts of our data-model into a custom storage, outside the
+database. There is currently only the :class:`BytesFileSystemStorage` available,
+but you can expect to see more coming in a near future (or write your own!).
+
+Also, we can know start to feed our web-site with some nice pictures!
+The site isn't perfect (far from it actually) but it's usable, and we can
+now start using it and improve it on the way. The Incremental Cubic Way :)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part04_ui-base.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,361 @@
+Let's make it more user friendly
+================================
+
+
+Step 1: let's improve site's usability for our visitors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The first thing I've noticed is that people to whom I send links to photos with
+some login/password authentication get lost, because they don't grasp they have
+to login by clicking on the 'authenticate' link. That's much probably because
+they only get a 404 when trying to access an unauthorized folder, and the site
+doesn't make clear that 1. you're not authenticated, 2. you could get more
+content by authenticating yourself.
+
+So, to improve this situation, I decided that I should:
+
+* make a login box appears for anonymous, so they see at a first glance a place
+ to put the login / password information I provided
+
+* customize the 404 page, proposing to login to anonymous.
+
+Here is the code, samples from my cube's `views.py` file:
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import is_instance
+ from cubicweb.web import component
+ from cubicweb.web.views import error
+ from cubicweb.predicates import anonymous_user
+
+ class FourOhFour(error.FourOhFour):
+ __select__ = error.FourOhFour.__select__ & anonymous_user()
+
+ def call(self):
+ self.w(u"<h1>%s</h1>" % self._cw._('this resource does not exist'))
+ self.w(u"<p>%s</p>" % self._cw._('have you tried to login?'))
+
+
+ class LoginBox(component.CtxComponent):
+ """display a box containing links to all startup views"""
+ __regid__ = 'sytweb.loginbox'
+ __select__ = component.CtxComponent.__select__ & anonymous_user()
+
+ title = _('Authenticate yourself')
+ order = 70
+
+ def render_body(self, w):
+ cw = self._cw
+ form = cw.vreg['forms'].select('logform', cw)
+ form.render(w=w, table_class='', display_progress_div=False)
+
+The first class provides a new specific implementation of the default page you
+get on 404 error, to display an adapted message to anonymous user.
+
+.. Note::
+
+ Thanks to the selection mecanism, it will be selected for anoymous user,
+ since the additional `anonymous_user()` selector gives it a higher score than
+ the default, and not for authenticated since this selector will return 0 in
+ such case (hence the object won't be selectable)
+
+The second class defines a simple box, that will be displayed by default with
+boxes in the left column, thanks to default :class:`component.CtxComponent`
+selector. The HTML is written to match default CubicWeb boxes style. The code
+fetch the actual login form and render it.
+
+
+.. figure:: ../../images/tutos-photowebsite_login-box.png
+ :alt: login box / 404 screenshot
+
+ The login box and the custom 404 page for an anonymous visitor (translated in french)
+
+
+Step 2: providing a custom index page
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Another thing we can easily do to improve the site is... A nicer index page
+(e.g. the first page you get when accessing the web site)! The default one is
+quite intimidating (that should change in a near future). I will provide a much
+simpler index page that simply list available folders (e.g. photo albums in that
+site).
+
+.. sourcecode:: python
+
+ from cubicweb.web.views import startup
+
+ class IndexView(startup.IndexView):
+ def call(self, **kwargs):
+ self.w(u'<div>\n')
+ if self._cw.cnx.anonymous_connection:
+ self.w(u'<h4>%s</h4>\n' % self._cw._('Public Albums'))
+ else:
+ self.w(u'<h4>%s</h4>\n' % self._cw._('Albums for %s') % self._cw.user.login)
+ self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
+ self.w(u'</div>\n')
+
+ def registration_callback(vreg):
+ vreg.register_all(globals().values(), __name__, (IndexView,))
+ vreg.register_and_replace(IndexView, startup.IndexView)
+
+As you can see, we override the default index view found in
+`cubicweb.web.views.startup`, geting back nothing but its identifier and selector
+since we override the top level view's `call` method.
+
+.. Note::
+
+ in that case, we want our index view to **replace** the existing one. To do so
+ we've to implements the `registration_callback` function, in which we tell to
+ register everything in the module *but* our IndexView, then we register it
+ instead of the former index view.
+
+Also, we added a title that tries to make it more evident that the visitor is
+authenticated, or not. Hopefuly people will get it now!
+
+
+.. figure:: ../../images/tutos-photowebsite_index-before.png
+ :alt: default index page screenshot
+
+ The default index page
+
+.. figure:: ../../images/tutos-photowebsite_index-after.png
+ :alt: new index page screenshot
+
+ Our simpler, less intimidating, index page (still translated in french)
+
+
+Step 3: more navigation improvments
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are still a few problems I want to solve...
+
+* Images in a folder are displayed in a somewhat random order. I would like to
+ have them ordered by file's name (which will usually, inside a given folder,
+ also result ordering photo by their date and time)
+
+* When clicking a photo from an album view, you've to get back to the gallery
+ view to go to the next photo. This is pretty annoying...
+
+* Also, when viewing an image, there is no clue about the folder to which this
+ image belongs to.
+
+I will first try to explain the ordering problem. By default, when accessing
+related entities by using the ORM's API, you should get them ordered according to
+the target's class `cw_fetch_order`. If we take a look at the file cube'schema,
+we can see:
+
+.. sourcecode:: python
+
+ class File(AnyEntity):
+ """customized class for File entities"""
+ __regid__ = 'File'
+ fetch_attrs, cw_fetch_order = fetch_config(['data_name', 'title'])
+
+
+By default, `fetch_config` will return a `cw_fetch_order` method that will order
+on the first attribute in the list. So, we could expect to get files ordered by
+their name. But we don't. What's up doc ?
+
+The problem is that files are related to folder using the `filed_under` relation.
+And that relation is ambiguous, eg it can lead to `File` entities, but also to
+`Folder` entities. In such case, since both entity types doesn't share the
+attribute on which we want to sort, we'll get linked entities sorted on a common
+attribute (usually `modification_date`).
+
+To fix this, we've to help the ORM. We'll do this in the method from the `ITree`
+folder's adapter, used in the folder's primary view to display the folder's
+content. Here's the code, that I've put in our cube's `entities.py` file, since
+it's more logical stuff than view stuff:
+
+.. sourcecode:: python
+
+ from cubes.folder import entities as folder
+
+ class FolderITreeAdapter(folder.FolderITreeAdapter):
+
+ def different_type_children(self, entities=True):
+ rql = self.entity.cw_related_rql(self.tree_relation,
+ self.parent_role, ('File',))
+ rset = self._cw.execute(rql, {'x': self.entity.eid})
+ if entities:
+ return list(rset.entities())
+ return rset
+
+ def registration_callback(vreg):
+ vreg.register_and_replace(FolderITreeAdapter, folder.FolderITreeAdapter)
+
+As you can see, we simple inherit from the adapter defined in the `folder` cube,
+then we override the `different_type_children` method to give a clue to the ORM's
+`cw_related_rql` method, that is responsible to generate the rql to get entities
+related to the folder by the `filed_under` relation (the value of the
+`tree_relation` attribute). The clue is that we only want to consider the `File`
+target entity type. By doing this, we remove the ambiguity and get back a RQL
+query that correctly order files by their `data_name` attribute.
+
+
+.. Note::
+
+ * As seen earlier, we want to **replace** the folder's `ITree` adapter by our
+ implementation, hence the custom `registration_callback` method.
+
+
+Ouf. That one was tricky...
+
+Now the easier parts. Let's start by adding some links on the file's primary view
+to see the previous / next image in the same folder. CubicWeb's provide a
+component that do exactly that. To make it appears, one have to be adaptable to
+the `IPrevNext` interface. Here is the related code sample, extracted from our
+cube's `views.py` file:
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import is_instance
+ from cubicweb.web.views import navigation
+
+
+ class FileIPrevNextAdapter(navigation.IPrevNextAdapter):
+ __select__ = is_instance('File')
+
+ def previous_entity(self):
+ rset = self._cw.execute('File F ORDERBY FDN DESC LIMIT 1 WHERE '
+ 'X filed_under FOLDER, F filed_under FOLDER, '
+ 'F data_name FDN, X data_name > FDN, X eid %(x)s',
+ {'x': self.entity.eid})
+ if rset:
+ return rset.get_entity(0, 0)
+
+ def next_entity(self):
+ rset = self._cw.execute('File F ORDERBY FDN ASC LIMIT 1 WHERE '
+ 'X filed_under FOLDER, F filed_under FOLDER, '
+ 'F data_name FDN, X data_name < FDN, X eid %(x)s',
+ {'x': self.entity.eid})
+ if rset:
+ return rset.get_entity(0, 0)
+
+
+The `IPrevNext` interface implemented by the adapter simply consist in the
+`previous_entity` / `next_entity` methods, that should respectivly return the
+previous / next entity or `None`. We make an RQL query to get files in the same
+folder, ordered similarly (eg by their `data_name` attribute). We set
+ascendant/descendant ordering and a strict comparison with current file's name
+(the "X" variable representing the current file).
+
+Notice that this query supposes we wont have two files of the same name in the
+same folder, else things may go wrong. Fixing this is out of the scope of this
+blog. And as I would like to have at some point a smarter, context sensitive
+previous/next entity, I'll probably never fix this query (though if I had to, I
+would probably choosing to add a constraint in the schema so that we can't add
+two files of the same name in a folder).
+
+One more thing: by default, the component will be displayed below the content
+zone (the one with the white background). You can change this in the site's
+properties through the ui, but you can also change the default value in the code
+by modifying the `context` attribute of the component:
+
+.. sourcecode:: python
+
+ navigation.NextPrevNavigationComponent.context = 'navcontentbottom'
+
+.. Note::
+
+ `context` may be one of 'navtop', 'navbottom', 'navcontenttop' or
+ 'navcontentbottom'; the first two being outside the main content zone, the two
+ others inside it.
+
+.. figure:: ../../images/tutos-photowebsite_prevnext.png
+ :alt: screenshot of the previous/next entity component
+
+ The previous/next entity component, at the bottom of the main content zone.
+
+Now, the only remaining stuff in my todo list is to see the file's folder. I'll use
+the standard breadcrumb component to do so. Similarly as what we've seen before, this
+component is controled by the :class:`IBreadCrumbs` interface, so we'll have to provide a custom
+adapter for `File` entity, telling the a file's parent entity is its folder:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views import ibreadcrumbs
+
+ class FileIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = is_instance('File')
+
+ def parent_entity(self):
+ if self.entity.filed_under:
+ return self.entity.filed_under[0]
+
+In that case, we simply use attribute notation provided by the ORM to get the
+folder in which the current file (e.g. `self.entity`) is located.
+
+.. Note::
+
+ The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default
+ :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look
+ at the value returned by its `parent_entity` method. It also provides a
+ default implementation for this method for entities adapting to the `ITree`
+ interface, but as our `File` doesn't, we've to provide a custom adapter.
+
+.. figure:: ../../images/tutos-photowebsite_breadcrumbs.png
+ :alt: screenshot of the breadcrumb component
+
+ The breadcrumb component when on a file entity, now displaying parent folder.
+
+
+Step 4: preparing the release and migrating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Now that greatly enhanced our cube, it's time to release it to upgrade production site.
+I'll probably detail that process later, but I currently simply transfer the new code
+to the server running the web site.
+
+However, I've still today some step to respect to get things done properly...
+
+First, as I've added some translatable string, I've to run: ::
+
+ $ cubicweb-ctl i18ncube sytweb
+
+To update the cube's gettext catalogs (the '.po' files under the cube's `i18n`
+directory). Once the above command is executed, I'll then update translations.
+
+To see if everything is ok on my test instance, I do: ::
+
+ $ cubicweb-ctl i18ninstance sytweb
+ $ cubicweb-ctl start -D sytweb
+
+The first command compile i18n catalogs (e.g. generates '.mo' files) for my test
+instance. The second command start it in debug mode, so I can open my browser and
+navigate through the web site to see if everything is ok...
+
+.. Note::
+
+ In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while
+ in the two other, it refers to the **instance** (if you can't see the
+ difference, reread CubicWeb's concept chapter !).
+
+
+Once I've checked it's ok, I simply have to bump the version number in the
+`__pkginfo__` module to trigger a migration once I'll have updated the code on
+the production site. I can check then check the migration is also going fine, by
+first restoring a dump from the production site, then upgrading my test instance.
+
+To generate a dump from the production site: ::
+
+ $ cubicweb-ctl db-dump sytweb
+ pg_dump -Fc --username=syt --no-owner --file /home/syt/etc/cubicweb.d/sytweb/backup/tmpYIN0YI/system sytweb
+ -> backup file /home/syt/etc/cubicweb.d/sytweb/backup/sytweb-2010-07-13_10-22-40.tar.gz
+
+I can now get back the dump file ('sytweb-2010-07-13_10-22-40.tar.gz') to my test
+machine (using `scp` for instance) to restore it and start migration: ::
+
+ $ cubicweb-ctl db-restore sytweb sytweb-2010-07-13_10-22-40.tar.gz
+ $ cubicweb-ctl upgrade sytweb
+
+You'll have to answer some questions, as we've seen in `an earlier post`_.
+
+Now that everything is tested, I can transfer the new code to the production
+server, `apt-get upgrade` cubicweb and its dependencies, and eventually
+upgrade the production instance.
+
+
+.. _`several improvments`: http://www.cubicweb.org/blogentry/1179899
+.. _`3.8`: http://www.cubicweb.org/blogentry/917107
+.. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642
+.. _`an earlier post`: http://www.cubicweb.org/867464
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/advanced/part05_ui-advanced.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,374 @@
+Building my photos web site with |cubicweb| part V: let's make it even more user friendly
+=========================================================================================
+
+.. _uiprops:
+
+Step 1: tired of the default look?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+OK... Now our site has its most desired features. But... I would like to make it look
+somewhat like *my* website. It is not www.cubicweb.org after all. Let's tackle this
+first!
+
+The first thing we can to is to change the logo. There are various way to achieve
+this. The easiest way is to put a :file:`logo.png` file into the cube's :file:`data`
+directory. As data files are looked at according to cubes order (CubicWeb
+resources coming last), that file will be selected instead of CubicWeb's one.
+
+.. Note::
+ As the location for static resources are cached, you'll have to restart
+ your instance for this to be taken into account.
+
+Though there are some cases where you don't want to use a :file:`logo.png` file.
+For instance if it's a JPEG file. You can still change the logo by defining in
+the cube's :file:`uiprops.py` file:
+
+.. sourcecode:: python
+
+ LOGO = data('logo.jpg')
+
+The uiprops machinery is used to define some static file resources,
+such as the logo, default Javascript / CSS files, as well as CSS
+properties (we'll see that later).
+
+.. Note::
+ This file is imported specifically by |cubicweb|, with a predefined name space,
+ containing for instance the `data` function, telling the file is somewhere
+ in a cube or CubicWeb's data directory.
+
+ One side effect of this is that it can't be imported as a regular python
+ module.
+
+The nice thing is that in debug mode, change to a :file:`uiprops.py` file are detected
+and then automatically reloaded.
+
+Now, as it's a photos web-site, I would like to have a photo of mine as background...
+After some trials I won't detail here, I've found a working recipe explained `here`_.
+All I've to do is to override some stuff of the default CubicWeb user interface to
+apply it as explained.
+
+The first thing to to get the ``<img/>`` tag as first element after the
+``<body>`` tag. If you know a way to avoid this by simply specifying the image
+in the CSS, tell me! The easiest way to do so is to override the
+:class:`HTMLPageHeader` view, since that's the one that is directly called once
+the ``<body>`` has been written. How did I find this? By looking in the
+:mod:`cubiweb.web.views.basetemplates` module, since I know that global page
+layouts sits there. I could also have grep the "body" tag in
+:mod:`cubicweb.web.views`... Finding this was the hardest part. Now all I need is
+to customize it to write that ``img`` tag, as below:
+
+.. sourcecode:: python
+
+ class HTMLPageHeader(basetemplates.HTMLPageHeader):
+ # override this since it's the easier way to have our bg image
+ # as the first element following <body>
+ def call(self, **kwargs):
+ self.w(u'<img id="bg-image" src="%sbackground.jpg" alt="background image"/>'
+ % self._cw.datadir_url)
+ super(HTMLPageHeader, self).call(**kwargs)
+
+
+ def registration_callback(vreg):
+ vreg.register_all(globals().values(), __name__, (HTMLPageHeader))
+ vreg.register_and_replace(HTMLPageHeader, basetemplates.HTMLPageHeader)
+
+
+As you may have guessed, my background image is in a :file:`background.jpg` file
+in the cube's :file:`data` directory, but there are still some things to explain
+to newcomers here:
+
+* The :meth:`call` method is there the main access point of the view. It's called by
+ the view's :meth:`render` method. It is not the only access point for a view, but
+ this will be detailed later.
+
+* Calling `self.w` writes something to the output stream. Except for binary views
+ (which do not generate text), it *must* be passed an Unicode string.
+
+* The proper way to get a file in :file:`data` directory is to use the `datadir_url`
+ attribute of the incoming request (e.g. `self._cw`).
+
+I won't explain again the :func:`registration_callback` stuff, you should understand it
+now! If not, go back to previous posts in the series :)
+
+Fine. Now all I've to do is to add a bit of CSS to get it to behave nicely (which
+is not the case at all for now). I'll put all this in a :file:`cubes.sytweb.css`
+file, stored as usual in our :file:`data` directory:
+
+.. sourcecode:: css
+
+
+ /* fixed full screen background image
+ * as explained on http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
+ *
+ * syt update: set z-index=0 on the img instead of z-index=1 on div#page & co to
+ * avoid pb with the user actions menu
+ */
+ img#bg-image {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ }
+
+ div#page, table#header, div#footer {
+ background: transparent;
+ position: relative;
+ }
+
+ /* add some space around the logo
+ */
+ img#logo {
+ padding: 5px 15px 0px 15px;
+ }
+
+ /* more dark font for metadata to have a chance to see them with the background
+ * image
+ */
+ div.metadata {
+ color: black;
+ }
+
+You can see here stuff explained in the cited page, with only a slight modification
+explained in the comments, plus some additional rules to make things somewhat cleaner:
+
+* a bit of padding around the logo
+
+* darker metadata which appears by default below the content (the white frame in the page)
+
+To get this CSS file used everywhere in the site, I have to modify the :file:`uiprops.py` file
+introduced above:
+
+.. sourcecode:: python
+
+ STYLESHEETS = sheet['STYLESHEETS'] + [data('cubes.sytweb.css')]
+
+.. Note::
+ `sheet` is another predefined variable containing values defined by
+ already process `:file:`uiprops.py`` file, notably the CubicWeb's one.
+
+Here we simply want our CSS in addition to CubicWeb's base CSS files, so we
+redefine the `STYLESHEETS` variable to existing CSS (accessed through the `sheet`
+variable) with our one added. I could also have done:
+
+.. sourcecode:: python
+
+ sheet['STYLESHEETS'].append(data('cubes.sytweb.css'))
+
+But this is less interesting since we don't see the overriding mechanism...
+
+At this point, the site should start looking good, the background image being
+resized to fit the screen.
+
+.. image:: ../../images/tutos-photowebsite_background-image.png
+
+The final touch: let's customize CubicWeb's CSS to get less orange... By simply adding
+
+.. sourcecode:: python
+
+ contextualBoxTitleBg = incontextBoxTitleBg = '#AAAAAA'
+
+and reloading the page we've just seen, we know have a nice greyed box instead of
+the orange one:
+
+.. image:: ../../images/tutos-photowebsite_grey-box.png
+
+This is because CubicWeb's CSS include some variables which are
+expanded by values defined in uiprops file. In our case we controlled the
+properties of the CSS `background` property of boxes with CSS class
+`contextualBoxTitleBg` and `incontextBoxTitleBg`.
+
+
+Step 2: configuring boxes
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Boxes present to the user some ways to use the application. Let's first do a few
+user interface tweaks in our :file:`views.py` file:
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import none_rset
+ from cubicweb.web.views import bookmark
+ from cubes.zone import views as zone
+ from cubes.tag import views as tag
+
+ # change bookmarks box selector so it's only displayed on startup views
+ bookmark.BookmarksBox.__select__ = bookmark.BookmarksBox.__select__ & none_rset()
+ # move zone box to the left instead of in the context frame and tweak its order
+ zone.ZoneBox.context = 'left'
+ zone.ZoneBox.order = 100
+ # move tags box to the left instead of in the context frame and tweak its order
+ tag.TagsBox.context = 'left'
+ tag.TagsBox.order = 102
+ # hide similarity box, not interested
+ tag.SimilarityBox.visible = False
+
+The idea is to move all boxes in the left column, so we get more space for the
+photos. Now, serious things: I want a box similar to the tags box but to handle
+the `Person displayed_on File` relation. We can do this simply by adding a
+:class:`AjaxEditRelationCtxComponent` subclass to our views, as below:
+
+.. sourcecode:: python
+
+ from logilab.common.decorators import monkeypatch
+ from cubicweb import ValidationError
+ from cubicweb.web.views import uicfg, component
+ from cubicweb.web.views import basecontrollers
+
+ # hide displayed_on relation using uicfg since it will be displayed by the box below
+ uicfg.primaryview_section.tag_object_of(('*', 'displayed_on', '*'), 'hidden')
+
+ class PersonBox(component.AjaxEditRelationCtxComponent):
+ __regid__ = 'sytweb.displayed-on-box'
+ # box position
+ order = 101
+ context = 'left'
+ # define relation to be handled
+ rtype = 'displayed_on'
+ role = 'object'
+ target_etype = 'Person'
+ # messages
+ added_msg = _('person has been added')
+ removed_msg = _('person has been removed')
+ # bind to js_* methods of the json controller
+ fname_vocabulary = 'unrelated_persons'
+ fname_validate = 'link_to_person'
+ fname_remove = 'unlink_person'
+
+
+ @monkeypatch(basecontrollers.JSonController)
+ @basecontrollers.jsonize
+ def js_unrelated_persons(self, eid):
+ """return tag unrelated to an entity"""
+ rql = "Any F + ' ' + S WHERE P surname S, P firstname F, X eid %(x)s, NOT P displayed_on X"
+ return [name for (name,) in self._cw.execute(rql, {'x' : eid})]
+
+
+ @monkeypatch(basecontrollers.JSonController)
+ def js_link_to_person(self, eid, people):
+ req = self._cw
+ for name in people:
+ name = name.strip().title()
+ if not name:
+ continue
+ try:
+ firstname, surname = name.split(None, 1)
+ except:
+ raise ValidationError(eid, {('displayed_on', 'object'): 'provide <first name> <surname>'})
+ rset = req.execute('Person P WHERE '
+ 'P firstname %(firstname)s, P surname %(surname)s',
+ locals())
+ if rset:
+ person = rset.get_entity(0, 0)
+ else:
+ person = req.create_entity('Person', firstname=firstname,
+ surname=surname)
+ req.execute('SET P displayed_on X WHERE '
+ 'P eid %(p)s, X eid %(x)s, NOT P displayed_on X',
+ {'p': person.eid, 'x' : eid})
+
+ @monkeypatch(basecontrollers.JSonController)
+ def js_unlink_person(self, eid, personeid):
+ self._cw.execute('DELETE P displayed_on X WHERE P eid %(p)s, X eid %(x)s',
+ {'p': personeid, 'x': eid})
+
+
+You basically subclass to configure with some class attributes. The `fname_*`
+attributes give the name of methods that should be defined on the json control to
+make the AJAX part of the widget work: one to get the vocabulary, one to add a
+relation and another to delete a relation. These methods must start by a `js_`
+prefix and are added to the controller using the `@monkeypatch` decorator. In my
+case, the most complicated method is the one which adds a relation, since it
+tries to see if the person already exists, and else automatically create it,
+assuming the user entered "firstname surname".
+
+Let's see how it looks like on a file primary view:
+
+.. image:: ../../images/tutos-photowebsite_boxes.png
+
+Great, it's now as easy for me to link my pictures to people than to tag them.
+Also, visitors get a consistent display of these two pieces of information.
+
+.. Note::
+ The ui component system has been refactored in `CubicWeb 3.10`_, which also
+ introduced the :class:`AjaxEditRelationCtxComponent` class.
+
+
+Step 3: configuring facets
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The last feature we'll add today is facet configuration. If you access to the
+'/file' url, you'll see a set of 'facets' appearing in the left column. Facets
+provide an intuitive way to build a query incrementally, by proposing to the user
+various way to restrict the result set. For instance CubicWeb proposes a facet to
+restrict based on who created an entity; the tag cube proposes a facet to
+restrict based on tags; the zoe cube a facet to restrict based on geographical
+location, and so on. In that gist, I want to propose a facet to restrict based on
+the people displayed on the picture. To do so, there are various classes in the
+:mod:`cubicweb.web.facet` module which simply have to be configured using class
+attributes as we've done for the box. In our case, we'll define a subclass of
+:class:`RelationFacet`.
+
+.. Note::
+
+ Since that's ui stuff, we'll continue to add code below to our
+ :file:`views.py` file. Though we begin to have a lot of various code their, so
+ it's may be a good time to split our views module into submodules of a `view`
+ package. In our case of a simple application (glue) cube, we could start using
+ for instance the layout below: ::
+
+ views/__init__.py # uicfg configuration, facets
+ views/layout.py # header/footer/background stuff
+ views/components.py # boxes, adapters
+ views/pages.py # index view, 404 view
+
+.. sourcecode:: python
+
+ from cubicweb.web import facet
+
+ class DisplayedOnFacet(facet.RelationFacet):
+ __regid__ = 'displayed_on-facet'
+ # relation to be displayed
+ rtype = 'displayed_on'
+ role = 'object'
+ # view to use to display persons
+ label_vid = 'combobox'
+
+Let's say we also want to filter according to the `visibility` attribute. This is
+even simpler as we just have to derive from the :class:`AttributeFacet` class:
+
+.. sourcecode:: python
+
+ class VisibilityFacet(facet.AttributeFacet):
+ __regid__ = 'visibility-facet'
+ rtype = 'visibility'
+
+Now if I search for some pictures on my site, I get the following facets available:
+
+.. image:: ../../images/tutos-photowebsite_facets.png
+
+.. Note::
+
+ By default a facet must be applyable to every entity in the result set and
+ provide at leat two elements of vocabulary to be displayed (for instance you
+ won't see the `created_by` facet if the same user has created all
+ entities). This may explain why you don't see yours...
+
+
+Conclusion
+~~~~~~~~~~
+
+We started to see the power behind the infrastructure provided by the
+framework, both on the pure ui (CSS, Javascript) side and on the Python side
+(high level generic classes for components, including boxes and facets). We now
+have, with a few lines of code, a full-featured web site with a personalized look.
+
+Of course we'll probably want more as time goes, but we can now
+concentrate on making good pictures, publishing albums and sharing them with
+friends...
+
+
+
+.. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518
+.. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/blog-in-five-minutes.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,70 @@
+.. -*- coding: utf-8 -*-
+
+.. _TutosBaseBlogFiveMinutes:
+
+Get a blog running in five minutes!
+-----------------------------------
+
+For Debian or Ubuntu users, first install the following packages
+(:ref:`DebianInstallation`)::
+
+ cubicweb, cubicweb-dev, cubicweb-blog
+
+Windows or Mac OS X users must install |cubicweb| from source (see
+:ref:`SourceInstallation` and :ref:`WindowsInstallation`).
+
+Then create and initialize your instance::
+
+ cubicweb-ctl create blog myblog
+
+You'll be asked a few questions, and you can keep the default answer for most of
+them. The one question you'll have to think about is the database you'll want to
+use for that instance. For a quick test, if you don't have `postgresql` installed
+and configured (see :ref:`PostgresqlConfiguration`), it's highly recommended to
+choose `sqlite` when asked for which database driver to use, since it has a much
+simple setup (no database server needed).
+
+One the process is completed (including database initialisation), you can start
+your instance by using: ::
+
+ cubicweb-ctl start -D myblog
+
+The `-D` option activates the debugging mode. Removing it will launch the instance
+as a daemon in the background, and ``cubicweb-ctl stop myblog`` will stop
+it in that case.
+
+
+About file system permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Unless you installed from sources, the above commands assume that you have root
+access to the :file:`/etc/` directory. In order to initialize your instance as a
+regular user, within your home directory, you can use the :envvar:`CW_MODE`
+environment variable: ::
+
+ export CW_MODE=user
+
+then create a :file:`~/etc/cubicweb.d` directory that will hold your instances.
+
+More information about how to configure your own environment is
+available in :ref:`ResourceMode`.
+
+
+Instance parameters
+~~~~~~~~~~~~~~~~~~~
+
+If you would like to change database parameters such as the database host or the
+user name used to connect to the database, edit the `sources` file located in the
+:file:`/etc/cubicweb.d/myblog` directory.
+
+Then relaunch the database creation::
+
+ cubicweb-ctl db-create myblog
+
+Other parameters, like web server or emails parameters, can be modified in the
+:file:`/etc/cubicweb.d/myblog/all-in-one.conf` file.
+
+You'll have to restart the instance after modification in one of those files.
+
+This is it. Your blog is functional and running. Visit http://localhost:8080 and enjoy it!
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/conclusion.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,18 @@
+.. -*- coding: utf-8 -*-
+
+What's next?
+------------
+
+In this tutorial, we have seen that you can, right after the installation of
+|cubicweb|, build a web application in a few minutes by defining a data model as
+assembling cubes. You get a working application that you can then customize there
+and there while keeping something that works. This is important in agile
+development practices, you can right from the start of the project show things
+to customer and so take the right decision early in the process.
+
+The next steps will be to discover hooks, security, data sources, digging deeper
+into view writing and interface customisation... Yet a lot of fun stuff to
+discover! You will find more `tutorials and howtos`_ in the blog published on the
+CubicWeb.org website.
+
+.. _`tutorials and howtos`: http://www.cubicweb.org/view?rql=Any+X+ORDERBY+D+DESC+WHERE+X+is+BlogEntry%2C+T+tags+X%2C+T+name+IN+%28%22tutorial%22%2C+%22howto%22%29%2C+X+creation_date+D
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/customizing-the-application.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,539 @@
+.. -*- coding: utf-8 -*-
+
+.. _TutosBaseCustomizingTheApplication:
+
+Customizing your application
+----------------------------
+
+So far so good. The point is that usually, you won't get enough by assembling
+cubes out-of-the-box. You will want to customize them, have a personal look and
+feel, add your own data model and so on. Or maybe start from scratch?
+
+So let's get a bit deeper and start coding our own cube. In our case, we want
+to customize the blog we created to add more features to it.
+
+
+Create your own cube
+~~~~~~~~~~~~~~~~~~~~
+
+First, notice that if you've installed |cubicweb| using Debian packages, you will
+need the additional ``cubicweb-dev`` package to get the commands necessary to
+|cubicweb| development. All `cubicweb-ctl` commands are described in details in
+:ref:`cubicweb-ctl`.
+
+Once your |cubicweb| development environment is set up, you can create a new
+cube::
+
+ cubicweb-ctl newcube myblog
+
+This will create in the cubes directory (:file:`/path/to/grshell/cubes` for source
+installation, :file:`/usr/share/cubicweb/cubes` for Debian packages installation)
+a directory named :file:`blog` reflecting the structure described in
+:ref:`cubelayout`.
+
+For packages installation, you can still create new cubes in your home directory
+using the following configuration. Let's say you want to develop your new cubes
+in `~src/cubes`, then set the following environment variables: ::
+
+ CW_CUBES_PATH=~/src/cubes
+
+and then create your new cube using: ::
+
+ cubicweb-ctl newcube --directory=~/src/cubes myblog
+
+.. Note::
+
+ We previously used `myblog` as the name of our *instance*. We're now creating
+ a *cube* with the same name. Both are different things. We'll now try to
+ specify when we talk about one or another, but keep in mind this difference.
+
+
+Cube metadata
+~~~~~~~~~~~~~
+
+A simple set of metadata about your cube are stored in the :file:`__pkginfo__.py`
+file. In our case, we want to extend the blog cube, so we have to tell that our
+cube depends on this cube, by modifying the ``__depends__`` dictionary in that
+file:
+
+.. sourcecode:: python
+
+ __depends__ = {'cubicweb': '>= 3.10.7',
+ 'cubicweb-blog': None}
+
+where the ``None`` means we do not depends on a particular version of the cube.
+
+.. _TutosBaseCustomizingTheApplicationDataModel:
+
+Extending the data model
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The data model or schema is the core of your |cubicweb| application. It defines
+the type of content your application will handle. It is defined in the file
+:file:`schema.py` of the cube.
+
+
+Defining our model
+******************
+
+For the sake of example, let's say we want a new entity type named `Community`
+with a name, a description. A `Community` will hold several blogs.
+
+.. sourcecode:: python
+
+ from yams.buildobjs import EntityType, RelationDefinition, String, RichString
+
+ class Community(EntityType):
+ name = String(maxsize=50, required=True)
+ description = RichString()
+
+ class community_blog(RelationDefinition):
+ subject = 'Community'
+ object = 'Blog'
+ cardinality = '*?'
+ composite = 'subject'
+
+The first step is the import from the :mod:`yams` package necessary classes to build
+the schema.
+
+This file defines the following:
+
+* a `Community` has a title and a description as attributes
+
+ - the name is a string that is required and can't be longer than 50 characters
+
+ - the description is a string that is not constrained and may contains rich
+ content such as HTML or Restructured text.
+
+* a `Community` may be linked to a `Blog` using the `community_blog` relation
+
+ - ``*`` means a community may be linked to 0 to N blog, ``?`` means a blog may
+ be linked to 0 to 1 community. For completeness, remember that you can also
+ use ``+`` for 1 to N, and ``1`` for single, mandatory relation (e.g. one to one);
+
+ - this is a composite relation where `Community` (e.g. the subject of the
+ relation) is the composite. That means that if you delete a community, its
+ blog will be deleted as well.
+
+Of course, there are a lot of other data types and things such as constraints,
+permissions, etc, that may be defined in the schema, but those won't be covered
+in this tutorial.
+
+Notice that our schema refers to the `Blog` entity type which is not defined
+here. But we know this type is available since we depend on the `blog` cube
+which is defining it.
+
+
+Applying changes to the model into our instance
+***********************************************
+
+Now the problem is that we created an instance using the `blog` cube, not our
+`myblog` cube, so if we don't do anything there is no way that we'll see anything
+changing in the instance.
+
+One easy way, as we've no really valuable data in the instance would be to trash and recreated it::
+
+ cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
+ cubicweb-ctl delete myblog
+ cubicweb-ctl create myblog
+ cubicweb-ctl start -D myblog
+
+Another way is to add our cube to the instance using the cubicweb-ctl shell
+facility. It's a python shell connected to the instance with some special
+commands available to manipulate it (the same as you'll have in migration
+scripts, which are not covered in this tutorial). In that case, we're interested
+in the `add_cube` command: ::
+
+ $ cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
+ $ cubicweb-ctl shell myblog
+ entering the migration python shell
+ just type migration commands or arbitrary python code and type ENTER to execute it
+ type "exit" or Ctrl-D to quit the shell and resume operation
+ >>> add_cube('myblog')
+ >>>
+ $ cubicweb-ctl start -D myblog
+
+The `add_cube` command is enough since it automatically updates our
+application to the cube's schema. There are plenty of other migration
+commands of a more finer grain. They are described in :ref:`migration`
+
+As explained, leave the shell by typing Ctrl-D. If you restart the instance and
+take another look at the schema, you'll see that changes to the data model have
+actually been applied (meaning database schema updates and all necessary stuff
+has been done).
+
+.. image:: ../../images/tutos-base_myblog-schema_en.png
+ :alt: the instance schema after adding our cube
+
+If you follow the 'info' link in the user pop-up menu, you'll also see that the
+instance is using blog and myblog cubes.
+
+.. image:: ../../images/tutos-base_myblog-siteinfo_en.png
+ :alt: the instance schema after adding our cube
+
+You can now add some communities, link them to blog, etc... You'll see that the
+framework provides default views for this entity type (we have not yet defined any
+view for it!), and also that the blog primary view will show the community it's
+linked to if any. All this thanks to the model driven interface provided by the
+framework.
+
+You'll then be able to redefine each of them according to your needs
+and preferences. We'll now see how to do such thing.
+
+.. _TutosBaseCustomizingTheApplicationCustomViews:
+
+Defining your views
+~~~~~~~~~~~~~~~~~~~
+
+|cubicweb| provides a lot of standard views in directory
+:file:`cubicweb/web/views/`. We already talked about 'primary' and 'list' views,
+which are views which apply to one ore more entities.
+
+A view is defined by a python class which includes:
+
+ - an identifier: all objects used to build the user interface in |cubicweb| are
+ recorded in a registry and this identifier will be used as a key in that
+ registry. There may be multiple views for the same identifier.
+
+ - a *selector*, which is a kind of filter telling how well a view suit to a
+ particular context. When looking for a particular view (e.g. given an
+ identifier), |cubicweb| computes for each available view with that identifier
+ a score which is returned by the selector. Then the view with the highest
+ score is used. The standard library of predicates is in
+ :mod:`cubicweb.predicates`.
+
+A view has a set of methods inherited from the :class:`cubicweb.view.View` class,
+though you usually don't derive directly from this class but from one of its more
+specific child class.
+
+Last but not least, |cubicweb| provides a set of default views accepting any kind
+of entities.
+
+Want a proof? Create a community as you've already done for other entity types
+through the index page, you'll then see something like that:
+
+.. image:: ../../images/tutos-base_myblog-community-default-primary_en.png
+ :alt: the default primary view for our community entity type
+
+
+If you notice the weird messages that appear in the page: those are messages
+generated for the new data model, which have no translation yet. To fix that,
+we'll have to use dedicated `cubicweb-ctl` commands:
+
+.. sourcecode: bash
+
+ cubicweb-ctl i18ncube myblog # build/update cube's message catalogs
+ # then add translation into .po file into the cube's i18n directory
+ cubicweb-ctl i18ninstance myblog # recompile instance's message catalogs
+ cubicweb-ctl restart -D myblog # instance has to be restarted to consider new catalogs
+
+You'll then be able to redefine each of them according to your needs and
+preferences. So let's see how to do such thing.
+
+Changing the layout of the application
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The layout is the general organization of the pages in the site. Views that generate
+the layout are sometimes referred to as 'templates'. They are implemented in the
+framework in the module :mod:`cubicweb.web.views.basetemplates`. By overriding
+classes in this module, you can customize whatever part you wish of the default
+layout.
+
+But notice that |cubicweb| provides many other ways to customize the
+interface, thanks to actions and components (which you can individually
+(de)activate, control their location, customize their look...) as well as
+"simple" CSS customization. You should first try to achieve your goal using such
+fine grained parametrization rather then overriding a whole template, which usually
+embeds customisation access points that you may loose in the process.
+
+But for the sake of example, let's say we want to change the generic page
+footer... We can simply add to the module ``views`` of our cube,
+e.g. :file:`cubes/myblog/views.py`, the code below:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views import basetemplates
+
+ class MyHTMLPageFooter(basetemplates.HTMLPageFooter):
+
+ def footer_content(self):
+ self.w(u'This website has been created with <a href="http://cubicweb.org">CubicWeb</a>.')
+
+ def registration_callback(vreg):
+ vreg.register_all(globals().values(), __name__, (MyHTMLPageFooter,))
+ vreg.register_and_replace(MyHTMLPageFooter, basetemplates.HTMLPageFooter)
+
+
+* Our class inherits from the default page footer to ease getting things right,
+ but this is not mandatory.
+
+* When we want to write something to the output stream, we simply call `self.w`,
+ which *must be passed a unicode string*.
+
+* The latest function is the most exotic stuff. The point is that without it, you
+ would get an error at display time because the framework wouldn't be able to
+ choose which footer to use between :class:`HTMLPageFooter` and
+ :class:`MyHTMLPageFooter`, since both have the same selector, hence the same
+ score... In this case, we want our footer to replace the default one, so we have
+ to define a :func:`registration_callback` function to control object
+ registration: the first instruction tells to register everything in the module
+ but the :class:`MyHTMLPageFooter` class, then the second to register it instead
+ of :class:`HTMLPageFooter`. Without this function, everything in the module is
+ registered blindly.
+
+.. Note::
+
+ When a view is modified while running in debug mode, it is not required to
+ restart the instance server. Save the Python file and reload the page in your
+ web browser to view the changes.
+
+We will now have this simple footer on every page of the site.
+
+
+Primary view customization
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The 'primary' view (i.e. any view with the identifier set to 'primary') is the one used to
+display all the information about a single entity. The standard primary view is one
+of the most sophisticated views of all. It has several customisation points, but
+its power comes with `uicfg`, allowing you to control it without having to
+subclass it.
+
+However this is a bit off-topic for this first tutorial. Let's say we simply want a
+custom primary view for my `Community` entity type, using directly the view
+interface without trying to benefit from the default implementation (you should
+do that though if you're rewriting reusable cubes; everything is described in more
+details in :ref:`primary_view`).
+
+
+So... Some code! That we'll put again in the module ``views`` of our cube.
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import is_instance
+ from cubicweb.web.views import primary
+
+ class CommunityPrimaryView(primary.PrimaryView):
+ __select__ = is_instance('Community')
+
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(row, col)
+ self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
+ if entity.description:
+ self.w(u'<p>%s</p>' % entity.printable_value('description'))
+
+What's going on here?
+
+* Our class inherits from the default primary view, here mainly to get the correct
+ view identifier, since we don't use any of its features.
+
+* We set on it a selector telling that it only applies when trying to display
+ some entity of the `Community` type. This is enough to get an higher score than
+ the default view for entities of this type.
+
+* View applying to entities usually have to define `cell_call` as entry point,
+ and are given `row` and `col` arguments tell to which entity in the result set
+ the view is applied. We can then get this entity from the result set
+ (`self.cw_rset`) by using the `get_entity` method.
+
+* To ease thing, we access our entity's attribute for display using its
+ printable_value method, which will handle formatting and escaping when
+ necessary. As you can see, you can also access attributes by their name on the
+ entity to get the raw value.
+
+
+You can now reload the page of the community we just created and see the changes.
+
+.. image:: ../../images/tutos-base_myblog-community-custom-primary_en.png
+ :alt: the custom primary view for our community entity type
+
+We've seen here a lot of thing you'll have to deal with to write views in
+|cubicweb|. The good news is that this is almost everything that is used to
+build higher level layers.
+
+.. Note::
+
+ As things get complicated and the volume of code in your cube increases, you can
+ of course still split your views module into a python package with subpackages.
+
+You can find more details about views and selectors in :ref:`Views`.
+
+
+Write entities to add logic in your data
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+|cubicweb| provides an ORM to easily programmaticaly manipulate
+entities (just like the one we have fetched earlier by calling
+`get_entity` on a result set). By default, entity
+types are instances of the :class:`AnyEntity` class, which holds a set of
+predefined methods as well as property automatically generated for
+attributes/relations of the type it represents.
+
+You can redefine each entity to provide additional methods or whatever you want
+to help you write your application. Customizing an entity requires that your
+entity:
+
+- inherits from :class:`cubicweb.entities.AnyEntity` or any subclass
+
+- defines a :attr:`__regid__` linked to the corresponding data type of your schema
+
+You may then want to add your own methods, override default implementation of some
+method, etc...
+
+.. sourcecode:: python
+
+ from cubicweb.entities import AnyEntity, fetch_config
+
+
+ class Community(AnyEntity):
+ """customized class for Community entities"""
+ __regid__ = 'Community'
+
+ fetch_attrs, cw_fetch_order = fetch_config(['name'])
+
+ def dc_title(self):
+ return self.name
+
+ def display_cw_logo(self):
+ return 'CubicWeb' in self.description
+
+In this example:
+
+* we used convenience :func:`fetch_config` function to tell which attributes
+ should be prefetched by the ORM when looking for some related entities of this
+ type, and how they should be ordered
+
+* we overrode the standard `dc_title` method, used in various place in the interface
+ to display the entity (though in this case the default implementation would
+ have had the same result)
+
+* we implemented here a method :meth:`display_cw_logo` which tests if the blog
+ entry title contains 'CW'. It can then be used when you're writing code
+ involving 'Community' entities in your views, hooks, etc. For instance, you can
+ modify your previous views as follows:
+
+.. sourcecode:: python
+
+
+ class CommunityPrimaryView(primary.PrimaryView):
+ __select__ = is_instance('Community')
+
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(row, col)
+ self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
+ if entity.display_cw_logo():
+ self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
+ if entity.description:
+ self.w(u'<p>%s</p>' % entity.printable_value('description'))
+
+Then each community whose description contains 'CW' is shown with the |cubicweb|
+logo in front of it.
+
+.. Note::
+
+ As for view, you don't have to restart your instance when modifying some entity
+ classes while your server is running in debug mode, the code will be
+ automatically reloaded.
+
+
+Extending the application by using more cubes!
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One of the goal of the |cubicweb| framework was to have truly reusable
+components. To do so, they must both behave nicely when plugged into the
+application and be easily customisable, from the data model to the user
+interface. And I think the result is pretty successful, thanks to system such as
+the selection mechanism and the choice to write views as python code which allows
+to build our page using true object oriented programming techniques, that no
+template language provides.
+
+
+A library of standard cubes is available from `CubicWeb Forge`_, to address a
+lot of common concerns such has manipulating people, files, things to do, etc. In
+our community blog case, we could be interested for instance in functionalities
+provided by the `comment` and `tag` cubes. The former provides threaded
+discussion functionalities, the latter a simple tag mechanism to classify content.
+Let's say we want to try those. We will first modify our cube's :file:`__pkginfo__.py`
+file:
+
+.. sourcecode:: python
+
+ __depends__ = {'cubicweb': '>= 3.10.7',
+ 'cubicweb-blog': None,
+ 'cubicweb-comment': None,
+ 'cubicweb-tag': None}
+
+Now, we'll simply tell on which entity types we want to activate the 'comment'
+and 'tag' facilities by adding respectively the 'comments' and 'tags' relations on
+them in our schema (:file:`schema.py`).
+
+.. sourcecode:: python
+
+ class comments(RelationDefinition):
+ subject = 'Comment'
+ object = 'BlogEntry'
+ cardinality = '1*'
+ composite = 'object'
+
+ class tags(RelationDefinition):
+ subject = 'Tag'
+ object = ('Community', 'BlogEntry')
+
+
+So in the case above we activated comments on `BlogEntry` entities and tags on
+both `Community` and `BlogEntry`. Various views from both `comment` and `tag`
+cubes will then be automatically displayed when one of those relations is
+supported.
+
+Let's synchronize the data model as we've done earlier: ::
+
+
+ $ cubicweb-ctl stop myblog
+ $ cubicweb-ctl shell myblog
+ entering the migration python shell
+ just type migration commands or arbitrary python code and type ENTER to execute it
+ type "exit" or Ctrl-D to quit the shell and resume operation
+ >>> add_cubes(('comment', 'tag'))
+ >>>
+
+Then restart the instance. Let's look at a blog entry:
+
+.. image:: ../../images/tutos-base_myblog-blogentry-taggable-commentable-primary_en.png
+ :alt: the primary view for a blog entry with comments and tags activated
+
+As you can see, we now have a box displaying tags and a section proposing to add
+a comment and displaying existing one below the post. All this without changing
+anything in our views, thanks to the design of generic views provided by the
+framework. Though if we take a look at a community, we won't see the tags box!
+That's because by default this box try to locate itself in the left column within
+the white frame, and this column is handled by the primary view we
+hijacked. Let's change our view to make it more extensible, by keeping both our
+custom rendering but also extension points provided by the default
+implementation.
+
+
+.. sourcecode:: python
+
+ class CommunityPrimaryView(primary.PrimaryView):
+ __select__ = is_instance('Community')
+
+ def render_entity_title(self, entity):
+ self.w(u'<h1>Welcome to the "%s" community</h1>' % entity.printable_value('name'))
+
+ def render_entity_attributes(self, entity):
+ if entity.display_cw_logo():
+ self.w(u'<img src="http://www.cubicweb.org/doc/en/_static/cubicweb.png"/>')
+ if entity.description:
+ self.w(u'<p>%s</p>' % entity.printable_value('description'))
+
+It appears now properly:
+
+.. image:: ../../images/tutos-base_myblog-community-taggable-primary_en.png
+ :alt: the custom primary view for a community entry with tags activated
+
+You can control part of the interface independently from each others, piece by
+piece. Really.
+
+
+
+.. _`CubicWeb Forge`: http://www.cubicweb.org/project
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/discovering-the-ui.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,161 @@
+
+.. _TutosBaseDiscoveringTheUI:
+
+Discovering the web interface
+-----------------------------
+
+You can now access your web instance to create blogs and post messages
+by visiting the URL http://localhost:8080/.
+
+By default, anonymous access is disabled, so a login form will appear. If you
+asked to allow anonymous access when initializing the instance, click on the
+'login' link in the top right hand corner. To login, you need then use the admin
+account you specified at the time you initialized the database with
+``cubicweb-ctl create``.
+
+.. image:: ../../images/tutos-base_login-form_en.png
+ :alt: the login form
+
+
+Once authenticated, you can start playing with your instance. The default index
+page looks like the following:
+
+.. image:: ../../images/tutos-base_index_en.png
+ :alt: the index page
+
+
+Minimal configuration
+~~~~~~~~~~~~~~~~~~~~~
+
+Before creating entities, let's change that 'unset title' thing that appears
+here and there. This comes from a |cubicweb| system properties. To set it,
+click on the 'site configuration link' in the pop-up menu behind your login name
+in the upper left-hand corner
+
+.. image:: ../../images/tutos-base_user-menu_en.png
+ :alt: the user pop-up menu
+
+The site title is in the 'Ui' section. Simply set it to the desired value and
+click the 'validate' button.
+
+.. image:: ../../images/tutos-base_siteconfig_en.png
+ :alt: the site configuration form
+
+You should see a 'changes applied' message. You can now go back to the
+index page by clicking on the |cubicweb| logo in the upper left-hand corner.
+
+You will much likely still see 'unset title' at this point. This is because by
+default the index page is cached. Force a refresh of the page (by typing Ctrl-R
+in Firefox for instance) and you should now see the title you entered.
+
+
+Adding entities
+~~~~~~~~~~~~~~~
+
+The ``blog`` cube defines several entity types, among them ``Blog`` which is a
+container for ``BlogEntry`` (i.e. posts) on a particular topic. We can get a
+graphical view of the schema by clicking on the 'site schema' link in the user
+pop-up menu we've already seen:
+
+.. image:: ../../images/tutos-base_schema_en.png
+ :alt: graphical view of the schema (aka data-model)
+
+Nice isn't it? Notice that this, as most other stuff we'll see in this tutorial,
+is generated by the framework according to the model of the application. In our
+case, the model defined by the ``blog`` cube.
+
+Now let us create a few of these entities.
+
+
+Add a blog
+**********
+
+Clicking on the `[+]` at the left of the 'Blog' link on the index page will lead
+you to an HTML form to create a blog.
+
+.. image:: ../../images/tutos-base_blog-form_en.png
+ :alt: the blog creation form
+
+For instance, call this new blog 'Tech-blog' and type in 'everything about
+technology' as the description , then validate the form by clicking on
+'Validate'. You will be redirected to the `primary` view of the newly created blog.
+
+.. image:: ../../images/tutos-base_blog-primary_en.png
+ :alt: the blog primary view
+
+
+Add a blog post
+***************
+
+There are several ways to add a blog entry. The simplest is to click on the 'add
+blog entry' link in the actions box on viewing the blog you have just created.
+You will then see a form to create a post, with a 'blog entry of' field preset
+to the blog we're coming from. Enter a title, some content, click the 'validate'
+button and you're done. You will be redirected to the blog primary view, though you
+now see that it contains the blog post you've just created.
+
+.. image:: ../../images/tutos-base_blog-primary-after-post-creation_en.png
+ :alt: the blog primary view after creation of a post
+
+Notice there are some new boxes that appears in the left column.
+
+You can achieve the same thing by following the same path as we did for the blog
+creation, e.g. by clicking on the `[+]` at the left of the 'Blog entry' link on
+the index page. The diffidence being that since there is no context information,
+the 'blog entry of' selector won't be preset to the blog.
+
+
+If you click on the 'modify' link of the action box, you are back to
+the form to edit the entity you just created, except that the form now
+has another section with a combo-box entitled 'add relation'. It
+provisos a generic way to edit relations which don't appears in the
+above form. Choose the relation you want to add and a second combo box
+appears where you can pick existing entities. If there are too many
+of them, you will be offered to navigate to the target entity, that is
+go away from the form and go back to it later, once you've selected
+the entity you want to link with.
+
+.. image:: ../../images/tutos-base_form-generic-relations_en.png
+ :alt: the generic relations combo box
+
+This combo box can't appear until the entity is actually created. That's why you
+haven't seen it at creation time. You could also have hit 'Apply' instead of
+'validate' and it would have showed up.
+
+
+About ui auto-adaptation
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+One of the things that make |cubicweb| different of other frameworks is
+its automatic user interface that adapts itself according to the data being
+displayed. Let's see an example.
+
+If you go back to the home page an click on the 'Blog' link, you will be redirected
+to the primary view of the blog, the same we've seen earlier. Now, add another
+blog, go back to the index page, and click again on this link. You will see
+a very different view (namely the 'list' view).
+
+.. image:: ../../images/tutos-base_blogs-list_en.png
+ :alt: the list view when there are more than one blog to display
+
+This is because in the first case, the framework chose to use the 'primary'
+view since there was only one entity in the data to be displayed. Now that there
+are two entities, the 'list' view is more appropriate and hence is being used.
+
+There are various other places where |cubicweb| adapts to display data in the best
+way, the main being provided by the view *selection* mechanism that will be detailed
+later.
+
+
+Digging deeper
+~~~~~~~~~~~~~~
+
+By following principles explained below, you should now be able to
+create new users for your application, to configure with a finer
+grain, etc... You will notice that the index page lists a lot of types
+you don't know about. Most are built-in types provided by the framework
+to make the whole system work. You may ignore them in a first time and
+discover them as time goes.
+
+One thing that is worth playing with is the search box. It may be used in various
+way, from simple full text search to advanced queries using the :ref:`RQL` .
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/base/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,48 @@
+.. -*- coding: utf-8 -*-
+
+.. _TutosBase:
+
+Building a simple blog with |cubicweb|
+======================================
+
+|cubicweb| is a semantic web application framework that favors reuse and
+object-oriented design.
+
+
+This tutorial is designed to help in your very first steps to start with
+|cubicweb|. We will tour through basic concepts such as:
+
+* getting an application running by using existing components
+* discovering the default user interface
+* basic extending and customizing the look and feel of that application
+
+More advanced concepts are covered in :ref:`TutosPhotoWebSite`.
+
+
+.. _TutosBaseVocab:
+
+Some vocabulary
+---------------
+
+|cubicweb| comes with a few words of vocabulary that you should know to
+understand what we're talking about. To follow this tutorial, you should at least
+know that:
+
+* a `cube` is a component that usually includes a model defining some data types
+ and a set of views to display them. A cube can be built by assembling other
+ cubes;
+
+* an `instance` is a specific installation of one or more cubes and includes
+ configuration files, a web server and a database.
+
+Reading :ref:`Concepts` for more vocabulary will be required at some point.
+
+Now, let's start the hot stuff!
+
+.. toctree::
+ :maxdepth: 2
+
+ blog-in-five-minutes
+ discovering-the-ui
+ customizing-the-application
+ conclusion
--- a/doc/tutorials/dataimport/data_import_tutorial.rst Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,646 +0,0 @@
-Importing relational data into a CubicWeb instance
-==================================================
-
-Introduction
-~~~~~~~~~~~~
-
-This tutorial explains how to import data from an external source (e.g. a collection of files)
-into a CubicWeb cube instance.
-
-First, once we know the format of the data we wish to import, we devise a
-*data model*, that is, a CubicWeb (Yams) schema which reflects the way the data
-is structured. This schema is implemented in the ``schema.py`` file.
-In this tutorial, we will describe such a schema for a particular data set,
-the Diseasome data (see below).
-
-Once the schema is defined, we create a cube and an instance.
-The cube is a specification of an application, whereas an instance
-is the application per se.
-
-Once the schema is defined and the instance is created, the import can be performed, via
-the following steps:
-
-1. Build a custom parser for the data to be imported. Thus, one obtains a Python
- memory representation of the data.
-
-2. Map the parsed data to the data model defined in ``schema.py``.
-
-3. Perform the actual import of the data. This comes down to "populating"
- the data model with the memory representation obtained at 1, according to
- the mapping defined at 2.
-
-This tutorial illustrates all the above steps in the context of relational data
-stored in the RDF format.
-
-More specifically, we describe the import of Diseasome_ RDF/OWL data.
-
-.. _Diseasome: http://datahub.io/dataset/fu-berlin-diseasome
-
-Building a data model
-~~~~~~~~~~~~~~~~~~~~~
-
-The first thing to do when using CubicWeb for creating an application from scratch
-is to devise a *data model*, that is, a relational representation of the problem to be
-modeled or of the structure of the data to be imported.
-
-In such a schema, we define
-an entity type (``EntityType`` objects) for each type of entity to import. Each such type
-has several attributes. If the attributes are of known CubicWeb (Yams) types, viz. numbers,
-strings or characters, then they are defined as attributes, as e.g. ``attribute = Int()``
-for an attribute named ``attribute`` which is an integer.
-
-Each such type also has a set of
-relations, which are defined like the attributes, except that they represent, in fact,
-relations between the entities of the type under discussion and the objects of a type which
-is specified in the relation definition.
-
-For example, for the Diseasome data, we have two types of entities, genes and diseases.
-Thus, we create two classes which inherit from ``EntityType``::
-
- class Disease(EntityType):
- # Corresponds to http://www.w3.org/2000/01/rdf-schema#label
- label = String(maxsize=512, fulltextindexed=True)
- ...
-
- #Corresponds to http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/associatedGene
- associated_genes = SubjectRelation('Gene', cardinality='**')
- ...
-
- #Corresponds to 'http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/chromosomalLocation'
- chromosomal_location = SubjectRelation('ExternalUri', cardinality='?*', inlined=True)
-
-
- class Gene(EntityType):
- ...
-
-In this schema, there are attributes whose values are numbers or strings. Thus, they are
-defined by using the CubicWeb / Yams primitive types, e.g., ``label = String(maxsize=12)``.
-These types can have several constraints or attributes, such as ``maxsize``.
-There are also relations, either between the entity types themselves, or between them
-and a CubicWeb type, ``ExternalUri``. The latter defines a class of URI objects in
-CubicWeb. For instance, the ``chromosomal_location`` attribute is a relation between
-a ``Disease`` entity and an ``ExternalUri`` entity. The relation is marked by the CubicWeb /
-Yams ``SubjectRelation`` method. The latter can have several optional keyword arguments, such as
-``cardinality`` which specifies the number of subjects and objects related by the relation type
-specified. For example, the ``'?*'`` cardinality in the ``chromosomal_relation`` relation type says
-that zero or more ``Disease`` entities are related to zero or one ``ExternalUri`` entities.
-In other words, a ``Disease`` entity is related to at most one ``ExternalUri`` entity via the
-``chromosomal_location`` relation type, and that we can have zero or more ``Disease`` entities in the
-data base.
-For a relation between the entity types themselves, the ``associated_genes`` between a ``Disease``
-entity and a ``Gene`` entity is defined, so that any number of ``Gene`` entities can be associated
-to a ``Disease``, and there can be any number of ``Disease`` s if a ``Gene`` exists.
-
-Of course, before being able to use the CubicWeb / Yams built-in objects, we need to import them::
-
-
- from yams.buildobjs import EntityType, SubjectRelation, String, Int
- from cubicweb.schemas.base import ExternalUri
-
-Building a custom data parser
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The data we wish to import is structured in the RDF format,
-as a text file containing a set of lines.
-On each line, there are three fields.
-The first two fields are URIs ("Universal Resource Identifiers").
-The third field is either an URI or a string. Each field bares a particular meaning:
-
-- the leftmost field is an URI that holds the entity to be imported.
- Note that the entities defined in the data model (i.e., in ``schema.py``) should
- correspond to the entities whose URIs are specified in the import file.
-
-- the middle field is an URI that holds a relation whose subject is the entity
- defined by the leftmost field. Note that this should also correspond
- to the definitions in the data model.
-
-- the rightmost field is either an URI or a string. When this field is an URI,
- it gives the object of the relation defined by the middle field.
- When the rightmost field is a string, the middle field is interpreted as an attribute
- of the subject (introduced by the leftmost field) and the rightmost field is
- interpreted as the value of the attribute.
-
-Note however that some attributes (i.e. relations whose objects are strings)
-have their objects defined as strings followed by ``^^`` and by another URI;
-we ignore this part.
-
-Let us show some examples:
-
-- of line holding an attribute definition:
- ``<http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/CYP17A1>
- <http://www.w3.org/2000/01/rdf-schema#label> "CYP17A1" .``
- The line contains the definition of the ``label`` attribute of an
- entity of type ``gene``. The value of ``label`` is '``CYP17A1``'.
-
-- of line holding a relation definition:
- ``<http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseases/1>
- <http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/associatedGene>
- <http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/HADH2> .``
- The line contains the definition of the ``associatedGene`` relation between
- a ``disease`` subject entity identified by ``1`` and a ``gene`` object
- entity defined by ``HADH2``.
-
-Thus, for parsing the data, we can (:note: see the ``diseasome_parser`` module):
-
-1. define a couple of regular expressions for parsing the two kinds of lines,
- ``RE_ATTS`` for parsing the attribute definitions, and ``RE_RELS`` for parsing
- the relation definitions.
-
-2. define a function that iterates through the lines of the file and retrieves
- (``yield`` s) a (subject, relation, object) tuple for each line.
- We called it ``_retrieve_structure`` in the ``diseasome_parser`` module.
- The function needs the file name and the types for which information
- should be retrieved.
-
-Alternatively, instead of hand-making the parser, one could use the RDF parser provided
-in the ``dataio`` cube.
-
-.. XXX To further study and detail the ``dataio`` cube usage.
-
-Once we get to have the (subject, relation, object) triples, we need to map them into
-the data model.
-
-
-Mapping the data to the schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In the case of diseasome data, we can just define two dictionaries for mapping
-the names of the relations as extracted by the parser, to the names of the relations
-as defined in the ``schema.py`` data model. In the ``diseasome_parser`` module
-they are called ``MAPPING_ATTS`` and ``MAPPING_RELS``.
-Given that the relation and attribute names are given in CamelCase in the original data,
-mappings are necessary if we follow the PEP08 when naming the attributes in the data model.
-For example, the RDF relation ``chromosomalLocation`` is mapped into the schema relation
-``chromosomal_location``.
-
-Once these mappings have been defined, we just iterate over the (subject, relation, object)
-tuples provided by the parser and we extract the entities, with their attributes and relations.
-For each entity, we thus have a dictionary with two keys, ``attributes`` and ``relations``.
-The value associated to the ``attributes`` key is a dictionary containing (attribute: value)
-pairs, where "value" is a string, plus the ``cwuri`` key / attribute holding the URI of
-the entity itself.
-The value associated to the ``relations`` key is a dictionary containing (relation: value)
-pairs, where "value" is an URI.
-This is implemented in the ``entities_from_rdf`` interface function of the module
-``diseasome_parser``. This function provides an iterator on the dictionaries containing
-the ``attributes`` and ``relations`` keys for all entities.
-
-However, this is a simple case. In real life, things can get much more complicated, and the
-mapping can be far from trivial, especially when several data sources (which can follow
-different formatting and even structuring conventions) must be mapped into the same data model.
-
-Importing the data
-~~~~~~~~~~~~~~~~~~
-
-The data import code should be placed in a Python module. Let us call it
-``diseasome_import.py``. Then, this module should be called via
-``cubicweb-ctl``, as follows::
-
- cubicweb-ctl shell diseasome_import.py -- <other arguments e.g. data file>
-
-In the import module, we should use a *store* for doing the import.
-A store is an object which provides three kinds of methods for
-importing data:
-
-- a method for importing the entities, along with the values
- of their attributes.
-- a method for importing the relations between the entities.
-- a method for committing the imports to the database.
-
-In CubicWeb, we have four stores:
-
-1. ``ObjectStore`` base class for the stores in CubicWeb.
- It only provides a skeleton for all other stores and
- provides the means for creating the memory structures
- (dictionaries) that hold the entities and the relations
- between them.
-
-2. ``RQLObjectStore``: store which uses the RQL language for performing
- database insertions and updates. It relies on all the CubicWeb hooks
- machinery, especially for dealing with security issues (database access
- permissions).
-
-2. ``NoHookRQLObjectStore``: store which uses the RQL language for
- performing database insertions and updates, but for which
- all hooks are deactivated. This implies that
- certain checks with respect to the CubicWeb / Yams schema
- (data model) are not performed. However, all SQL queries
- obtained from the RQL ones are executed in a sequential
- manner, one query per inserted entity.
-
-4. ``SQLGenObjectStore``: store which uses the SQL language directly.
- It inserts entities either sequentially, by executing an SQL query
- for each entity, or directly by using one PostGRES ``COPY FROM``
- query for a set of similarly structured entities.
-
-For really massive imports (millions or billions of entities), there
-is a cube ``dataio`` which contains another store, called
-``MassiveObjectStore``. This store is similar to ``SQLGenObjectStore``,
-except that anything related to CubicWeb is bypassed. That is, even the
-CubicWeb EID entity identifiers are not handled. This store is the fastest,
-but has a slightly different API from the other four stores mentioned above.
-Moreover, it has an important limitation, in that it doesn't insert inlined [#]_
-relations in the database.
-
-.. [#] An inlined relation is a relation defined in the schema
- with the keyword argument ``inlined=True``. Such a relation
- is inserted in the database as an attribute of the entity
- whose subject it is.
-
-In the following section we will see how to import data by using the stores
-in CubicWeb's ``dataimport`` module.
-
-Using the stores in ``dataimport``
-++++++++++++++++++++++++++++++++++
-
-``ObjectStore`` is seldom used in real life for importing data, since it is
-only the base store for the other stores and it doesn't perform an actual
-import of the data. Nevertheless, the other three stores, which import data,
-are based on ``ObjectStore`` and provide the same API.
-
-All three stores ``RQLObjectStore``, ``NoHookRQLObjectStore`` and
-``SQLGenObjectStore`` provide exactly the same API for importing data, that is
-entities and relations, in an SQL database.
-
-Before using a store, one must import the ``dataimport`` module and then initialize
-the store, with the current ``session`` as a parameter::
-
- import cubicweb.dataimport as cwdi
- ...
-
- store = cwdi.RQLObjectStore(session)
-
-Each such store provides three methods for data import:
-
-#. ``create_entity(Etype, **attributes)``, which allows us to add
- an entity of the Yams type ``Etype`` to the database. This entity's attributes
- are specified in the ``attributes`` dictionary. The method returns the entity
- created in the database. For example, we add two entities,
- a person, of ``Person`` type, and a location, of ``Location`` type::
-
- person = store.create_entity('Person', name='Toto', age='18', height='190')
-
- location = store.create_entity('Location', town='Paris', arrondissement='13')
-
-#. ``relate(subject_eid, r_type, object_eid)``, which allows us to add a relation
- of the Yams type ``r_type`` to the database. The relation's subject is an entity
- whose EID is ``subject_eid``; its object is another entity, whose EID is
- ``object_eid``. For example [#]_::
-
- store.relate(person.eid(), 'lives_in', location.eid(), **kwargs)
-
- ``kwargs`` is only used by the ``SQLGenObjectStore``'s ``relate`` method and is here
- to allow us to specify the type of the subject of the relation, when the relation is
- defined as inlined in the schema.
-
-.. [#] The ``eid`` method of an entity defined via ``create_entity`` returns
- the entity identifier as assigned by CubicWeb when creating the entity.
- This only works for entities defined via the stores in the CubicWeb's
- ``dataimport`` module.
-
- The keyword argument that is understood by ``SQLGenObjectStore`` is called
- ``subjtype`` and holds the type of the subject entity. For the example considered here,
- this comes to having [#]_::
-
- store.relate(person.eid(), 'lives_in', location.eid(), subjtype=person.cw_etype)
-
- If ``subjtype`` is not specified, then the store tries to infer the type of the subject.
- However, this doesn't always work, e.g. when there are several possible subject types
- for a given relation type.
-
-.. [#] The ``cw_etype`` attribute of an entity defined via ``create_entity`` holds
- the type of the entity just created. This only works for entities defined via
- the stores in the CubicWeb's ``dataimport`` module. In the example considered
- here, ``person.cw_etype`` holds ``'Person'``.
-
- All the other stores but ``SQLGenObjectStore`` ignore the ``kwargs`` parameters.
-
-#. ``flush()``, which allows us to perform the actual commit into the database, along
- with some cleanup operations. Ideally, this method should be called as often as
- possible, that is after each insertion in the database, so that database sessions
- are kept as atomic as possible. In practice, we usually call this method twice:
- first, after all the entities have been created, second, after all relations have
- been created.
-
- Note however that before each commit the database insertions
- have to be consistent with the schema. Thus, if, for instance,
- an entity has an attribute defined through a relation (viz.
- a ``SubjectRelation``) with a ``"1"`` or ``"+"`` object
- cardinality, we have to create the entity under discussion,
- the object entity of the relation under discussion, and the
- relation itself, before committing the additions to the database.
-
- The ``flush`` method is simply called as::
-
- store.flush().
-
-
-Using the ``MassiveObjectStore`` in the ``dataio`` cube
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-This store, available in the ``dataio`` cube, allows us to
-fully dispense with the CubicWeb import mechanisms and hence
-to interact directly with the database server, via SQL queries.
-
-Moreover, these queries rely on PostGreSQL's ``COPY FROM`` instruction
-to create several entities in a single query. This brings tremendous
-performance improvements with respect to the RQL-based data insertion
-procedures.
-
-However, the API of this store is slightly different from the API of
-the stores in CubicWeb's ``dataimport`` module.
-
-Before using the store, one has to import the ``dataio`` cube's
-``dataimport`` module, then initialize the store by giving it the
-``session`` parameter::
-
- from cubes.dataio import dataimport as mcwdi
- ...
-
- store = mcwdi.MassiveObjectStore(session)
-
-The ``MassiveObjectStore`` provides six methods for inserting data
-into the database:
-
-#. ``init_rtype_table(SubjEtype, r_type, ObjEtype)``, which specifies the
- creation of the tables associated to the relation types in the database.
- Each such table has three column, the type of the subject entity, the
- type of the relation (that is, the name of the attribute in the subject
- entity which is defined via the relation), and the type of the object
- entity. For example::
-
- store.init_rtype_table('Person', 'lives_in', 'Location')
-
- Please note that these tables can be created before the entities, since
- they only specify their types, not their unique identifiers.
-
-#. ``create_entity(Etype, **attributes)``, which allows us to add new entities,
- whose attributes are given in the ``attributes`` dictionary.
- Please note however that, by default, this method does *not* return
- the created entity. The method is called, for example, as in::
-
- store.create_entity('Person', name='Toto', age='18', height='190',
- uri='http://link/to/person/toto_18_190')
- store.create_entity('Location', town='Paris', arrondissement='13',
- uri='http://link/to/location/paris_13')
-
- In order to be able to link these entities via the relations when needed,
- we must provide ourselves a means for uniquely identifying the entities.
- In general, this is done via URIs, stored in attributes like ``uri`` or
- ``cwuri``. The name of the attribute is irrelevant as long as its value is
- unique for each entity.
-
-#. ``relate_by_iid(subject_iid, r_type, object_iid)`` allows us to actually
- relate the entities uniquely identified by ``subject_iid`` and
- ``object_iid`` via a relation of type ``r_type``. For example::
-
- store.relate_by_iid('http://link/to/person/toto_18_190',
- 'lives_in',
- 'http://link/to/location/paris_13')
-
- Please note that this method does *not* work for inlined relations!
-
-#. ``convert_relations(SubjEtype, r_type, ObjEtype, subj_iid_attribute,
- obj_iid_attribute)``
- allows us to actually insert
- the relations in the database. At one call of this method, one inserts
- all the relations of type ``rtype`` between entities of given types.
- ``subj_iid_attribute`` and ``object_iid_attribute`` are the names
- of the attributes which store the unique identifiers of the entities,
- as assigned by the user. These names can be identical, as long as
- their values are unique. For example, for inserting all relations
- of type ``lives_in`` between ``People`` and ``Location`` entities,
- we write::
-
- store.convert_relations('Person', 'lives_in', 'Location', 'uri', 'uri')
-
-#. ``flush()`` performs the actual commit in the database. It only needs
- to be called after ``create_entity`` and ``relate_by_iid`` calls.
- Please note that ``relate_by_iid`` does *not* perform insertions into
- the database, hence calling ``flush()`` for it would have no effect.
-
-#. ``cleanup()`` performs database cleanups, by removing temporary tables.
- It should only be called at the end of the import.
-
-
-
-.. XXX to add smth on the store's parameter initialization.
-
-
-
-Application to the Diseasome data
-+++++++++++++++++++++++++++++++++
-
-Import setup
-############
-
-We define an import function, ``diseasome_import``, which does basically four things:
-
-#. creates and initializes the store to be used, via a line such as::
-
- store = cwdi.SQLGenObjectStore(session)
-
- where ``cwdi`` is the imported ``cubicweb.dataimport`` or
- ``cubes.dataio.dataimport``.
-
-#. calls the diseasome parser, that is, the ``entities_from_rdf`` function in the
- ``diseasome_parser`` module and iterates on its result, in a line such as::
-
- for entity, relations in parser.entities_from_rdf(filename, ('gene', 'disease')):
-
- where ``parser`` is the imported ``diseasome_parser`` module, and ``filename`` is the
- name of the file containing the data (with its path), e.g. ``../data/diseasome_dump.nt``.
-
-#. creates the entities to be inserted in the database; for Diseasome, there are two
- kinds of entities:
-
- #. entities defined in the data model, viz. ``Gene`` and ``Disease`` in our case.
- #. entities which are built in CubicWeb / Yams, viz. ``ExternalUri`` which define
- URIs.
-
- As we are working with RDF data, each entity is defined through a series of URIs. Hence,
- each "relational attribute" [#]_ of an entity is defined via an URI, that is, in CubicWeb
- terms, via an ``ExternalUri`` entity. The entities are created, in the loop presented above,
- as such::
-
- ent = store.create_entity(etype, **entity)
-
- where ``etype`` is the appropriate entity type, either ``Gene`` or ``Disease``.
-
-.. [#] By "relational attribute" we denote an attribute (of an entity) which
- is defined through a relation, e.g. the ``chromosomal_location`` attribute
- of ``Disease`` entities, which is defined through a relation between a
- ``Disease`` and an ``ExternalUri``.
-
- The ``ExternalUri`` entities are as many as URIs in the data file. For them, we define a unique
- attribute, ``uri``, which holds the URI under discussion::
-
- extu = store.create_entity('ExternalUri', uri="http://path/of/the/uri")
-
-#. creates the relations between the entities. We have relations between:
-
- #. entities defined in the schema, e.g. between ``Disease`` and ``Gene``
- entities, such as the ``associated_genes`` relation defined for
- ``Disease`` entities.
- #. entities defined in the schema and ``ExternalUri`` entities, such as ``gene_id``.
-
- The way relations are added to the database depends on the store:
-
- - for the stores in the CubicWeb ``dataimport`` module, we only use
- ``store.relate``, in
- another loop, on the relations (that is, a
- loop inside the preceding one, mentioned at step 2)::
-
- for rtype, rels in relations.iteritems():
- ...
-
- store.relate(ent.eid(), rtype, extu.eid(), **kwargs)
-
- where ``kwargs`` is a dictionary designed to accommodate the need for specifying
- the type of the subject entity of the relation, when the relation is inlined and
- ``SQLGenObjectStore`` is used. For example::
-
- ...
- store.relate(ent.eid(), 'chromosomal_location', extu.eid(), subjtype='Disease')
-
- - for the ``MassiveObjectStore`` in the ``dataio`` cube's ``dataimport`` module,
- the relations are created in three steps:
-
- #. first, a table is created for each relation type, as in::
-
- ...
- store.init_rtype_table(ent.cw_etype, rtype, extu.cw_etype)
-
- which comes down to lines such as::
-
- store.init_rtype_table('Disease', 'associated_genes', 'Gene')
- store.init_rtype_table('Gene', 'gene_id', 'ExternalUri')
-
- #. second, the URI of each entity will be used as its identifier, in the
- ``relate_by_iid`` method, such as::
-
- disease_uri = 'http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseases/3'
- gene_uri = '<http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/HSD3B2'
- store.relate_by_iid(disease_uri, 'associated_genes', gene_uri)
-
- #. third, the relations for each relation type will be added to the database,
- via the ``convert_relations`` method, such as in::
-
- store.convert_relations('Disease', 'associated_genes', 'Gene', 'cwuri', 'cwuri')
-
- and::
-
- store.convert_relations('Gene', 'hgnc_id', 'ExternalUri', 'cwuri', 'uri')
-
- where ``cwuri`` and ``uri`` are the attributes which store the URIs of the entities
- defined in the data model, and of the ``ExternalUri`` entities, respectively.
-
-#. flushes all relations and entities::
-
- store.flush()
-
- which performs the actual commit of the inserted entities and relations in the database.
-
-If the ``MassiveObjectStore`` is used, then a cleanup of temporary SQL tables should be performed
-at the end of the import::
-
- store.cleanup()
-
-Timing benchmarks
-#################
-
-In order to time the import script, we just decorate the import function with the ``timed``
-decorator::
-
- from logilab.common.decorators import timed
- ...
-
- @timed
- def diseasome_import(session, filename):
- ...
-
-After running the import function as shown in the "Importing the data" section, we obtain two time measurements::
-
- diseasome_import clock: ... / time: ...
-
-Here, the meanings of these measurements are [#]_:
-
-- ``clock`` is the time spent by CubicWeb, on the server side (i.e. hooks and data pre- / post-processing on SQL
- queries),
-
-- ``time`` is the sum between ``clock`` and the time spent in PostGreSQL.
-
-.. [#] The meanings of the ``clock`` and ``time`` measurements, when using the ``@timed``
- decorators, were taken from `a blog post on massive data import in CubicWeb`_.
-
-.. _a blog post on massive data import in CubicWeb: http://www.cubicweb.org/blogentry/2116712
-
-The import function is put in an import module, named ``diseasome_import`` here. The module is called
-directly from the CubicWeb shell, as follows::
-
- cubicweb-ctl shell diseasome_instance diseasome_import.py \
- -- -df diseasome_import_file.nt -st StoreName
-
-The module accepts two arguments:
-
-- the data file, introduced by ``-df [--datafile]``, and
-- the store, introduced by ``-st [--store]``.
-
-The timings (in seconds) for different stores are given in the following table, for
-importing 4213 ``Disease`` entities and 3919 ``Gene`` entities with the import module
-just described:
-
-+--------------------------+------------------------+--------------------------------+------------+
-| Store | CubicWeb time (clock) | PostGreSQL time (time - clock) | Total time |
-+==========================+========================+================================+============+
-| ``RQLObjectStore`` | 225.98 | 62.05 | 288.03 |
-+--------------------------+------------------------+--------------------------------+------------+
-| ``NoHookRQLObjectStore`` | 62.73 | 51.38 | 114.11 |
-+--------------------------+------------------------+--------------------------------+------------+
-| ``SQLGenObjectStore`` | 20.41 | 11.03 | 31.44 |
-+--------------------------+------------------------+--------------------------------+------------+
-| ``MassiveObjectStore`` | 4.84 | 6.93 | 11.77 |
-+--------------------------+------------------------+--------------------------------+------------+
-
-
-Conclusions
-~~~~~~~~~~~
-
-In this tutorial we have seen how to import data in a CubicWeb application instance. We have first seen how to
-create a schema, then how to create a parser of the data and a mapping of the data to the schema.
-Finally, we have seen four ways of importing data into CubicWeb.
-
-Three of those are integrated into CubicWeb, namely the ``RQLObjectStore``, ``NoHookRQLObjectStore`` and
-``SQLGenObjectStore`` stores, which have a common API:
-
-- ``RQLObjectStore`` is by far the slowest, especially its time spent on the
- CubicWeb side, and so it should be used only for small amounts of
- "sensitive" data (i.e. where security is a concern).
-
-- ``NoHookRQLObjectStore`` slashes by almost four the time spent on the CubicWeb side,
- but is also quite slow; on the PostGres side it is as slow as the previous store.
- It should be used for data where security is not a concern,
- but consistency (with the data model) is.
-
-- ``SQLGenObjectStore`` slashes by three the time spent on the CubicWeb side and by five the time
- spent on the PostGreSQL side. It should be used for relatively great amounts of data, where
- security and data consistency are not a concern. Compared to the previous store, it has the
- disadvantage that, for inlined relations, we must specify their subjects' types.
-
-For really huge amounts of data there is a fourth store, ``MassiveObjectStore``, available
-from the ``dataio`` cube. It provides a blazing performance with respect to all other stores:
-it is almost 25 times faster than ``RQLObjectStore`` and almost three times faster than
-``SQLGenObjectStore``. However, it has a few usage caveats that should be taken into account:
-
-#. it cannot insert relations defined as inlined in the schema,
-#. no security or consistency check is performed on the data,
-#. its API is slightly different from the other stores.
-
-Hence, this store should be used when security and data consistency are not a concern,
-and there are no inlined relations in the schema.
-
-
-
-
-
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/dataimport/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,646 @@
+Importing relational data into a CubicWeb instance
+==================================================
+
+Introduction
+~~~~~~~~~~~~
+
+This tutorial explains how to import data from an external source (e.g. a collection of files)
+into a CubicWeb cube instance.
+
+First, once we know the format of the data we wish to import, we devise a
+*data model*, that is, a CubicWeb (Yams) schema which reflects the way the data
+is structured. This schema is implemented in the ``schema.py`` file.
+In this tutorial, we will describe such a schema for a particular data set,
+the Diseasome data (see below).
+
+Once the schema is defined, we create a cube and an instance.
+The cube is a specification of an application, whereas an instance
+is the application per se.
+
+Once the schema is defined and the instance is created, the import can be performed, via
+the following steps:
+
+1. Build a custom parser for the data to be imported. Thus, one obtains a Python
+ memory representation of the data.
+
+2. Map the parsed data to the data model defined in ``schema.py``.
+
+3. Perform the actual import of the data. This comes down to "populating"
+ the data model with the memory representation obtained at 1, according to
+ the mapping defined at 2.
+
+This tutorial illustrates all the above steps in the context of relational data
+stored in the RDF format.
+
+More specifically, we describe the import of Diseasome_ RDF/OWL data.
+
+.. _Diseasome: http://datahub.io/dataset/fu-berlin-diseasome
+
+Building a data model
+~~~~~~~~~~~~~~~~~~~~~
+
+The first thing to do when using CubicWeb for creating an application from scratch
+is to devise a *data model*, that is, a relational representation of the problem to be
+modeled or of the structure of the data to be imported.
+
+In such a schema, we define
+an entity type (``EntityType`` objects) for each type of entity to import. Each such type
+has several attributes. If the attributes are of known CubicWeb (Yams) types, viz. numbers,
+strings or characters, then they are defined as attributes, as e.g. ``attribute = Int()``
+for an attribute named ``attribute`` which is an integer.
+
+Each such type also has a set of
+relations, which are defined like the attributes, except that they represent, in fact,
+relations between the entities of the type under discussion and the objects of a type which
+is specified in the relation definition.
+
+For example, for the Diseasome data, we have two types of entities, genes and diseases.
+Thus, we create two classes which inherit from ``EntityType``::
+
+ class Disease(EntityType):
+ # Corresponds to http://www.w3.org/2000/01/rdf-schema#label
+ label = String(maxsize=512, fulltextindexed=True)
+ ...
+
+ #Corresponds to http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/associatedGene
+ associated_genes = SubjectRelation('Gene', cardinality='**')
+ ...
+
+ #Corresponds to 'http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/chromosomalLocation'
+ chromosomal_location = SubjectRelation('ExternalUri', cardinality='?*', inlined=True)
+
+
+ class Gene(EntityType):
+ ...
+
+In this schema, there are attributes whose values are numbers or strings. Thus, they are
+defined by using the CubicWeb / Yams primitive types, e.g., ``label = String(maxsize=12)``.
+These types can have several constraints or attributes, such as ``maxsize``.
+There are also relations, either between the entity types themselves, or between them
+and a CubicWeb type, ``ExternalUri``. The latter defines a class of URI objects in
+CubicWeb. For instance, the ``chromosomal_location`` attribute is a relation between
+a ``Disease`` entity and an ``ExternalUri`` entity. The relation is marked by the CubicWeb /
+Yams ``SubjectRelation`` method. The latter can have several optional keyword arguments, such as
+``cardinality`` which specifies the number of subjects and objects related by the relation type
+specified. For example, the ``'?*'`` cardinality in the ``chromosomal_relation`` relation type says
+that zero or more ``Disease`` entities are related to zero or one ``ExternalUri`` entities.
+In other words, a ``Disease`` entity is related to at most one ``ExternalUri`` entity via the
+``chromosomal_location`` relation type, and that we can have zero or more ``Disease`` entities in the
+data base.
+For a relation between the entity types themselves, the ``associated_genes`` between a ``Disease``
+entity and a ``Gene`` entity is defined, so that any number of ``Gene`` entities can be associated
+to a ``Disease``, and there can be any number of ``Disease`` s if a ``Gene`` exists.
+
+Of course, before being able to use the CubicWeb / Yams built-in objects, we need to import them::
+
+
+ from yams.buildobjs import EntityType, SubjectRelation, String, Int
+ from cubicweb.schemas.base import ExternalUri
+
+Building a custom data parser
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The data we wish to import is structured in the RDF format,
+as a text file containing a set of lines.
+On each line, there are three fields.
+The first two fields are URIs ("Universal Resource Identifiers").
+The third field is either an URI or a string. Each field bares a particular meaning:
+
+- the leftmost field is an URI that holds the entity to be imported.
+ Note that the entities defined in the data model (i.e., in ``schema.py``) should
+ correspond to the entities whose URIs are specified in the import file.
+
+- the middle field is an URI that holds a relation whose subject is the entity
+ defined by the leftmost field. Note that this should also correspond
+ to the definitions in the data model.
+
+- the rightmost field is either an URI or a string. When this field is an URI,
+ it gives the object of the relation defined by the middle field.
+ When the rightmost field is a string, the middle field is interpreted as an attribute
+ of the subject (introduced by the leftmost field) and the rightmost field is
+ interpreted as the value of the attribute.
+
+Note however that some attributes (i.e. relations whose objects are strings)
+have their objects defined as strings followed by ``^^`` and by another URI;
+we ignore this part.
+
+Let us show some examples:
+
+- of line holding an attribute definition:
+ ``<http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/CYP17A1>
+ <http://www.w3.org/2000/01/rdf-schema#label> "CYP17A1" .``
+ The line contains the definition of the ``label`` attribute of an
+ entity of type ``gene``. The value of ``label`` is '``CYP17A1``'.
+
+- of line holding a relation definition:
+ ``<http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseases/1>
+ <http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseasome/associatedGene>
+ <http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/HADH2> .``
+ The line contains the definition of the ``associatedGene`` relation between
+ a ``disease`` subject entity identified by ``1`` and a ``gene`` object
+ entity defined by ``HADH2``.
+
+Thus, for parsing the data, we can (:note: see the ``diseasome_parser`` module):
+
+1. define a couple of regular expressions for parsing the two kinds of lines,
+ ``RE_ATTS`` for parsing the attribute definitions, and ``RE_RELS`` for parsing
+ the relation definitions.
+
+2. define a function that iterates through the lines of the file and retrieves
+ (``yield`` s) a (subject, relation, object) tuple for each line.
+ We called it ``_retrieve_structure`` in the ``diseasome_parser`` module.
+ The function needs the file name and the types for which information
+ should be retrieved.
+
+Alternatively, instead of hand-making the parser, one could use the RDF parser provided
+in the ``dataio`` cube.
+
+.. XXX To further study and detail the ``dataio`` cube usage.
+
+Once we get to have the (subject, relation, object) triples, we need to map them into
+the data model.
+
+
+Mapping the data to the schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the case of diseasome data, we can just define two dictionaries for mapping
+the names of the relations as extracted by the parser, to the names of the relations
+as defined in the ``schema.py`` data model. In the ``diseasome_parser`` module
+they are called ``MAPPING_ATTS`` and ``MAPPING_RELS``.
+Given that the relation and attribute names are given in CamelCase in the original data,
+mappings are necessary if we follow the PEP08 when naming the attributes in the data model.
+For example, the RDF relation ``chromosomalLocation`` is mapped into the schema relation
+``chromosomal_location``.
+
+Once these mappings have been defined, we just iterate over the (subject, relation, object)
+tuples provided by the parser and we extract the entities, with their attributes and relations.
+For each entity, we thus have a dictionary with two keys, ``attributes`` and ``relations``.
+The value associated to the ``attributes`` key is a dictionary containing (attribute: value)
+pairs, where "value" is a string, plus the ``cwuri`` key / attribute holding the URI of
+the entity itself.
+The value associated to the ``relations`` key is a dictionary containing (relation: value)
+pairs, where "value" is an URI.
+This is implemented in the ``entities_from_rdf`` interface function of the module
+``diseasome_parser``. This function provides an iterator on the dictionaries containing
+the ``attributes`` and ``relations`` keys for all entities.
+
+However, this is a simple case. In real life, things can get much more complicated, and the
+mapping can be far from trivial, especially when several data sources (which can follow
+different formatting and even structuring conventions) must be mapped into the same data model.
+
+Importing the data
+~~~~~~~~~~~~~~~~~~
+
+The data import code should be placed in a Python module. Let us call it
+``diseasome_import.py``. Then, this module should be called via
+``cubicweb-ctl``, as follows::
+
+ cubicweb-ctl shell diseasome_import.py -- <other arguments e.g. data file>
+
+In the import module, we should use a *store* for doing the import.
+A store is an object which provides three kinds of methods for
+importing data:
+
+- a method for importing the entities, along with the values
+ of their attributes.
+- a method for importing the relations between the entities.
+- a method for committing the imports to the database.
+
+In CubicWeb, we have four stores:
+
+1. ``ObjectStore`` base class for the stores in CubicWeb.
+ It only provides a skeleton for all other stores and
+ provides the means for creating the memory structures
+ (dictionaries) that hold the entities and the relations
+ between them.
+
+2. ``RQLObjectStore``: store which uses the RQL language for performing
+ database insertions and updates. It relies on all the CubicWeb hooks
+ machinery, especially for dealing with security issues (database access
+ permissions).
+
+2. ``NoHookRQLObjectStore``: store which uses the RQL language for
+ performing database insertions and updates, but for which
+ all hooks are deactivated. This implies that
+ certain checks with respect to the CubicWeb / Yams schema
+ (data model) are not performed. However, all SQL queries
+ obtained from the RQL ones are executed in a sequential
+ manner, one query per inserted entity.
+
+4. ``SQLGenObjectStore``: store which uses the SQL language directly.
+ It inserts entities either sequentially, by executing an SQL query
+ for each entity, or directly by using one PostGRES ``COPY FROM``
+ query for a set of similarly structured entities.
+
+For really massive imports (millions or billions of entities), there
+is a cube ``dataio`` which contains another store, called
+``MassiveObjectStore``. This store is similar to ``SQLGenObjectStore``,
+except that anything related to CubicWeb is bypassed. That is, even the
+CubicWeb EID entity identifiers are not handled. This store is the fastest,
+but has a slightly different API from the other four stores mentioned above.
+Moreover, it has an important limitation, in that it doesn't insert inlined [#]_
+relations in the database.
+
+.. [#] An inlined relation is a relation defined in the schema
+ with the keyword argument ``inlined=True``. Such a relation
+ is inserted in the database as an attribute of the entity
+ whose subject it is.
+
+In the following section we will see how to import data by using the stores
+in CubicWeb's ``dataimport`` module.
+
+Using the stores in ``dataimport``
+++++++++++++++++++++++++++++++++++
+
+``ObjectStore`` is seldom used in real life for importing data, since it is
+only the base store for the other stores and it doesn't perform an actual
+import of the data. Nevertheless, the other three stores, which import data,
+are based on ``ObjectStore`` and provide the same API.
+
+All three stores ``RQLObjectStore``, ``NoHookRQLObjectStore`` and
+``SQLGenObjectStore`` provide exactly the same API for importing data, that is
+entities and relations, in an SQL database.
+
+Before using a store, one must import the ``dataimport`` module and then initialize
+the store, with the current ``session`` as a parameter::
+
+ import cubicweb.dataimport as cwdi
+ ...
+
+ store = cwdi.RQLObjectStore(session)
+
+Each such store provides three methods for data import:
+
+#. ``create_entity(Etype, **attributes)``, which allows us to add
+ an entity of the Yams type ``Etype`` to the database. This entity's attributes
+ are specified in the ``attributes`` dictionary. The method returns the entity
+ created in the database. For example, we add two entities,
+ a person, of ``Person`` type, and a location, of ``Location`` type::
+
+ person = store.create_entity('Person', name='Toto', age='18', height='190')
+
+ location = store.create_entity('Location', town='Paris', arrondissement='13')
+
+#. ``relate(subject_eid, r_type, object_eid)``, which allows us to add a relation
+ of the Yams type ``r_type`` to the database. The relation's subject is an entity
+ whose EID is ``subject_eid``; its object is another entity, whose EID is
+ ``object_eid``. For example [#]_::
+
+ store.relate(person.eid(), 'lives_in', location.eid(), **kwargs)
+
+ ``kwargs`` is only used by the ``SQLGenObjectStore``'s ``relate`` method and is here
+ to allow us to specify the type of the subject of the relation, when the relation is
+ defined as inlined in the schema.
+
+.. [#] The ``eid`` method of an entity defined via ``create_entity`` returns
+ the entity identifier as assigned by CubicWeb when creating the entity.
+ This only works for entities defined via the stores in the CubicWeb's
+ ``dataimport`` module.
+
+ The keyword argument that is understood by ``SQLGenObjectStore`` is called
+ ``subjtype`` and holds the type of the subject entity. For the example considered here,
+ this comes to having [#]_::
+
+ store.relate(person.eid(), 'lives_in', location.eid(), subjtype=person.cw_etype)
+
+ If ``subjtype`` is not specified, then the store tries to infer the type of the subject.
+ However, this doesn't always work, e.g. when there are several possible subject types
+ for a given relation type.
+
+.. [#] The ``cw_etype`` attribute of an entity defined via ``create_entity`` holds
+ the type of the entity just created. This only works for entities defined via
+ the stores in the CubicWeb's ``dataimport`` module. In the example considered
+ here, ``person.cw_etype`` holds ``'Person'``.
+
+ All the other stores but ``SQLGenObjectStore`` ignore the ``kwargs`` parameters.
+
+#. ``flush()``, which allows us to perform the actual commit into the database, along
+ with some cleanup operations. Ideally, this method should be called as often as
+ possible, that is after each insertion in the database, so that database sessions
+ are kept as atomic as possible. In practice, we usually call this method twice:
+ first, after all the entities have been created, second, after all relations have
+ been created.
+
+ Note however that before each commit the database insertions
+ have to be consistent with the schema. Thus, if, for instance,
+ an entity has an attribute defined through a relation (viz.
+ a ``SubjectRelation``) with a ``"1"`` or ``"+"`` object
+ cardinality, we have to create the entity under discussion,
+ the object entity of the relation under discussion, and the
+ relation itself, before committing the additions to the database.
+
+ The ``flush`` method is simply called as::
+
+ store.flush().
+
+
+Using the ``MassiveObjectStore`` in the ``dataio`` cube
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+This store, available in the ``dataio`` cube, allows us to
+fully dispense with the CubicWeb import mechanisms and hence
+to interact directly with the database server, via SQL queries.
+
+Moreover, these queries rely on PostGreSQL's ``COPY FROM`` instruction
+to create several entities in a single query. This brings tremendous
+performance improvements with respect to the RQL-based data insertion
+procedures.
+
+However, the API of this store is slightly different from the API of
+the stores in CubicWeb's ``dataimport`` module.
+
+Before using the store, one has to import the ``dataio`` cube's
+``dataimport`` module, then initialize the store by giving it the
+``session`` parameter::
+
+ from cubes.dataio import dataimport as mcwdi
+ ...
+
+ store = mcwdi.MassiveObjectStore(session)
+
+The ``MassiveObjectStore`` provides six methods for inserting data
+into the database:
+
+#. ``init_rtype_table(SubjEtype, r_type, ObjEtype)``, which specifies the
+ creation of the tables associated to the relation types in the database.
+ Each such table has three column, the type of the subject entity, the
+ type of the relation (that is, the name of the attribute in the subject
+ entity which is defined via the relation), and the type of the object
+ entity. For example::
+
+ store.init_rtype_table('Person', 'lives_in', 'Location')
+
+ Please note that these tables can be created before the entities, since
+ they only specify their types, not their unique identifiers.
+
+#. ``create_entity(Etype, **attributes)``, which allows us to add new entities,
+ whose attributes are given in the ``attributes`` dictionary.
+ Please note however that, by default, this method does *not* return
+ the created entity. The method is called, for example, as in::
+
+ store.create_entity('Person', name='Toto', age='18', height='190',
+ uri='http://link/to/person/toto_18_190')
+ store.create_entity('Location', town='Paris', arrondissement='13',
+ uri='http://link/to/location/paris_13')
+
+ In order to be able to link these entities via the relations when needed,
+ we must provide ourselves a means for uniquely identifying the entities.
+ In general, this is done via URIs, stored in attributes like ``uri`` or
+ ``cwuri``. The name of the attribute is irrelevant as long as its value is
+ unique for each entity.
+
+#. ``relate_by_iid(subject_iid, r_type, object_iid)`` allows us to actually
+ relate the entities uniquely identified by ``subject_iid`` and
+ ``object_iid`` via a relation of type ``r_type``. For example::
+
+ store.relate_by_iid('http://link/to/person/toto_18_190',
+ 'lives_in',
+ 'http://link/to/location/paris_13')
+
+ Please note that this method does *not* work for inlined relations!
+
+#. ``convert_relations(SubjEtype, r_type, ObjEtype, subj_iid_attribute,
+ obj_iid_attribute)``
+ allows us to actually insert
+ the relations in the database. At one call of this method, one inserts
+ all the relations of type ``rtype`` between entities of given types.
+ ``subj_iid_attribute`` and ``object_iid_attribute`` are the names
+ of the attributes which store the unique identifiers of the entities,
+ as assigned by the user. These names can be identical, as long as
+ their values are unique. For example, for inserting all relations
+ of type ``lives_in`` between ``People`` and ``Location`` entities,
+ we write::
+
+ store.convert_relations('Person', 'lives_in', 'Location', 'uri', 'uri')
+
+#. ``flush()`` performs the actual commit in the database. It only needs
+ to be called after ``create_entity`` and ``relate_by_iid`` calls.
+ Please note that ``relate_by_iid`` does *not* perform insertions into
+ the database, hence calling ``flush()`` for it would have no effect.
+
+#. ``cleanup()`` performs database cleanups, by removing temporary tables.
+ It should only be called at the end of the import.
+
+
+
+.. XXX to add smth on the store's parameter initialization.
+
+
+
+Application to the Diseasome data
++++++++++++++++++++++++++++++++++
+
+Import setup
+############
+
+We define an import function, ``diseasome_import``, which does basically four things:
+
+#. creates and initializes the store to be used, via a line such as::
+
+ store = cwdi.SQLGenObjectStore(session)
+
+ where ``cwdi`` is the imported ``cubicweb.dataimport`` or
+ ``cubes.dataio.dataimport``.
+
+#. calls the diseasome parser, that is, the ``entities_from_rdf`` function in the
+ ``diseasome_parser`` module and iterates on its result, in a line such as::
+
+ for entity, relations in parser.entities_from_rdf(filename, ('gene', 'disease')):
+
+ where ``parser`` is the imported ``diseasome_parser`` module, and ``filename`` is the
+ name of the file containing the data (with its path), e.g. ``../data/diseasome_dump.nt``.
+
+#. creates the entities to be inserted in the database; for Diseasome, there are two
+ kinds of entities:
+
+ #. entities defined in the data model, viz. ``Gene`` and ``Disease`` in our case.
+ #. entities which are built in CubicWeb / Yams, viz. ``ExternalUri`` which define
+ URIs.
+
+ As we are working with RDF data, each entity is defined through a series of URIs. Hence,
+ each "relational attribute" [#]_ of an entity is defined via an URI, that is, in CubicWeb
+ terms, via an ``ExternalUri`` entity. The entities are created, in the loop presented above,
+ as such::
+
+ ent = store.create_entity(etype, **entity)
+
+ where ``etype`` is the appropriate entity type, either ``Gene`` or ``Disease``.
+
+.. [#] By "relational attribute" we denote an attribute (of an entity) which
+ is defined through a relation, e.g. the ``chromosomal_location`` attribute
+ of ``Disease`` entities, which is defined through a relation between a
+ ``Disease`` and an ``ExternalUri``.
+
+ The ``ExternalUri`` entities are as many as URIs in the data file. For them, we define a unique
+ attribute, ``uri``, which holds the URI under discussion::
+
+ extu = store.create_entity('ExternalUri', uri="http://path/of/the/uri")
+
+#. creates the relations between the entities. We have relations between:
+
+ #. entities defined in the schema, e.g. between ``Disease`` and ``Gene``
+ entities, such as the ``associated_genes`` relation defined for
+ ``Disease`` entities.
+ #. entities defined in the schema and ``ExternalUri`` entities, such as ``gene_id``.
+
+ The way relations are added to the database depends on the store:
+
+ - for the stores in the CubicWeb ``dataimport`` module, we only use
+ ``store.relate``, in
+ another loop, on the relations (that is, a
+ loop inside the preceding one, mentioned at step 2)::
+
+ for rtype, rels in relations.iteritems():
+ ...
+
+ store.relate(ent.eid(), rtype, extu.eid(), **kwargs)
+
+ where ``kwargs`` is a dictionary designed to accommodate the need for specifying
+ the type of the subject entity of the relation, when the relation is inlined and
+ ``SQLGenObjectStore`` is used. For example::
+
+ ...
+ store.relate(ent.eid(), 'chromosomal_location', extu.eid(), subjtype='Disease')
+
+ - for the ``MassiveObjectStore`` in the ``dataio`` cube's ``dataimport`` module,
+ the relations are created in three steps:
+
+ #. first, a table is created for each relation type, as in::
+
+ ...
+ store.init_rtype_table(ent.cw_etype, rtype, extu.cw_etype)
+
+ which comes down to lines such as::
+
+ store.init_rtype_table('Disease', 'associated_genes', 'Gene')
+ store.init_rtype_table('Gene', 'gene_id', 'ExternalUri')
+
+ #. second, the URI of each entity will be used as its identifier, in the
+ ``relate_by_iid`` method, such as::
+
+ disease_uri = 'http://www4.wiwiss.fu-berlin.de/diseasome/resource/diseases/3'
+ gene_uri = '<http://www4.wiwiss.fu-berlin.de/diseasome/resource/genes/HSD3B2'
+ store.relate_by_iid(disease_uri, 'associated_genes', gene_uri)
+
+ #. third, the relations for each relation type will be added to the database,
+ via the ``convert_relations`` method, such as in::
+
+ store.convert_relations('Disease', 'associated_genes', 'Gene', 'cwuri', 'cwuri')
+
+ and::
+
+ store.convert_relations('Gene', 'hgnc_id', 'ExternalUri', 'cwuri', 'uri')
+
+ where ``cwuri`` and ``uri`` are the attributes which store the URIs of the entities
+ defined in the data model, and of the ``ExternalUri`` entities, respectively.
+
+#. flushes all relations and entities::
+
+ store.flush()
+
+ which performs the actual commit of the inserted entities and relations in the database.
+
+If the ``MassiveObjectStore`` is used, then a cleanup of temporary SQL tables should be performed
+at the end of the import::
+
+ store.cleanup()
+
+Timing benchmarks
+#################
+
+In order to time the import script, we just decorate the import function with the ``timed``
+decorator::
+
+ from logilab.common.decorators import timed
+ ...
+
+ @timed
+ def diseasome_import(session, filename):
+ ...
+
+After running the import function as shown in the "Importing the data" section, we obtain two time measurements::
+
+ diseasome_import clock: ... / time: ...
+
+Here, the meanings of these measurements are [#]_:
+
+- ``clock`` is the time spent by CubicWeb, on the server side (i.e. hooks and data pre- / post-processing on SQL
+ queries),
+
+- ``time`` is the sum between ``clock`` and the time spent in PostGreSQL.
+
+.. [#] The meanings of the ``clock`` and ``time`` measurements, when using the ``@timed``
+ decorators, were taken from `a blog post on massive data import in CubicWeb`_.
+
+.. _a blog post on massive data import in CubicWeb: http://www.cubicweb.org/blogentry/2116712
+
+The import function is put in an import module, named ``diseasome_import`` here. The module is called
+directly from the CubicWeb shell, as follows::
+
+ cubicweb-ctl shell diseasome_instance diseasome_import.py \
+ -- -df diseasome_import_file.nt -st StoreName
+
+The module accepts two arguments:
+
+- the data file, introduced by ``-df [--datafile]``, and
+- the store, introduced by ``-st [--store]``.
+
+The timings (in seconds) for different stores are given in the following table, for
+importing 4213 ``Disease`` entities and 3919 ``Gene`` entities with the import module
+just described:
+
++--------------------------+------------------------+--------------------------------+------------+
+| Store | CubicWeb time (clock) | PostGreSQL time (time - clock) | Total time |
++==========================+========================+================================+============+
+| ``RQLObjectStore`` | 225.98 | 62.05 | 288.03 |
++--------------------------+------------------------+--------------------------------+------------+
+| ``NoHookRQLObjectStore`` | 62.73 | 51.38 | 114.11 |
++--------------------------+------------------------+--------------------------------+------------+
+| ``SQLGenObjectStore`` | 20.41 | 11.03 | 31.44 |
++--------------------------+------------------------+--------------------------------+------------+
+| ``MassiveObjectStore`` | 4.84 | 6.93 | 11.77 |
++--------------------------+------------------------+--------------------------------+------------+
+
+
+Conclusions
+~~~~~~~~~~~
+
+In this tutorial we have seen how to import data in a CubicWeb application instance. We have first seen how to
+create a schema, then how to create a parser of the data and a mapping of the data to the schema.
+Finally, we have seen four ways of importing data into CubicWeb.
+
+Three of those are integrated into CubicWeb, namely the ``RQLObjectStore``, ``NoHookRQLObjectStore`` and
+``SQLGenObjectStore`` stores, which have a common API:
+
+- ``RQLObjectStore`` is by far the slowest, especially its time spent on the
+ CubicWeb side, and so it should be used only for small amounts of
+ "sensitive" data (i.e. where security is a concern).
+
+- ``NoHookRQLObjectStore`` slashes by almost four the time spent on the CubicWeb side,
+ but is also quite slow; on the PostGres side it is as slow as the previous store.
+ It should be used for data where security is not a concern,
+ but consistency (with the data model) is.
+
+- ``SQLGenObjectStore`` slashes by three the time spent on the CubicWeb side and by five the time
+ spent on the PostGreSQL side. It should be used for relatively great amounts of data, where
+ security and data consistency are not a concern. Compared to the previous store, it has the
+ disadvantage that, for inlined relations, we must specify their subjects' types.
+
+For really huge amounts of data there is a fourth store, ``MassiveObjectStore``, available
+from the ``dataio`` cube. It provides a blazing performance with respect to all other stores:
+it is almost 25 times faster than ``RQLObjectStore`` and almost three times faster than
+``SQLGenObjectStore``. However, it has a few usage caveats that should be taken into account:
+
+#. it cannot insert relations defined as inlined in the schema,
+#. no security or consistency check is performed on the data,
+#. its API is slightly different from the other stores.
+
+Hence, this store should be used when security and data consistency are not a concern,
+and there are no inlined relations in the schema.
+
+
+
+
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,18 @@
+Tutorials
+~~~~~~~~~
+
+We present a few tutorials of different levels. The blog building
+tutorial introduces one smoothly to the basic concepts.
+
+Then there is a photo gallery construction tutorial which highlights
+more advanced concepts such as unit tests, security settings,
+migration scripts.
+
+.. toctree::
+ :maxdepth: 1
+
+ base/index
+ advanced/index
+ tools/windmill.rst
+ textreports/index
+ dataimport/index
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/textreports/index.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,13 @@
+.. -*- coding: utf-8 -*-
+
+Writing text reports with RestructuredText
+==========================================
+
+|cubicweb| offers several text formats for the RichString type used in schemas,
+including restructuredtext.
+
+Three additional restructuredtext roles are defined by |cubicweb|:
+
+.. autofunction:: cubicweb.ext.rest.eid_reference_role
+.. autofunction:: cubicweb.ext.rest.rql_role
+.. autofunction:: cubicweb.ext.rest.bookmark_role
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorials/tools/windmill.rst Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,229 @@
+==========================
+Use Windmill with CubicWeb
+==========================
+
+Windmill_ implements cross browser testing, in-browser recording and playback,
+and functionality for fast accurate debugging and test environment integration.
+
+.. _Windmill: http://www.getwindmill.com/
+
+`Online features list <http://www.getwindmill.com/features>`_ is available.
+
+
+Installation
+============
+
+Windmill
+--------
+
+You have to install Windmill manually for now. If you're using Debian, there is
+no binary package (`yet <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=579109>`_).
+
+The simplest solution is to use a *setuptools/pip* command (for a clean
+environment, take a look to the `virtualenv
+<http://pypi.python.org/pypi/virtualenv>`_ project as well)::
+
+ $ pip install windmill
+ $ curl -O http://github.com/windmill/windmill/tarball/master
+
+However, the Windmill project doesn't release frequently. Our recommandation is
+to used the last snapshot of the Git repository::
+
+ $ git clone git://github.com/windmill/windmill.git HEAD
+ $ cd windmill
+ $ python setup.py develop
+
+Install instructions are `available <http://wiki.github.com/windmill/windmill/installing>`_.
+
+Be sure to have the windmill module in your PYTHONPATH afterwards::
+
+ $ python -c "import windmill"
+
+X dummy
+-------
+
+In order to reduce unecessary system load from your test machines, It's
+recommended to use X dummy server for testing the Unix web clients, you need a
+dummy video X driver (as xserver-xorg-video-dummy package in Debian) coupled
+with a light X server as `Xvfb <http://en.wikipedia.org/wiki/Xvfb>`_.
+
+ The dummy driver is a special driver available with the XFree86 DDX. To use
+ the dummy driver, simply substitue it for your normal card driver in the
+ Device section of your xorg.conf configuration file. For example, if you
+ normally uses an ati driver, then you will have a Device section with
+ Driver "ati" to let the X server know that you want it to load and use the
+ ati driver; however, for these conformance tests, you would change that
+ line to Driver "dummy" and remove any other ati specific options from the
+ Device section.
+
+ *From: http://www.x.org/wiki/XorgTesting*
+
+Then, you can run the X server with the following command ::
+
+ $ /usr/bin/X11/Xvfb :1 -ac -screen 0 1280x1024x8 -fbdir /tmp
+
+
+Windmill usage
+==============
+
+Record your use case
+--------------------
+
+- start your instance manually
+- start Windmill_ with url site as last argument (read Usage_ or use *'-h'*
+ option to find required command line arguments)
+- use the record button
+- click on save to obtain python code of your use case
+- copy the content to a new file in a *windmill* directory
+
+.. _Usage: http://wiki.github.com/windmill/windmill/running-tests
+
+If you are using firefox as client, consider the "firebug" option.
+
+If you have a running instance, you can refine the test by the *loadtest* windmill option::
+
+ $ windmill -m firebug loadtest=<test_file.py> <instance url>
+
+Or use the internal windmill shell to explore available commands::
+
+ $ windmill -m firebug shell <instance url>
+
+And enter python commands:
+
+.. sourcecode:: python
+
+ >>> load_test(<your test file>)
+ >>> run_test(<your test file>)
+
+
+
+Integrate Windmill tests into CubicWeb
+======================================
+
+Set environment
+---------------
+
+You have to create a new unit test file and a `windmill` directory and copy all
+your windmill use case into it.
+
+.. sourcecode:: python
+
+ # test_windmill.py
+
+ # Run all scenarii found in windmill directory
+ from cubicweb.devtools.cwwindmill import (CubicWebWindmillUseCase,
+ unittest_main)
+
+ if __name__ == '__main__':
+ unittest_main()
+
+Run your tests
+--------------
+
+You can easily run your windmill test suite through `pytest` or :mod:`unittest`.
+You have to copy a *test_windmill.py* file from :mod:`web.test`.
+
+To run your test series::
+
+ $ pytest test/test_windmill.py
+
+By default, CubicWeb will use **firefox** as the default browser and will try
+to run test instance server on localhost. In the general case, You've no need
+to change anything.
+
+Check :class:`cubicweb.devtools.cwwindmill.CubicWebWindmillUseCase` for
+Windmill configuration. You can edit windmill settings with following class attributes:
+
+* browser
+ identification string (firefox|ie|safari|chrome) (firefox by default)
+* test_dir
+ testing file path or directory (windmill directory under your unit case
+ file by default)
+* edit_test
+ load and edit test for debugging (False by default)
+
+Examples:
+
+.. sourcecode:: python
+
+ browser = 'firefox'
+ test_dir = osp.join(__file__, 'windmill')
+ edit_test = False
+
+If you want to change cubicweb test server parameters, you can check class
+variables from :class:`CubicWebServerConfig` or inherit it with overriding the
+:attr:`configcls` attribute in :class:`CubicWebServerTC` ::
+
+.. sourcecode:: python
+
+ class OtherCubicWebServerConfig(CubicWebServerConfig):
+ port = 9999
+
+ class NewCubicWebServerTC(CubicWebServerTC):
+ configcls = OtherCubicWebServerConfig
+
+For instance, CubicWeb framework windmill tests can be manually run by::
+
+ $ pytest web/test/test_windmill.py
+
+Edit your tests
+---------------
+
+You can toggle the `edit_test` variable to enable test edition.
+
+But if you are using `pytest` as test runner, use the `-i` option directly.
+The test series will be loaded and you can run assertions step-by-step::
+
+ $ pytest -i test/test_windmill.py
+
+In this case, the `firebug` extension will be loaded automatically for you.
+
+Afterwards, don't forget to save your edited test into the right file (no autosave feature).
+
+Best practises
+--------------
+
+Don't run another instance on the same port. You risk to silence some
+regressions (test runner will automatically fail in further versions).
+
+Start your use case by using an assert on the expected primary url page.
+Otherwise all your tests could fail without clear explanation of the used
+navigation.
+
+In the same location of the *test_windmill.py*, create a *windmill/* with your
+windmill recorded use cases.
+
+
+Caveats
+=======
+
+File Upload
+-----------
+
+Windmill can't do file uploads. This is a limitation of browser Javascript
+support / sandboxing, not of Windmill per se. It would be nice if there were
+some command that would prime the Windmill HTTP proxy to add a particular file
+to the next HTTP request that comes through, so that uploads could at least be
+faked.
+
+.. http://groups.google.com/group/windmill-dev/browse_thread/thread/cf9dc969722bd6bb/01aa18fdd652f7ff?lnk=gst&q=input+type+file#01aa18fdd652f7ff
+
+.. http://davisagli.com/blog/in-browser-integration-testing-with-windmill
+
+.. http://groups.google.com/group/windmill-dev/browse_thread/thread/b7bebcc38ed30dc7
+
+
+Preferences
+===========
+
+A *.windmill/prefs.py* could be used to redefine default configuration values.
+
+.. define CubicWeb preferences in the parent test case instead with a dedicated firefox profile
+
+For managing browser extensions, read `advanced topic chapter
+<http://wiki.github.com/windmill/windmill/advanced-topics>`_.
+
+More configuration examples could be seen in *windmill/conf/global_settings.py*
+as template.
+
+
--- a/entities/__init__.py Tue Jul 19 15:59:02 2016 +0200
+++ b/entities/__init__.py Tue Jul 19 16:13:12 2016 +0200
@@ -53,7 +53,7 @@
"""
restrictions = ['X is %s' % cls.__regid__]
selected = ['X']
- for attrschema in cls.e_schema.indexable_attributes():
+ for attrschema in sorted(cls.e_schema.indexable_attributes()):
varname = attrschema.type.upper()
restrictions.append('X %s %s' % (attrschema, varname))
selected.append(varname)
--- a/entities/adapters.py Tue Jul 19 15:59:02 2016 +0200
+++ b/entities/adapters.py Tue Jul 19 16:13:12 2016 +0200
@@ -24,11 +24,13 @@
from itertools import chain
from warnings import warn
+from hashlib import md5
from logilab.mtconverter import TransformError
from logilab.common.decorators import cached
-from cubicweb import ValidationError, view
+from cubicweb import ValidationError, view, ViolatedConstraint
+from cubicweb.schema import CONSTRAINTS
from cubicweb.predicates import is_instance, relation_possible, match_exception
@@ -79,6 +81,8 @@
itree = self.entity.cw_adapt_to('ITree')
if itree is not None:
return itree.path()[:-1]
+ if view.msgid_timestamp:
+ return (self.entity.eid,)
return ()
@@ -139,7 +143,7 @@
continue
weight = self.attr_weight.get(rschema, 'C')
try:
- value = entity.printable_value(rschema, format='text/plain')
+ value = entity.printable_value(rschema, format=u'text/plain')
except TransformError:
continue
except Exception:
@@ -370,3 +374,25 @@
i18nvalues.append(rtype + '-rtype')
errors[''] = _('some relations violate a unicity constraint')
raise ValidationError(self.entity.eid, errors, msgargs=msgargs, i18nvalues=i18nvalues)
+
+
+class IUserFriendlyCheckConstraint(IUserFriendlyError):
+ __select__ = match_exception(ViolatedConstraint)
+
+ def raise_user_exception(self):
+ _ = self._cw._
+ cstrname = self.exc.cstrname
+ eschema = self.entity.e_schema
+ for rschema, attrschema in eschema.attribute_definitions():
+ rdef = rschema.rdef(eschema, attrschema)
+ for constraint in rdef.constraints:
+ if cstrname == 'cstr' + md5(eschema.type + rschema.type + constraint.type() + (constraint.serialize() or '')).hexdigest():
+ break
+ else:
+ continue
+ break
+ else:
+ assert 0
+ key = rschema.type + '-subject'
+ msg, args = constraint.failed_message(key, self.entity.cw_edited[rschema.type])
+ raise ValidationError(self.entity.eid, {key: msg}, args)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+docutils
--- a/entities/test/unittest_base.py Tue Jul 19 15:59:02 2016 +0200
+++ b/entities/test/unittest_base.py Tue Jul 19 16:13:12 2016 +0200
@@ -66,8 +66,8 @@
def test_fti_rql_method(self):
with self.admin_access.web_request() as req:
eclass = self.vreg['etypes'].etype_class('EmailAddress')
- self.assertEqual(['Any X, ALIAS, ADDRESS WHERE X is EmailAddress, '
- 'X alias ALIAS, X address ADDRESS'],
+ self.assertEqual(['Any X, ADDRESS, ALIAS WHERE X is EmailAddress, '
+ 'X address ADDRESS, X alias ALIAS'],
eclass.cw_fti_index_rql_queries(req))
--- a/entities/test/unittest_wfobjs.py Tue Jul 19 15:59:02 2016 +0200
+++ b/entities/test/unittest_wfobjs.py Tue Jul 19 16:13:12 2016 +0200
@@ -87,12 +87,12 @@
shell.rollback()
# no pb if not in the same workflow
wf2 = add_wf(shell, 'Company')
- foo = wf.add_state(u'foo', initial=True)
- bar = wf.add_state(u'bar')
- wf.add_transition(u'baz', (foo,), bar, ('managers',))
+ foo = wf2.add_state(u'foo', initial=True)
+ bar = wf2.add_state(u'bar')
+ wf2.add_transition(u'baz', (foo,), bar, ('managers',))
shell.commit()
# gnark gnark
- biz = wf.add_transition(u'biz', (bar,), foo)
+ biz = wf2.add_transition(u'biz', (bar,), foo)
shell.commit()
with self.assertRaises(ValidationError) as cm:
biz.cw_set(name=u'baz')
@@ -416,6 +416,32 @@
group.cw_clear_all_caches()
self.assertEqual(iworkflowable.state, nextstate)
+ def test_replace_state(self):
+ with self.admin_access.shell() as shell:
+ wf = add_wf(shell, 'CWGroup', name='groupwf', default=True)
+ s_new = wf.add_state('new', initial=True)
+ s_state1 = wf.add_state('state1')
+ wf.add_transition('tr', (s_new,), s_state1)
+ shell.commit()
+
+ with self.admin_access.repo_cnx() as cnx:
+ group = cnx.create_entity('CWGroup', name=u'grp1')
+ cnx.commit()
+
+ iwf = group.cw_adapt_to('IWorkflowable')
+ iwf.fire_transition('tr')
+ cnx.commit()
+ group.cw_clear_all_caches()
+
+ wf = cnx.entity_from_eid(wf.eid)
+ wf.add_state('state2')
+ with cnx.security_enabled(write=False):
+ wf.replace_state('state1', 'state2')
+ cnx.commit()
+
+ self.assertEqual(iwf.state, 'state2')
+ self.assertEqual(iwf.latest_trinfo().to_state[0].name, 'state2')
+
class CustomWorkflowTC(CubicWebTC):
@@ -569,8 +595,9 @@
with self.admin_access.web_request() as req:
user = self.create_user(req, 'member', surname=u'toto')
req.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
- {'wf': wf.eid, 'x': user.eid})
+ {'wf': wf.eid, 'x': user.eid})
req.cnx.commit()
+ user.cw_clear_all_caches()
iworkflowable = user.cw_adapt_to('IWorkflowable')
self.assertEqual(iworkflowable.state, 'dead')
--- a/entities/wfobjs.py Tue Jul 19 15:59:02 2016 +0200
+++ b/entities/wfobjs.py Tue Jul 19 16:13:12 2016 +0200
@@ -174,12 +174,14 @@
todelstate = self.state_by_name(todelstate)
if not hasattr(replacement, 'eid'):
replacement = self.state_by_name(replacement)
+ args = {'os': todelstate.eid, 'ns': replacement.eid}
execute = self._cw.execute
- execute('SET X in_state S WHERE S eid %(s)s', {'s': todelstate.eid})
- execute('SET X from_state NS WHERE X to_state OS, OS eid %(os)s, NS eid %(ns)s',
- {'os': todelstate.eid, 'ns': replacement.eid})
- execute('SET X to_state NS WHERE X to_state OS, OS eid %(os)s, NS eid %(ns)s',
- {'os': todelstate.eid, 'ns': replacement.eid})
+ execute('SET X in_state NS WHERE X in_state OS, '
+ 'NS eid %(ns)s, OS eid %(os)s', args)
+ execute('SET X from_state NS WHERE X from_state OS, '
+ 'OS eid %(os)s, NS eid %(ns)s', args)
+ execute('SET X to_state NS WHERE X to_state OS, '
+ 'OS eid %(os)s, NS eid %(ns)s', args)
todelstate.cw_delete()
@@ -412,7 +414,7 @@
"""return the default workflow for entities of this type"""
# XXX CWEType method
wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
- 'ET name %(et)s', {'et': self.entity.cw_etype})
+ 'ET name %(et)s', {'et': unicode(self.entity.cw_etype)})
if wfrset:
return wfrset.get_entity(0, 0)
self.warning("can't find any workflow for %s", self.entity.cw_etype)
--- a/entity.py Tue Jul 19 15:59:02 2016 +0200
+++ b/entity.py Tue Jul 19 16:13:12 2016 +0200
@@ -336,7 +336,7 @@
else:
visited.add(eschema.type)
_fetchattrs = []
- for attr in fetchattrs:
+ for attr in sorted(fetchattrs):
try:
rschema = eschema.subjrels[attr]
except KeyError:
@@ -425,7 +425,7 @@
else:
for rschema in cls.e_schema.subject_relations():
if (rschema.final
- and rschema != 'eid'
+ and rschema not in ('eid', 'cwuri')
and cls.e_schema.has_unique_values(rschema)
and cls.e_schema.rdef(rschema.type).cardinality[0] == '1'):
mainattr = str(rschema)
@@ -514,7 +514,7 @@
prefixing the relation name by 'reverse_'. Also, relation values may be
an entity or eid, a list of entities or eids.
"""
- rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
+ rql, qargs, pendingrels, _attrcache = cls._cw_build_entity_query(kwargs)
if rql:
rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
else:
@@ -524,7 +524,6 @@
except IndexError:
raise Exception('could not create a %r with %r (%r)' %
(cls.__regid__, rql, qargs))
- created._cw_update_attr_cache(attrcache)
cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
return created
@@ -555,43 +554,6 @@
return self.eid
return super(Entity, self).__hash__()
- def _cw_update_attr_cache(self, attrcache):
- # if context is a repository session, don't consider dont-cache-attrs as
- # the instance already holds modified values and loosing them could
- # introduce severe problems
- trdata = self._cw.transaction_data
- uncached_attrs = trdata.get('%s.storage-special-process-attrs' % self.eid, set())
- if self._cw.is_request:
- uncached_attrs.update(trdata.get('%s.dont-cache-attrs' % self.eid, set()))
- for attr in uncached_attrs:
- attrcache.pop(attr, None)
- self.cw_attr_cache.pop(attr, None)
- self.cw_attr_cache.update(attrcache)
-
- def _cw_dont_cache_attribute(self, attr, repo_side=False):
- """Repository side method called when some attribute has been
- transformed by a hook, hence original value should not be cached by
- the client.
-
- If repo_side is True, this means that the attribute has been
- transformed by a *storage*, hence the original value should
- not be cached **by anyone**.
-
- This only applies to a storage special case where the value
- specified in creation or update is **not** the value that will
- be transparently exposed later.
-
- For example we have a special "fs_importing" mode in BFSS
- where a file path is given as attribute value and stored as is
- in the data base. Later access to the attribute will provide
- the content of the file at the specified path. We do not want
- the "filepath" value to be cached.
- """
- self._cw.transaction_data.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
- if repo_side:
- trdata = self._cw.transaction_data
- trdata.setdefault('%s.storage-special-process-attrs' % self.eid, set()).add(attr)
-
def __json_encode__(self):
"""custom json dumps hook to dump the entity's eid
which is not part of dict structure itself
@@ -836,7 +798,6 @@
# data fetching methods ###################################################
- @cached
def as_rset(self): # XXX .cw_as_rset
"""returns a resultset containing `self` information"""
rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
@@ -865,7 +826,7 @@
attr = rschema.type
if attr == 'eid':
continue
- # password retreival is blocked at the repository server level
+ # password retrieval is blocked at the repository server level
rdef = rschema.rdef(self.e_schema, attrschema)
if not self._cw.user.matching_groups(rdef.get_groups('read')) \
or (attrschema.type == 'Password' and skip_pwd):
@@ -1329,10 +1290,6 @@
else:
rql += ' WHERE X eid %(x)s'
self._cw.execute(rql, qargs)
- # update current local object _after_ the rql query to avoid
- # interferences between the query execution itself and the cw_edited /
- # skip_security machinery
- self._cw_update_attr_cache(attrcache)
self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute)
# XXX update relation cache
--- a/etwist/server.py Tue Jul 19 15:59:02 2016 +0200
+++ b/etwist/server.py Tue Jul 19 16:13:12 2016 +0200
@@ -24,6 +24,7 @@
import threading
from urlparse import urlsplit, urlunsplit
from cgi import FieldStorage, parse_header
+from cubicweb.statsd_logger import statsd_timeit
from twisted.internet import reactor, task, threads
from twisted.web import http, server
@@ -65,14 +66,6 @@
# when we have an in-memory repository, clean unused sessions every XX
# seconds and properly shutdown the server
if config['repository-uri'] == 'inmemory://':
- if config.pyro_enabled():
- # if pyro is enabled, we have to register to the pyro name
- # server, create a pyro daemon, and create a task to handle pyro
- # requests
- self.appli.repo.warning('remote repository access through pyro is deprecated')
- self.pyro_daemon = self.appli.repo.pyro_register()
- self.pyro_listen_timeout = 0.02
- self.appli.repo.looping_task(1, self.pyro_loop_event)
if config.mode != 'test':
reactor.addSystemEventTrigger('before', 'shutdown',
self.shutdown_event)
@@ -93,13 +86,6 @@
"""
self.appli.repo.shutdown()
- def pyro_loop_event(self):
- """listen for pyro events"""
- try:
- self.pyro_daemon.handleRequests(self.pyro_listen_timeout)
- except select.error:
- return
-
def getChild(self, path, request):
"""Indicate which resource to use to process down the URL's path"""
return self
@@ -118,6 +104,7 @@
deferred = threads.deferToThread(self.render_request, request)
return NOT_DONE_YET
+ @statsd_timeit
def render_request(self, request):
try:
# processing HUGE files (hundred of megabytes) in http.processReceived
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/etwist/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+Twisted
--- a/etwist/twconfig.py Tue Jul 19 15:59:02 2016 +0200
+++ b/etwist/twconfig.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -17,9 +17,6 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""twisted server configurations:
-* the "twisted" configuration to get a web instance running in a standalone
- twisted web server which talk to a repository server using Pyro
-
* the "all-in-one" configuration to get a web instance running in a twisted
web server integrating a repository server in the same process (only available
if the repository part of the software is installed
@@ -82,13 +79,6 @@
the repository rather than the user running the command',
'group': 'main', 'level': WebConfiguration.mode == 'system'
}),
- ('pyro-server',
- {'type' : 'yn',
- # pyro is only a recommends by default, so don't activate it here
- 'default': False,
- 'help': 'run a pyro server',
- 'group': 'main', 'level': 1,
- }),
('webserver-threadpool-size',
{'type': 'int',
'default': 4,
@@ -117,9 +107,6 @@
cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path
- def pyro_enabled(self):
- """tell if pyro is activated for the in memory repository"""
- return self['pyro-server']
CONFIGURATIONS.append(AllInOneConfiguration)
--- a/etwist/twctl.py Tue Jul 19 15:59:02 2016 +0200
+++ b/etwist/twctl.py Tue Jul 19 16:13:12 2016 +0200
@@ -71,11 +71,14 @@
cfgname = 'all-in-one'
subcommand = 'cubicweb-twisted'
- class AllInOneStopHandler(serverctl.RepositoryStopHandler):
+ class AllInOneStopHandler(CommandHandler):
cmdname = 'stop'
cfgname = 'all-in-one'
subcommand = 'cubicweb-twisted'
+ def poststop(self):
+ pass
+
class AllInOneUpgradeHandler(TWUpgradeHandler):
cfgname = 'all-in-one'
--- a/ext/rest.py Tue Jul 19 15:59:02 2016 +0200
+++ b/ext/rest.py Tue Jul 19 16:13:12 2016 +0200
@@ -35,7 +35,6 @@
__docformat__ = "restructuredtext en"
import sys
-from cStringIO import StringIO
from itertools import chain
from logging import getLogger
from os.path import join
@@ -411,7 +410,7 @@
# remove unprintable characters unauthorized in xml
data = data.translate(ESC_CAR_TABLE)
settings = {'input_encoding': encoding, 'output_encoding': 'unicode',
- 'warning_stream': StringIO(),
+ 'warning_stream': False,
'traceback': True, # don't sys.exit
'stylesheet': None, # don't try to embed stylesheet (may cause
# obscure bug due to docutils computing
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+docutils
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/logstats.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,59 @@
+# copyright 2014 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/>.
+
+"""looping task for dumping instance's stats in a file
+"""
+
+__docformat__ = "restructuredtext en"
+
+from datetime import datetime
+import json
+
+from cubicweb.server import hook
+
+class LogStatsStartHook(hook.Hook):
+ """register task to regularly dump instance's stats in a file
+
+ data are stored as one json entry per row
+ """
+ __regid__ = 'cubicweb.hook.logstats.start'
+ events = ('server_startup',)
+
+ def __call__(self):
+ interval = self.repo.config.get('logstat-interval', 0)
+ if interval <= 0:
+ return
+
+ def dump_stats(repo):
+ statsfile = repo.config.get('logstat-file')
+ with repo.internal_cnx() as cnx:
+ stats = cnx.call_service('repo_stats')
+ gcstats = cnx.call_service('repo_gc_stats', nmax=5)
+
+ allstats = {'resources': stats,
+ 'memory': gcstats,
+ 'timestamp': datetime.utcnow().isoformat(),
+ }
+ try:
+ with open(statsfile, 'ab') as ofile:
+ json.dump(allstats, ofile)
+ ofile.write('\n')
+ except IOError:
+ repo.warning('Cannot open stats file for writing: %s', statsfile)
+
+ self.repo.looping_task(interval, dump_stats, self.repo)
--- a/hooks/metadata.py Tue Jul 19 15:59:02 2016 +0200
+++ b/hooks/metadata.py Tue Jul 19 16:13:12 2016 +0200
@@ -20,6 +20,7 @@
__docformat__ = "restructuredtext en"
from datetime import datetime
+from base64 import b64encode
from cubicweb.predicates import is_instance
from cubicweb.server import hook
@@ -199,17 +200,17 @@
oldsource = self._cw.entity_from_eid(schange[self.eidfrom])
entity = self._cw.entity_from_eid(self.eidfrom)
# we don't want the moved entity to be reimported later. To
- # distinguish this state, the trick is to change the associated
- # record in the 'entities' system table with eid=-eid while leaving
- # other fields unchanged, and to add a new record with eid=eid,
- # source='system'. External source will then have consider case
- # where `extid2eid` return a negative eid as 'this entity was known
- # but has been moved, ignore it'.
- self._cw.system_sql('UPDATE entities SET eid=-eid WHERE eid=%(eid)s',
- {'eid': self.eidfrom})
+ # distinguish this state, move the record from the 'entities' table
+ # to 'moved_entities'. External source will then have consider
+ # case where `extid2eid` returns a negative eid as 'this entity was
+ # known but has been moved, ignore it'.
+ extid = self._cw.entity_metas(entity.eid)['extid']
+ assert extid is not None
+ attrs = {'eid': entity.eid, 'extid': b64encode(extid).decode('ascii')}
+ self._cw.system_sql(syssource.sqlgen.insert('moved_entities', attrs), attrs)
attrs = {'type': entity.cw_etype, 'eid': entity.eid, 'extid': None,
'asource': 'system'}
- self._cw.system_sql(syssource.sqlgen.insert('entities', attrs), attrs)
+ self._cw.system_sql(syssource.sqlgen.update('entities', attrs, ['eid']), attrs)
# register an operation to update repository/sources caches
ChangeEntitySourceUpdateCaches(self._cw, entity=entity,
oldsource=oldsource.repo_source,
--- a/hooks/notification.py Tue Jul 19 15:59:02 2016 +0200
+++ b/hooks/notification.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -165,7 +165,7 @@
class EntityUpdateHook(NotificationHook):
__regid__ = 'notifentityupdated'
__abstract__ = True # do not register by default
- __select__ = NotificationHook.__select__ & hook.from_dbapi_query()
+ __select__ = NotificationHook.__select__ & hook.issued_from_user_query()
events = ('before_update_entity',)
skip_attrs = set()
@@ -200,7 +200,7 @@
class SomethingChangedHook(NotificationHook):
__regid__ = 'supervising'
- __select__ = NotificationHook.__select__ & hook.from_dbapi_query()
+ __select__ = NotificationHook.__select__ & hook.issued_from_user_query()
events = ('before_add_relation', 'before_delete_relation',
'after_add_entity', 'before_update_entity')
--- a/hooks/syncschema.py Tue Jul 19 15:59:02 2016 +0200
+++ b/hooks/syncschema.py Tue Jul 19 16:13:12 2016 +0200
@@ -27,9 +27,10 @@
_ = unicode
from copy import copy
+from hashlib import md5
from yams.schema import (BASE_TYPES, BadSchemaDefinition,
RelationSchema, RelationDefinitionSchema)
-from yams import buildobjs as ybo, schema2sql as y2sql, convert_default_value
+from yams import buildobjs as ybo, convert_default_value
from logilab.common.decorators import clear_cache
@@ -37,7 +38,7 @@
from cubicweb.predicates import is_instance
from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
CONSTRAINTS, ETYPE_NAME_MAP, display_name)
-from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server import hook, schemaserial as ss, schema2sql as y2sql
from cubicweb.server.sqlutils import SQL_PREFIX
from cubicweb.hooks.synccomputed import RecomputeAttributeOperation
@@ -72,7 +73,7 @@
table = SQL_PREFIX + etype
column = SQL_PREFIX + rtype
try:
- cnx.system_sql(str('ALTER TABLE %s ADD %s integer' % (table, column)),
+ cnx.system_sql(str('ALTER TABLE %s ADD %s integer REFERENCES entities (eid)' % (table, column)),
rollback_on_failure=False)
cnx.info('added column %s to table %s', column, table)
except Exception:
@@ -319,8 +320,12 @@
if 'fulltext_container' in self.values:
op = UpdateFTIndexOp.get_instance(cnx)
for subjtype, objtype in rschema.rdefs:
- op.add_data(subjtype)
- op.add_data(objtype)
+ if self.values['fulltext_container'] == 'subject':
+ op.add_data(subjtype)
+ op.add_data(objtype)
+ else:
+ op.add_data(objtype)
+ op.add_data(subjtype)
# update the in-memory schema first
self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
self.rschema.__dict__.update(self.values)
@@ -711,6 +716,10 @@
elif cstrtype == 'UniqueConstraint':
syssource.update_rdef_unique(cnx, rdef)
self.unique_changed = True
+ if cstrtype in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
+ cstrname = 'cstr' + md5(rdef.subject.type + rdef.rtype.type + cstrtype +
+ (self.oldcstr.serialize() or '')).hexdigest()
+ cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s' % (SQL_PREFIX, rdef.subject.type, cstrname))
def revertprecommit_event(self):
# revert changes on in memory schema
@@ -758,6 +767,16 @@
elif cstrtype == 'UniqueConstraint' and oldcstr is None:
syssource.update_rdef_unique(cnx, rdef)
self.unique_changed = True
+ if cstrtype in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
+ if oldcstr is not None:
+ oldcstrname = 'cstr' + md5(rdef.subject.type + rdef.rtype.type + cstrtype +
+ (self.oldcstr.serialize() or '')).hexdigest()
+ cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s' %
+ (SQL_PREFIX, rdef.subject.type, oldcstrname))
+ cstrname, check = y2sql.check_constraint(rdef.subject, rdef.object, rdef.rtype.type,
+ newcstr, syssource.dbhelper, prefix=SQL_PREFIX)
+ cnx.system_sql('ALTER TABLE %s%s ADD CONSTRAINT %s CHECK(%s)' %
+ (SQL_PREFIX, rdef.subject.type, cstrname, check))
class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
@@ -1335,6 +1354,7 @@
We wait after the commit to as the schema in memory is only updated after
the commit.
"""
+ containercls = list
def postcommit_event(self):
cnx = self.cnx
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+psycopg2
--- a/hooks/test/unittest_hooks.py Tue Jul 19 15:59:02 2016 +0200
+++ b/hooks/test/unittest_hooks.py Tue Jul 19 16:13:12 2016 +0200
@@ -146,20 +146,6 @@
class UserGroupHooksTC(CubicWebTC):
- def test_user_synchronization(self):
- with self.admin_access.repo_cnx() as cnx:
- self.create_user(cnx, 'toto', password='hop', commit=False)
- self.assertRaises(AuthenticationError,
- self.repo.connect, u'toto', password='hop')
- cnx.commit()
- cnxid = self.repo.connect(u'toto', password='hop')
- self.assertNotEqual(cnxid, cnx.sessionid)
- cnx.execute('DELETE CWUser X WHERE X login "toto"')
- self.repo.execute(cnxid, 'State X')
- cnx.commit()
- self.assertRaises(BadConnectionId,
- self.repo.execute, cnxid, 'State X')
-
def test_user_group_synchronization(self):
with self.admin_access.repo_cnx() as cnx:
user = cnx.user
--- a/hooks/test/unittest_syncschema.py Tue Jul 19 15:59:02 2016 +0200
+++ b/hooks/test/unittest_syncschema.py Tue Jul 19 16:13:12 2016 +0200
@@ -21,15 +21,23 @@
from cubicweb import ValidationError, Binary
from cubicweb.schema import META_RTYPES
+from cubicweb.devtools import startpgcluster, stoppgcluster, PostgresApptestConfiguration
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.server.sqlutils import SQL_PREFIX
from cubicweb.devtools.repotest import schema_eids_idx
+def setUpModule():
+ startpgcluster(__file__)
+
+
def tearDownModule(*args):
+ stoppgcluster(__file__)
del SchemaModificationHooksTC.schema_eids
+
class SchemaModificationHooksTC(CubicWebTC):
+ configcls = PostgresApptestConfiguration
def setUp(self):
super(SchemaModificationHooksTC, self).setUp()
@@ -38,12 +46,11 @@
def index_exists(self, cnx, etype, attr, unique=False):
dbhelper = self.repo.system_source.dbhelper
- with cnx.ensure_cnx_set:
- sqlcursor = cnx.cnxset.cu
- return dbhelper.index_exists(sqlcursor,
- SQL_PREFIX + etype,
- SQL_PREFIX + attr,
- unique=unique)
+ sqlcursor = cnx.cnxset.cu
+ return dbhelper.index_exists(sqlcursor,
+ SQL_PREFIX + etype,
+ SQL_PREFIX + attr,
+ unique=unique)
def _set_perms(self, cnx, eid):
cnx.execute('SET X read_permission G WHERE X eid %(x)s, G is CWGroup',
@@ -296,6 +303,7 @@
cnx.execute('SET DEF cardinality "11" '
'WHERE DEF relation_type RT, DEF from_entity E,'
'RT name "surname", E name "CWUser"')
+ cnx.execute('SET U surname "Doe" WHERE U surname NULL')
cnx.commit()
# should not be able anymore to add cwuser without surname
self.assertRaises(ValidationError, self.create_user, cnx, "toto")
--- a/hooks/zmq.py Tue Jul 19 15:59:02 2016 +0200
+++ b/hooks/zmq.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -50,30 +50,3 @@
self.repo.app_instances_bus.start()
-class ZMQRepositoryServerStopHook(hook.Hook):
- __regid__ = 'zmqrepositoryserverstop'
- events = ('server_shutdown',)
-
- def __call__(self):
- server = getattr(self.repo, 'zmq_repo_server', None)
- if server:
- self.repo.zmq_repo_server.quit()
-
-class ZMQRepositoryServerStartHook(hook.Hook):
- __regid__ = 'zmqrepositoryserverstart'
- events = ('server_startup',)
-
- def __call__(self):
- config = self.repo.config
- if config.name == 'repository':
- # start-repository command already starts a zmq repo
- return
- address = config.get('zmq-repository-address')
- if not address:
- return
- self.repo.warning('remote access to the repository via zmq/pickle is deprecated')
- from cubicweb.server import cwzmq
- self.repo.zmq_repo_server = server = cwzmq.ZMQRepositoryServer(self.repo)
- server.connect(address)
- self.repo.threaded_task(server.run)
-
--- a/i18n/de.po Tue Jul 19 15:59:02 2016 +0200
+++ b/i18n/de.po Tue Jul 19 16:13:12 2016 +0200
@@ -642,10 +642,6 @@
msgid "New WorkflowTransition"
msgstr "Neuer workflow-Übergang"
-#, python-format
-msgid "No account? Try public access at %s"
-msgstr "Kein Konto? Zur öffentlichen Website: %s"
-
msgid "No result matching query"
msgstr "Ihre Suche ergab keine Treffer."
@@ -1132,6 +1128,9 @@
msgid "add a CWCache"
msgstr ""
+msgid "add a CWComputedRType"
+msgstr ""
+
msgid "add a CWConstraint"
msgstr ""
@@ -4158,9 +4157,6 @@
msgid "thursday"
msgstr "Donnerstag"
-msgid "timeline"
-msgstr "Zeitleiste"
-
msgid "timestamp"
msgstr "Datum"
@@ -4509,7 +4505,7 @@
msgstr "Wert"
#, python-format
-msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
+msgid "value %(KEY-value)s must be < %(KEY-boundary)s"
msgstr ""
#, python-format
@@ -4517,6 +4513,10 @@
msgstr ""
#, python-format
+msgid "value %(KEY-value)s must be > %(KEY-boundary)s"
+msgstr ""
+
+#, python-format
msgid "value %(KEY-value)s must be >= %(KEY-boundary)s"
msgstr ""
@@ -4663,6 +4663,9 @@
#~ msgid "Browse by category"
#~ msgstr "nach Kategorien navigieren"
+#~ msgid "No account? Try public access at %s"
+#~ msgstr "Kein Konto? Zur öffentlichen Website: %s"
+
#~ msgid "anonymous"
#~ msgstr "anonym"
@@ -4682,3 +4685,6 @@
#~ msgid "no edited fields specified for entity %s"
#~ msgstr "kein Eingabefeld spezifiziert Für Entität %s"
+
+#~ msgid "timeline"
+#~ msgstr "Zeitleiste"
--- a/i18n/en.po Tue Jul 19 15:59:02 2016 +0200
+++ b/i18n/en.po Tue Jul 19 16:13:12 2016 +0200
@@ -620,10 +620,6 @@
msgid "New WorkflowTransition"
msgstr "New workflow-transition"
-#, python-format
-msgid "No account? Try public access at %s"
-msgstr ""
-
msgid "No result matching query"
msgstr ""
@@ -1094,6 +1090,9 @@
msgid "add a CWCache"
msgstr ""
+msgid "add a CWComputedRType"
+msgstr ""
+
msgid "add a CWConstraint"
msgstr ""
@@ -4060,9 +4059,6 @@
msgid "thursday"
msgstr ""
-msgid "timeline"
-msgstr ""
-
msgid "timestamp"
msgstr ""
@@ -4402,7 +4398,7 @@
msgstr ""
#, python-format
-msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
+msgid "value %(KEY-value)s must be < %(KEY-boundary)s"
msgstr ""
#, python-format
@@ -4410,6 +4406,10 @@
msgstr ""
#, python-format
+msgid "value %(KEY-value)s must be > %(KEY-boundary)s"
+msgstr ""
+
+#, python-format
msgid "value %(KEY-value)s must be >= %(KEY-boundary)s"
msgstr ""
--- a/i18n/es.po Tue Jul 19 15:59:02 2016 +0200
+++ b/i18n/es.po Tue Jul 19 16:13:12 2016 +0200
@@ -651,10 +651,6 @@
msgid "New WorkflowTransition"
msgstr "Agregar transición de Workflow"
-#, python-format
-msgid "No account? Try public access at %s"
-msgstr "No esta registrado? Use el acceso público en %s"
-
msgid "No result matching query"
msgstr "Ningún resultado corresponde a su búsqueda"
@@ -1151,6 +1147,9 @@
msgid "add a CWCache"
msgstr ""
+msgid "add a CWComputedRType"
+msgstr ""
+
msgid "add a CWConstraint"
msgstr ""
@@ -4222,9 +4221,6 @@
msgid "thursday"
msgstr "Jueves"
-msgid "timeline"
-msgstr "Escala de Tiempo"
-
msgid "timestamp"
msgstr "Fecha"
@@ -4573,14 +4569,18 @@
msgstr "Vampr"
#, python-format
-msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
-msgstr "El valor %(KEY-value)s debe ser %(KEY-op)s %(KEY-boundary)s"
+msgid "value %(KEY-value)s must be < %(KEY-boundary)s"
+msgstr ""
#, python-format
msgid "value %(KEY-value)s must be <= %(KEY-boundary)s"
msgstr "el valor %(KEY-value)s debe ser <= %(KEY-boundary)s"
#, python-format
+msgid "value %(KEY-value)s must be > %(KEY-boundary)s"
+msgstr ""
+
+#, python-format
msgid "value %(KEY-value)s must be >= %(KEY-boundary)s"
msgstr "el valor %(KEY-value)s debe ser >= %(KEY-boundary)s"
@@ -4729,6 +4729,9 @@
#~ msgid "Browse by category"
#~ msgstr "Busca por categorÃa"
+#~ msgid "No account? Try public access at %s"
+#~ msgstr "No esta registrado? Use el acceso público en %s"
+
#~ msgid "anonymous"
#~ msgstr "anónimo"
@@ -4765,9 +4768,15 @@
#~ msgid "no edited fields specified for entity %s"
#~ msgstr "Ningún campo editable especificado para la entidad %s"
+#~ msgid "timeline"
+#~ msgstr "Escala de Tiempo"
+
#~ msgid "unknown option(s): %s"
#~ msgstr "opcion(es) desconocida(s): %s"
+#~ msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
+#~ msgstr "El valor %(KEY-value)s debe ser %(KEY-op)s %(KEY-boundary)s"
+
#~ msgid "web sessions without CNX"
#~ msgstr "sesiones web sin conexión asociada"
--- a/i18n/fr.po Tue Jul 19 15:59:02 2016 +0200
+++ b/i18n/fr.po Tue Jul 19 16:13:12 2016 +0200
@@ -647,10 +647,6 @@
msgid "New WorkflowTransition"
msgstr "Nouvelle transition workflow"
-#, python-format
-msgid "No account? Try public access at %s"
-msgstr "Pas de compte ? Accédez au site public : %s"
-
msgid "No result matching query"
msgstr "Aucun résultat ne correspond à la requête"
@@ -1147,6 +1143,9 @@
msgid "add a CWCache"
msgstr ""
+msgid "add a CWComputedRType"
+msgstr ""
+
msgid "add a CWConstraint"
msgstr ""
@@ -4221,9 +4220,6 @@
msgid "thursday"
msgstr "jeudi"
-msgid "timeline"
-msgstr "échelle de temps"
-
msgid "timestamp"
msgstr "date"
@@ -4570,18 +4566,22 @@
msgstr "valeur"
#, python-format
-msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s"
-msgstr "la valeur %(KEY-value)s n'est pas %(KEY-op)s %(KEY-boundary)s"
+msgid "value %(KEY-value)s must be < %(KEY-boundary)s"
+msgstr "la valeur %(KEY-value)s doit être strictement inférieure à %(KEY-boundary)s"
#, python-format
msgid "value %(KEY-value)s must be <= %(KEY-boundary)s"
msgstr ""
-"la valeur %(KEY-value)s n'est pas inférieure ou égale à %(KEY-boundary)s"
+"la valeur %(KEY-value)s doit être inférieure ou égale à %(KEY-boundary)s"
+
+#, python-format
+msgid "value %(KEY-value)s must be > %(KEY-boundary)s"
+msgstr "la valeur %(KEY-value)s doit être strictement supérieure à %(KEY-boundary)s"
#, python-format
msgid "value %(KEY-value)s must be >= %(KEY-boundary)s"
msgstr ""
-"la valeur %(KEY-value)s n'est pas supérieure ou égale à %(KEY-boundary)s"
+"la valeur %(KEY-value)s doit être supérieure ou égale à %(KEY-boundary)s"
msgid "value associated to this key is not editable manually"
msgstr "la valeur associée à cette clé n'est pas éditable manuellement"
@@ -4723,69 +4723,3 @@
msgid "you should probably delete that property"
msgstr "vous devriez probablement supprimer cette propriété"
-
-#~ msgid "%s relation should not be in mapped"
-#~ msgstr "la relation %s ne devrait pas ếtre mappé"
-
-#~ msgid "Any"
-#~ msgstr "Tous"
-
-#~ msgid "Browse by category"
-#~ msgstr "Naviguer par catégorie"
-
-#~ msgid "anonymous"
-#~ msgstr "anonyme"
-
-#~ 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 "can't connect to source %s, some data may be missing"
-#~ msgstr "ne peut se connecter à la source %s, des données peuvent manquer"
-
-#~ 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"
-
-#~ msgid "components_etypenavigation"
-#~ msgstr "filtrage par type"
-
-#~ msgid "components_etypenavigation_description"
-#~ msgstr "permet de filtrer par type d'entité les résultats d'une recherche"
-
-#~ msgid "error while querying source %s, some data may be missing"
-#~ msgstr ""
-#~ "une erreur est survenue en interrogeant %s, il est possible que les\n"
-#~ "données affichées soient incomplètes"
-
-#~ 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 "no edited fields specified for entity %s"
-#~ msgstr "aucun champ à éditer spécifié pour l'entité %s"
-
-#~ msgid "unknown option(s): %s"
-#~ msgstr "option(s) inconnue(s) : %s"
-
-#~ msgid "web sessions without CNX"
-#~ msgstr "sessions web sans connexion associée"
-
-#~ msgid "workflow already has a state of that name"
-#~ msgstr "le workflow a déja un état du même nom"
-
-#~ msgid "workflow already has a transition of that name"
-#~ msgstr "le workflow a déja une transition du même nom"
-
-#~ 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 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"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.21.0_Any.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,172 @@
+from cubicweb.schema import PURE_VIRTUAL_RTYPES
+from cubicweb.server.schema2sql import rschema_has_table
+
+
+def add_foreign_keys():
+ source = repo.system_source
+ if not source.dbhelper.alter_column_support:
+ return
+ for rschema in schema.relations():
+ if rschema.inlined:
+ add_foreign_keys_inlined(rschema)
+ elif rschema_has_table(rschema, skip_relations=PURE_VIRTUAL_RTYPES):
+ add_foreign_keys_relation(rschema)
+ for eschema in schema.entities():
+ if eschema.final:
+ continue
+ add_foreign_key_etype(eschema)
+
+
+def add_foreign_keys_relation(rschema):
+ args = {'r': rschema.type}
+ count = sql('SELECT COUNT(*) FROM ('
+ ' SELECT eid_from FROM %(r)s_relation'
+ ' UNION'
+ ' SELECT eid_to FROM %(r)s_relation'
+ ' EXCEPT'
+ ' SELECT eid FROM entities) AS eids' % args,
+ ask_confirm=False)[0][0]
+ if count:
+ print '%s references %d unknown entities, deleting' % (rschema, count)
+ sql('DELETE FROM %(r)s_relation '
+ 'WHERE eid_from IN (SELECT eid_from FROM %(r)s_relation EXCEPT SELECT eid FROM entities)' % args)
+ sql('DELETE FROM %(r)s_relation '
+ 'WHERE eid_to IN (SELECT eid_to FROM %(r)s_relation EXCEPT SELECT eid FROM entities)' % args)
+
+ args['from_fk'] = '%(r)s_relation_eid_from_fkey' % args
+ args['to_fk'] = '%(r)s_relation_eid_to_fkey' % args
+ args['table'] = '%(r)s_relation' % args
+ if repo.system_source.dbdriver == 'postgres':
+ sql('ALTER TABLE %(table)s DROP CONSTRAINT IF EXISTS %(from_fk)s' % args,
+ ask_confirm=False)
+ sql('ALTER TABLE %(table)s DROP CONSTRAINT IF EXISTS %(to_fk)s' % args,
+ ask_confirm=False)
+ elif repo.system_source.dbdriver.startswith('sqlserver'):
+ sql("IF OBJECT_ID('%(from_fk)s', 'F') IS NOT NULL "
+ "ALTER TABLE %(table)s DROP CONSTRAINT %(from_fk)s" % args,
+ ask_confirm=False)
+ sql("IF OBJECT_ID('%(to_fk)s', 'F') IS NOT NULL "
+ "ALTER TABLE %(table)s DROP CONSTRAINT %(to_fk)s" % args,
+ ask_confirm=False)
+ sql('ALTER TABLE %(table)s ADD CONSTRAINT %(from_fk)s '
+ 'FOREIGN KEY (eid_from) REFERENCES entities (eid)' % args,
+ ask_confirm=False)
+ sql('ALTER TABLE %(table)s ADD CONSTRAINT %(to_fk)s '
+ 'FOREIGN KEY (eid_to) REFERENCES entities (eid)' % args,
+ ask_confirm=False)
+
+
+def add_foreign_keys_inlined(rschema):
+ for eschema in rschema.subjects():
+ args = {'e': eschema.type, 'r': rschema.type}
+ args['c'] = 'cw_%(e)s_cw_%(r)s_fkey' % args
+
+ if eschema.rdef(rschema).cardinality[0] == '1':
+ broken_eids = sql('SELECT cw_eid FROM cw_%(e)s WHERE cw_%(r)s IS NULL' % args,
+ ask_confirm=False)
+ if broken_eids:
+ print 'Required relation %(e)s.%(r)s missing' % args
+ args['eids'] = ', '.join(str(eid) for eid, in broken_eids)
+ rql('DELETE %(e)s X WHERE X eid IN (%(eids)s)' % args)
+ broken_eids = sql('SELECT cw_eid FROM cw_%(e)s WHERE cw_%(r)s IN (SELECT cw_%(r)s FROM cw_%(e)s '
+ 'EXCEPT SELECT eid FROM entities)' % args,
+ ask_confirm=False)
+ if broken_eids:
+ print 'Required relation %(e)s.%(r)s references unknown objects, deleting subject entities' % args
+ args['eids'] = ', '.join(str(eid) for eid, in broken_eids)
+ rql('DELETE %(e)s X WHERE X eid IN (%(eids)s)' % args)
+ else:
+ if sql('SELECT COUNT(*) FROM ('
+ ' SELECT cw_%(r)s FROM cw_%(e)s WHERE cw_%(r)s IS NOT NULL'
+ ' EXCEPT'
+ ' SELECT eid FROM entities) AS eids' % args,
+ ask_confirm=False)[0][0]:
+ print '%(e)s.%(r)s references unknown entities, deleting relation' % args
+ sql('UPDATE cw_%(e)s SET cw_%(r)s = NULL WHERE cw_%(r)s IS NOT NULL AND cw_%(r)s IN '
+ '(SELECT cw_%(r)s FROM cw_%(e)s EXCEPT SELECT eid FROM entities)' % args)
+
+ if repo.system_source.dbdriver == 'postgres':
+ sql('ALTER TABLE cw_%(e)s DROP CONSTRAINT IF EXISTS %(c)s' % args,
+ ask_confirm=False)
+ elif repo.system_source.dbdriver.startswith('sqlserver'):
+ sql("IF OBJECT_ID('%(c)s', 'F') IS NOT NULL "
+ "ALTER TABLE cw_%(e)s DROP CONSTRAINT %(c)s" % args,
+ ask_confirm=False)
+ sql('ALTER TABLE cw_%(e)s ADD CONSTRAINT %(c)s '
+ 'FOREIGN KEY (cw_%(r)s) references entities(eid)' % args,
+ ask_confirm=False)
+
+
+def add_foreign_key_etype(eschema):
+ args = {'e': eschema.type}
+ if sql('SELECT COUNT(*) FROM ('
+ ' SELECT cw_eid FROM cw_%(e)s'
+ ' EXCEPT'
+ ' SELECT eid FROM entities) AS eids' % args,
+ ask_confirm=False)[0][0]:
+ print '%(e)s has nonexistent entities, deleting' % args
+ sql('DELETE FROM cw_%(e)s WHERE cw_eid IN '
+ '(SELECT cw_eid FROM cw_%(e)s EXCEPT SELECT eid FROM entities)' % args)
+ args['c'] = 'cw_%(e)s_cw_eid_fkey' % args
+ if repo.system_source.dbdriver == 'postgres':
+ sql('ALTER TABLE cw_%(e)s DROP CONSTRAINT IF EXISTS %(c)s' % args,
+ ask_confirm=False)
+ elif repo.system_source.dbdriver.startswith('sqlserver'):
+ sql("IF OBJECT_ID('%(c)s', 'F') IS NOT NULL "
+ "ALTER TABLE cw_%(e)s DROP CONSTRAINT %(c)s" % args,
+ ask_confirm=False)
+ sql('ALTER TABLE cw_%(e)s ADD CONSTRAINT %(c)s '
+ 'FOREIGN KEY (cw_eid) REFERENCES entities (eid)' % args,
+ ask_confirm=False)
+
+
+add_foreign_keys()
+
+cu = session.cnxset.cu
+helper = repo.system_source.dbhelper
+
+helper.drop_index(cu, 'entities', 'extid', False)
+# don't use create_index because it doesn't work for columns that may be NULL
+# on sqlserver
+for query in helper.sqls_create_multicol_unique_index('entities', ['extid']):
+ cu.execute(query)
+
+if 'moved_entities' not in helper.list_tables(cu):
+ sql('''
+ CREATE TABLE moved_entities (
+ eid INTEGER PRIMARY KEY NOT NULL,
+ extid VARCHAR(256) UNIQUE
+ )
+ ''')
+
+moved_entities = sql('SELECT -eid, extid FROM entities WHERE eid < 0',
+ ask_confirm=False)
+if moved_entities:
+ cu.executemany('INSERT INTO moved_entities (eid, extid) VALUES (%s, %s)',
+ moved_entities)
+ sql('DELETE FROM entities WHERE eid < 0')
+
+commit()
+
+sync_schema_props_perms('CWEType')
+
+sync_schema_props_perms('cwuri')
+
+from cubicweb.server.schema2sql import check_constraint
+
+for cwconstraint in rql('Any C WHERE R constrained_by C').entities():
+ cwrdef = cwconstraint.reverse_constrained_by[0]
+ rdef = cwrdef.yams_schema()
+ cstr = rdef.constraint_by_eid(cwconstraint.eid)
+ if cstr.type() not in ('BoundaryConstraint', 'IntervalBoundConstraint', 'StaticVocabularyConstraint'):
+ continue
+ cstrname, check = check_constraint(rdef.subject, rdef.object, rdef.rtype.type,
+ cstr, helper, prefix='cw_')
+ args = {'e': rdef.subject.type, 'c': cstrname, 'v': check}
+ if repo.system_source.dbdriver == 'postgres':
+ sql('ALTER TABLE cw_%(e)s DROP CONSTRAINT IF EXISTS %(c)s' % args)
+ elif repo.system_source.dbdriver.startswith('sqlserver'):
+ sql("IF OBJECT_ID('%(c)s', 'C') IS NOT NULL "
+ "ALTER TABLE cw_%(e)s DROP CONSTRAINT %(c)s" % args)
+ sql('ALTER TABLE cw_%(e)s ADD CONSTRAINT %(c)s CHECK(%(v)s)' % args)
+commit()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.21.1_Any.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,4 @@
+# re-read ComputedRelation permissions from schema.py now that we're
+# able to serialize them
+for computedrtype in schema.iter_computed_relations():
+ sync_schema_props_perms(computedrtype.type)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.21.2_Any.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,7 @@
+sync_schema_props_perms('cwuri')
+
+helper = repo.system_source.dbhelper
+cu = session.cnxset.cu
+helper.set_null_allowed(cu, 'moved_entities', 'extid', 'VARCHAR(256)', False)
+
+commit()
--- a/misc/migration/bootstrapmigration_repository.py Tue Jul 19 15:59:02 2016 +0200
+++ b/misc/migration/bootstrapmigration_repository.py Tue Jul 19 16:13:12 2016 +0200
@@ -57,7 +57,7 @@
commit()
if applcubicwebversion <= (3, 14, 4) and cubicwebversion >= (3, 14, 4):
- from yams import schema2sql as y2sql
+ from cubicweb.server import schema2sql as y2sql
dbhelper = repo.system_source.dbhelper
rdefdef = schema['CWSource'].rdef('name')
attrtype = y2sql.type_from_constraints(dbhelper, rdefdef.object, rdefdef.constraints).split()[0]
@@ -434,6 +434,12 @@
if applcubicwebversion < (3, 2, 0) and cubicwebversion >= (3, 2, 0):
add_cube('card', update_database=False)
+
+if applcubicwebversion < (3, 21, 1) and cubicwebversion >= (3, 21, 1):
+ add_relation_definition('CWComputedRType', 'read_permission', 'CWGroup')
+ add_relation_definition('CWComputedRType', 'read_permission', 'RQLExpression')
+
+
def sync_constraint_types():
"""Make sure the repository knows about all constraint types defined in the code"""
from cubicweb.schema import CONSTRAINTS
--- a/misc/migration/postcreate.py Tue Jul 19 15:59:02 2016 +0200
+++ b/misc/migration/postcreate.py Tue Jul 19 16:13:12 2016 +0200
@@ -38,9 +38,9 @@
activated = userwf.add_state(_('activated'), initial=True)
deactivated = userwf.add_state(_('deactivated'))
userwf.add_transition(_('deactivate'), (activated,), deactivated,
- requiredgroups=('managers',))
+ requiredgroups=(u'managers',))
userwf.add_transition(_('activate'), (deactivated,), activated,
- requiredgroups=('managers',))
+ requiredgroups=(u'managers',))
# create anonymous user if all-in-one config and anonymous user has been specified
if hasattr(config, 'anonymous_user'):
@@ -50,7 +50,7 @@
print 'Hopefully this is not a production instance...'
elif anonlogin:
from cubicweb.server import create_user
- create_user(session, unicode(anonlogin), anonpwd, 'guests')
+ create_user(session, unicode(anonlogin), anonpwd, u'guests')
# need this since we already have at least one user in the database (the default admin)
for user in rql('Any X WHERE X is CWUser').entities():
@@ -63,7 +63,7 @@
cfg.input_config(inputlevel=0)
for section, options in cfg.options_by_section():
for optname, optdict, value in options:
- key = '%s.%s' % (section, optname)
+ key = u'%s.%s' % (section, optname)
default = cfg.option_default(optname, optdict)
# only record values differing from default
if value != default:
--- a/misc/scripts/ldapuser2ldapfeed.py Tue Jul 19 15:59:02 2016 +0200
+++ b/misc/scripts/ldapuser2ldapfeed.py Tue Jul 19 16:13:12 2016 +0200
@@ -31,8 +31,6 @@
from cubicweb.server.edition import EditedEntity
-session.mode = 'write' # hold on the connections set
-
print '******************** backport entity content ***************************'
todelete = defaultdict(list)
--- a/misc/scripts/pyroforge2datafeed.py Tue Jul 19 15:59:02 2016 +0200
+++ b/misc/scripts/pyroforge2datafeed.py Tue Jul 19 16:13:12 2016 +0200
@@ -39,8 +39,6 @@
))
-session.mode = 'write' # hold on the connections set
-
print '******************** backport entity content ***************************'
from cubicweb.server import debugged
--- a/predicates.py Tue Jul 19 15:59:02 2016 +0200
+++ b/predicates.py Tue Jul 19 16:13:12 2016 +0200
@@ -15,171 +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/>.
-""".. _Selectors:
-
-Predicates and selectors
-------------------------
-
-A predicate is a class testing a particular aspect of a context. A selector is
-built by combining existant predicates or even selectors.
-
-Using and combining existant predicates
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-You can combine predicates using the `&`, `|` and `~` operators.
-
-When two predicates are combined using the `&` operator, it means that
-both should return a positive score. On success, the sum of scores is
-returned.
-
-When two predicates are combined using the `|` operator, it means that
-one of them should return a positive score. On success, the first
-positive score is returned.
-
-You can also "negate" a predicate by precedeing it by the `~` unary operator.
-
-Of course you can use parenthesis to balance expressions.
-
-Example
-~~~~~~~
-
-The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
-the blog entity itself.
-
-To do that, one defines a method on entity classes that returns the
-RSS stream url for a given entity. The default implementation on
-:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
-as base for all others) and a specific implementation on `Blog` will
-do what we want.
-
-But when we have a result set containing several `Blog` entities (or
-different entities), we don't know on which entity to call the
-aforementioned method. In this case, we keep the generic behaviour.
-
-Hence we have two cases here, one for a single-entity rsets, the other for
-multi-entities rsets.
-
-In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
-
-.. sourcecode:: python
-
- class RSSIconBox(box.Box):
- ''' just display the RSS icon on uniform result set '''
- __select__ = box.Box.__select__ & non_final_entity()
-
-It takes into account:
-
-* the inherited selection criteria (one has to look them up in the class
- hierarchy to know the details)
-
-* :class:`~cubicweb.predicates.non_final_entity`, which filters on result sets
- containing non final entities (a 'final entity' being synonym for entity
- attributes type, eg `String`, `Int`, etc)
-
-This matches our second case. Hence we have to provide a specific component for
-the first case:
-
-.. sourcecode:: python
-
- class EntityRSSIconBox(RSSIconBox):
- '''just display the RSS icon on uniform result set for a single entity'''
- __select__ = RSSIconBox.__select__ & one_line_rset()
-
-Here, one adds the :class:`~cubicweb.predicates.one_line_rset` predicate, which
-filters result sets of size 1. Thus, on a result set containing multiple
-entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
-selectable. However for a result set with one entity, the `EntityRSSIconBox`
-class will have a higher score than `RSSIconBox`, which is what we wanted.
-
-Of course, once this is done, you have to:
-
-* fill in the call method of `EntityRSSIconBox`
-
-* provide the default implementation of the method returning the RSS stream url
- on :class:`~cubicweb.entities.AnyEntity`
-
-* redefine this method on `Blog`.
-
-
-When to use selectors?
-~~~~~~~~~~~~~~~~~~~~~~
-
-Selectors are to be used whenever arises the need of dispatching on the shape or
-content of a result set or whatever else context (value in request form params,
-authenticated user groups, etc...). That is, almost all the time.
-
-Here is a quick example:
-
-.. sourcecode:: python
-
- class UserLink(component.Component):
- '''if the user is the anonymous user, build a link to login else a link
- to the connected user object with a logout link
- '''
- __regid__ = 'loggeduserlink'
-
- def call(self):
- if self._cw.session.anonymous_session:
- # display login link
- ...
- else:
- # display a link to the connected user object with a loggout link
- ...
-
-The proper way to implement this with |cubicweb| is two have two different
-classes sharing the same identifier but with different selectors so you'll get
-the correct one according to the context.
-
-.. sourcecode:: python
-
- class UserLink(component.Component):
- '''display a link to the connected user object with a loggout link'''
- __regid__ = 'loggeduserlink'
- __select__ = component.Component.__select__ & authenticated_user()
-
- def call(self):
- # display useractions and siteactions
- ...
-
- class AnonUserLink(component.Component):
- '''build a link to login'''
- __regid__ = 'loggeduserlink'
- __select__ = component.Component.__select__ & anonymous_user()
-
- def call(self):
- # display login link
- ...
-
-The big advantage, aside readability once you're familiar with the
-system, is that your cube becomes much more easily customizable by
-improving componentization.
-
-
-.. _CustomPredicates:
-
-Defining your own predicates
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. autodocstring:: cubicweb.appobject::objectify_predicate
-
-In other cases, you can take a look at the following abstract base classes:
-
-.. autoclass:: cubicweb.predicates.ExpectedValuePredicate
-.. autoclass:: cubicweb.predicates.EClassPredicate
-.. autoclass:: cubicweb.predicates.EntityPredicate
-
-.. _DebuggingSelectors:
-
-Debugging selection
-~~~~~~~~~~~~~~~~~~~
-
-Once in a while, one needs to understand why a view (or any application object)
-is, or is not selected appropriately. Looking at which predicates fired (or did
-not) is the way. The :class:`logilab.common.registry.traced_selection` context
-manager to help with that, *if you're running your instance in debug mode*.
-
-.. autoclass:: logilab.common.registry.traced_selection
-
+"""Predicate classes
"""
__docformat__ = "restructuredtext en"
@@ -394,7 +230,7 @@
"""
def __init__(self, *expected, **kwargs):
assert expected, self
- if len(expected) == 1 and isinstance(expected[0], set):
+ if len(expected) == 1 and isinstance(expected[0], (set, dict)):
self.expected = expected[0]
else:
self.expected = frozenset(expected)
@@ -409,7 +245,21 @@
def __call__(self, cls, req, **kwargs):
values = self._values_set(cls, req, **kwargs)
- matching = len(values & self.expected)
+ if isinstance(values, dict):
+ if isinstance(self.expected, dict):
+ matching = 0
+ for key, expected_value in self.expected.items():
+ if key in values:
+ if (isinstance(expected_value, (list, tuple, frozenset, set))
+ and values[key] in expected_value):
+ matching += 1
+ elif values[key] == expected_value:
+ matching += 1
+ if isinstance(self.expected, (set, frozenset)):
+ values = frozenset(values)
+ matching = len(values & self.expected)
+ else:
+ matching = len(values & self.expected)
if self.once_is_enough:
return matching
if matching == len(self.expected):
@@ -438,7 +288,7 @@
"""
def _values_set(self, cls, req, **kwargs):
- return frozenset(kwargs)
+ return kwargs
class appobject_selectable(Predicate):
@@ -1278,31 +1128,29 @@
def no_cnx(cls, req, **kwargs):
"""Return 1 if the web session has no connection set. This occurs when
anonymous access is not allowed and user isn't authenticated.
-
- May only be used on the web side, not on the data repository side.
"""
if not req.cnx:
return 1
return 0
+
@objectify_predicate
def authenticated_user(cls, req, **kwargs):
"""Return 1 if the user is authenticated (i.e. not the anonymous user).
-
- May only be used on the web side, not on the data repository side.
"""
if req.session.anonymous_session:
return 0
return 1
-# XXX == ~ authenticated_user()
-def anonymous_user():
+@objectify_predicate
+def anonymous_user(cls, req, **kwargs):
"""Return 1 if the user is not authenticated (i.e. is the anonymous user).
+ """
+ if req.session.anonymous_session:
+ return 1
+ return 0
- May only be used on the web side, not on the data repository side.
- """
- return ~ authenticated_user()
class match_user_groups(ExpectedValuePredicate):
"""Return a non-zero score if request's user is in at least one of the
@@ -1435,8 +1283,23 @@
in which case a single matching parameter is enough.
"""
+ def __init__(self, *expected, **kwargs):
+ """override default __init__ to allow either named or positional
+ parameters.
+ """
+ if kwargs and expected:
+ raise ValueError("match_form_params() can't be called with both "
+ "positional and named arguments")
+ if expected:
+ if len(expected) == 1 and not isinstance(expected[0], basestring):
+ raise ValueError("match_form_params() positional arguments "
+ "must be strings")
+ super(match_form_params, self).__init__(*expected)
+ else:
+ super(match_form_params, self).__init__(kwargs)
+
def _values_set(self, cls, req, **kwargs):
- return frozenset(req.form)
+ return req.form
class match_http_method(ExpectedValuePredicate):
--- a/repoapi.py Tue Jul 19 15:59:02 2016 +0200
+++ b/repoapi.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2013-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2013-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -17,14 +17,12 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""Official API to access the content of a repository
"""
-from logilab.common.deprecation import deprecated
+from logilab.common.deprecation import class_deprecated
from cubicweb.utils import parse_repo_uri
-from cubicweb import ConnectionError, ProgrammingError, AuthenticationError
-from uuid import uuid4
-from contextlib import contextmanager
-from cubicweb.req import RequestSessionBase
-from functools import wraps
+from cubicweb import ConnectionError, AuthenticationError
+from cubicweb.server.session import Connection
+
### private function for specific method ############################
@@ -41,7 +39,7 @@
loading the repository for a client, eg web server, configuration).
The returned repository may be an in-memory repository or a proxy object
- using a specific RPC method, depending on the given URI (pyro or zmq).
+ using a specific RPC method, depending on the given URI.
"""
if uri is None:
return _get_inmemory_repo(config, vreg)
@@ -52,49 +50,20 @@
# me may have been called with a dummy 'inmemory://' uri ...
return _get_inmemory_repo(config, vreg)
- if protocol == 'pyroloc': # direct connection to the instance
- from logilab.common.pyro_ext import get_proxy
- uri = uri.replace('pyroloc', 'PYRO')
- return get_proxy(uri)
-
- if protocol == 'pyro': # connection mediated through the pyro ns
- from logilab.common.pyro_ext import ns_get_proxy
- path = appid.strip('/')
- if not path:
- raise ConnectionError(
- "can't find instance name in %s (expected to be the path component)"
- % uri)
- if '.' in path:
- nsgroup, nsid = path.rsplit('.', 1)
- else:
- nsgroup = 'cubicweb'
- nsid = path
- return ns_get_proxy(nsid, defaultnsgroup=nsgroup, nshost=hostport)
-
- if protocol.startswith('zmqpickle-'):
- from cubicweb.zmqclient import ZMQRepositoryClient
- return ZMQRepositoryClient(uri)
- else:
- raise ConnectionError('unknown protocol: `%s`' % protocol)
+ raise ConnectionError('unknown protocol: `%s`' % protocol)
def connect(repo, login, **kwargs):
- """Take credential and return associated ClientConnection.
-
- The ClientConnection is associated to a new Session object that will be
- closed when the ClientConnection is closed.
+ """Take credential and return associated Connection.
raise AuthenticationError if the credential are invalid."""
sessionid = repo.connect(login, **kwargs)
session = repo._get_session(sessionid)
# XXX the autoclose_session should probably be handle on the session directly
# this is something to consider once we have proper server side Connection.
- return ClientConnection(session, autoclose_session=True)
+ return Connection(session)
def anonymous_cnx(repo):
- """return a ClientConnection for Anonymous user.
-
- The ClientConnection is associated to a new Session object that will be
- closed when the ClientConnection is closed.
+ """return a Connection for Anonymous user.
raises an AuthenticationError if anonymous usage is not allowed
"""
@@ -105,292 +74,7 @@
# use vreg's repository cache
return connect(repo, anon_login, password=anon_password)
-def _srv_cnx_func(name):
- """Decorate ClientConnection method blindly forward to Connection
- THIS TRANSITIONAL PURPOSE
- will be dropped when we have standalone connection"""
- def proxy(clt_cnx, *args, **kwargs):
- # the ``with`` dance is transitional. We do not have Standalone
- # Connection yet so we use this trick to unsure the session have the
- # proper cnx loaded. This can be simplified one we have Standalone
- # Connection object
- if not clt_cnx._open:
- raise ProgrammingError('Closed client connection')
- return getattr(clt_cnx._cnx, name)(*args, **kwargs)
- return proxy
-
-def _open_only(func):
- """decorator for ClientConnection method that check it is open"""
- @wraps(func)
- def check_open(clt_cnx, *args, **kwargs):
- if not clt_cnx._open:
- raise ProgrammingError('Closed client connection')
- return func(clt_cnx, *args, **kwargs)
- return check_open
-
-
-class ClientConnection(RequestSessionBase):
- """A Connection object to be used Client side.
-
- This object is aimed to be used client side (so potential communication
- with the repo through RPC) and aims to offer some compatibility with the
- cubicweb.dbapi.Connection interface.
-
- The autoclose_session parameter informs the connection that this session
- has been opened explicitly and only for this client connection. The
- connection will close the session on exit.
- """
- # make exceptions available through the connection object
- ProgrammingError = ProgrammingError
- # attributes that may be overriden per connection instance
- anonymous_connection = False # XXX really needed ?
- is_repo_in_memory = True # BC, always true
-
- def __init__(self, session, autoclose_session=False):
- super(ClientConnection, self).__init__(session.vreg)
- self._session = session # XXX there is no real reason to keep the
- # session around function still using it should
- # be rewritten and migrated.
- self._cnx = None
- self._open = None
- self._web_request = False
- #: cache entities built during the connection
- self._eid_cache = {}
- self._set_user(session.user)
- self._autoclose_session = autoclose_session
-
- def __enter__(self):
- assert self._open is None
- self._open = True
- self._cnx = self._session.new_cnx()
- self._cnx.__enter__()
- self._cnx.ctx_count += 1
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self._open = False
- self._cnx.ctx_count -= 1
- self._cnx.__exit__(exc_type, exc_val, exc_tb)
- self._cnx = None
- if self._autoclose_session:
- # we have to call repo.close to ensure the repo properly forgets the
- # session; calling session.close() is not enough :-(
- self._session.repo.close(self._session.sessionid)
-
-
- # begin silly BC
- @property
- def _closed(self):
- return not self._open
-
- def close(self):
- if self._open:
- self.__exit__(None, None, None)
-
- def __repr__(self):
- # XXX we probably want to reference the user of the session here
- if self._open is None:
- return '<ClientConnection (not open yet)>'
- elif not self._open:
- return '<ClientConnection (closed)>'
- elif self.anonymous_connection:
- return '<ClientConnection %s (anonymous)>' % self._cnx.connectionid
- else:
- return '<ClientConnection %s>' % self._cnx.connectionid
- # end silly BC
-
- # Main Connection purpose in life #########################################
-
- call_service = _srv_cnx_func('call_service')
-
- @_open_only
- def execute(self, *args, **kwargs):
- # the ``with`` dance is transitional. We do not have Standalone
- # Connection yet so we use this trick to unsure the session have the
- # proper cnx loaded. This can be simplified one we have Standalone
- # Connection object
- rset = self._cnx.execute(*args, **kwargs)
- rset.req = self
- return rset
-
- @_open_only
- def commit(self, *args, **kwargs):
- try:
- return self._cnx.commit(*args, **kwargs)
- finally:
- self.drop_entity_cache()
-
- @_open_only
- def rollback(self, *args, **kwargs):
- try:
- return self._cnx.rollback(*args, **kwargs)
- finally:
- self.drop_entity_cache()
-
- # security #################################################################
-
- allow_all_hooks_but = _srv_cnx_func('allow_all_hooks_but')
- deny_all_hooks_but = _srv_cnx_func('deny_all_hooks_but')
- security_enabled = _srv_cnx_func('security_enabled')
-
- # direct sql ###############################################################
-
- system_sql = _srv_cnx_func('system_sql')
-
- # session data methods #####################################################
-
- get_shared_data = _srv_cnx_func('get_shared_data')
- set_shared_data = _srv_cnx_func('set_shared_data')
-
- @property
- def transaction_data(self):
- return self._cnx.transaction_data
-
- # meta-data accessors ######################################################
-
- @_open_only
- def source_defs(self):
- """Return the definition of sources used by the repository."""
- return self._session.repo.source_defs()
-
- @_open_only
- def get_schema(self):
- """Return the schema currently used by the repository."""
- return self._session.repo.source_defs()
-
- @_open_only
- def get_option_value(self, option):
- """Return the value for `option` in the configuration."""
- return self._session.repo.get_option_value(option)
-
- entity_metas = _srv_cnx_func('entity_metas')
- describe = _srv_cnx_func('describe') # XXX deprecated in 3.19
-
- # undo support ############################################################
-
- @_open_only
- def undoable_transactions(self, ueid=None, req=None, **actionfilters):
- """Return a list of undoable transaction objects by the connection's
- user, ordered by descendant transaction time.
-
- Managers may filter according to user (eid) who has done the transaction
- using the `ueid` argument. Others will only see their own transactions.
-
- Additional filtering capabilities is provided by using the following
- named arguments:
-
- * `etype` to get only transactions creating/updating/deleting entities
- of the given type
-
- * `eid` to get only transactions applied to entity of the given eid
-
- * `action` to get only transactions doing the given action (action in
- 'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
- 'D'.
-
- * `public`: when additional filtering is provided, their are by default
- only searched in 'public' actions, unless a `public` argument is given
- and set to false.
- """
- # the ``with`` dance is transitional. We do not have Standalone
- # Connection yet so we use this trick to unsure the session have the
- # proper cnx loaded. This can be simplified one we have Standalone
- # Connection object
- source = self._cnx.repo.system_source
- txinfos = source.undoable_transactions(self._cnx, ueid, **actionfilters)
- for txinfo in txinfos:
- txinfo.req = req or self # XXX mostly wrong
- return txinfos
-
- @_open_only
- def transaction_info(self, txuuid, req=None):
- """Return transaction object for the given uid.
-
- raise `NoSuchTransaction` if not found or if session's user is not
- allowed (eg not in managers group and the transaction doesn't belong to
- him).
- """
- # the ``with`` dance is transitional. We do not have Standalone
- # Connection yet so we use this trick to unsure the session have the
- # proper cnx loaded. This can be simplified one we have Standalone
- # Connection object
- txinfo = self._cnx.repo.system_source.tx_info(self._cnx, txuuid)
- if req:
- txinfo.req = req
- else:
- txinfo.cnx = self
- return txinfo
-
- @_open_only
- def transaction_actions(self, txuuid, public=True):
- """Return an ordered list of action effectued during that transaction.
-
- If public is true, return only 'public' actions, eg not ones triggered
- under the cover by hooks, else return all actions.
-
- raise `NoSuchTransaction` if the transaction is not found or if
- session's user is not allowed (eg not in managers group and the
- transaction doesn't belong to him).
- """
- # the ``with`` dance is transitional. We do not have Standalone
- # Connection yet so we use this trick to unsure the session have the
- # proper cnx loaded. This can be simplified one we have Standalone
- # Connection object
- return self._cnx.repo.system_source.tx_actions(self._cnx, txuuid, public)
-
- @_open_only
- def undo_transaction(self, txuuid):
- """Undo the given transaction. Return potential restoration errors.
-
- raise `NoSuchTransaction` if not found or if session's user is not
- allowed (eg not in managers group and the transaction doesn't belong to
- him).
- """
- # the ``with`` dance is transitional. We do not have Standalone
- # Connection yet so we use this trick to unsure the session have the
- # proper cnx loaded. This can be simplified one we have Standalone
- # Connection object
- return self._cnx.repo.system_source.undo_transaction(self._cnx, txuuid)
-
- # cache management
-
- def entity_cache(self, eid):
- return self._eid_cache[eid]
-
- def set_entity_cache(self, entity):
- self._eid_cache[entity.eid] = entity
-
- def cached_entities(self):
- return self._eid_cache.values()
-
- def drop_entity_cache(self, eid=None):
- if eid is None:
- self._eid_cache = {}
- else:
- del self._eid_cache[eid]
-
- # deprecated stuff
-
- @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
- def request(self):
- return self
-
- @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
- def cursor(self):
- return self
-
- @property
- @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
- def sessionid(self):
- return self._session.sessionid
-
- @property
- @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
- def connection(self):
- return self
-
- @property
- @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
- def _repo(self):
- return self._session.repo
+class ClientConnection(Connection):
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.20] %(cls)s is deprecated, use Connection instead'
--- a/req.py Tue Jul 19 15:59:02 2016 +0200
+++ b/req.py Tue Jul 19 16:13:12 2016 +0200
@@ -81,7 +81,6 @@
A special method is needed to ensure the linked user is linked to the
connection too.
"""
- # cnx validity is checked by the call to .user_info
rset = self.eid_rset(orig_user.eid, 'CWUser')
user_cls = self.vreg['etypes'].etype_class('CWUser')
user = user_cls(self, rset, row=0, groups=orig_user.groups,
@@ -357,7 +356,7 @@
for key, val in sorted(newparams.iteritems()):
query[key] = (self.url_quote(val),)
query = '&'.join(u'%s=%s' % (param, value)
- for param, values in query.items()
+ for param, values in sorted(query.items())
for value in values)
return urlunsplit((schema, netloc, path, query, fragment))
--- a/rqlrewrite.py Tue Jul 19 15:59:02 2016 +0200
+++ b/rqlrewrite.py Tue Jul 19 16:13:12 2016 +0200
@@ -89,7 +89,7 @@
mytyperel.r_type = 'is'
if len(possibletypes) > 1:
node = n.Function('IN')
- for etype in possibletypes:
+ for etype in sorted(possibletypes):
node.append(n.Constant(etype, 'etype'))
else:
etype = iter(possibletypes).next()
--- a/rset.py Tue Jul 19 15:59:02 2016 +0200
+++ b/rset.py Tue Jul 19 16:13:12 2016 +0200
@@ -21,13 +21,16 @@
from warnings import warn
+from logilab.common import nullobject
from logilab.common.decorators import cached, clear_cache, copy_cache
-
from rql import nodes, stmts
from cubicweb import NotAnEntity, NoResultError, MultipleResultsError
+_MARKER = nullobject()
+
+
class ResultSet(object):
"""A result set wraps a RQL query result. This object implements
partially the list protocol to allow direct use as a list of
@@ -362,12 +365,14 @@
rset.limited = (limit, offset)
return rset
- def printable_rql(self, encoded=False):
+ def printable_rql(self, encoded=_MARKER):
"""return the result set's origin rql as a string, with arguments
substitued
"""
+ if encoded is not _MARKER:
+ warn('[3.21] the "encoded" argument is deprecated', DeprecationWarning)
encoding = self.req.encoding
- rqlstr = self.syntax_tree().as_string(encoding, self.args)
+ rqlstr = self.syntax_tree().as_string(kwargs=self.args)
# sounds like we get encoded or unicode string due to a bug in as_string
if not encoded:
if isinstance(rqlstr, unicode):
@@ -478,6 +483,7 @@
# new attributes found in this resultset ?
try:
entity = req.entity_cache(eid)
+ entity._cw = req
except KeyError:
pass
else:
--- a/schema.py Tue Jul 19 15:59:02 2016 +0200
+++ b/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -32,13 +32,14 @@
from logilab.common.textutils import splitstrip
from logilab.common.graph import get_cycles
+import yams
from yams import BadSchemaDefinition, buildobjs as ybo
from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
RelationDefinitionSchema, PermissionMixIn, role_name
-from yams.constraints import BaseConstraint, FormatConstraint
+from yams.constraints import (BaseConstraint, FormatConstraint, BoundaryConstraint,
+ IntervalBoundConstraint, StaticVocabularyConstraint)
from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
- obsolete as yobsolete, cleanup_sys_modules,
- fill_schema_from_namespace)
+ cleanup_sys_modules, fill_schema_from_namespace)
from rql import parse, nodes, RQLSyntaxError, TypeResolverException
from rql.analyze import ETypeResolver
@@ -462,6 +463,13 @@
ybo.DEFAULT_ATTRPERMS['update'] = ('managers', ERQLExpression('U has_update_permission X'))
ybo.DEFAULT_ATTRPERMS['add'] = ('managers', ERQLExpression('U has_add_permission X'))
+# we don't want 'add' or 'delete' permissions on computed relation types
+# (they're hardcoded to '()' on computed relation definitions)
+if 'add' in yams.DEFAULT_COMPUTED_RELPERMS:
+ del yams.DEFAULT_COMPUTED_RELPERMS['add']
+if 'delete' in yams.DEFAULT_COMPUTED_RELPERMS:
+ del yams.DEFAULT_COMPUTED_RELPERMS['delete']
+
PUB_SYSTEM_ENTITY_PERMS = {
'read': ('managers', 'users', 'guests',),
@@ -657,7 +665,7 @@
groups = self.get_groups(action)
if _cw.user.matching_groups(groups):
if DBG:
- print 'check_perm: %r %r: user matches %s' % (action, _self_str, groups)
+ print ('check_perm: %r %r: user matches %s' % (action, _self_str, groups))
return
# if 'owners' in allowed groups, check if the user actually owns this
# object, if so that's enough
@@ -859,7 +867,9 @@
return ERQLExpression(expression, mainvars, eid)
-class CubicWebRelationSchema(RelationSchema):
+class CubicWebRelationSchema(PermissionMixIn, RelationSchema):
+ permissions = {}
+ ACTIONS = ()
def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
if rdef is not None:
@@ -870,6 +880,17 @@
eid = getattr(rdef, 'eid', None)
self.eid = eid
+ def init_computed_relation(self, rdef):
+ self.ACTIONS = ('read',)
+ super(CubicWebRelationSchema, self).init_computed_relation(rdef)
+
+ def advertise_new_add_permission(self):
+ pass
+
+ def check_permission_definitions(self):
+ RelationSchema.check_permission_definitions(self)
+ PermissionMixIn.check_permission_definitions(self)
+
@property
def meta(self):
return self.type in META_RTYPES
@@ -1097,7 +1118,7 @@
subjtype, rschema.type, objtype,
__permissions__={'add': (),
'delete': (),
- 'read': ('managers', 'users', 'guests')})
+ 'read': rschema.permissions['read']})
rdef.infered = True
self.add_relation_def(rdef)
@@ -1108,6 +1129,12 @@
# additional cw specific constraints ###########################################
+# these are implemented as CHECK constraints in sql, don't do the work
+# twice
+StaticVocabularyConstraint.check = lambda *args: True
+IntervalBoundConstraint.check = lambda *args: True
+BoundaryConstraint.check = lambda *args: True
+
class BaseRQLConstraint(RRQLExpression, BaseConstraint):
"""base class for rql constraints"""
distinct_query = None
@@ -1396,13 +1423,6 @@
return self.regular_formats + tuple(NEED_PERM_FORMATS)
return self.regular_formats
-# XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is
-# necessary
-orig_import_erschema = PyFileReader.import_erschema
-def bw_import_erschema(self, ertype, schemamod=None, instantiate=True):
- return orig_import_erschema(self, bw_normalize_etype(ertype), schemamod, instantiate)
-PyFileReader.import_erschema = bw_import_erschema
-
# XXX itou for some Statement methods
from rql import stmts
orig_get_etype = stmts.ScopeNode.get_etype
@@ -1424,16 +1444,3 @@
def bw_set_statement_type(self, etype):
return orig_set_statement_type(self, bw_normalize_etype(etype))
stmts.Select.set_statement_type = bw_set_statement_type
-
-# XXX deprecated
-
-from yams.constraints import StaticVocabularyConstraint
-
-RichString = moved('yams.buildobjs', 'RichString')
-
-StaticVocabularyConstraint = class_moved(StaticVocabularyConstraint)
-FormatConstraint = class_moved(FormatConstraint)
-
-PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression)
-PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
-PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
--- a/schemas/base.py Tue Jul 19 15:59:02 2016 +0200
+++ b/schemas/base.py Tue Jul 19 16:13:12 2016 +0200
@@ -23,7 +23,7 @@
from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
SubjectRelation,
String, TZDatetime, Datetime, Password, Interval,
- Boolean)
+ Boolean, UniqueConstraint)
from cubicweb.schema import (
RQLConstraint, WorkflowableEntityType, ERQLExpression, RRQLExpression,
PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS, PUB_SYSTEM_ATTR_PERMS,
@@ -184,7 +184,6 @@
cardinality = '?*'
-
class ExternalUri(EntityType):
"""a URI representing an object in external data store"""
uri = String(required=True, unique=True, maxsize=256,
--- a/schemas/bootstrap.py Tue Jul 19 15:59:02 2016 +0200
+++ b/schemas/bootstrap.py Tue Jul 19 16:13:12 2016 +0200
@@ -38,7 +38,7 @@
description = RichString(internationalizable=True,
description=_('semantic description of this entity type'))
# necessary to filter using RQL
- final = Boolean(description=_('automatic'))
+ final = Boolean(default=False, description=_('automatic'))
class CWRType(EntityType):
@@ -239,7 +239,7 @@
"""groups allowed to read entities/relations of this type"""
__permissions__ = PUB_SYSTEM_REL_PERMS
name = 'read_permission'
- subject = ('CWEType', 'CWAttribute', 'CWRelation')
+ subject = ('CWEType', 'CWAttribute', 'CWRelation', 'CWComputedRType')
object = 'CWGroup'
cardinality = '**'
@@ -271,7 +271,7 @@
"""rql expression allowing to read entities/relations of this type"""
__permissions__ = PUB_SYSTEM_REL_PERMS
name = 'read_permission'
- subject = ('CWEType', 'CWAttribute', 'CWRelation')
+ subject = ('CWEType', 'CWAttribute', 'CWRelation', 'CWComputedRType')
object = 'RQLExpression'
cardinality = '*?'
composite = 'subject'
--- a/server/__init__.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/__init__.py Tue Jul 19 16:13:12 2016 +0200
@@ -280,7 +280,7 @@
# sort for eid predicatability as expected in some server tests
for group in sorted(BASE_GROUPS):
cnx.create_entity('CWGroup', name=unicode(group))
- admin = create_user(cnx, login, pwd, 'managers')
+ admin = create_user(cnx, login, pwd, u'managers')
cnx.execute('SET X owned_by U WHERE X is IN (CWGroup,CWSource), U eid %(u)s',
{'u': admin.eid})
cnx.commit()
--- a/server/checkintegrity.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/checkintegrity.py Tue Jul 19 16:13:12 2016 +0200
@@ -88,11 +88,10 @@
# to be updated due to the reindexation
repo = cnx.repo
dbhelper = repo.system_source.dbhelper
- with cnx.ensure_cnx_set:
- cursor = cnx.cnxset.cu
- if not dbhelper.has_fti_table(cursor):
- print 'no text index table'
- dbhelper.init_fti(cursor)
+ cursor = cnx.cnxset.cu
+ if not dbhelper.has_fti_table(cursor):
+ print 'no text index table'
+ dbhelper.init_fti(cursor)
repo.system_source.do_fti = True # ensure full-text indexation is activated
if etypes is None:
print 'Reindexing entities'
@@ -208,7 +207,7 @@
' WHERE cs.eid_from=e.eid AND cs.eid_to=s.cw_eid) '
'ORDER BY e.eid')
for row in cursor.fetchall():
- sys.stderr.write(msg % row)
+ sys.stderr.write(msg % tuple(row))
if fix:
cnx.system_sql('INSERT INTO is_relation (eid_from, eid_to) '
'SELECT e.eid, s.cw_eid FROM entities as e, cw_CWEType as s '
@@ -222,7 +221,7 @@
' WHERE cs.eid_from=e.eid AND cs.eid_to=s.cw_eid) '
'ORDER BY e.eid')
for row in cursor.fetchall():
- sys.stderr.write(msg % row)
+ sys.stderr.write(msg % tuple(row))
if fix:
cnx.system_sql('INSERT INTO is_instance_of_relation (eid_from, eid_to) '
'SELECT e.eid, s.cw_eid FROM entities as e, cw_CWEType as s '
@@ -400,8 +399,7 @@
with cnx.security_enabled(read=False, write=False): # ensure no read security
for check in checks:
check_func = globals()['check_%s' % check]
- with cnx.ensure_cnx_set:
- check_func(repo.schema, cnx, eids_cache, fix=fix)
+ check_func(repo.schema, cnx, eids_cache, fix=fix)
if fix:
cnx.commit()
else:
@@ -410,6 +408,5 @@
print 'WARNING: Diagnostic run, nothing has been corrected'
if reindex:
cnx.rollback()
- with cnx.ensure_cnx_set:
- reindex_entities(repo.schema, cnx, withpb=withpb)
+ reindex_entities(repo.schema, cnx, withpb=withpb)
cnx.commit()
--- a/server/cwzmq.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/cwzmq.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# copyright 2012-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2012-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -17,8 +17,6 @@
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-import cPickle
-import traceback
from threading import Thread
from logging import getLogger
@@ -27,16 +25,10 @@
import zmq.eventloop.zmqstream
from cubicweb import set_log_methods
-from cubicweb.server.server import QuitEvent, Finished
+
ctx = zmq.Context()
-def cwproto_to_zmqaddr(address):
- """ converts a cw-zmq address (like zmqpickle-tcp://<ip>:<port>)
- into a proper zmq address (tcp://<ip>:<port>)
- """
- assert address.startswith('zmqpickle-'), 'bad protocol string %s' % address
- return address.split('-', 1)[1] # chop the `zmqpickle-` prefix
class ZMQComm(object):
"""
@@ -134,132 +126,5 @@
self.ioloop.add_callback(lambda: self.stream.setsockopt(zmq.SUBSCRIBE, topic))
-class ZMQRepositoryServer(object):
-
- def __init__(self, repository):
- """make the repository available as a PyRO object"""
- self.address = None
- self.repo = repository
- self.socket = None
- self.stream = None
- self.loop = ioloop.IOLoop()
-
- # event queue
- self.events = []
-
- def connect(self, address):
- self.address = cwproto_to_zmqaddr(address)
-
- def run(self):
- """enter the service loop"""
- # start repository looping tasks
- self.socket = ctx.socket(zmq.REP)
- self.stream = zmq.eventloop.zmqstream.ZMQStream(self.socket, io_loop=self.loop)
- self.stream.bind(self.address)
- self.info('ZMQ server bound on: %s', self.address)
-
- self.stream.on_recv(self.process_cmds)
-
- try:
- self.loop.start()
- except zmq.ZMQError:
- self.warning('ZMQ event loop killed')
- self.quit()
-
- def trigger_events(self):
- """trigger ready events"""
- for event in self.events[:]:
- if event.is_ready():
- self.info('starting event %s', event)
- event.fire(self)
- try:
- event.update()
- except Finished:
- self.events.remove(event)
-
- def process_cmd(self, cmd):
- """Delegate the given command to the repository.
-
- ``cmd`` is a list of (method_name, args, kwargs)
- where ``args`` is a list of positional arguments
- and ``kwargs`` is a dictionnary of named arguments.
-
- >>> rset = delegate_to_repo(["execute", [sessionid], {'rql': rql}])
-
- :note1: ``kwargs`` may be ommited
-
- >>> rset = delegate_to_repo(["execute", [sessionid, rql]])
-
- :note2: both ``args`` and ``kwargs`` may be omitted
-
- >>> schema = delegate_to_repo(["get_schema"])
- >>> schema = delegate_to_repo("get_schema") # also allowed
-
- """
- cmd = cPickle.loads(cmd)
- if not cmd:
- raise AttributeError('function name required')
- if isinstance(cmd, basestring):
- cmd = [cmd]
- if len(cmd) < 2:
- cmd.append(())
- if len(cmd) < 3:
- cmd.append({})
- cmd = list(cmd) + [(), {}]
- funcname, args, kwargs = cmd[:3]
- result = getattr(self.repo, funcname)(*args, **kwargs)
- return result
-
- def process_cmds(self, cmds):
- """Callback intended to be used with ``on_recv``.
-
- Call ``delegate_to_repo`` on each command and send a pickled of
- each result recursively.
-
- Any exception are catched, pickled and sent.
- """
- try:
- for cmd in cmds:
- result = self.process_cmd(cmd)
- self.send_data(result)
- except Exception as exc:
- traceback.print_exc()
- self.send_data(exc)
-
- def send_data(self, data):
- self.socket.send_pyobj(data)
-
- def quit(self, shutdown_repo=False):
- """stop the server"""
- self.info('Quitting ZMQ server')
- try:
- self.loop.add_callback(self.loop.stop)
- self.stream.on_recv(None)
- self.stream.close()
- except Exception as e:
- print e
- pass
- if shutdown_repo and not self.repo.shutting_down:
- event = QuitEvent()
- event.fire(self)
-
- # server utilitities ######################################################
-
- def install_sig_handlers(self):
- """install signal handlers"""
- import signal
- self.info('installing signal handlers')
- signal.signal(signal.SIGINT, lambda x, y, s=self: s.quit(shutdown_repo=True))
- signal.signal(signal.SIGTERM, lambda x, y, s=self: s.quit(shutdown_repo=True))
-
-
- # these are overridden by set_log_methods below
- # only defining here to prevent pylint from complaining
- @classmethod
- def info(cls, msg, *a, **kw):
- pass
-
-
set_log_methods(Publisher, getLogger('cubicweb.zmq.pub'))
set_log_methods(Subscriber, getLogger('cubicweb.zmq.sub'))
-set_log_methods(ZMQRepositoryServer, getLogger('cubicweb.zmq.repo'))
--- a/server/edition.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/edition.py Tue Jul 19 16:13:12 2016 +0200
@@ -103,8 +103,6 @@
assert not self.saved, 'too late to modify edited attributes'
super(EditedEntity, self).__setitem__(attr, value)
self.entity.cw_attr_cache[attr] = value
- # mark attribute as needing purge by the client
- self.entity._cw_dont_cache_attribute(attr)
def oldnewvalue(self, attr):
"""returns the couple (old attr value, new attr value)
--- a/server/hook.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/hook.py Tue Jul 19 16:13:12 2016 +0200
@@ -320,6 +320,7 @@
eids_from_to = []
pruned = self.get_pruned_hooks(cnx, event,
entities, eids_from_to, kwargs)
+
# by default, hooks are executed with security turned off
with cnx.security_enabled(read=False):
for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
@@ -327,10 +328,11 @@
key=lambda x: x.order)
debug = server.DEBUG & server.DBG_HOOKS
with cnx.security_enabled(write=False):
- for hook in hooks:
- if debug:
- print event, _kwargs, hook
- hook()
+ with cnx.running_hooks_ops():
+ for hook in hooks:
+ if debug:
+ print event, _kwargs, hook
+ hook()
def get_pruned_hooks(self, cnx, event, entities, eids_from_to, kwargs):
"""return a set of hooks that should not be considered by filtered_possible objects
@@ -425,10 +427,13 @@
return req.is_hook_activated(cls)
@objectify_predicate
-def from_dbapi_query(cls, req, **kwargs):
- if req.running_dbapi_query:
- return 1
- return 0
+def issued_from_user_query(cls, req, **kwargs):
+ return 0 if req.hooks_in_progress else 1
+
+from_dbapi_query = class_renamed('from_dbapi_query',
+ issued_from_user_query,
+ message='[3.21] ')
+
class rechain(object):
def __init__(self, *iterators):
--- a/server/migractions.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/migractions.py Tue Jul 19 16:13:12 2016 +0200
@@ -44,7 +44,6 @@
from logilab.common.decorators import cached, clear_cache
from yams.constraints import SizeConstraint
-from yams.schema2sql import eschema2sql, rschema2sql, unique_index_name
from yams.schema import RelationDefinitionSchema
from cubicweb import CW_SOFTWARE_ROOT, AuthenticationError, ExecutionError
@@ -56,13 +55,11 @@
from cubicweb import repoapi
from cubicweb.migration import MigrationHelper, yes
from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server.schema2sql import eschema2sql, rschema2sql, unique_index_name
from cubicweb.server.utils import manager_userpasswd
from cubicweb.server.sqlutils import sqlexec, SQL_PREFIX
-def mock_object(**params):
- return type('Mock', (), params)()
-
class ClearGroupMap(hook.Hook):
__regid__ = 'cw.migration.clear_group_mapping'
__select__ = hook.Hook.__select__ & is_instance('CWGroup')
@@ -94,10 +91,10 @@
assert repo
self.cnx = cnx
self.repo = repo
- self.session = cnx._session
+ self.session = cnx.session
elif connect:
self.repo_connect()
- self.set_session()
+ self.set_cnx()
else:
self.session = None
# no config on shell to a remote instance
@@ -125,7 +122,9 @@
self.fs_schema = schema
self._synchronized = set()
- def set_session(self):
+ # overriden from base MigrationHelper ######################################
+
+ def set_cnx(self):
try:
login = self.repo.config.default_admin_config['login']
pwd = self.repo.config.default_admin_config['password']
@@ -149,9 +148,7 @@
print 'aborting...'
sys.exit(0)
self.session = self.repo._get_session(self.cnx.sessionid)
- self.session.keep_cnxset_mode('transaction')
- # overriden from base MigrationHelper ######################################
@cached
def repo_connect(self):
@@ -178,15 +175,14 @@
super(ServerMigrationHelper, self).migrate(vcconf, toupgrade, options)
def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs):
- with self.cnx._cnx.ensure_cnx_set:
- try:
- return super(ServerMigrationHelper, self).cmd_process_script(
- migrscript, funcname, *args, **kwargs)
- except ExecutionError as err:
- sys.stderr.write("-> %s\n" % err)
- except BaseException:
- self.rollback()
- raise
+ try:
+ return super(ServerMigrationHelper, self).cmd_process_script(
+ migrscript, funcname, *args, **kwargs)
+ except ExecutionError as err:
+ sys.stderr.write("-> %s\n" % err)
+ except BaseException:
+ self.rollback()
+ raise
# Adjust docstring
cmd_process_script.__doc__ = MigrationHelper.cmd_process_script.__doc__
@@ -287,12 +283,10 @@
print '-> database restored.'
def commit(self):
- if hasattr(self, 'cnx'):
- self.cnx.commit(free_cnxset=False)
+ self.cnx.commit()
def rollback(self):
- if hasattr(self, 'cnx'):
- self.cnx.rollback(free_cnxset=False)
+ self.cnx.rollback()
def rqlexecall(self, rqliter, ask_confirm=False):
for rql, kwargs in rqliter:
@@ -310,7 +304,7 @@
'schema': self.repo.get_schema(),
'cnx': self.cnx,
'fsschema': self.fs_schema,
- 'session' : self.cnx._cnx,
+ 'session' : self.cnx,
'repo' : self.repo,
})
return context
@@ -961,7 +955,6 @@
% (rtype, new.eid, oldeid), ask_confirm=False)
# delete relations using SQL to avoid relations content removal
# triggered by schema synchronization hooks.
- session = self.session
for rdeftype in ('CWRelation', 'CWAttribute'):
thispending = set( (eid for eid, in self.sqlexec(
'SELECT cw_eid FROM cw_%s WHERE cw_from_entity=%%(eid)s OR '
@@ -971,10 +964,10 @@
# get some validation error on commit since integrity hooks
# may think some required relation is missing... This also ensure
# repository caches are properly cleanup
- hook.CleanupDeletedEidsCacheOp.get_instance(session).union(thispending)
+ hook.CleanupDeletedEidsCacheOp.get_instance(self.cnx).union(thispending)
# and don't forget to remove record from system tables
entities = [self.cnx.entity_from_eid(eid, rdeftype) for eid in thispending]
- self.repo.system_source.delete_info_multi(self.cnx._cnx, entities)
+ self.repo.system_source.delete_info_multi(self.cnx, entities)
self.sqlexec('DELETE FROM cw_%s WHERE cw_from_entity=%%(eid)s OR '
'cw_to_entity=%%(eid)s' % rdeftype,
{'eid': oldeid}, ask_confirm=False)
@@ -1027,7 +1020,8 @@
print 'warning: relation type %s is already known, skip addition' % (
rtype)
elif rschema.rule:
- ss.execschemarql(execute, rschema, ss.crschema2rql(rschema))
+ gmap = self.group_mapping()
+ ss.execschemarql(execute, rschema, ss.crschema2rql(rschema, gmap))
else:
# register the relation into CWRType and insert necessary relation
# definitions
@@ -1086,7 +1080,7 @@
if not self.confirm('Relation %s is still present in the filesystem schema,'
' do you really want to drop it?' % oldname,
default='n'):
- raise SystemExit(1)
+ return
self.cmd_add_relation_type(newname, commit=True)
if not self.repo.schema[oldname].rule:
self.rqlexec('SET X %s Y WHERE X %s Y' % (newname, oldname),
@@ -1284,6 +1278,7 @@
return 'missing workflow relations, see make_workflowable(%s)' % etype
for etype in wfof:
eschema = self.repo.schema[etype]
+ etype = unicode(etype)
if ensure_workflowable:
assert 'in_state' in eschema.subjrels, _missing_wf_rel(etype)
assert 'custom_workflow' in eschema.subjrels, _missing_wf_rel(etype)
@@ -1396,7 +1391,7 @@
indexable entity types
"""
from cubicweb.server.checkintegrity import reindex_entities
- reindex_entities(self.repo.schema, self.cnx._cnx, etypes=etypes)
+ reindex_entities(self.repo.schema, self.cnx, etypes=etypes)
@contextmanager
def cmd_dropped_constraints(self, etype, attrname, cstrtype=None,
@@ -1486,19 +1481,28 @@
* the actual schema won't be updated until next startup
"""
rschema = self.repo.schema.rschema(attr)
- oldtype = rschema.objects(etype)[0]
- rdefeid = rschema.rdef(etype, oldtype).eid
- allownull = rschema.rdef(etype, oldtype).cardinality[0] != '1'
+ oldschema = rschema.objects(etype)[0]
+ rdef = rschema.rdef(etype, oldschema)
sql = ("UPDATE cw_CWAttribute "
"SET cw_to_entity=(SELECT cw_eid FROM cw_CWEType WHERE cw_name='%s')"
- "WHERE cw_eid=%s") % (newtype, rdefeid)
+ "WHERE cw_eid=%s") % (newtype, rdef.eid)
self.sqlexec(sql, ask_confirm=False)
dbhelper = self.repo.system_source.dbhelper
sqltype = dbhelper.TYPE_MAPPING[newtype]
- cursor = self.cnx._cnx.cnxset.cu
- dbhelper.change_col_type(cursor, 'cw_%s' % etype, 'cw_%s' % attr, sqltype, allownull)
+ cursor = self.cnx.cnxset.cu
+ allownull = rdef.cardinality[0] != '1'
+ dbhelper.change_col_type(cursor, 'cw_%s' % etype, 'cw_%s' % attr, sqltype, allownull)
if commit:
self.commit()
+ # manually update live schema
+ eschema = self.repo.schema[etype]
+ rschema._subj_schemas[eschema].remove(oldschema)
+ rschema._obj_schemas[oldschema].remove(eschema)
+ newschema = self.repo.schema[newtype]
+ rschema._update(eschema, newschema)
+ rdef.object = newschema
+ del rschema.rdefs[(eschema, oldschema)]
+ rschema.rdefs[(eschema, newschema)] = rdef
def cmd_add_entity_type_table(self, etype, commit=True):
"""low level method to create the sql table for an existing entity.
--- a/server/querier.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/querier.py Tue Jul 19 16:13:12 2016 +0200
@@ -37,6 +37,7 @@
from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
from cubicweb.server.edition import EditedEntity
from cubicweb.server.ssplanner import SSPlanner
+from cubicweb.statsd_logger import statsd_timeit, statsd_c
ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
@@ -75,7 +76,7 @@
def check_relations_read_access(cnx, select, args):
"""Raise :exc:`Unauthorized` if the given user doesn't have credentials to
- read relations used in the givel syntaxt tree
+ read relations used in the given syntax tree
"""
# use `term_etype` since we've to deal with rewritten constants here,
# when used as an external source by another repository.
@@ -516,6 +517,7 @@
return InsertPlan(self, rqlst, args, cnx)
return ExecutionPlan(self, rqlst, args, cnx)
+ @statsd_timeit
def execute(self, cnx, rql, args=None, build_descr=True):
"""execute a rql query, return resulting rows and their description in
a `ResultSet` object
@@ -558,8 +560,10 @@
return empty_rset(rql, args)
rqlst = self._rql_cache[cachekey]
self.cache_hit += 1
+ statsd_c('cache_hit')
except KeyError:
self.cache_miss += 1
+ statsd_c('cache_miss')
rqlst = self.parse(rql)
try:
# compute solutions for rqlst and return named args in query
@@ -570,7 +574,7 @@
except UnknownEid:
# we want queries such as "Any X WHERE X eid 9999" return an
# empty result instead of raising UnknownEid
- return empty_rset(rql, args, rqlst)
+ return empty_rset(rql, args)
if args and rql not in self._rql_ck_cache:
self._rql_ck_cache[rql] = eidkeys
if eidkeys:
@@ -580,9 +584,6 @@
if rqlst.TYPE != 'select':
if cnx.read_security:
check_no_password_selected(rqlst)
- # write query, ensure connection's mode is 'write' so connections
- # won't be released until commit/rollback
- cnx.mode = 'write'
cachekey = None
else:
if cnx.read_security:
--- a/server/repository.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/repository.py Tue Jul 19 16:13:12 2016 +0200
@@ -24,34 +24,29 @@
* brings these classes all together to provide a single access
point to a cubicweb instance.
* handles session management
-* provides method for pyro registration, to call if pyro is enabled
"""
__docformat__ = "restructuredtext en"
-import sys
import threading
import Queue
from warnings import warn
from itertools import chain
from time import time, localtime, strftime
from contextlib import contextmanager
-from warnings import warn
from logilab.common.decorators import cached, clear_cache
from logilab.common.deprecation import deprecated
from yams import BadSchemaDefinition
-from rql import RQLSyntaxError
from rql.utils import rqlvar_maker
from cubicweb import (CW_MIGRATION_MAP, QueryError,
UnknownEid, AuthenticationError, ExecutionError,
- BadConnectionId, Unauthorized, ValidationError,
- UniqueTogetherError, onevent)
+ BadConnectionId, ValidationError, Unauthorized,
+ UniqueTogetherError, onevent, ViolatedConstraint)
from cubicweb import cwvreg, schema, server
from cubicweb.server import ShuttingDown, utils, hook, querier, sources
-from cubicweb.server.session import Session, InternalSession, InternalManager
-from cubicweb.server.ssplanner import EditedEntity
+from cubicweb.server.session import Session, InternalManager
NO_CACHE_RELATIONS = set( [('owned_by', 'object'),
('created_by', 'object'),
@@ -59,7 +54,7 @@
])
def prefill_entity_caches(entity):
- session = entity._cw
+ cnx = entity._cw
# prefill entity relation caches
for rschema in entity.e_schema.subject_relations():
rtype = str(rschema)
@@ -69,14 +64,14 @@
entity.cw_attr_cache.setdefault(rtype, None)
else:
entity.cw_set_relation_cache(rtype, 'subject',
- session.empty_rset())
+ cnx.empty_rset())
for rschema in entity.e_schema.object_relations():
rtype = str(rschema)
if rtype in schema.VIRTUAL_RTYPES or (rtype, 'object') in NO_CACHE_RELATIONS:
continue
- entity.cw_set_relation_cache(rtype, 'object', session.empty_rset())
+ entity.cw_set_relation_cache(rtype, 'object', cnx.empty_rset())
-def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
+def del_existing_rel_if_needed(cnx, eidfrom, rtype, eidto):
"""delete existing relation when adding a new one if card is 1 or ?
have to be done once the new relation has been inserted to avoid having
@@ -86,9 +81,9 @@
hooks order hazardness
"""
# skip that if integrity explicitly disabled
- if not session.is_hook_category_activated('activeintegrity'):
+ if not cnx.is_hook_category_activated('activeintegrity'):
return
- rdef = session.rtype_eids_rdef(rtype, eidfrom, eidto)
+ rdef = cnx.rtype_eids_rdef(rtype, eidfrom, eidto)
card = rdef.cardinality
# one may be tented to check for neweids but this may cause more than one
# relation even with '1?' cardinality if thoses relations are added in the
@@ -102,34 +97,34 @@
# * we don't want read permissions to be applied but we want delete
# permission to be checked
if card[0] in '1?':
- with session.security_enabled(read=False):
- session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
- 'NOT Y eid %%(y)s' % rtype,
- {'x': eidfrom, 'y': eidto})
+ with cnx.security_enabled(read=False):
+ cnx.execute('DELETE X %s Y WHERE X eid %%(x)s, '
+ 'NOT Y eid %%(y)s' % rtype,
+ {'x': eidfrom, 'y': eidto})
if card[1] in '1?':
- with session.security_enabled(read=False):
- session.execute('DELETE X %s Y WHERE Y eid %%(y)s, '
- 'NOT X eid %%(x)s' % rtype,
- {'x': eidfrom, 'y': eidto})
+ with cnx.security_enabled(read=False):
+ cnx.execute('DELETE X %s Y WHERE Y eid %%(y)s, '
+ 'NOT X eid %%(x)s' % rtype,
+ {'x': eidfrom, 'y': eidto})
-def preprocess_inlined_relations(session, entity):
+def preprocess_inlined_relations(cnx, entity):
"""when an entity is added, check if it has some inlined relation which
requires to be extrated for proper call hooks
"""
relations = []
- activeintegrity = session.is_hook_category_activated('activeintegrity')
+ activeintegrity = cnx.is_hook_category_activated('activeintegrity')
eschema = entity.e_schema
for attr in entity.cw_edited:
rschema = eschema.subjrels[attr]
if not rschema.final: # inlined relation
value = entity.cw_edited[attr]
relations.append((attr, value))
- session.update_rel_cache_add(entity.eid, attr, value)
- rdef = session.rtype_eids_rdef(attr, entity.eid, value)
+ cnx.update_rel_cache_add(entity.eid, attr, value)
+ rdef = cnx.rtype_eids_rdef(attr, entity.eid, value)
if rdef.cardinality[1] in '1?' and activeintegrity:
- with session.security_enabled(read=False):
- session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
+ with cnx.security_enabled(read=False):
+ cnx.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
{'x': entity.eid, 'y': value})
return relations
@@ -151,8 +146,6 @@
class Repository(object):
"""a repository provides access to a set of persistent storages for
entities and relations
-
- XXX protect pyro access
"""
def __init__(self, config, tasks_manager=None, vreg=None):
@@ -162,17 +155,11 @@
self.vreg = vreg
self._tasks_manager = tasks_manager
- self.pyro_registered = False
- self.pyro_uri = None
- # every pyro client is handled in its own thread; map these threads to
- # the session we opened for them so we can clean up when they go away
- self._pyro_sessions = {}
self.app_instances_bus = NullEventBus()
self.info('starting repository from %s', self.config.apphome)
# dictionary of opened sessions
self._sessions = {}
-
# list of functions to be called at regular interval
# list of running threads
self._running_threads = []
@@ -234,7 +221,7 @@
if config.quick_start or config.creating or not config.read_instance_schema:
# load schema from the file system
if not config.creating:
- self.warning("set fs instance'schema")
+ self.info("set fs instance'schema")
self.set_schema(config.load_schema(expand_cubes=True))
else:
# normal start: load the instance schema from the database
@@ -435,10 +422,6 @@
except Exception:
self.exception('error while closing %s' % cnxset)
continue
- if self.pyro_registered:
- if self._use_pyrons():
- pyro_unregister(self.config)
- self.pyro_uri = None
hits, misses = self.querier.cache_hit, self.querier.cache_miss
try:
self.info('rql st cache hit/miss: %s/%s (%s%% hits)', hits, misses,
@@ -461,8 +444,7 @@
for source in self.sources_by_uri.itervalues():
if self.config.source_enabled(source) and source.support_entity('CWUser'):
try:
- with cnx.ensure_cnx_set:
- return source.authenticate(cnx, login, **authinfo)
+ return source.authenticate(cnx, login, **authinfo)
except AuthenticationError:
continue
else:
@@ -481,19 +463,18 @@
def _build_user(self, cnx, eid):
"""return a CWUser entity for user with the given eid"""
- with cnx.ensure_cnx_set:
- cls = self.vreg['etypes'].etype_class('CWUser')
- st = cls.fetch_rqlst(cnx.user, ordermethod=None)
- st.add_eid_restriction(st.get_variable('X'), 'x', 'Substitute')
- rset = cnx.execute(st.as_string(), {'x': eid})
- assert len(rset) == 1, rset
- cwuser = rset.get_entity(0, 0)
- # pylint: disable=W0104
- # prefetch / cache cwuser's groups and properties. This is especially
- # useful for internal sessions to avoid security insertions
- cwuser.groups
- cwuser.properties
- return cwuser
+ cls = self.vreg['etypes'].etype_class('CWUser')
+ st = cls.fetch_rqlst(cnx.user, ordermethod=None)
+ st.add_eid_restriction(st.get_variable('X'), 'x', 'Substitute')
+ rset = cnx.execute(st.as_string(), {'x': eid})
+ assert len(rset) == 1, rset
+ cwuser = rset.get_entity(0, 0)
+ # pylint: disable=W0104
+ # prefetch / cache cwuser's groups and properties. This is especially
+ # useful for internal sessions to avoid security insertions
+ cwuser.groups
+ cwuser.properties
+ return cwuser
# public (dbapi) interface ################################################
@@ -640,14 +621,14 @@
for k in chain(fetch_attrs, query_attrs):
if k not in cwuserattrs:
raise Exception('bad input for find_user')
- with self.internal_session() as session:
+ with self.internal_cnx() as cnx:
varmaker = rqlvar_maker()
vars = [(attr, varmaker.next()) for attr in fetch_attrs]
rql = 'Any %s WHERE X is CWUser, ' % ','.join(var[1] for var in vars)
rql += ','.join('X %s %s' % (var[0], var[1]) for var in vars) + ','
- rset = session.execute(rql + ','.join('X %s %%(%s)s' % (attr, attr)
- for attr in query_attrs),
- query_attrs)
+ rset = cnx.execute(rql + ','.join('X %s %%(%s)s' % (attr, attr)
+ for attr in query_attrs),
+ query_attrs)
return rset.rows
def new_session(self, login, **kwargs):
@@ -662,12 +643,6 @@
# try to get a user object
user = self.authenticate_user(cnx, login, **kwargs)
session = Session(user, self, cnxprops)
- if threading.currentThread() in self._pyro_sessions:
- # assume no pyro client does one get_repository followed by
- # multiple repo.connect
- assert self._pyro_sessions[threading.currentThread()] == None
- self.debug('record session %s', session)
- self._pyro_sessions[threading.currentThread()] = session
user._cw = user.cw_rset.req = session
user.cw_clear_relation_cache()
self._sessions[session.sessionid] = session
@@ -683,190 +658,26 @@
"""open a new session for a given user and return its sessionid """
return self.new_session(login, **kwargs).sessionid
- def execute(self, sessionid, rqlstring, args=None, build_descr=True,
- txid=None):
- """execute a RQL query
-
- * rqlstring should be a unicode string or a plain ascii string
- * args the optional parameters used in the query
- * build_descr is a flag indicating if the description should be
- built on select queries
- """
- session = self._get_session(sessionid, setcnxset=True, txid=txid)
- try:
- try:
- rset = self.querier.execute(session, rqlstring, args,
- build_descr)
- # NOTE: the web front will (re)build it when needed
- # e.g in facets
- # Zeroed to avoid useless overhead with pyro
- rset._rqlst = None
- return rset
- except (ValidationError, Unauthorized, RQLSyntaxError):
- raise
- except Exception:
- # FIXME: check error to catch internal errors
- self.exception('unexpected error while executing %s with %s', rqlstring, args)
- raise
- finally:
- session.free_cnxset()
-
- @deprecated('[3.19] use .entity_metas(sessionid, eid, txid) instead')
- def describe(self, sessionid, eid, txid=None):
- """return a tuple `(type, physical source uri, extid, actual source
- uri)` for the entity of the given `eid`
-
- As of 3.19, physical source uri is always the system source.
- """
- session = self._get_session(sessionid, setcnxset=True, txid=txid)
- try:
- etype, extid, source = self.type_and_source_from_eid(eid, session)
- return etype, source, extid, source
- finally:
- session.free_cnxset()
-
- def entity_metas(self, sessionid, eid, txid=None):
- """return a dictionary containing meta-datas for the entity of the given
- `eid`. Available keys are:
-
- * 'type', the entity's type name,
-
- * 'source', the name of the source from which this entity's coming from,
-
- * 'extid', the identifierfor this entity in its originating source, as
- an encoded string or `None` for entities from the 'system' source.
- """
- session = self._get_session(sessionid, setcnxset=True, txid=txid)
- try:
- etype, extid, source = self.type_and_source_from_eid(eid, session)
- return {'type': etype, 'source': source, 'extid': extid}
- finally:
- session.free_cnxset()
-
def check_session(self, sessionid):
"""raise `BadConnectionId` if the connection is no more valid, else
return its latest activity timestamp.
"""
- return self._get_session(sessionid, setcnxset=False).timestamp
-
- @deprecated('[3.19] use session or transaction data')
- def get_shared_data(self, sessionid, key, default=None, pop=False, txdata=False):
- """return value associated to key in the session's data dictionary or
- session's transaction's data if `txdata` is true.
-
- If pop is True, value will be removed from the dictionary.
-
- If key isn't defined in the dictionary, value specified by the
- `default` argument will be returned.
- """
- session = self._get_session(sessionid, setcnxset=False)
- return session.get_shared_data(key, default, pop, txdata)
-
- @deprecated('[3.19] use session or transaction data')
- def set_shared_data(self, sessionid, key, value, txdata=False):
- """set value associated to `key` in shared data
-
- if `txdata` is true, the value will be added to the repository session's
- transaction's data which are cleared on commit/rollback of the current
- transaction.
- """
- session = self._get_session(sessionid, setcnxset=False)
- session.set_shared_data(key, value, txdata)
-
- def commit(self, sessionid, txid=None):
- """commit transaction for the session with the given id"""
- self.debug('begin commit for session %s', sessionid)
- try:
- session = self._get_session(sessionid)
- session.set_cnx(txid)
- return session.commit()
- except (ValidationError, Unauthorized):
- raise
- except Exception:
- self.exception('unexpected error')
- raise
-
- def rollback(self, sessionid, txid=None):
- """commit transaction for the session with the given id"""
- self.debug('begin rollback for session %s', sessionid)
- try:
- session = self._get_session(sessionid)
- session.set_cnx(txid)
- session.rollback()
- except Exception:
- self.exception('unexpected error')
- raise
+ return self._get_session(sessionid).timestamp
def close(self, sessionid, txid=None, checkshuttingdown=True):
"""close the session with the given id"""
session = self._get_session(sessionid, txid=txid,
checkshuttingdown=checkshuttingdown)
# operation uncommited before close are rolled back before hook is called
- if session._cnx._session_handled:
- session._cnx.rollback(free_cnxset=False)
with session.new_cnx() as cnx:
self.hm.call_hooks('session_close', cnx)
# commit connection at this point in case write operation has been
# done during `session_close` hooks
cnx.commit()
session.close()
- if threading.currentThread() in self._pyro_sessions:
- self._pyro_sessions[threading.currentThread()] = None
del self._sessions[sessionid]
self.info('closed session %s for user %s', sessionid, session.user.login)
- def call_service(self, sessionid, regid, **kwargs):
- """
- See :class:`cubicweb.dbapi.Connection.call_service`
- and :class:`cubicweb.server.Service`
- """
- # XXX lack a txid
- session = self._get_session(sessionid)
- return session._cnx.call_service(regid, **kwargs)
-
- def user_info(self, sessionid, props=None):
- """this method should be used by client to:
- * check session id validity
- * update user information on each user's request (i.e. groups and
- custom properties)
- """
- user = self._get_session(sessionid, setcnxset=False).user
- return user.eid, user.login, user.groups, user.properties
-
- def undoable_transactions(self, sessionid, ueid=None, txid=None,
- **actionfilters):
- """See :class:`cubicweb.dbapi.Connection.undoable_transactions`"""
- session = self._get_session(sessionid, setcnxset=True, txid=txid)
- try:
- return self.system_source.undoable_transactions(session, ueid,
- **actionfilters)
- finally:
- session.free_cnxset()
-
- def transaction_info(self, sessionid, txuuid, txid=None):
- """See :class:`cubicweb.dbapi.Connection.transaction_info`"""
- session = self._get_session(sessionid, setcnxset=True, txid=txid)
- try:
- return self.system_source.tx_info(session, txuuid)
- finally:
- session.free_cnxset()
-
- def transaction_actions(self, sessionid, txuuid, public=True, txid=None):
- """See :class:`cubicweb.dbapi.Connection.transaction_actions`"""
- session = self._get_session(sessionid, setcnxset=True, txid=txid)
- try:
- return self.system_source.tx_actions(session, txuuid, public)
- finally:
- session.free_cnxset()
-
- def undo_transaction(self, sessionid, txuuid, txid=None):
- """See :class:`cubicweb.dbapi.Connection.undo_transaction`"""
- session = self._get_session(sessionid, setcnxset=True, txid=txid)
- try:
- return self.system_source.undo_transaction(session, txuuid)
- finally:
- session.free_cnxset()
-
# session handling ########################################################
def close_sessions(self):
@@ -891,27 +702,6 @@
nbclosed += 1
return nbclosed
- @deprecated("[3.19] use internal_cnx now\n"
- "(Beware that integrity hook are now enabled by default)")
- def internal_session(self, cnxprops=None, safe=False):
- """return a dbapi like connection/cursor using internal user which have
- every rights on the repository. The `safe` argument is a boolean flag
- telling if integrity hooks should be activated or not.
-
- /!\ the safe argument is False by default.
-
- *YOU HAVE TO* commit/rollback or close (rollback implicitly) the
- session once the job's done, else you'll leak connections set up to the
- time where no one is available, causing irremediable freeze...
- """
- session = InternalSession(self, cnxprops)
- if not safe:
- session.disable_hook_categories('integrity')
- session.disable_hook_categories('security')
- session._cnx.ctx_count += 1
- session.set_cnxset()
- return session
-
@contextmanager
def internal_cnx(self):
"""Context manager returning a Connection using internal user which have
@@ -920,14 +710,14 @@
Beware that unlike the older :meth:`internal_session`, internal
connections have all hooks beside security enabled.
"""
- with InternalSession(self) as session:
+ with Session(InternalManager(), self) as session:
with session.new_cnx() as cnx:
+ cnx.user._cw = cnx # XXX remove when "vreg = user._cw.vreg"
+ # hack in entity.py is gone
with cnx.security_enabled(read=False, write=False):
- with cnx.ensure_cnx_set:
- yield cnx
+ yield cnx
- def _get_session(self, sessionid, setcnxset=False, txid=None,
- checkshuttingdown=True):
+ def _get_session(self, sessionid, txid=None, checkshuttingdown=True):
"""return the session associated with the given session identifier"""
if checkshuttingdown and self.shutting_down:
raise ShuttingDown('Repository is shutting down')
@@ -935,9 +725,6 @@
session = self._sessions[sessionid]
except KeyError:
raise BadConnectionId('No such session %s' % sessionid)
- if setcnxset:
- session.set_cnx(txid) # must be done before set_cnxset
- session.set_cnxset()
return session
# data sources handling ###################################################
@@ -977,11 +764,11 @@
"""return the type of the entity with id <eid>"""
return self.type_and_source_from_eid(eid, cnx)[0]
- def querier_cache_key(self, session, rql, args, eidkeys):
+ def querier_cache_key(self, cnx, rql, args, eidkeys):
cachekey = [rql]
for key in sorted(eidkeys):
try:
- etype = self.type_from_eid(args[key], session)
+ etype = self.type_from_eid(args[key], cnx)
except KeyError:
raise QueryError('bad cache key %s (no value)' % key)
except TypeError:
@@ -1020,13 +807,7 @@
return self._extid_cache[extid]
except KeyError:
pass
- try:
- # bw compat: cnx may be a session, get at the Connection
- cnx = cnx._cnx
- except AttributeError:
- pass
- with cnx.ensure_cnx_set:
- eid = self.system_source.extid2eid(cnx, extid)
+ eid = self.system_source.extid2eid(cnx, extid)
if eid is not None:
self._extid_cache[extid] = eid
self._type_source_cache[eid] = (etype, extid, source.uri)
@@ -1034,123 +815,80 @@
if not insert:
return
# no link between extid and eid, create one
- with cnx.ensure_cnx_set:
- # write query, ensure connection's mode is 'write' so connections
- # won't be released until commit/rollback
- cnx.mode = 'write'
- try:
- eid = self.system_source.create_eid(cnx)
- self._extid_cache[extid] = eid
- self._type_source_cache[eid] = (etype, extid, source.uri)
- entity = source.before_entity_insertion(
- cnx, extid, etype, eid, sourceparams)
+ # write query, ensure connection's mode is 'write' so connections
+ # won't be released until commit/rollback
+ try:
+ eid = self.system_source.create_eid(cnx)
+ self._extid_cache[extid] = eid
+ self._type_source_cache[eid] = (etype, extid, source.uri)
+ entity = source.before_entity_insertion(
+ cnx, extid, etype, eid, sourceparams)
+ if source.should_call_hooks:
+ # get back a copy of operation for later restore if
+ # necessary, see below
+ pending_operations = cnx.pending_operations[:]
+ self.hm.call_hooks('before_add_entity', cnx, entity=entity)
+ self.add_info(cnx, entity, source, extid)
+ source.after_entity_insertion(cnx, extid, entity, sourceparams)
+ if source.should_call_hooks:
+ self.hm.call_hooks('after_add_entity', cnx, entity=entity)
+ return eid
+ except Exception:
+ # XXX do some cleanup manually so that the transaction has a
+ # chance to be commited, with simply this entity discarded
+ self._extid_cache.pop(extid, None)
+ self._type_source_cache.pop(eid, None)
+ if 'entity' in locals():
+ hook.CleanupDeletedEidsCacheOp.get_instance(cnx).add_data(entity.eid)
+ self.system_source.delete_info_multi(cnx, [entity])
if source.should_call_hooks:
- # get back a copy of operation for later restore if
- # necessary, see below
- pending_operations = cnx.pending_operations[:]
- self.hm.call_hooks('before_add_entity', cnx, entity=entity)
- self.add_info(cnx, entity, source, extid)
- source.after_entity_insertion(cnx, extid, entity, sourceparams)
- if source.should_call_hooks:
- self.hm.call_hooks('after_add_entity', cnx, entity=entity)
- return eid
- except Exception:
- # XXX do some cleanup manually so that the transaction has a
- # chance to be commited, with simply this entity discarded
- self._extid_cache.pop(extid, None)
- self._type_source_cache.pop(eid, None)
- if 'entity' in locals():
- hook.CleanupDeletedEidsCacheOp.get_instance(cnx).add_data(entity.eid)
- self.system_source.delete_info_multi(cnx, [entity])
- if source.should_call_hooks:
- cnx.pending_operations = pending_operations
- raise
+ cnx.pending_operations = pending_operations
+ raise
- def add_info(self, session, entity, source, extid=None):
+ def add_info(self, cnx, entity, source, extid=None):
"""add type and source info for an eid into the system table,
and index the entity with the full text index
"""
# begin by inserting eid/type/source/extid into the entities table
- hook.CleanupNewEidsCacheOp.get_instance(session).add_data(entity.eid)
- self.system_source.add_info(session, entity, source, extid)
+ hook.CleanupNewEidsCacheOp.get_instance(cnx).add_data(entity.eid)
+ self.system_source.add_info(cnx, entity, source, extid)
- def delete_info(self, session, entity, sourceuri):
- """called by external source when some entity known by the system source
- has been deleted in the external source
+ def _delete_cascade_multi(self, cnx, entities):
+ """same as _delete_cascade but accepts a list of entities with
+ the same etype and belonging to the same source.
"""
- # mark eid as being deleted in session info and setup cache update
- # operation
- hook.CleanupDeletedEidsCacheOp.get_instance(session).add_data(entity.eid)
- self._delete_info(session, entity, sourceuri)
-
- def _delete_info(self, session, entity, sourceuri):
- """delete system information on deletion of an entity:
-
- * delete all remaining relations from/to this entity
- * call delete info on the system source
- """
- pendingrtypes = session.transaction_data.get('pendingrtypes', ())
+ pendingrtypes = cnx.transaction_data.get('pendingrtypes', ())
# delete remaining relations: if user can delete the entity, he can
# delete all its relations without security checking
- with session.security_enabled(read=False, write=False):
- eid = entity.eid
- for rschema, _, role in entity.e_schema.relation_definitions():
- if rschema.rule:
- continue # computed relation
- rtype = rschema.type
- if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
- continue
- if role == 'subject':
- # don't skip inlined relation so they are regularly
- # deleted and so hooks are correctly called
- rql = 'DELETE X %s Y WHERE X eid %%(x)s' % rtype
- else:
- rql = 'DELETE Y %s X WHERE X eid %%(x)s' % rtype
- try:
- session.execute(rql, {'x': eid}, build_descr=False)
- except Exception:
- if self.config.mode == 'test':
+ with cnx.security_enabled(read=False, write=False):
+ in_eids = ','.join([str(_e.eid) for _e in entities])
+ with cnx.running_hooks_ops():
+ for rschema, _, role in entities[0].e_schema.relation_definitions():
+ if rschema.rule:
+ continue # computed relation
+ rtype = rschema.type
+ if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
+ continue
+ if role == 'subject':
+ # don't skip inlined relation so they are regularly
+ # deleted and so hooks are correctly called
+ 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)
+ try:
+ cnx.execute(rql, build_descr=False)
+ except ValidationError:
raise
- self.exception('error while cascading delete for entity %s '
- 'from %s. RQL: %s', entity, sourceuri, rql)
- self.system_source.delete_info_multi(session, [entity])
-
- def _delete_info_multi(self, session, entities):
- """same as _delete_info but accepts a list of entities with
- the same etype and belinging to the same source.
- """
- pendingrtypes = session.transaction_data.get('pendingrtypes', ())
- # delete remaining relations: if user can delete the entity, he can
- # delete all its relations without security checking
- with session.security_enabled(read=False, write=False):
- in_eids = ','.join([str(_e.eid) for _e in entities])
- for rschema, _, role in entities[0].e_schema.relation_definitions():
- if rschema.rule:
- continue # computed relation
- rtype = rschema.type
- if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
- continue
- if role == 'subject':
- # don't skip inlined relation so they are regularly
- # deleted and so hooks are correctly called
- 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)
- try:
- session.execute(rql, build_descr=False)
- except ValidationError:
- raise
- except Unauthorized:
- self.exception('Unauthorized exception while cascading delete for entity %s. '
- 'RQL: %s.\nThis should not happen since security is disabled here.',
- entities, rql)
- raise
- except Exception:
- if self.config.mode == 'test':
+ except Unauthorized:
+ self.exception('Unauthorized exception while cascading delete for entity %s. '
+ 'RQL: %s.\nThis should not happen since security is disabled here.',
+ entities, rql)
raise
- self.exception('error while cascading delete for entity %s. RQL: %s',
- entities, rql)
- self.system_source.delete_info_multi(session, entities)
+ except Exception:
+ if self.config.mode == 'test':
+ raise
+ self.exception('error while cascading delete for entity %s. RQL: %s',
+ entities, rql)
def init_entity_caches(self, cnx, entity, source):
"""add entity to connection entities cache and repo's extid cache.
@@ -1188,13 +926,13 @@
edited.set_defaults()
if cnx.is_hook_category_activated('integrity'):
edited.check(creation=True)
+ self.add_info(cnx, entity, source, extid)
try:
source.add_entity(cnx, entity)
- except UniqueTogetherError as exc:
+ except (UniqueTogetherError, ViolatedConstraint) as exc:
userhdlr = cnx.vreg['adapters'].select(
'IUserFriendlyError', cnx, entity=entity, exc=exc)
userhdlr.raise_user_exception()
- self.add_info(cnx, entity, source, extid)
edited.saved = entity._cw_is_saved = True
# trigger after_add_entity after after_add_relation
self.hm.call_hooks('after_add_entity', cnx, entity=entity)
@@ -1254,7 +992,7 @@
try:
source.update_entity(cnx, entity)
edited.saved = True
- except UniqueTogetherError as exc:
+ except (UniqueTogetherError, ViolatedConstraint) as exc:
userhdlr = cnx.vreg['adapters'].select(
'IUserFriendlyError', cnx, entity=entity, exc=exc)
userhdlr.raise_user_exception()
@@ -1309,8 +1047,9 @@
if server.DEBUG & server.DBG_REPO:
print 'DELETE entities', etype, [entity.eid for entity in entities]
self.hm.call_hooks('before_delete_entity', cnx, entities=entities)
- self._delete_info_multi(cnx, entities)
+ self._delete_cascade_multi(cnx, entities)
source.delete_entities(cnx, entities)
+ source.delete_info_multi(cnx, entities)
self.hm.call_hooks('after_delete_entity', cnx, entities=entities)
# don't clear cache here, it is done in a hook on commit
@@ -1341,7 +1080,7 @@
continue
# take care to relation of cardinality '?1', as all eids will
# be inserted later, we've remove duplicated eids since they
- # won't be catched by `del_existing_rel_if_needed`
+ # won't be caught by `del_existing_rel_if_needed`
rdef = cnx.rtype_eids_rdef(rtype, subjeid, objeid)
card = rdef.cardinality
if card[0] in '?1':
@@ -1392,79 +1131,12 @@
eidfrom=subject, rtype=rtype, eidto=object)
- # pyro handling ###########################################################
-
- @property
- @cached
- def pyro_appid(self):
- from logilab.common import pyro_ext as pyro
- config = self.config
- appid = '%s.%s' % pyro.ns_group_and_id(
- config['pyro-instance-id'] or config.appid,
- config['pyro-ns-group'])
- # ensure config['pyro-instance-id'] is a full qualified pyro name
- config['pyro-instance-id'] = appid
- return appid
-
- def _use_pyrons(self):
- """return True if the pyro-ns-host is set to something else
- than NO_PYRONS, meaning we want to go through a pyro
- nameserver"""
- return self.config['pyro-ns-host'] != 'NO_PYRONS'
-
- def pyro_register(self, host=''):
- """register the repository as a pyro object"""
- from logilab.common import pyro_ext as pyro
- daemon = pyro.register_object(self, self.pyro_appid,
- daemonhost=self.config['pyro-host'],
- nshost=self.config['pyro-ns-host'],
- use_pyrons=self._use_pyrons())
- self.info('repository registered as a pyro object %s', self.pyro_appid)
- self.pyro_uri = pyro.get_object_uri(self.pyro_appid)
- self.info('pyro uri is: %s', self.pyro_uri)
- self.pyro_registered = True
- # register a looping task to regularly ensure we're still registered
- # into the pyro name server
- if self._use_pyrons():
- self.looping_task(60*10, self._ensure_pyro_ns)
- pyro_sessions = self._pyro_sessions
- # install hacky function to free cnxset
- def handleConnection(conn, tcpserver, sessions=pyro_sessions):
- sessions[threading.currentThread()] = None
- return tcpserver.getAdapter().__class__.handleConnection(tcpserver.getAdapter(), conn, tcpserver)
- daemon.getAdapter().handleConnection = handleConnection
- def removeConnection(conn, sessions=pyro_sessions):
- daemon.__class__.removeConnection(daemon, conn)
- session = sessions.pop(threading.currentThread(), None)
- if session is None:
- # client was not yet connected to the repo
- return
- if not session.closed:
- self.close(session.sessionid)
- daemon.removeConnection = removeConnection
- return daemon
-
- def _ensure_pyro_ns(self):
- if not self._use_pyrons():
- return
- from logilab.common import pyro_ext as pyro
- pyro.ns_reregister(self.pyro_appid, nshost=self.config['pyro-ns-host'])
- self.info('repository re-registered as a pyro object %s',
- self.pyro_appid)
# these are overridden by set_log_methods below
# only defining here to prevent pylint from complaining
info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
-
-def pyro_unregister(config):
- """unregister the repository from the pyro name server"""
- from logilab.common.pyro_ext import ns_unregister
- appid = config['pyro-instance-id'] or config.appid
- ns_unregister(appid, config['pyro-ns-group'], config['pyro-ns-host'])
-
-
from logging import getLogger
from cubicweb import set_log_methods
set_log_methods(Repository, getLogger('cubicweb.repository'))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/schema2sql.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,293 @@
+# copyright 2004-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of cubicweb.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+"""write a schema as sql"""
+
+__docformat__ = "restructuredtext en"
+
+from hashlib import md5
+
+from six import string_types
+from six.moves import range
+
+from yams.constraints import (SizeConstraint, UniqueConstraint, Attribute,
+ NOW, TODAY)
+
+# default are usually not handled at the sql level. If you want them, set
+# SET_DEFAULT to True
+SET_DEFAULT = False
+
+def rschema_has_table(rschema, skip_relations):
+ """Return True if the given schema should have a table in the database"""
+ return not (rschema.final or rschema.inlined or rschema.rule or rschema.type in skip_relations)
+
+
+def schema2sql(dbhelper, schema, skip_entities=(), skip_relations=(), prefix=''):
+ """write to the output stream a SQL schema to store the objects
+ corresponding to the given schema
+ """
+ output = []
+ w = output.append
+ for etype in sorted(schema.entities()):
+ eschema = schema.eschema(etype)
+ if eschema.final or eschema.type in skip_entities:
+ continue
+ w(eschema2sql(dbhelper, eschema, skip_relations, prefix=prefix))
+ for rtype in sorted(schema.relations()):
+ rschema = schema.rschema(rtype)
+ if rschema_has_table(rschema, skip_relations):
+ w(rschema2sql(rschema))
+ return '\n'.join(output)
+
+
+def dropschema2sql(dbhelper, schema, skip_entities=(), skip_relations=(), prefix=''):
+ """write to the output stream a SQL schema to store the objects
+ corresponding to the given schema
+ """
+ output = []
+ w = output.append
+ for etype in sorted(schema.entities()):
+ eschema = schema.eschema(etype)
+ if eschema.final or eschema.type in skip_entities:
+ continue
+ stmts = dropeschema2sql(dbhelper, eschema, skip_relations, prefix=prefix)
+ for stmt in stmts:
+ w(stmt)
+ for rtype in sorted(schema.relations()):
+ rschema = schema.rschema(rtype)
+ if rschema_has_table(rschema, skip_relations):
+ w(droprschema2sql(rschema))
+ return '\n'.join(output)
+
+
+def eschema_attrs(eschema, skip_relations):
+ attrs = [attrdef for attrdef in eschema.attribute_definitions()
+ if not attrdef[0].type in skip_relations]
+ attrs += [(rschema, None)
+ for rschema in eschema.subject_relations()
+ if not rschema.final and rschema.inlined]
+ return attrs
+
+def unique_index_name(eschema, columns):
+ return u'unique_%s' % md5((eschema.type +
+ ',' +
+ ','.join(sorted(columns))).encode('ascii')).hexdigest()
+
+def iter_unique_index_names(eschema):
+ for columns in eschema._unique_together or ():
+ yield columns, unique_index_name(eschema, columns)
+
+def dropeschema2sql(dbhelper, eschema, skip_relations=(), prefix=''):
+ """return sql to drop an entity type's table"""
+ # not necessary to drop indexes, that's implictly done when
+ # dropping the table, but we need to drop SQLServer views used to
+ # create multicol unique indices
+ statements = []
+ tablename = prefix + eschema.type
+ if eschema._unique_together is not None:
+ for columns, index_name in iter_unique_index_names(eschema):
+ cols = ['%s%s' % (prefix, col) for col in columns]
+ sqls = dbhelper.sqls_drop_multicol_unique_index(tablename, cols, index_name)
+ statements += sqls
+ statements += ['DROP TABLE %s;' % (tablename)]
+ return statements
+
+
+def eschema2sql(dbhelper, eschema, skip_relations=(), prefix=''):
+ """write an entity schema as SQL statements to stdout"""
+ output = []
+ w = output.append
+ table = prefix + eschema.type
+ w('CREATE TABLE %s(' % (table))
+ attrs = eschema_attrs(eschema, skip_relations)
+ # XXX handle objectinline physical mode
+ for i in range(len(attrs)):
+ rschema, attrschema = attrs[i]
+ if attrschema is not None:
+ sqltype = aschema2sql(dbhelper, eschema, rschema, attrschema,
+ indent=' ')
+ else: # inline relation
+ sqltype = 'integer REFERENCES entities (eid)'
+ if i == len(attrs) - 1:
+ w(' %s%s %s' % (prefix, rschema.type, sqltype))
+ else:
+ w(' %s%s %s,' % (prefix, rschema.type, sqltype))
+ for rschema, aschema in attrs:
+ if aschema is None: # inline relation
+ continue
+ attr = rschema.type
+ rdef = rschema.rdef(eschema.type, aschema.type)
+ for constraint in rdef.constraints:
+ cstrname, check = check_constraint(eschema, aschema, attr, constraint, dbhelper, prefix=prefix)
+ if cstrname is not None:
+ w(', CONSTRAINT %s CHECK(%s)' % (cstrname, check))
+ w(');')
+ # create indexes
+ for i in range(len(attrs)):
+ rschema, attrschema = attrs[i]
+ if attrschema is None or eschema.rdef(rschema).indexed:
+ w(dbhelper.sql_create_index(table, prefix + rschema.type))
+ for columns, index_name in iter_unique_index_names(eschema):
+ cols = ['%s%s' % (prefix, col) for col in columns]
+ sqls = dbhelper.sqls_create_multicol_unique_index(table, cols, index_name)
+ for sql in sqls:
+ w(sql)
+ w('')
+ return '\n'.join(output)
+
+def as_sql(value, dbhelper, prefix):
+ if isinstance(value, Attribute):
+ return prefix + value.attr
+ elif isinstance(value, TODAY):
+ return dbhelper.sql_current_date()
+ elif isinstance(value, NOW):
+ return dbhelper.sql_current_timestamp()
+ else:
+ # XXX more quoting for literals?
+ return value
+
+def check_constraint(eschema, aschema, attr, constraint, dbhelper, prefix=''):
+ # XXX should find a better name
+ cstrname = 'cstr' + md5(eschema.type + attr + constraint.type() +
+ (constraint.serialize() or '')).hexdigest()
+ if constraint.type() == 'BoundaryConstraint':
+ value = as_sql(constraint.boundary, dbhelper, prefix)
+ return cstrname, '%s%s %s %s' % (prefix, attr, constraint.operator, value)
+ elif constraint.type() == 'IntervalBoundConstraint':
+ condition = []
+ if constraint.minvalue is not None:
+ value = as_sql(constraint.minvalue, dbhelper, prefix)
+ condition.append('%s%s >= %s' % (prefix, attr, value))
+ if constraint.maxvalue is not None:
+ value = as_sql(constraint.maxvalue, dbhelper, prefix)
+ condition.append('%s%s <= %s' % (prefix, attr, value))
+ return cstrname, ' AND '.join(condition)
+ elif constraint.type() == 'StaticVocabularyConstraint':
+ sample = next(iter(constraint.vocabulary()))
+ if not isinstance(sample, string_types):
+ values = ', '.join(str(word) for word in constraint.vocabulary())
+ else:
+ # XXX better quoting?
+ values = ', '.join("'%s'" % word.replace("'", "''") for word in constraint.vocabulary())
+ return cstrname, '%s%s IN (%s)' % (prefix, attr, values)
+ return None, None
+
+def aschema2sql(dbhelper, eschema, rschema, aschema, creating=True, indent=''):
+ """write an attribute schema as SQL statements to stdout"""
+ attr = rschema.type
+ rdef = rschema.rdef(eschema.type, aschema.type)
+ sqltype = type_from_constraints(dbhelper, aschema.type, rdef.constraints,
+ creating)
+ if SET_DEFAULT:
+ default = eschema.default(attr)
+ if default is not None:
+ if aschema.type == 'Boolean':
+ sqltype += ' DEFAULT %s' % dbhelper.boolean_value(default)
+ elif aschema.type == 'String':
+ sqltype += ' DEFAULT %r' % str(default)
+ elif aschema.type in ('Int', 'BigInt', 'Float'):
+ sqltype += ' DEFAULT %s' % default
+ # XXX ignore default for other type
+ # this is expected for NOW / TODAY
+ if creating:
+ if rdef.uid:
+ sqltype += ' PRIMARY KEY REFERENCES entities (eid)'
+ elif rdef.cardinality[0] == '1':
+ # don't set NOT NULL if backend isn't able to change it later
+ if dbhelper.alter_column_support:
+ sqltype += ' NOT NULL'
+ # else we're getting sql type to alter a column, we don't want key / indexes
+ # / null modifiers
+ return sqltype
+
+
+def type_from_constraints(dbhelper, etype, constraints, creating=True):
+ """return a sql type string corresponding to the constraints"""
+ constraints = list(constraints)
+ unique, sqltype = False, None
+ size_constrained_string = dbhelper.TYPE_MAPPING.get('SizeConstrainedString', 'varchar(%s)')
+ if etype == 'String':
+ for constraint in constraints:
+ if isinstance(constraint, SizeConstraint):
+ if constraint.max is not None:
+ sqltype = size_constrained_string % constraint.max
+ elif isinstance(constraint, UniqueConstraint):
+ unique = True
+ if sqltype is None:
+ sqltype = dbhelper.TYPE_MAPPING[etype]
+ if creating and unique:
+ sqltype += ' UNIQUE'
+ return sqltype
+
+
+_SQL_SCHEMA = """
+CREATE TABLE %(table)s (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT %(table)s_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX %(table)s_from_idx ON %(table)s(eid_from);
+CREATE INDEX %(table)s_to_idx ON %(table)s(eid_to);"""
+
+
+def rschema2sql(rschema):
+ assert not rschema.rule
+ return _SQL_SCHEMA % {'table': '%s_relation' % rschema.type}
+
+
+def droprschema2sql(rschema):
+ """return sql to drop a relation type's table"""
+ # not necessary to drop indexes, that's implictly done when dropping
+ # the table
+ return 'DROP TABLE %s_relation;' % rschema.type
+
+
+def grant_schema(schema, user, set_owner=True, skip_entities=(), prefix=''):
+ """write to the output stream a SQL schema to store the objects
+ corresponding to the given schema
+ """
+ output = []
+ w = output.append
+ for etype in sorted(schema.entities()):
+ eschema = schema.eschema(etype)
+ if eschema.final or etype in skip_entities:
+ continue
+ w(grant_eschema(eschema, user, set_owner, prefix=prefix))
+ for rtype in sorted(schema.relations()):
+ rschema = schema.rschema(rtype)
+ if rschema_has_table(rschema, skip_relations=()): # XXX skip_relations should be specified
+ w(grant_rschema(rschema, user, set_owner))
+ return '\n'.join(output)
+
+
+def grant_eschema(eschema, user, set_owner=True, prefix=''):
+ output = []
+ w = output.append
+ etype = eschema.type
+ if set_owner:
+ w('ALTER TABLE %s%s OWNER TO %s;' % (prefix, etype, user))
+ w('GRANT ALL ON %s%s TO %s;' % (prefix, etype, user))
+ return '\n'.join(output)
+
+
+def grant_rschema(rschema, user, set_owner=True):
+ output = []
+ if set_owner:
+ output.append('ALTER TABLE %s_relation OWNER TO %s;' % (rschema.type, user))
+ output.append('GRANT ALL ON %s_relation TO %s;' % (rschema.type, user))
+ return '\n'.join(output)
--- a/server/schemaserial.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/schemaserial.py Tue Jul 19 16:13:12 2016 +0200
@@ -25,13 +25,12 @@
from logilab.common.shellutils import ProgressBar, DummyProgressBar
-from yams import (BadSchemaDefinition, schema as schemamod, buildobjs as ybo,
- schema2sql as y2sql)
+from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo
from cubicweb import Binary
from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS, ETYPE_NAME_MAP,
VIRTUAL_RTYPES)
-from cubicweb.server import sqlutils
+from cubicweb.server import sqlutils, schema2sql as y2sql
def group_mapping(cnx, interactive=True):
@@ -93,14 +92,6 @@
with cnx.ensure_cnx_set:
tables = set(t.lower() for t in dbhelper.list_tables(cnx.cnxset.cu))
has_computed_relations = 'cw_cwcomputedrtype' in tables
- if has_computed_relations:
- rset = cnx.execute(
- 'Any X, N, R, D WHERE X is CWComputedRType, X name N, '
- 'X rule R, X description D')
- for eid, rule_name, rule, description in rset.rows:
- rtype = ybo.ComputedRelation(name=rule_name, rule=rule, eid=eid,
- description=description)
- schema.add_relation_type(rtype)
# computed attribute
try:
cnx.system_sql("SELECT cw_formula FROM cw_CWAttribute")
@@ -110,14 +101,13 @@
has_computed_attributes = False
# XXX bw compat (3.6 migration)
- with cnx.ensure_cnx_set:
- sqlcu = cnx.system_sql("SELECT * FROM cw_CWRType WHERE cw_name='symetric'")
- if sqlcu.fetchall():
- sql = dbhelper.sql_rename_col('cw_CWRType', 'cw_symetric', 'cw_symmetric',
- dbhelper.TYPE_MAPPING['Boolean'], True)
- sqlcu.execute(sql)
- sqlcu.execute("UPDATE cw_CWRType SET cw_name='symmetric' WHERE cw_name='symetric'")
- cnx.commit(False)
+ sqlcu = cnx.system_sql("SELECT * FROM cw_CWRType WHERE cw_name='symetric'")
+ if sqlcu.fetchall():
+ sql = dbhelper.sql_rename_col('cw_CWRType', 'cw_symetric', 'cw_symmetric',
+ dbhelper.TYPE_MAPPING['Boolean'], True)
+ sqlcu.execute(sql)
+ sqlcu.execute("UPDATE cw_CWRType SET cw_name='symmetric' WHERE cw_name='symetric'")
+ cnx.commit()
ertidx = {}
copiedeids = set()
permsidx = deserialize_ertype_permissions(cnx)
@@ -179,6 +169,15 @@
stype = ETYPE_NAME_MAP.get(stype, stype)
schema.eschema(etype)._specialized_type = stype
schema.eschema(stype)._specialized_by.append(etype)
+ if has_computed_relations:
+ rset = cnx.execute(
+ 'Any X, N, R, D WHERE X is CWComputedRType, X name N, '
+ 'X rule R, X description D')
+ for eid, rule_name, rule, description in rset.rows:
+ rtype = ybo.ComputedRelation(name=rule_name, rule=rule, eid=eid,
+ description=description)
+ rschema = schema.add_relation_type(rtype)
+ set_perms(rschema, permsidx)
# load every relation types
for eid, rtype, desc, sym, il, ftc in cnx.execute(
'Any X,N,D,S,I,FTC WHERE X is CWRType, X name N, X description D, '
@@ -377,7 +376,7 @@
pb.update()
continue
if rschema.rule:
- execschemarql(execute, rschema, crschema2rql(rschema))
+ execschemarql(execute, rschema, crschema2rql(rschema, groupmap))
pb.update()
continue
execschemarql(execute, rschema, rschema2rql(rschema, addrdef=False))
@@ -527,9 +526,12 @@
relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
return relations, values
-def crschema2rql(crschema):
+def crschema2rql(crschema, groupmap):
relations, values = crschema_relations_values(crschema)
yield 'INSERT CWComputedRType X: %s' % ','.join(relations), values
+ if groupmap:
+ for rql, args in _erperms2rql(crschema, groupmap):
+ yield rql, args
def crschema_relations_values(crschema):
values = _ervalues(crschema)
--- a/server/server.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,140 +0,0 @@
-# copyright 2003-2013 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/>.
-"""Pyro RQL server"""
-
-__docformat__ = "restructuredtext en"
-
-import select
-from time import localtime, mktime
-
-from cubicweb.server.utils import TasksManager
-from cubicweb.server.repository import Repository
-
-class Finished(Exception):
- """raise to remove an event from the event loop"""
-
-class TimeEvent:
- """base event"""
- # timefunc = staticmethod(localtime)
- timefunc = localtime
-
- def __init__(self, absolute=None, period=None):
- # local time tuple
- if absolute is None:
- absolute = self.timefunc()
- self.absolute = absolute
- # optional period in seconds
- self.period = period
-
- def is_ready(self):
- """return true if the event is ready to be fired"""
- now = self.timefunc()
- if self.absolute <= now:
- return True
- return False
-
- def fire(self, server):
- """fire the event
- must be overridden by concrete events
- """
- raise NotImplementedError()
-
- def update(self):
- """update the absolute date for the event or raise a finished exception
- """
- if self.period is None:
- raise Finished
- self.absolute = localtime(mktime(self.absolute) + self.period)
-
-
-class QuitEvent(TimeEvent):
- """stop the server"""
- def fire(self, server):
- server.repo.shutdown()
- server.quiting = True
-
-
-class RepositoryServer(object):
-
- def __init__(self, config):
- """make the repository available as a PyRO object"""
- self.config = config
- self.repo = Repository(config, TasksManager())
- self.ns = None
- self.quiting = None
- # event queue
- self.events = []
-
- def add_event(self, event):
- """add an event to the loop"""
- self.info('adding event %s', event)
- self.events.append(event)
-
- def trigger_events(self):
- """trigger ready events"""
- for event in self.events[:]:
- if event.is_ready():
- self.info('starting event %s', event)
- event.fire(self)
- try:
- event.update()
- except Finished:
- self.events.remove(event)
-
- def run(self, req_timeout=5.0):
- """enter the service loop"""
- # start repository looping tasks
- self.repo.start_looping_tasks()
- while self.quiting is None:
- try:
- self.daemon.handleRequests(req_timeout)
- except select.error:
- continue
- finally:
- self.trigger_events()
-
- def quit(self):
- """stop the server"""
- self.add_event(QuitEvent())
-
- def connect(self, host='', port=0):
- """the connect method on the repository only register to pyro if
- necessary
- """
- self.daemon = self.repo.pyro_register(host)
-
- # server utilitities ######################################################
-
- def install_sig_handlers(self):
- """install signal handlers"""
- import signal
- self.info('installing signal handlers')
- signal.signal(signal.SIGINT, lambda x, y, s=self: s.quit())
- signal.signal(signal.SIGTERM, lambda x, y, s=self: s.quit())
-
-
- # these are overridden by set_log_methods below
- # only defining here to prevent pylint from complaining
- @classmethod
- def info(cls, msg, *a, **kw):
- pass
-
-from logging import getLogger
-from cubicweb import set_log_methods
-LOGGER = getLogger('cubicweb.reposerver')
-set_log_methods(RepositoryServer, LOGGER)
--- a/server/serverconfig.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/serverconfig.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -197,44 +197,6 @@
notified of every changes.',
'group': 'email', 'level': 2,
}),
- # pyro services config
- ('pyro-host',
- {'type' : 'string',
- 'default': None,
- 'help': 'Pyro server host, if not detectable correctly through \
-gethostname(). It may contains port information using <host>:<port> notation, \
-and if not set, it will be choosen randomly',
- 'group': 'pyro', 'level': 3,
- }),
- ('pyro-instance-id',
- {'type' : 'string',
- 'default': lgconfig.Method('default_instance_id'),
- 'help': 'identifier of the CubicWeb instance in the Pyro name server',
- 'group': 'pyro', 'level': 1,
- }),
- ('pyro-ns-host',
- {'type' : 'string',
- 'default': '',
- 'help': 'Pyro name server\'s host. If not set, will be detected by a \
-broadcast query. It may contains port information using <host>:<port> notation. \
-Use "NO_PYRONS" to create a Pyro server but not register to a pyro nameserver',
- 'group': 'pyro', 'level': 1,
- }),
- ('pyro-ns-group',
- {'type' : 'string',
- 'default': 'cubicweb',
- 'help': 'Pyro name server\'s group where the repository will be \
-registered.',
- 'group': 'pyro', 'level': 1,
- }),
- # zmq services config
- ('zmq-repository-address',
- {'type' : 'string',
- 'default': None,
- 'help': ('ZMQ URI on which the repository will be bound '
- 'to (of the form `zmqpickle-tcp://<ipaddr>:<port>`).'),
- 'group': 'zmq', 'level': 3,
- }),
('zmq-address-sub',
{'type' : 'csv',
'default' : (),
@@ -350,10 +312,6 @@
stream.write('[%s]\n%s\n' % (section, generate_source_config(sconfig)))
restrict_perms_to_user(sourcesfile)
- def pyro_enabled(self):
- """pyro is always enabled in standalone repository configuration"""
- return True
-
def load_schema(self, expand_cubes=False, **kwargs):
from cubicweb.schema import CubicWebSchemaLoader
if expand_cubes:
@@ -387,6 +345,3 @@
return ServerMigrationHelper(self, schema, interactive=interactive,
cnx=cnx, repo=repo, connect=connect,
verbosity=verbosity)
-
-
-CONFIGURATIONS.append(ServerConfiguration)
--- a/server/serverctl.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/serverctl.py Tue Jul 19 16:13:12 2016 +0200
@@ -38,7 +38,6 @@
from cubicweb.toolsutils import Command, CommandHandler, underline_title
from cubicweb.cwctl import CWCTL, check_options_consistency, ConfigureInstanceCommand
from cubicweb.server import SOURCE_TYPES
-from cubicweb.server.repository import Repository
from cubicweb.server.serverconfig import (
USER_OPTIONS, ServerConfiguration, SourceConfiguration,
ask_source_config, generate_source_config)
@@ -167,11 +166,6 @@
if not automatic:
print underline_title('Configuring the repository')
config.input_config('email', inputlevel)
- # ask for pyro configuration if pyro is activated and we're not
- # using a all-in-one config, in which case this is done by the web
- # side command handler
- if config.pyro_enabled() and config.name != 'all-in-one':
- config.input_config('pyro', inputlevel)
print '\n'+underline_title('Configuring the sources')
sourcesfile = config.sources_file()
# hack to make Method('default_instance_id') usable in db option defs
@@ -301,33 +295,6 @@
raise ExecutionError(str(exc))
-class RepositoryStartHandler(CommandHandler):
- cmdname = 'start'
- cfgname = 'repository'
-
- def start_server(self, config):
- command = ['cubicweb-ctl', 'start-repository']
- if config.debugmode:
- command.append('--debug')
- command.append('--loglevel')
- command.append(config['log-threshold'].lower())
- command.append(config.appid)
- subprocess.call(command)
- return 1
-
-
-class RepositoryStopHandler(CommandHandler):
- cmdname = 'stop'
- cfgname = 'repository'
-
- def poststop(self):
- """if pyro is enabled, ensure the repository is correctly unregistered
- """
- if self.config.pyro_enabled():
- from cubicweb.server.repository import pyro_unregister
- pyro_unregister(self.config)
-
-
# repository specific commands ################################################
def createdb(helper, source, dbcnx, cursor, **kwargs):
@@ -530,51 +497,55 @@
appid = args[0]
config = ServerConfiguration.config_for(appid)
repo, cnx = repo_cnx(config)
- with cnx:
- used = set(n for n, in cnx.execute('Any SN WHERE S is CWSource, S name SN'))
- cubes = repo.get_cubes()
- while True:
- type = raw_input('source type (%s): '
- % ', '.join(sorted(SOURCE_TYPES)))
- if type not in SOURCE_TYPES:
- print '-> unknown source type, use one of the available types.'
- continue
- sourcemodule = SOURCE_TYPES[type].module
- if not sourcemodule.startswith('cubicweb.'):
- # module names look like cubes.mycube.themodule
- sourcecube = SOURCE_TYPES[type].module.split('.', 2)[1]
- # if the source adapter is coming from an external component,
- # ensure it's specified in used cubes
- if not sourcecube in cubes:
- print ('-> this source type require the %s cube which is '
- 'not used by the instance.')
+ repo.hm.call_hooks('server_maintenance', repo=repo)
+ try:
+ with cnx:
+ used = set(n for n, in cnx.execute('Any SN WHERE S is CWSource, S name SN'))
+ cubes = repo.get_cubes()
+ while True:
+ type = raw_input('source type (%s): '
+ % ', '.join(sorted(SOURCE_TYPES)))
+ if type not in SOURCE_TYPES:
+ print '-> unknown source type, use one of the available types.'
continue
- break
- while True:
- parser = raw_input('parser type (%s): '
- % ', '.join(sorted(repo.vreg['parsers'])))
- if parser in repo.vreg['parsers']:
+ sourcemodule = SOURCE_TYPES[type].module
+ if not sourcemodule.startswith('cubicweb.'):
+ # module names look like cubes.mycube.themodule
+ sourcecube = SOURCE_TYPES[type].module.split('.', 2)[1]
+ # if the source adapter is coming from an external component,
+ # ensure it's specified in used cubes
+ if not sourcecube in cubes:
+ print ('-> this source type require the %s cube which is '
+ 'not used by the instance.')
+ continue
break
- print '-> unknown parser identifier, use one of the available types.'
- while True:
- sourceuri = raw_input('source identifier (a unique name used to '
- 'tell sources apart): ').strip()
- if not sourceuri:
- print '-> mandatory.'
- else:
- sourceuri = unicode(sourceuri, sys.stdin.encoding)
- if sourceuri in used:
- print '-> uri already used, choose another one.'
+ while True:
+ parser = raw_input('parser type (%s): '
+ % ', '.join(sorted(repo.vreg['parsers'])))
+ if parser in repo.vreg['parsers']:
+ break
+ print '-> unknown parser identifier, use one of the available types.'
+ while True:
+ sourceuri = raw_input('source identifier (a unique name used to '
+ 'tell sources apart): ').strip()
+ if not sourceuri:
+ print '-> mandatory.'
else:
- break
- url = raw_input('source URL (leave empty for none): ').strip()
- url = unicode(url) if url else None
- # XXX configurable inputlevel
- sconfig = ask_source_config(config, type, inputlevel=self.config.config_level)
- cfgstr = unicode(generate_source_config(sconfig), sys.stdin.encoding)
- cnx.create_entity('CWSource', name=sourceuri, type=unicode(type),
- config=cfgstr, parser=unicode(parser), url=unicode(url))
- cnx.commit()
+ sourceuri = unicode(sourceuri, sys.stdin.encoding)
+ if sourceuri in used:
+ print '-> uri already used, choose another one.'
+ else:
+ break
+ url = raw_input('source URL (leave empty for none): ').strip()
+ url = unicode(url) if url else None
+ # XXX configurable inputlevel
+ sconfig = ask_source_config(config, type, inputlevel=self.config.config_level)
+ cfgstr = unicode(generate_source_config(sconfig), sys.stdin.encoding)
+ cnx.create_entity('CWSource', name=sourceuri, type=unicode(type),
+ config=cfgstr, parser=unicode(parser), url=unicode(url))
+ cnx.commit()
+ finally:
+ repo.hm.call_hooks('server_shutdown')
class GrantUserOnInstanceCommand(Command):
@@ -686,77 +657,6 @@
cnx.close()
-class StartRepositoryCommand(Command):
- """Start a CubicWeb RQL server for a given instance.
-
- The server will be remotely accessible through pyro or ZMQ
-
- <instance>
- the identifier of the instance to initialize.
- """
- name = 'start-repository'
- arguments = '<instance>'
- min_args = max_args = 1
- options = (
- ('debug',
- {'short': 'D', 'action' : 'store_true',
- 'help': 'start server in debug mode.'}),
- ('loglevel',
- {'short': 'l', 'type' : 'choice', 'metavar': '<log level>',
- 'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
- 'help': 'debug if -D is set, error otherwise',
- }),
- ('address',
- {'short': 'a', 'type': 'string', 'metavar': '<protocol>://<host>:<port>',
- 'default': '',
- 'help': ('specify a ZMQ URI on which to bind, or use "pyro://"'
- 'to create a pyro-based repository'),
- }),
- )
-
- def create_repo(self, config):
- address = self['address']
- if not address:
- address = config.get('zmq-repository-address') or 'pyro://'
- if address.startswith('pyro://'):
- from cubicweb.server.server import RepositoryServer
- return RepositoryServer(config), config['host']
- else:
- from cubicweb.server.utils import TasksManager
- from cubicweb.server.cwzmq import ZMQRepositoryServer
- repo = Repository(config, TasksManager())
- return ZMQRepositoryServer(repo), address
-
- def run(self, args):
- from logilab.common.daemon import daemonize, setugid
- from cubicweb.cwctl import init_cmdline_log_threshold
- print 'WARNING: Standalone repository with pyro or zmq access is deprecated'
- appid = args[0]
- debug = self['debug']
- if sys.platform == 'win32' and not debug:
- logger = logging.getLogger('cubicweb.ctl')
- logger.info('Forcing debug mode on win32 platform')
- debug = True
- config = ServerConfiguration.config_for(appid, debugmode=debug)
- init_cmdline_log_threshold(config, self['loglevel'])
- # create the server
- server, address = self.create_repo(config)
- # ensure the directory where the pid-file should be set exists (for
- # instance /var/run/cubicweb may be deleted on computer restart)
- pidfile = config['pid-file']
- piddir = os.path.dirname(pidfile)
- # go ! (don't daemonize in debug mode)
- if not os.path.exists(piddir):
- os.makedirs(piddir)
- if not debug and daemonize(pidfile, umask=config['umask']):
- return
- uid = config['uid']
- if uid is not None:
- setugid(uid)
- server.install_sig_handlers()
- server.connect(address)
- server.run()
-
def _remote_dump(host, appid, output, sudo=False):
# XXX generate unique/portable file name
@@ -1061,7 +961,7 @@
config = ServerConfiguration.config_for(appid)
repo, cnx = repo_cnx(config)
with cnx:
- reindex_entities(repo.schema, cnx._cnx, etypes=etypes)
+ reindex_entities(repo.schema, cnx, etypes=etypes)
cnx.commit()
@@ -1084,20 +984,23 @@
)
def run(self, args):
+ from cubicweb import repoapi
from cubicweb.cwctl import init_cmdline_log_threshold
config = ServerConfiguration.config_for(args[0])
config.global_set_option('log-file', None)
config.log_format = '%(levelname)s %(name)s: %(message)s'
init_cmdline_log_threshold(config, self['loglevel'])
- # only retrieve cnx to trigger authentication, close it right away
- repo, cnx = repo_cnx(config)
- cnx.close()
+ repo = repoapi.get_repository(config=config)
+ repo.hm.call_hooks('server_maintenance', repo=repo)
try:
- source = repo.sources_by_uri[args[1]]
- except KeyError:
- raise ExecutionError('no source named %r' % args[1])
- session = repo.internal_session()
- stats = source.pull_data(session, force=True, raise_on_error=True)
+ try:
+ source = repo.sources_by_uri[args[1]]
+ except KeyError:
+ raise ExecutionError('no source named %r' % args[1])
+ with repo.internal_cnx() as cnx:
+ stats = source.pull_data(cnx, force=True, raise_on_error=True)
+ finally:
+ repo.shutdown()
for key, val in stats.iteritems():
if val:
print key, ':', val
@@ -1135,18 +1038,17 @@
def run(self, args):
from yams.diff import schema_diff
+ from cubicweb import repoapi
appid = args.pop(0)
diff_tool = args.pop(0)
config = ServerConfiguration.config_for(appid)
- repo, cnx = repo_cnx(config)
- cnx.close()
+ repo = repoapi.get_repository(config=config)
fsschema = config.load_schema(expand_cubes=True)
schema_diff(fsschema, repo.schema, permissionshandler, diff_tool, ignore=('eid',))
for cmdclass in (CreateInstanceDBCommand, InitInstanceCommand,
GrantUserOnInstanceCommand, ResetAdminPasswordCommand,
- StartRepositoryCommand,
DBDumpCommand, DBRestoreCommand, DBCopyCommand,
AddSourceCommand, CheckRepositoryCommand, RebuildFTICommand,
SynchronizeSourceCommand, SchemaDiffCommand,
--- a/server/session.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/session.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -19,7 +19,6 @@
__docformat__ = "restructuredtext en"
import sys
-import threading
from time import time
from uuid import uuid4
from warnings import warn
@@ -34,7 +33,6 @@
from cubicweb.req import RequestSessionBase
from cubicweb.utils import make_uid
from cubicweb.rqlrewrite import RQLRewriter
-from cubicweb.server import ShuttingDown
from cubicweb.server.edition import EditedEntity
@@ -68,27 +66,6 @@
return req.vreg.config.repairing
-class transaction(object):
- """Ensure that the transaction is either commited or rolled back at exit
-
- Context manager to enter a transaction for a session: when exiting the
- `with` block on exception, call `session.rollback()`, else call
- `session.commit()` on normal exit
- """
- def __init__(self, session, free_cnxset=True):
- self.session = session
- self.free_cnxset = free_cnxset
-
- def __enter__(self):
- # ensure session has a cnxset
- self.session.set_cnxset()
-
- def __exit__(self, exctype, exc, traceback):
- if exctype:
- self.session.rollback(free_cnxset=self.free_cnxset)
- else:
- self.session.commit(free_cnxset=self.free_cnxset)
-
@deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead')
def hooks_control(obj, mode, *categories):
assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
@@ -98,8 +75,7 @@
return obj.deny_all_hooks_but(*categories)
-class _hooks_control(object): # XXX repoapi: remove me when
- # session stop being connection
+class _hooks_control(object):
"""context manager to control activated hooks categories.
If mode is `HOOKS_DENY_ALL`, given hooks categories will
@@ -149,24 +125,6 @@
finally:
self.cnx.hooks_mode = self.oldmode
-class _session_hooks_control(_hooks_control): # XXX repoapi: remove me when
- # session stop being connection
- """hook control context manager for session
-
- Necessary to handle some unholy transaction scope logic."""
-
-
- def __init__(self, session, mode, *categories):
- self.session = session
- super_init = super(_session_hooks_control, self).__init__
- super_init(session._cnx, mode, *categories)
-
- def __exit__(self, exctype, exc, traceback):
- super_exit = super(_session_hooks_control, self).__exit__
- ret = super_exit(exctype, exc, traceback)
- if self.cnx.ctx_count == 0:
- self.session._close_cnx(self.cnx)
- return ret
@deprecated('[3.17] use <object>.security_enabled instead')
def security_enabled(obj, *args, **kwargs):
@@ -205,24 +163,6 @@
if self.oldwrite is not None:
self.cnx.write_security = self.oldwrite
-class _session_security_enabled(_security_enabled):
- """hook security context manager for session
-
- Necessary To handle some unholy transaction scope logic."""
-
-
- def __init__(self, session, read=None, write=None):
- self.session = session
- super_init = super(_session_security_enabled, self).__init__
- super_init(session._cnx, read=read, write=write)
-
- def __exit__(self, exctype, exc, traceback):
- super_exit = super(_session_security_enabled, self).__exit__
- ret = super_exit(exctype, exc, traceback)
- if self.cnx.ctx_count == 0:
- self.session._close_cnx(self.cnx)
- return ret
-
HOOKS_ALLOW_ALL = object()
HOOKS_DENY_ALL = object()
DEFAULT_SECURITY = object() # evaluated to true by design
@@ -230,146 +170,6 @@
class SessionClosedError(RuntimeError):
pass
-class CnxSetTracker(object):
- """Keep track of which connection use which cnxset.
-
- There should be one of these objects per session (including internal sessions).
-
- Session objects are responsible for creating their CnxSetTracker object.
-
- Connections should use the :meth:`record` and :meth:`forget` to inform the
- tracker of cnxsets they have acquired.
-
- .. automethod:: cubicweb.server.session.CnxSetTracker.record
- .. automethod:: cubicweb.server.session.CnxSetTracker.forget
-
- Sessions use the :meth:`close` and :meth:`wait` methods when closing.
-
- .. automethod:: cubicweb.server.session.CnxSetTracker.close
- .. automethod:: cubicweb.server.session.CnxSetTracker.wait
-
- This object itself is threadsafe. It also requires caller to acquired its
- lock in some situation.
- """
-
- def __init__(self):
- self._active = True
- self._condition = threading.Condition()
- self._record = {}
-
- def __enter__(self):
- return self._condition.__enter__()
-
- def __exit__(self, *args):
- return self._condition.__exit__(*args)
-
- def record(self, cnxid, cnxset):
- """Inform the tracker that a cnxid has acquired a cnxset
-
- This method is to be used by Connection objects.
-
- This method fails when:
- - The cnxid already has a recorded cnxset.
- - The tracker is not active anymore.
-
- Notes about the caller:
- (1) It is responsible for retrieving a cnxset.
- (2) It must be prepared to release the cnxset if the
- `cnxsettracker.forget` call fails.
- (3) It should acquire the tracker lock until the very end of the operation.
- (4) However it must only lock the CnxSetTracker object after having
- retrieved the cnxset to prevent deadlock.
-
- A typical usage look like::
-
- cnxset = repo._get_cnxset() # (1)
- try:
- with cnxset_tracker: # (3) and (4)
- cnxset_tracker.record(caller.id, cnxset)
- # (3') operation ends when caller is in expected state only
- caller.cnxset = cnxset
- except Exception:
- repo._free_cnxset(cnxset) # (2)
- raise
- """
- # dubious since the caller is supposed to have acquired it anyway.
- with self._condition:
- if not self._active:
- raise SessionClosedError('Closed')
- old = self._record.get(cnxid)
- if old is not None:
- raise ValueError('connection "%s" already has a cnx_set (%r)'
- % (cnxid, old))
- self._record[cnxid] = cnxset
-
- def forget(self, cnxid, cnxset):
- """Inform the tracker that a cnxid have release a cnxset
-
- This methode is to be used by Connection object.
-
- This method fails when:
- - The cnxset for the cnxid does not match the recorded one.
-
- Notes about the caller:
- (1) It is responsible for releasing the cnxset.
- (2) It should acquire the tracker lock during the operation to ensure
- the internal tracker state is always accurate regarding its own state.
-
- A typical usage look like::
-
- cnxset = caller.cnxset
- try:
- with cnxset_tracker:
- # (2) you can not have caller.cnxset out of sync with
- # cnxset_tracker state while unlocked
- caller.cnxset = None
- cnxset_tracker.forget(caller.id, cnxset)
- finally:
- cnxset = repo._free_cnxset(cnxset) # (1)
- """
- with self._condition:
- old = self._record.get(cnxid, None)
- if old is not cnxset:
- raise ValueError('recorded cnxset for "%s" mismatch: %r != %r'
- % (cnxid, old, cnxset))
- self._record.pop(cnxid)
- self._condition.notify_all()
-
- def close(self):
- """Marks the tracker as inactive.
-
- This method is to be used by Session objects.
-
- An inactive tracker does not accept new records anymore.
- """
- with self._condition:
- self._active = False
-
- def wait(self, timeout=10):
- """Wait for all recorded cnxsets to be released
-
- This method is to be used by Session objects.
-
- Returns a tuple of connection ids that remain open.
- """
- with self._condition:
- if self._active:
- raise RuntimeError('Cannot wait on active tracker.'
- ' Call tracker.close() first')
- while self._record and timeout > 0:
- start = time()
- self._condition.wait(timeout)
- timeout -= time() - start
- return tuple(self._record)
-
-
-def _with_cnx_set(func):
- """decorator for Connection method that ensure they run with a cnxset """
- @functools.wraps(func)
- def wrapper(cnx, *args, **kwargs):
- with cnx.ensure_cnx_set:
- return func(cnx, *args, **kwargs)
- return wrapper
def _open_only(func):
"""decorator for Connection method that check it is open"""
@@ -389,8 +189,9 @@
Database connection resources:
- :attr:`running_dbapi_query`, boolean flag telling if the executing query
- is coming from a dbapi connection or is a query from within the repository
+ :attr:`hooks_in_progress`, boolean flag telling if the executing
+ query is coming from a repoapi connection or is a query from
+ within the repository (e.g. started by hooks)
:attr:`cnxset`, the connections set to use to execute queries on sources.
If the transaction is read only, the connection set may be freed between
@@ -406,12 +207,18 @@
'transaction' (we want to keep the connections set during all the
transaction, with or without writing)
- Internal transaction data:
+ Shared data:
- :attr:`data` is a dictionary containing some shared data
- cleared at the end of the transaction. Hooks and operations may put
- arbitrary data in there, and this may also be used as a communication
- channel between the client and the repository.
+ :attr:`data` is a dictionary bound to the underlying session,
+ who will be present for the life time of the session. This may
+ be useful for web clients that rely on the server for managing
+ bits of session-scoped data.
+
+ :attr:`transaction_data` is a dictionary cleared at the end of
+ the transaction. Hooks and operations may put arbitrary data in
+ there.
+
+ Internal state:
:attr:`pending_operations`, ordered list of operations to be processed on
commit/rollback
@@ -438,33 +245,20 @@
read/write security is currently activated.
"""
+ is_request = False
+ hooks_in_progress = False
+ is_repo_in_memory = True # bw compat
- is_request = False
-
- def __init__(self, session, cnxid=None, session_handled=False):
+ def __init__(self, session):
# using super(Connection, self) confuse some test hack
RequestSessionBase.__init__(self, session.vreg)
- # only the session provide explicite
- if cnxid is not None:
- assert session_handled # only session profive explicite cnxid
#: connection unique id
self._open = None
- if cnxid is None:
- cnxid = '%s-%s' % (session.sessionid, uuid4().hex)
- self.connectionid = cnxid
+ self.connectionid = '%s-%s' % (session.sessionid, uuid4().hex)
+ self.session = session
self.sessionid = session.sessionid
- #: self._session_handled
- #: are the life cycle of this Connection automatically controlled by the
- #: Session This is the old backward compatibility mode
- self._session_handled = session_handled
#: reentrance handling
self.ctx_count = 0
- #: count the number of entry in a context needing a cnxset
- self._cnxset_count = 0
- #: Boolean for compat with the older explicite set_cnxset/free_cnx API
- #: When a call set_cnxset is done, no automatic freeing will be done
- #: until free_cnx is called.
- self._auto_free_cnx_set = True
#: server.Repository object
self.repo = session.repo
@@ -474,16 +268,8 @@
# other session utility
self._session_timestamp = session._timestamp
- #: connection handling mode
- self.mode = session.default_mode
- #: connection set used to execute queries on sources
- self._cnxset = None
- #: CnxSetTracker used to report cnxset usage
- self._cnxset_tracker = session._cnxset_tracker
- #: is this connection from a client or internal to the repo
- self.running_dbapi_query = True
# internal (root) session
- self.is_internal_session = session.is_internal_session
+ self.is_internal_session = isinstance(session.user, InternalManager)
#: dict containing arbitrary data cleared at the end of the transaction
self.transaction_data = {}
@@ -506,7 +292,7 @@
# undo control
config = session.repo.config
- if config.creating or config.repairing or session.is_internal_session:
+ if config.creating or config.repairing or self.is_internal_session:
self.undo_actions = False
else:
self.undo_actions = config['undo-enabled']
@@ -521,21 +307,109 @@
else:
self._set_user(session.user)
+ @_open_only
+ def source_defs(self):
+ """Return the definition of sources used by the repository."""
+ return self.session.repo.source_defs()
- # live cycle handling ####################################################
+ @_open_only
+ def get_schema(self):
+ """Return the schema currently used by the repository."""
+ return self.session.repo.source_defs()
+
+ @_open_only
+ def get_option_value(self, option):
+ """Return the value for `option` in the configuration."""
+ return self.session.repo.get_option_value(option)
+
+ # transaction api
+
+ @_open_only
+ def undoable_transactions(self, ueid=None, **actionfilters):
+ """Return a list of undoable transaction objects by the connection's
+ user, ordered by descendant transaction time.
+
+ Managers may filter according to user (eid) who has done the transaction
+ using the `ueid` argument. Others will only see their own transactions.
+
+ Additional filtering capabilities is provided by using the following
+ named arguments:
+
+ * `etype` to get only transactions creating/updating/deleting entities
+ of the given type
+
+ * `eid` to get only transactions applied to entity of the given eid
+
+ * `action` to get only transactions doing the given action (action in
+ 'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
+ 'D'.
+
+ * `public`: when additional filtering is provided, they are by default
+ only searched in 'public' actions, unless a `public` argument is given
+ and set to false.
+ """
+ return self.repo.system_source.undoable_transactions(self, ueid,
+ **actionfilters)
+
+ @_open_only
+ def transaction_info(self, txuuid):
+ """Return transaction object for the given uid.
+
+ raise `NoSuchTransaction` if not found or if session's user is
+ not allowed (eg not in managers group and the transaction
+ doesn't belong to him).
+ """
+ return self.repo.system_source.tx_info(self, txuuid)
+
+ @_open_only
+ def transaction_actions(self, txuuid, public=True):
+ """Return an ordered list of actions effectued during that transaction.
+
+ If public is true, return only 'public' actions, i.e. not ones
+ triggered under the cover by hooks, else return all actions.
+
+ raise `NoSuchTransaction` if the transaction is not found or
+ if the user is not allowed (eg not in managers group).
+ """
+ return self.repo.system_source.tx_actions(self, txuuid, public)
+
+ @_open_only
+ def undo_transaction(self, txuuid):
+ """Undo the given transaction. Return potential restoration errors.
+
+ raise `NoSuchTransaction` if not found or if user is not
+ allowed (eg not in managers group).
+ """
+ return self.repo.system_source.undo_transaction(self, txuuid)
+
+ # life cycle handling ####################################################
def __enter__(self):
assert self._open is None # first opening
self._open = True
+ self.cnxset = self.repo._get_cnxset()
return self
def __exit__(self, exctype=None, excvalue=None, tb=None):
assert self._open # actually already open
- assert self._cnxset_count == 0
self.rollback()
self._open = False
+ self.cnxset.cnxset_freed()
+ self.repo._free_cnxset(self.cnxset)
+ self.cnxset = None
+ @contextmanager
+ def running_hooks_ops(self):
+ """this context manager should be called whenever hooks or operations
+ are about to be run (but after hook selection)
+ It will help the undo logic record pertinent metadata or some
+ hooks to run (or not) depending on who/what issued the query.
+ """
+ prevmode = self.hooks_in_progress
+ self.hooks_in_progress = True
+ yield
+ self.hooks_in_progress = prevmode
# shared data handling ###################################################
@@ -580,83 +454,27 @@
self.local_perm_cache.clear()
self.rewriter = RQLRewriter(self)
- # Connection Set Management ###############################################
- @property
- @_open_only
- def cnxset(self):
- return self._cnxset
-
- @cnxset.setter
- @_open_only
- def cnxset(self, new_cnxset):
- with self._cnxset_tracker:
- old_cnxset = self._cnxset
- if new_cnxset is old_cnxset:
- return #nothing to do
- if old_cnxset is not None:
- old_cnxset.rollback()
- self._cnxset = None
- self.ctx_count -= 1
- self._cnxset_tracker.forget(self.connectionid, old_cnxset)
- if new_cnxset is not None:
- self._cnxset_tracker.record(self.connectionid, new_cnxset)
- self._cnxset = new_cnxset
- self.ctx_count += 1
-
- @_open_only
- def _set_cnxset(self):
- """the connection need a connections set to execute some queries"""
- if self.cnxset is None:
- cnxset = self.repo._get_cnxset()
- try:
- self.cnxset = cnxset
- except:
- self.repo._free_cnxset(cnxset)
- raise
- return self.cnxset
-
- @_open_only
- def _free_cnxset(self, ignoremode=False):
- """the connection is no longer using its connections set, at least for some time"""
- # cnxset may be none if no operation has been done since last commit
- # or rollback
- cnxset = self.cnxset
- if cnxset is not None and (ignoremode or self.mode == 'read'):
- assert self._cnxset_count == 0
- try:
- self.cnxset = None
- finally:
- cnxset.cnxset_freed()
- self.repo._free_cnxset(cnxset)
-
@deprecated('[3.19] cnxset are automatically managed now.'
' stop using explicit set and free.')
def set_cnxset(self):
- self._auto_free_cnx_set = False
- return self._set_cnxset()
+ pass
@deprecated('[3.19] cnxset are automatically managed now.'
' stop using explicit set and free.')
def free_cnxset(self, ignoremode=False):
- self._auto_free_cnx_set = True
- return self._free_cnxset(ignoremode=ignoremode)
-
+ pass
@property
@contextmanager
@_open_only
+ @deprecated('[3.21] a cnxset is automatically set on __enter__ call now.'
+ ' stop using .ensure_cnx_set')
def ensure_cnx_set(self):
- assert self._cnxset_count >= 0
- if self._cnxset_count == 0:
- self._set_cnxset()
- try:
- self._cnxset_count += 1
- yield
- finally:
- self._cnxset_count = max(self._cnxset_count - 1, 0)
- if self._cnxset_count == 0 and self._auto_free_cnx_set:
- self._free_cnxset()
+ yield
+ @property
+ def anonymous_connection(self):
+ return self.session.anonymous_session
# Entity cache management #################################################
#
@@ -939,27 +757,7 @@
@read_security.setter
@_open_only
def read_security(self, activated):
- oldmode = self._read_security
self._read_security = activated
- # running_dbapi_query used to detect hooks triggered by a 'dbapi' query
- # (eg not issued on the session). This is tricky since we the execution
- # model of a (write) user query is:
- #
- # repository.execute (security enabled)
- # \-> querier.execute
- # \-> repo.glob_xxx (add/update/delete entity/relation)
- # \-> deactivate security before calling hooks
- # \-> WE WANT TO CHECK QUERY NATURE HERE
- # \-> potentially, other calls to querier.execute
- #
- # so we can't rely on simply checking session.read_security, but
- # recalling the first transition from DEFAULT_SECURITY to something
- # else (False actually) is not perfect but should be enough
- #
- # also reset running_dbapi_query to true when we go back to
- # DEFAULT_SECURITY
- self.running_dbapi_query = (oldmode is DEFAULT_SECURITY
- or activated is DEFAULT_SECURITY)
# undo support ############################################################
@@ -971,7 +769,7 @@
def transaction_uuid(self, set=True):
uuid = self.transaction_data.get('tx_uuid')
if set and uuid is None:
- self.transaction_data['tx_uuid'] = uuid = uuid4().hex
+ self.transaction_data['tx_uuid'] = uuid = unicode(uuid4().hex)
self.repo.system_source.start_undoable_transaction(self, uuid)
return uuid
@@ -988,7 +786,6 @@
return self.repo.source_defs()
@deprecated('[3.19] use .entity_metas(eid) instead')
- @_with_cnx_set
@_open_only
def describe(self, eid, asdict=False):
"""return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
@@ -999,7 +796,6 @@
return metas
return etype, source, extid
- @_with_cnx_set
@_open_only
def entity_metas(self, eid):
"""return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
@@ -1008,7 +804,6 @@
# core method #############################################################
- @_with_cnx_set
@_open_only
def execute(self, rql, kwargs=None, build_descr=True):
"""db-api like method directly linked to the querier execute method.
@@ -1022,21 +817,16 @@
return rset
@_open_only
- def rollback(self, free_cnxset=True, reset_pool=None):
+ def rollback(self, free_cnxset=None, reset_pool=None):
"""rollback the current transaction"""
- if reset_pool is not None:
- warn('[3.13] use free_cnxset argument instead for reset_pool',
+ if free_cnxset is not None:
+ warn('[3.21] free_cnxset is now unneeded',
DeprecationWarning, stacklevel=2)
- free_cnxset = reset_pool
- if self._cnxset_count != 0:
- # we are inside ensure_cnx_set, don't lose it
- free_cnxset = False
+ if reset_pool is not None:
+ warn('[3.13] reset_pool is now unneeded',
+ DeprecationWarning, stacklevel=2)
cnxset = self.cnxset
- if cnxset is None:
- self.clear()
- self._session_timestamp.touch()
- self.debug('rollback transaction %s done (no db activity)', self.connectionid)
- return
+ assert cnxset is not None
try:
# by default, operations are executed with security turned off
with self.security_enabled(False, False):
@@ -1051,26 +841,18 @@
self.debug('rollback for transaction %s done', self.connectionid)
finally:
self._session_timestamp.touch()
- if free_cnxset:
- self._free_cnxset(ignoremode=True)
self.clear()
@_open_only
- def commit(self, free_cnxset=True, reset_pool=None):
+ def commit(self, free_cnxset=None, reset_pool=None):
"""commit the current session's transaction"""
- if reset_pool is not None:
- warn('[3.13] use free_cnxset argument instead for reset_pool',
+ if free_cnxset is not None:
+ warn('[3.21] free_cnxset is now unneeded',
DeprecationWarning, stacklevel=2)
- free_cnxset = reset_pool
- if self.cnxset is None:
- assert not self.pending_operations
- self.clear()
- self._session_timestamp.touch()
- self.debug('commit transaction %s done (no db activity)', self.connectionid)
- return
- if self._cnxset_count != 0:
- # we are inside ensure_cnx_set, don't lose it
- free_cnxset = False
+ if reset_pool is not None:
+ warn('[3.13] reset_pool is now unneeded',
+ DeprecationWarning, stacklevel=2)
+ assert self.cnxset is not None
cstate = self.commit_state
if cstate == 'uncommitable':
raise QueryError('transaction must be rolled back')
@@ -1094,13 +876,14 @@
if debug:
print self.commit_state, '*' * 20
try:
- while self.pending_operations:
- operation = self.pending_operations.pop(0)
- operation.processed = 'precommit'
- processed.append(operation)
- if debug:
- print operation
- operation.handle_event('precommit_event')
+ with self.running_hooks_ops():
+ while self.pending_operations:
+ operation = self.pending_operations.pop(0)
+ operation.processed = 'precommit'
+ processed.append(operation)
+ if debug:
+ print operation
+ operation.handle_event('precommit_event')
self.pending_operations[:] = processed
self.debug('precommit transaction %s done', self.connectionid)
except BaseException:
@@ -1117,56 +900,52 @@
operation.failed = True
if debug:
print self.commit_state, '*' * 20
- for operation in reversed(processed):
- if debug:
- print operation
- try:
- operation.handle_event('revertprecommit_event')
- except BaseException:
- self.critical('error while reverting precommit',
- exc_info=True)
+ with self.running_hooks_ops():
+ for operation in reversed(processed):
+ if debug:
+ print operation
+ try:
+ operation.handle_event('revertprecommit_event')
+ except BaseException:
+ self.critical('error while reverting precommit',
+ exc_info=True)
# XXX use slice notation since self.pending_operations is a
# read-only property.
self.pending_operations[:] = processed + self.pending_operations
- self.rollback(free_cnxset)
+ self.rollback()
raise
self.cnxset.commit()
self.commit_state = 'postcommit'
if debug:
print self.commit_state, '*' * 20
- while self.pending_operations:
- operation = self.pending_operations.pop(0)
- if debug:
- print operation
- operation.processed = 'postcommit'
- try:
- operation.handle_event('postcommit_event')
- except BaseException:
- self.critical('error while postcommit',
- exc_info=sys.exc_info())
+ with self.running_hooks_ops():
+ while self.pending_operations:
+ operation = self.pending_operations.pop(0)
+ if debug:
+ print operation
+ operation.processed = 'postcommit'
+ try:
+ operation.handle_event('postcommit_event')
+ except BaseException:
+ self.critical('error while postcommit',
+ exc_info=sys.exc_info())
self.debug('postcommit transaction %s done', self.connectionid)
return self.transaction_uuid(set=False)
finally:
self._session_timestamp.touch()
- if free_cnxset:
- self._free_cnxset(ignoremode=True)
self.clear()
# resource accessors ######################################################
- @_with_cnx_set
@_open_only
def call_service(self, regid, **kwargs):
self.debug('calling service %s', regid)
service = self.vreg['services'].select(regid, self, **kwargs)
return service.call(**kwargs)
- @_with_cnx_set
@_open_only
def system_sql(self, sql, args=None, rollback_on_failure=True):
"""return a sql cursor on the system database"""
- if sql.split(None, 1)[0].upper() != 'SELECT':
- self.mode = 'write'
source = self.repo.system_source
try:
return source.doexec(self, sql, args, rollback=rollback_on_failure)
@@ -1202,18 +981,6 @@
args['fset'] = write_attr
return property(**args)
-def cnx_meth(meth_name):
- """return a function forwarding calls to connection.
-
- This is to be used by session"""
- @deprecated('[3.19] use a Connection object instead')
- def meth_from_cnx(session, *args, **kwargs):
- result = getattr(session._cnx, meth_name)(*args, **kwargs)
- if getattr(result, '_cw', None) is not None:
- result._cw = session
- return result
- meth_from_cnx.__doc__ = getattr(Connection, meth_name).__doc__
- return meth_from_cnx
class Timestamp(object):
@@ -1227,149 +994,37 @@
return float(self.value)
-class Session(RequestSessionBase): # XXX repoapi: stop being a
- # RequestSessionBase at some point
+class Session(object):
"""Repository user session
This ties all together:
* session id,
* user,
- * connections set,
* other session data.
-
- **About session storage / transactions**
-
- Here is a description of internal session attributes. Besides :attr:`data`
- and :attr:`transaction_data`, you should not have to use attributes
- described here but higher level APIs.
-
- :attr:`data` is a dictionary containing shared data, used to communicate
- extra information between the client and the repository
-
- :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one
- for each running connection. The key is the connection id. By default
- the connection id is the thread name but it can be otherwise (per dbapi
- cursor for instance, or per thread name *from another process*).
-
- :attr:`__threaddata` is a thread local storage whose `cnx` attribute
- refers to the proper instance of :class:`Connection` according to the
- connection.
-
- You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`,
- simply access connection data transparently through the :attr:`_cnx`
- property. Also, you usually don't have to access it directly since current
- connection's data may be accessed/modified through properties / methods:
-
- :attr:`connection_data`, similarly to :attr:`data`, is a dictionary
- containing some shared data that should be cleared at the end of the
- connection. Hooks and operations may put arbitrary data in there, and
- this may also be used as a communication channel between the client and
- the repository.
-
- .. automethod:: cubicweb.server.session.Session.get_shared_data
- .. automethod:: cubicweb.server.session.Session.set_shared_data
- .. automethod:: cubicweb.server.session.Session.added_in_transaction
- .. automethod:: cubicweb.server.session.Session.deleted_in_transaction
-
- Connection state information:
-
- :attr:`running_dbapi_query`, boolean flag telling if the executing query
- is coming from a dbapi connection or is a query from within the repository
-
- :attr:`cnxset`, the connections set to use to execute queries on sources.
- During a transaction, the connection set may be freed so that is may be
- used by another session as long as no writing is done. This means we can
- have multiple sessions with a reasonably low connections set pool size.
-
- .. automethod:: cubicweb.server.session.Session.set_cnxset
- .. automethod:: cubicweb.server.session.Session.free_cnxset
-
- :attr:`mode`, string telling the connections set handling mode, may be one
- of 'read' (connections set may be freed), 'write' (some write was done in
- the connections set, it can't be freed before end of the transaction),
- 'transaction' (we want to keep the connections set during all the
- transaction, with or without writing)
-
- :attr:`pending_operations`, ordered list of operations to be processed on
- commit/rollback
-
- :attr:`commit_state`, describing the transaction commit state, may be one
- of None (not yet committing), 'precommit' (calling precommit event on
- operations), 'postcommit' (calling postcommit event on operations),
- 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
- has been raised during the transaction and so it must be rolled back).
-
- .. automethod:: cubicweb.server.session.Session.commit
- .. automethod:: cubicweb.server.session.Session.rollback
- .. automethod:: cubicweb.server.session.Session.close
- .. automethod:: cubicweb.server.session.Session.closed
-
- Security level Management:
-
- :attr:`read_security` and :attr:`write_security`, boolean flags telling if
- read/write security is currently activated.
-
- .. automethod:: cubicweb.server.session.Session.security_enabled
-
- Hooks Management:
-
- :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
-
- :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is
- `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
-
- :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is
- `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
-
- .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but
- .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but
- .. automethod:: cubicweb.server.session.Session.is_hook_category_activated
- .. automethod:: cubicweb.server.session.Session.is_hook_activated
-
- Data manipulation:
-
- .. automethod:: cubicweb.server.session.Session.add_relation
- .. automethod:: cubicweb.server.session.Session.add_relations
- .. automethod:: cubicweb.server.session.Session.delete_relation
-
- Other:
-
- .. automethod:: cubicweb.server.session.Session.call_service
-
-
-
"""
- is_request = False
- is_internal_session = False
def __init__(self, user, repo, cnxprops=None, _id=None):
- super(Session, self).__init__(repo.vreg)
self.sessionid = _id or make_uid(unormalize(user.login).encode('UTF8'))
self.user = user # XXX repoapi: deprecated and store only a login.
self.repo = repo
+ self.vreg = repo.vreg
self._timestamp = Timestamp()
- self.default_mode = 'read'
- # short cut to querier .execute method
- self._execute = repo.querier.execute
- # shared data, used to communicate extra information between the client
- # and the rql server
self.data = {}
- # i18n initialization
- self.set_language(user.prefered_language())
- ### internals
- # Connection of this section
- self._cnxs = {} # XXX repoapi: remove this when nobody use the session
- # as a Connection
- # Data local to the thread
- self.__threaddata = threading.local() # XXX repoapi: remove this when
- # nobody use the session as a Connection
- self._cnxset_tracker = CnxSetTracker()
- self._closed = False
- self._lock = threading.RLock()
+ self.closed = False
+
+ def close(self):
+ self.closed = True
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ pass
def __unicode__(self):
return '<session %s (%s 0x%x)>' % (
unicode(self.user.login), self.sessionid, id(self))
+
@property
def timestamp(self):
return float(self._timestamp)
@@ -1390,55 +1045,6 @@
"""
return Connection(self)
- def _get_cnx(self, cnxid):
- """return the <cnxid> connection attached to this session
-
- Connection is created if necessary"""
- with self._lock: # no connection exist with the same id
- try:
- if self.closed:
- raise SessionClosedError('try to access connections set on'
- ' a closed session %s' % self.id)
- cnx = self._cnxs[cnxid]
- assert cnx._session_handled
- except KeyError:
- cnx = Connection(self, cnxid=cnxid, session_handled=True)
- self._cnxs[cnxid] = cnx
- cnx.__enter__()
- return cnx
-
- def _close_cnx(self, cnx):
- """Close a Connection related to a session"""
- assert cnx._session_handled
- cnx.__exit__()
- self._cnxs.pop(cnx.connectionid, None)
- try:
- if self.__threaddata.cnx is cnx:
- del self.__threaddata.cnx
- except AttributeError:
- pass
-
- def set_cnx(self, cnxid=None):
- # XXX repoapi: remove this when nobody use the session as a Connection
- """set the default connection of the current thread to <cnxid>
-
- Connection is created if necessary"""
- if cnxid is None:
- cnxid = threading.currentThread().getName()
- cnx = self._get_cnx(cnxid)
- # New style session should not be accesed through the session.
- assert cnx._session_handled
- self.__threaddata.cnx = cnx
-
- @property
- def _cnx(self):
- """default connection for current session in current thread"""
- try:
- return self.__threaddata.cnx
- except AttributeError:
- self.set_cnx()
- return self.__threaddata.cnx
-
@deprecated('[3.19] use a Connection object instead')
def get_option_value(self, option, foreid=None):
if foreid is not None:
@@ -1446,108 +1052,6 @@
stacklevel=2)
return self.repo.get_option_value(option)
- @deprecated('[3.19] use a Connection object instead')
- def transaction(self, free_cnxset=True):
- """return context manager to enter a transaction for the session: when
- exiting the `with` block on exception, call `session.rollback()`, else
- call `session.commit()` on normal exit.
-
- The `free_cnxset` will be given to rollback/commit methods to indicate
- whether the connections set should be freed or not.
- """
- return transaction(self, free_cnxset)
-
- add_relation = cnx_meth('add_relation')
- add_relations = cnx_meth('add_relations')
- delete_relation = cnx_meth('delete_relation')
-
- # relations cache handling #################################################
-
- update_rel_cache_add = cnx_meth('update_rel_cache_add')
- update_rel_cache_del = cnx_meth('update_rel_cache_del')
-
- # resource accessors ######################################################
-
- system_sql = cnx_meth('system_sql')
- deleted_in_transaction = cnx_meth('deleted_in_transaction')
- added_in_transaction = cnx_meth('added_in_transaction')
- rtype_eids_rdef = cnx_meth('rtype_eids_rdef')
-
- # security control #########################################################
-
- @deprecated('[3.19] use a Connection object instead')
- def security_enabled(self, read=None, write=None):
- return _session_security_enabled(self, read=read, write=write)
-
- read_security = cnx_attr('read_security', writable=True)
- write_security = cnx_attr('write_security', writable=True)
- running_dbapi_query = cnx_attr('running_dbapi_query')
-
- # hooks activation control #################################################
- # all hooks should be activated during normal execution
-
-
- @deprecated('[3.19] use a Connection object instead')
- def allow_all_hooks_but(self, *categories):
- return _session_hooks_control(self, HOOKS_ALLOW_ALL, *categories)
- @deprecated('[3.19] use a Connection object instead')
- def deny_all_hooks_but(self, *categories):
- return _session_hooks_control(self, HOOKS_DENY_ALL, *categories)
-
- hooks_mode = cnx_attr('hooks_mode')
-
- disabled_hook_categories = cnx_attr('disabled_hook_cats')
- enabled_hook_categories = cnx_attr('enabled_hook_cats')
- disable_hook_categories = cnx_meth('disable_hook_categories')
- enable_hook_categories = cnx_meth('enable_hook_categories')
- is_hook_category_activated = cnx_meth('is_hook_category_activated')
- is_hook_activated = cnx_meth('is_hook_activated')
-
- # connection management ###################################################
-
- @deprecated('[3.19] use a Connection object instead')
- def keep_cnxset_mode(self, mode):
- """set `mode`, e.g. how the session will keep its connections set:
-
- * if mode == 'write', the connections set is freed after each read
- query, but kept until the transaction's end (eg commit or rollback)
- when a write query is detected (eg INSERT/SET/DELETE queries)
-
- * if mode == 'transaction', the connections set is only freed after the
- transaction's end
-
- notice that a repository has a limited set of connections sets, and a
- session has to wait for a free connections set to run any rql query
- (unless it already has one set).
- """
- assert mode in ('transaction', 'write')
- if mode == 'transaction':
- self.default_mode = 'transaction'
- else: # mode == 'write'
- self.default_mode = 'read'
-
- mode = cnx_attr('mode', writable=True)
- commit_state = cnx_attr('commit_state', writable=True)
-
- @property
- @deprecated('[3.19] use a Connection object instead')
- def cnxset(self):
- """connections set, set according to transaction mode for each query"""
- if self._closed:
- self.free_cnxset(True)
- raise SessionClosedError('try to access connections set on a closed session %s' % self.id)
- return self._cnx.cnxset
-
- def set_cnxset(self):
- """the session need a connections set to execute some queries"""
- with self._lock: # can probably be removed
- if self._closed:
- self.free_cnxset(True)
- raise SessionClosedError('try to set connections set on a closed session %s' % self.id)
- return self._cnx.set_cnxset()
- free_cnxset = cnx_meth('free_cnxset')
- ensure_cnx_set = cnx_attr('ensure_cnx_set')
-
def _touch(self):
"""update latest session usage timestamp and reset mode to read"""
self._timestamp.touch()
@@ -1559,156 +1063,6 @@
assert value == {}
pass
- # shared data handling ###################################################
-
- @deprecated('[3.19] use session or transaction data')
- def get_shared_data(self, key, default=None, pop=False, txdata=False):
- """return value associated to `key` in session data"""
- if txdata:
- return self._cnx.get_shared_data(key, default, pop, txdata=True)
- else:
- data = self.data
- if pop:
- return data.pop(key, default)
- else:
- return data.get(key, default)
-
- @deprecated('[3.19] use session or transaction data')
- def set_shared_data(self, key, value, txdata=False):
- """set value associated to `key` in session data"""
- if txdata:
- return self._cnx.set_shared_data(key, value, txdata=True)
- else:
- self.data[key] = value
-
- # server-side service call #################################################
-
- call_service = cnx_meth('call_service')
-
- # request interface #######################################################
-
- @property
- @deprecated('[3.19] use a Connection object instead')
- def cursor(self):
- """return a rql cursor"""
- return self
-
- set_entity_cache = cnx_meth('set_entity_cache')
- entity_cache = cnx_meth('entity_cache')
- cache_entities = cnx_meth('cached_entities')
- drop_entity_cache = cnx_meth('drop_entity_cache')
-
- source_defs = cnx_meth('source_defs')
- entity_metas = cnx_meth('entity_metas')
- describe = cnx_meth('describe') # XXX deprecated in 3.19
-
-
- @deprecated('[3.19] use a Connection object instead')
- def execute(self, *args, **kwargs):
- """db-api like method directly linked to the querier execute method.
-
- See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
- """
- rset = self._cnx.execute(*args, **kwargs)
- rset.req = self
- return rset
-
- def _clear_thread_data(self, free_cnxset=True):
- """remove everything from the thread local storage, except connections set
- which is explicitly removed by free_cnxset, and mode which is set anyway
- by _touch
- """
- try:
- cnx = self.__threaddata.cnx
- except AttributeError:
- pass
- else:
- if free_cnxset:
- cnx._free_cnxset()
- if cnx.ctx_count == 0:
- self._close_cnx(cnx)
- else:
- cnx.clear()
- else:
- cnx.clear()
-
- @deprecated('[3.19] use a Connection object instead')
- def commit(self, free_cnxset=True, reset_pool=None):
- """commit the current session's transaction"""
- cstate = self._cnx.commit_state
- if cstate == 'uncommitable':
- raise QueryError('transaction must be rolled back')
- try:
- return self._cnx.commit(free_cnxset, reset_pool)
- finally:
- self._clear_thread_data(free_cnxset)
-
- @deprecated('[3.19] use a Connection object instead')
- def rollback(self, *args, **kwargs):
- """rollback the current session's transaction"""
- return self._rollback(*args, **kwargs)
-
- def _rollback(self, free_cnxset=True, **kwargs):
- try:
- return self._cnx.rollback(free_cnxset, **kwargs)
- finally:
- self._clear_thread_data(free_cnxset)
-
- def close(self):
- # do not close connections set on session close, since they are shared now
- tracker = self._cnxset_tracker
- with self._lock:
- self._closed = True
- tracker.close()
- if self._cnx._session_handled:
- self._rollback()
- self.debug('waiting for open connection of session: %s', self)
- timeout = 10
- pendings = tracker.wait(timeout)
- if pendings:
- self.error('%i connection still alive after 10 seconds, will close '
- 'session anyway', len(pendings))
- for cnxid in pendings:
- cnx = self._cnxs.get(cnxid)
- if cnx is not None:
- # drop cnx.cnxset
- with tracker:
- try:
- cnxset = cnx.cnxset
- if cnxset is None:
- continue
- cnx.cnxset = None
- except RuntimeError:
- msg = 'issue while force free of cnxset in %s'
- self.error(msg, cnx)
- # cnxset.reconnect() do an hard reset of the cnxset
- # it force it to be freed
- cnxset.reconnect()
- self.repo._free_cnxset(cnxset)
- del self.__threaddata
- del self._cnxs
-
- @property
- def closed(self):
- return not hasattr(self, '_cnxs')
-
- # transaction data/operations management ##################################
-
- transaction_data = cnx_attr('transaction_data')
- pending_operations = cnx_attr('pending_operations')
- pruned_hooks_cache = cnx_attr('pruned_hooks_cache')
- add_operation = cnx_meth('add_operation')
-
- # undo support ############################################################
-
- ertype_supports_undo = cnx_meth('ertype_supports_undo')
- transaction_inc_action_counter = cnx_meth('transaction_inc_action_counter')
- transaction_uuid = cnx_meth('transaction_uuid')
-
- # querier helpers #########################################################
-
- rql_rewriter = cnx_attr('_rewriter')
-
# deprecated ###############################################################
@property
@@ -1725,52 +1079,10 @@
def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
- @property
- @deprecated("[3.13] use .cnxset attribute instead of .pool")
- def pool(self):
- return self.cnxset
-
- @deprecated("[3.13] use .set_cnxset() method instead of .set_pool()")
- def set_pool(self):
- return self.set_cnxset()
-
- @deprecated("[3.13] use .free_cnxset() method instead of .reset_pool()")
- def reset_pool(self):
- return self.free_cnxset()
-
# these are overridden by set_log_methods below
# only defining here to prevent pylint from complaining
info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-Session.HOOKS_ALLOW_ALL = HOOKS_ALLOW_ALL
-Session.HOOKS_DENY_ALL = HOOKS_DENY_ALL
-Session.DEFAULT_SECURITY = DEFAULT_SECURITY
-
-
-
-class InternalSession(Session):
- """special session created internally by the repository"""
- is_internal_session = True
- running_dbapi_query = False
-
- def __init__(self, repo, cnxprops=None, safe=False):
- super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
- _id='internal')
- self.user._cw = self # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
-
- def __enter__(self):
- return self
-
- def __exit__(self, exctype, excvalue, tb):
- self.close()
-
- @property
- def cnxset(self):
- """connections set, set according to transaction mode for each query"""
- if self.repo.shutting_down:
- self.free_cnxset(True)
- raise ShuttingDown('repository is shutting down')
- return self._cnx.cnxset
class InternalManager(object):
@@ -1778,6 +1090,7 @@
bootstrapping the repository or creating regular users according to
repository content
"""
+
def __init__(self, lang='en'):
self.eid = -1
self.login = u'__internal_manager__'
--- a/server/sources/__init__.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/sources/__init__.py Tue Jul 19 16:13:12 2016 +0200
@@ -128,6 +128,9 @@
def __eq__(self, other):
return self.uri == other.uri
+ def __ne__(self, other):
+ return not (self == other)
+
def backup(self, backupfile, confirm, format='native'):
"""method called to create a backup of source's data"""
pass
@@ -395,7 +398,7 @@
# system source interface #################################################
def eid_type_source(self, cnx, eid):
- """return a tuple (type, source, extid) for the entity with id <eid>"""
+ """return a tuple (type, extid, source) for the entity with id <eid>"""
raise NotImplementedError(self)
def create_eid(self, cnx):
--- a/server/sources/datafeed.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/sources/datafeed.py Tue Jul 19 16:13:12 2016 +0200
@@ -25,7 +25,7 @@
from datetime import datetime, timedelta
from base64 import b64decode
from cookielib import CookieJar
-
+import urlparse
from lxml import etree
from cubicweb import RegistryNotFound, ObjectNotFound, ValidationError, UnknownEid
@@ -319,24 +319,45 @@
return url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
return url
- def retrieve_url(self, url, data=None, headers=None):
+ def retrieve_url(self, url):
"""Return stream linked by the given url:
* HTTP urls will be normalized (see :meth:`normalize_url`)
* handle file:// URL
* other will be considered as plain content, useful for testing purpose
+
+ For http URLs, it will try to find a cwclientlib config entry
+ (if available) and use it as requester.
"""
- if headers is None:
- headers = {}
- if url.startswith('http'):
- url = self.normalize_url(url)
- if data:
- self.source.info('POST %s %s', url, data)
- else:
- self.source.info('GET %s', url)
- req = urllib2.Request(url, data, headers)
+ purl = urlparse.urlparse(url)
+ if purl.scheme == 'file':
+ return URLLibResponseAdapter(open(url[7:]), url)
+
+ url = self.normalize_url(url)
+
+ # first, try to use cwclientlib if it's available and if the
+ # url matches a configuration entry in ~/.config/cwclientlibrc
+ try:
+ from cwclientlib import cwproxy_for
+ # parse url again since it has been normalized
+ cnx = cwproxy_for(url)
+ cnx.timeout = self.source.http_timeout
+ self.source.info('Using cwclientlib for %s' % url)
+ resp = cnx.get(url)
+ resp.raise_for_status()
+ return URLLibResponseAdapter(StringIO.StringIO(resp.text), url)
+ except (ImportError, ValueError, EnvironmentError) as exc:
+ # ImportError: not available
+ # ValueError: no config entry found
+ # EnvironmentError: no cwclientlib config file found
+ self.source.debug(str(exc))
+
+ # no chance with cwclientlib, fall back to former implementation
+ if purl.scheme in ('http', 'https'):
+ self.source.info('GET %s', url)
+ req = urllib2.Request(url)
return _OPENER.open(req, timeout=self.source.http_timeout)
- if url.startswith('file://'):
- return URLLibResponseAdapter(open(url[7:]), url)
+
+ # url is probably plain content
return URLLibResponseAdapter(StringIO.StringIO(url), url)
def add_schema_config(self, schemacfg, checkonly=False):
@@ -489,15 +510,32 @@
raise NotImplementedError
def is_deleted(self, extid, etype, eid):
- if extid.startswith('http'):
+ if extid.startswith('file://'):
+ return exists(extid[7:])
+
+ url = self.normalize_url(extid)
+ # first, try to use cwclientlib if it's available and if the
+ # url matches a configuration entry in ~/.config/cwclientlibrc
+ try:
+ from cwclientlib import cwproxy_for
+ # parse url again since it has been normalized
+ cnx = cwproxy_for(url)
+ cnx.timeout = self.source.http_timeout
+ self.source.info('Using cwclientlib for checking %s' % url)
+ return cnx.get(url).status_code == 404
+ except (ImportError, ValueError, EnvironmentError) as exc:
+ # ImportError: not available
+ # ValueError: no config entry found
+ # EnvironmentError: no cwclientlib config file found
+ self.source.debug(str(exc))
+
+ # no chance with cwclientlib, fall back to former implementation
+ if urlparse.urlparse(url).scheme in ('http', 'https'):
try:
- _OPENER.open(self.normalize_url(extid), # XXX HTTP HEAD request
- timeout=self.source.http_timeout)
+ _OPENER.open(url, timeout=self.source.http_timeout)
except urllib2.HTTPError as ex:
if ex.code == 404:
return True
- elif extid.startswith('file://'):
- return exists(extid[7:])
return False
--- a/server/sources/native.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/sources/native.py Tue Jul 19 16:13:12 2016 +0200
@@ -43,16 +43,16 @@
from logilab.common.shellutils import getlogin, ASK
from logilab.database import get_db_helper, sqlgen
-from yams import schema2sql as y2sql
from yams.schema import role_name
from cubicweb import (UnknownEid, AuthenticationError, ValidationError, Binary,
- UniqueTogetherError, UndoTransactionException)
+ UniqueTogetherError, UndoTransactionException, ViolatedConstraint)
from cubicweb import transaction as tx, server, neg_role
from cubicweb.utils import QueryCache
from cubicweb.schema import VIRTUAL_RTYPES
from cubicweb.cwconfig import CubicWebNoAppConfiguration
from cubicweb.server import hook
+from cubicweb.server import schema2sql as y2sql
from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update
from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
from cubicweb.server.rqlannotation import set_qdata
@@ -60,6 +60,7 @@
from cubicweb.server.edition import EditedEntity
from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
from cubicweb.server.sources.rql2sql import SQLGenerator
+from cubicweb.statsd_logger import statsd_timeit
ATTR_MAP = {}
@@ -272,7 +273,7 @@
{'type' : 'string',
'default': 'postgres',
# XXX use choice type
- 'help': 'database driver (postgres, mysql, sqlite, sqlserver2005)',
+ 'help': 'database driver (postgres, sqlite, sqlserver2005)',
'group': 'native-source', 'level': 0,
}),
('db-host',
@@ -376,6 +377,7 @@
self._cache.pop('Any X WHERE X eid %s' % eid, None)
self._cache.pop('Any %s' % eid, None)
+ @statsd_timeit
def sqlexec(self, cnx, sql, args=None):
"""execute the query and return its result"""
return self.process_result(self.doexec(cnx, sql, args))
@@ -480,6 +482,7 @@
# ISource interface #######################################################
+ @statsd_timeit
def compile_rql(self, rql, sols):
rqlst = self.repo.vreg.rqlhelper.parse(rql)
rqlst.restricted_vars = ()
@@ -517,6 +520,7 @@
# can't claim not supporting a relation
return True #not rtype == 'content_for'
+ @statsd_timeit
def authenticate(self, cnx, login, **kwargs):
"""return CWUser eid for the given login and other authentication
information found in kwargs, else raise `AuthenticationError`
@@ -553,31 +557,22 @@
self._cache[cachekey] = sql, qargs, cbs
args = self.merge_args(args, qargs)
assert isinstance(sql, basestring), repr(sql)
- try:
- cursor = self.doexec(cnx, sql, args)
- except (self.OperationalError, self.InterfaceError):
- if cnx.mode == 'write':
- # do not attempt to reconnect if there has been some write
- # during the transaction
- raise
- # FIXME: better detection of deconnection pb
- self.warning("trying to reconnect")
- cnx.cnxset.reconnect()
- cursor = self.doexec(cnx, sql, args)
- except self.DbapiError as exc:
- # We get this one with pyodbc and SQL Server when connection was reset
- if exc.args[0] == '08S01' and cnx.mode != 'write':
- self.warning("trying to reconnect")
- cnx.cnxset.reconnect()
- cursor = self.doexec(cnx, sql, args)
- else:
- raise
+ cursor = self.doexec(cnx, sql, args)
results = self.process_result(cursor, cnx, cbs)
assert dbg_results(results)
return results
@contextmanager
- def _storage_handler(self, entity, event):
+ def _fixup_cw(self, cnx, entity):
+ _cw = entity._cw
+ entity._cw = cnx
+ try:
+ yield
+ finally:
+ entity._cw = _cw
+
+ @contextmanager
+ def _storage_handler(self, cnx, entity, event):
# 1/ memorize values as they are before the storage is called.
# For instance, the BFSStorage will replace the `data`
# binary value with a Binary containing the destination path
@@ -592,14 +587,15 @@
etype = entities[0].__regid__
for attr, storage in self._storages.get(etype, {}).items():
for entity in entities:
- if event == 'deleted':
- storage.entity_deleted(entity, attr)
- else:
- edited = entity.cw_edited
- if attr in edited:
- handler = getattr(storage, 'entity_%s' % event)
- to_restore = handler(entity, attr)
- restore_values.append((entity, attr, to_restore))
+ with self._fixup_cw(cnx, entity):
+ if event == 'deleted':
+ storage.entity_deleted(entity, attr)
+ else:
+ edited = entity.cw_edited
+ if attr in edited:
+ handler = getattr(storage, 'entity_%s' % event)
+ to_restore = handler(entity, attr)
+ restore_values.append((entity, attr, to_restore))
try:
yield # 2/ execute the source's instructions
finally:
@@ -609,22 +605,22 @@
def add_entity(self, cnx, entity):
"""add a new entity to the source"""
- with self._storage_handler(entity, 'added'):
+ with self._storage_handler(cnx, entity, 'added'):
attrs = self.preprocess_entity(entity)
sql = self.sqlgen.insert(SQL_PREFIX + entity.cw_etype, attrs)
self.doexec(cnx, sql, attrs)
if cnx.ertype_supports_undo(entity.cw_etype):
- self._record_tx_action(cnx, 'tx_entity_actions', 'C',
- etype=entity.cw_etype, eid=entity.eid)
+ self._record_tx_action(cnx, 'tx_entity_actions', u'C',
+ etype=unicode(entity.cw_etype), eid=entity.eid)
def update_entity(self, cnx, entity):
"""replace an entity in the source"""
- with self._storage_handler(entity, 'updated'):
+ with self._storage_handler(cnx, entity, 'updated'):
attrs = self.preprocess_entity(entity)
if cnx.ertype_supports_undo(entity.cw_etype):
changes = self._save_attrs(cnx, entity, attrs)
- self._record_tx_action(cnx, 'tx_entity_actions', 'U',
- etype=entity.cw_etype, eid=entity.eid,
+ self._record_tx_action(cnx, 'tx_entity_actions', u'U',
+ etype=unicode(entity.cw_etype), eid=entity.eid,
changes=self._binary(dumps(changes)))
sql = self.sqlgen.update(SQL_PREFIX + entity.cw_etype, attrs,
['cw_eid'])
@@ -632,14 +628,14 @@
def delete_entity(self, cnx, entity):
"""delete an entity from the source"""
- with self._storage_handler(entity, 'deleted'):
+ with self._storage_handler(cnx, entity, 'deleted'):
if cnx.ertype_supports_undo(entity.cw_etype):
attrs = [SQL_PREFIX + r.type
for r in entity.e_schema.subject_relations()
if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
changes = self._save_attrs(cnx, entity, attrs)
- self._record_tx_action(cnx, 'tx_entity_actions', 'D',
- etype=entity.cw_etype, eid=entity.eid,
+ self._record_tx_action(cnx, 'tx_entity_actions', u'D',
+ etype=unicode(entity.cw_etype), eid=entity.eid,
changes=self._binary(dumps(changes)))
attrs = {'cw_eid': entity.eid}
sql = self.sqlgen.delete(SQL_PREFIX + entity.cw_etype, attrs)
@@ -649,16 +645,16 @@
"""add a relation to the source"""
self._add_relations(cnx, rtype, [(subject, object)], inlined)
if cnx.ertype_supports_undo(rtype):
- self._record_tx_action(cnx, 'tx_relation_actions', 'A',
- eid_from=subject, rtype=rtype, eid_to=object)
+ self._record_tx_action(cnx, 'tx_relation_actions', u'A',
+ eid_from=subject, rtype=unicode(rtype), eid_to=object)
def add_relations(self, cnx, rtype, subj_obj_list, inlined=False):
"""add a relations to the source"""
self._add_relations(cnx, rtype, subj_obj_list, inlined)
if cnx.ertype_supports_undo(rtype):
for subject, object in subj_obj_list:
- self._record_tx_action(cnx, 'tx_relation_actions', 'A',
- eid_from=subject, rtype=rtype, eid_to=object)
+ self._record_tx_action(cnx, 'tx_relation_actions', u'A',
+ eid_from=subject, rtype=unicode(rtype), eid_to=object)
def _add_relations(self, cnx, rtype, subj_obj_list, inlined=False):
"""add a relation to the source"""
@@ -689,8 +685,8 @@
rschema = self.schema.rschema(rtype)
self._delete_relation(cnx, subject, rtype, object, rschema.inlined)
if cnx.ertype_supports_undo(rtype):
- self._record_tx_action(cnx, 'tx_relation_actions', 'R',
- eid_from=subject, rtype=rtype, eid_to=object)
+ self._record_tx_action(cnx, 'tx_relation_actions', u'R',
+ eid_from=subject, rtype=unicode(rtype), eid_to=object)
def _delete_relation(self, cnx, subject, rtype, object, inlined=False):
"""delete a relation from the source"""
@@ -705,6 +701,7 @@
sql = self.sqlgen.delete('%s_relation' % rtype, attrs)
self.doexec(cnx, sql, attrs)
+ @statsd_timeit
def doexec(self, cnx, query, args=None, rollback=True):
"""Execute a query.
it's a function just so that it shows up in profiling
@@ -749,15 +746,28 @@
columns = arg.split(':', 1)[1].split(',')
rtypes = [c.split('.', 1)[1].strip()[3:] for c in columns]
raise UniqueTogetherError(cnx, rtypes=rtypes)
+
+ mo = re.search('"cstr[a-f0-9]{32}"', arg)
+ if mo is not None:
+ # postgresql
+ raise ViolatedConstraint(cnx, cstrname=mo.group(0)[1:-1])
+ if arg.startswith('CHECK constraint failed:'):
+ # sqlite3 (new)
+ raise ViolatedConstraint(cnx, cstrname=arg.split(':', 1)[1].strip())
+ mo = re.match('^constraint (cstr.*) failed$', arg)
+ if mo is not None:
+ # sqlite3 (old)
+ raise ViolatedConstraint(cnx, cstrname=mo.group(1))
raise
return cursor
+ @statsd_timeit
def doexecmany(self, cnx, query, args):
"""Execute a query.
it's a function just so that it shows up in profiling
"""
if server.DEBUG & server.DBG_SQL:
- print 'execmany', query, 'with', len(args), 'arguments'
+ print 'execmany', query, 'with', len(args), 'arguments', cnx.cnxset.cnx
cursor = cnx.cnxset.cu
try:
# str(query) to avoid error if it's a unicode string
@@ -829,23 +839,17 @@
# system source interface #################################################
- def _eid_type_source(self, cnx, eid, sql, _retry=True):
+ def _eid_type_source(self, cnx, eid, sql):
try:
res = self.doexec(cnx, sql).fetchone()
if res is not None:
return res
- except (self.OperationalError, self.InterfaceError):
- if cnx.mode == 'read' and _retry:
- self.warning("trying to reconnect (eid_type_source())")
- cnx.cnxset.reconnect()
- return self._eid_type_source(cnx, eid, sql, _retry=False)
except Exception:
- assert cnx.cnxset, 'connection has no connections set'
self.exception('failed to query entities table for eid %s', eid)
raise UnknownEid(eid)
def eid_type_source(self, cnx, eid): # pylint: disable=E0202
- """return a tuple (type, source, extid) for the entity with id <eid>"""
+ """return a tuple (type, extid, source) for the entity with id <eid>"""
sql = 'SELECT type, extid, asource FROM entities WHERE eid=%s' % eid
res = self._eid_type_source(cnx, eid, sql)
if res[-2] is not None:
@@ -855,7 +859,7 @@
return res
def eid_type_source_pre_131(self, cnx, eid):
- """return a tuple (type, source, extid) for the entity with id <eid>"""
+ """return a tuple (type, extid, source) for the entity with id <eid>"""
sql = 'SELECT type, extid FROM entities WHERE eid=%s' % eid
res = self._eid_type_source(cnx, eid, sql)
if not isinstance(res, list):
@@ -868,9 +872,10 @@
def extid2eid(self, cnx, extid):
"""get eid from an external id. Return None if no record found."""
assert isinstance(extid, str)
+ args = {'x': b64encode(extid)}
cursor = self.doexec(cnx,
'SELECT eid FROM entities WHERE extid=%(x)s',
- {'x': b64encode(extid)})
+ args)
# XXX testing rowcount cause strange bug with sqlite, results are there
# but rowcount is 0
#if cursor.rowcount > 0:
@@ -880,6 +885,17 @@
return result[0]
except Exception:
pass
+ cursor = self.doexec(cnx,
+ 'SELECT eid FROM moved_entities WHERE extid=%(x)s',
+ args)
+ try:
+ result = cursor.fetchone()
+ if result:
+ # entity was moved to the system source, return negative
+ # number to tell the external source to ignore it
+ return -result[0]
+ except Exception:
+ pass
return None
def _handle_is_relation_sql(self, cnx, sql, attrs):
@@ -897,7 +913,7 @@
if extid is not None:
assert isinstance(extid, str)
extid = b64encode(extid)
- attrs = {'type': entity.cw_etype, 'eid': entity.eid, 'extid': extid and unicode(extid),
+ attrs = {'type': unicode(entity.cw_etype), 'eid': entity.eid, 'extid': extid and unicode(extid),
'asource': unicode(source.uri)}
self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
# insert core relations: is, is_instance_of and cw_source
@@ -940,7 +956,7 @@
# undo support #############################################################
def undoable_transactions(self, cnx, ueid=None, **actionfilters):
- """See :class:`cubicweb.repoapi.ClientConnection.undoable_transactions`"""
+ """See :class:`cubicweb.repoapi.Connection.undoable_transactions`"""
# force filtering to connection's user if not a manager
if not cnx.user.is_in_group('managers'):
ueid = cnx.user.eid
@@ -965,7 +981,7 @@
# only, and with no eid specified
assert actionfilters.get('action', 'C') in 'CUD'
assert not 'eid' in actionfilters
- tearestr['etype'] = val
+ tearestr['etype'] = unicode(val)
elif key == 'eid':
# eid filter may apply to 'eid' of tx_entity_actions or to
# 'eid_from' OR 'eid_to' of tx_relation_actions
@@ -976,10 +992,10 @@
trarestr['eid_to'] = val
elif key == 'action':
if val in 'CUD':
- tearestr['txa_action'] = val
+ tearestr['txa_action'] = unicode(val)
else:
assert val in 'AR'
- trarestr['txa_action'] = val
+ trarestr['txa_action'] = unicode(val)
else:
raise AssertionError('unknow filter %s' % key)
assert trarestr or tearestr, "can't only filter on 'public'"
@@ -1007,17 +1023,17 @@
restr.update(tearestr)
# we want results ordered by transaction's time descendant
sql += ' ORDER BY tx_time DESC'
- with cnx.ensure_cnx_set:
- cu = self.doexec(cnx, sql, restr)
- # turn results into transaction objects
- return [tx.Transaction(*args) for args in cu.fetchall()]
+ cu = self.doexec(cnx, sql, restr)
+ # turn results into transaction objects
+ return [tx.Transaction(cnx, *args) for args in cu.fetchall()]
def tx_info(self, cnx, txuuid):
- """See :class:`cubicweb.repoapi.ClientConnection.transaction_info`"""
- return tx.Transaction(txuuid, *self._tx_info(cnx, txuuid))
+ """See :class:`cubicweb.repoapi.Connection.transaction_info`"""
+ return tx.Transaction(cnx, txuuid, *self._tx_info(cnx, unicode(txuuid)))
def tx_actions(self, cnx, txuuid, public):
- """See :class:`cubicweb.repoapi.ClientConnection.transaction_actions`"""
+ """See :class:`cubicweb.repoapi.Connection.transaction_actions`"""
+ txuuid = unicode(txuuid)
self._tx_info(cnx, txuuid)
restr = {'tx_uuid': txuuid}
if public:
@@ -1039,13 +1055,11 @@
return sorted(actions, key=lambda x: x.order)
def undo_transaction(self, cnx, txuuid):
- """See :class:`cubicweb.repoapi.ClientConnection.undo_transaction`
+ """See :class:`cubicweb.repoapi.Connection.undo_transaction`
important note: while undoing of a transaction, only hooks in the
'integrity', 'activeintegrity' and 'undo' categories are called.
"""
- # set mode so connections set isn't released subsquently until commit/rollback
- cnx.mode = 'write'
errors = []
cnx.transaction_data['undoing_uuid'] = txuuid
with cnx.deny_all_hooks_but('integrity', 'activeintegrity', 'undo'):
@@ -1097,7 +1111,7 @@
kwargs['tx_uuid'] = cnx.transaction_uuid()
kwargs['txa_action'] = action
kwargs['txa_order'] = cnx.transaction_inc_action_counter()
- kwargs['txa_public'] = cnx.running_dbapi_query
+ kwargs['txa_public'] = not cnx.hooks_in_progress
self.doexec(cnx, self.sqlgen.insert(table, kwargs), kwargs)
def _tx_info(self, cnx, txuuid):
@@ -1106,19 +1120,18 @@
raise `NoSuchTransaction` if there is no such transaction of if the
connection's user isn't allowed to see it.
"""
- with cnx.ensure_cnx_set:
- restr = {'tx_uuid': txuuid}
- sql = self.sqlgen.select('transactions', restr,
- ('tx_time', 'tx_user'))
- cu = self.doexec(cnx, sql, restr)
- try:
- time, ueid = cu.fetchone()
- except TypeError:
- raise tx.NoSuchTransaction(txuuid)
- if not (cnx.user.is_in_group('managers')
- or cnx.user.eid == ueid):
- raise tx.NoSuchTransaction(txuuid)
- return time, ueid
+ restr = {'tx_uuid': txuuid}
+ sql = self.sqlgen.select('transactions', restr,
+ ('tx_time', 'tx_user'))
+ cu = self.doexec(cnx, sql, restr)
+ try:
+ time, ueid = cu.fetchone()
+ except TypeError:
+ raise tx.NoSuchTransaction(txuuid)
+ if not (cnx.user.is_in_group('managers')
+ or cnx.user.eid == ueid):
+ raise tx.NoSuchTransaction(txuuid)
+ return time, ueid
def _reedit_entity(self, entity, changes, err):
cnx = entity._cw
@@ -1151,6 +1164,7 @@
err(cnx._("can't restore entity %(eid)s of type %(eschema)s, "
"target of %(rtype)s (eid %(value)s) does not exist any longer")
% locals())
+ changes[column] = None
elif eschema.destination(rtype) in ('Bytes', 'Password'):
changes[column] = self._binary(value)
edited[rtype] = Binary(value)
@@ -1182,10 +1196,10 @@
self.repo.hm.call_hooks('before_add_entity', cnx, entity=entity)
# restore the entity
action.changes['cw_eid'] = eid
+ # restore record in entities (will update fti if needed)
+ self.add_info(cnx, entity, self, None)
sql = self.sqlgen.insert(SQL_PREFIX + etype, action.changes)
self.doexec(cnx, sql, action.changes)
- # restore record in entities (will update fti if needed)
- self.add_info(cnx, entity, self, None)
self.repo.hm.call_hooks('after_add_entity', cnx, entity=entity)
return errors
@@ -1390,7 +1404,10 @@
extid VARCHAR(256)
);;
CREATE INDEX entities_type_idx ON entities(type);;
-CREATE INDEX entities_extid_idx ON entities(extid);;
+CREATE TABLE moved_entities (
+ eid INTEGER PRIMARY KEY NOT NULL,
+ extid VARCHAR(256) UNIQUE NOT NULL
+);;
CREATE TABLE transactions (
tx_uuid CHAR(32) PRIMARY KEY NOT NULL,
@@ -1442,18 +1459,22 @@
DELETE FROM tx_relation_actions WHERE tx_uuid=OLD.tx_uuid;
END;;
'''
+ schema += ';;'.join(helper.sqls_create_multicol_unique_index('entities', ['extid']))
+ schema += ';;\n'
return schema
def sql_drop_schema(driver):
helper = get_db_helper(driver)
return """
+%s;
%s
DROP TABLE entities;
DROP TABLE tx_entity_actions;
DROP TABLE tx_relation_actions;
DROP TABLE transactions;
-""" % helper.sql_drop_numrange('entities_id_seq')
+""" % (';'.join(helper.sqls_drop_multicol_unique_index('entities', ['extid'])),
+ helper.sql_drop_numrange('entities_id_seq'))
def grant_schema(user, set_owner=True):
@@ -1530,7 +1551,7 @@
SQL_PREFIX + 'login'),
{'newhash': self.source._binary(newhash),
'login': login})
- cnx.commit(free_cnxset=False)
+ cnx.commit()
return user
except IndexError:
raise AuthenticationError('bad password')
--- a/server/sources/rql2sql.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/sources/rql2sql.py Tue Jul 19 16:13:12 2016 +0200
@@ -50,9 +50,7 @@
__docformat__ = "restructuredtext en"
import threading
-from datetime import datetime, time
-from logilab.common.date import utcdatetime, utctime
from logilab.database import FunctionDescr, SQL_FUNCTIONS_REGISTRY
from rql import BadRQLQuery, CoercionError
@@ -397,11 +395,14 @@
yield 1
return
thisexistssols, thisexistsvars = self.existssols[exists]
+ notdone_outside_vars = set()
# when iterating other solutions inner to an EXISTS subquery, we should
# reset variables which have this exists node as scope at each iteration
for var in exists.stmt.defined_vars.itervalues():
if var.scope is exists:
thisexistsvars.add(var.name)
+ elif var.name not in self.done:
+ notdone_outside_vars.add(var)
origsol = self.solution
origtables = self.tables
done = self.done
@@ -417,6 +418,10 @@
for var in thisexistsvars:
if var in done:
done.remove(var)
+ for var in list(notdone_outside_vars):
+ if var.name in done and var._q_sqltable in self.tables:
+ origtables[var._q_sqltable] = self.tables[var._q_sqltable]
+ notdone_outside_vars.remove(var)
for rel in exists.iget_nodes(Relation):
if rel in done:
done.remove(rel)
@@ -1509,14 +1514,6 @@
_id = value
if isinstance(_id, unicode):
_id = _id.encode()
- # convert timestamp to utc.
- # expect SET TiME ZONE to UTC at connection opening time.
- # This shouldn't change anything for datetime without TZ.
- value = self._args[_id]
- if isinstance(value, datetime) and value.tzinfo is not None:
- self._query_attrs[_id] = utcdatetime(value)
- elif isinstance(value, time) and value.tzinfo is not None:
- self._query_attrs[_id] = utctime(value)
else:
_id = str(id(constant)).replace('-', '', 1)
self._query_attrs[_id] = value
--- a/server/sources/storages.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/sources/storages.py Tue Jul 19 16:13:12 2016 +0200
@@ -141,7 +141,6 @@
"""an entity using this storage for attr has been added"""
if entity._cw.transaction_data.get('fs_importing'):
binary = Binary.from_file(entity.cw_edited[attr].getvalue())
- entity._cw_dont_cache_attribute(attr, repo_side=True)
else:
binary = entity.cw_edited.pop(attr)
fd, fpath = self.new_fs_path(entity, attr)
@@ -160,7 +159,6 @@
# We do not need to create it but we need to fetch the content of
# the file as the actual content of the attribute
fpath = entity.cw_edited[attr].getvalue()
- entity._cw_dont_cache_attribute(attr, repo_side=True)
assert fpath is not None
binary = Binary.from_file(fpath)
else:
--- a/server/sqlutils.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/sqlutils.py Tue Jul 19 16:13:12 2016 +0200
@@ -20,17 +20,18 @@
__docformat__ = "restructuredtext en"
import sys
-import os
import re
import subprocess
from os.path import abspath
from itertools import ifilter
from logging import getLogger
+from datetime import time, datetime
from logilab import database as db, common as lgc
from logilab.common.shellutils import ProgressBar, DummyProgressBar
from logilab.common.deprecation import deprecated
from logilab.common.logging_ext import set_log_methods
+from logilab.common.date import utctime, utcdatetime
from logilab.database.sqlgen import SQLGenerator
from cubicweb import Binary, ConfigurationError
@@ -106,7 +107,7 @@
"""return sql to give all access privileges to the given user on the system
schema
"""
- from yams.schema2sql import grant_schema
+ from cubicweb.server.schema2sql import grant_schema
from cubicweb.server.sources import native
output = []
w = output.append
@@ -124,7 +125,7 @@
user=None, set_owner=False,
skip_relations=PURE_VIRTUAL_RTYPES, skip_entities=()):
"""return the system sql schema, according to the given parameters"""
- from yams.schema2sql import schema2sql
+ from cubicweb.server.schema2sql import schema2sql
from cubicweb.server.sources import native
if set_owner:
assert user, 'user is argument required when set_owner is true'
@@ -149,7 +150,7 @@
def sqldropschema(schema, driver, text_index=True,
skip_relations=PURE_VIRTUAL_RTYPES, skip_entities=()):
"""return the sql to drop the schema, according to the given parameters"""
- from yams.schema2sql import dropschema2sql
+ from cubicweb.server.schema2sql import dropschema2sql
from cubicweb.server.sources import native
output = []
w = output.append
@@ -373,8 +374,17 @@
# convert cubicweb binary into db binary
if isinstance(val, Binary):
val = self._binary(val.getvalue())
+ # convert timestamp to utc.
+ # expect SET TiME ZONE to UTC at connection opening time.
+ # This shouldn't change anything for datetime without TZ.
+ elif isinstance(val, datetime) and val.tzinfo is not None:
+ val = utcdatetime(val)
+ elif isinstance(val, time) and val.tzinfo is not None:
+ val = utctime(val)
newargs[key] = val
# should not collide
+ assert not (frozenset(newargs) & frozenset(query_args)), \
+ 'unexpected collision: %s' % (frozenset(newargs) & frozenset(query_args))
newargs.update(query_args)
return newargs
return query_args
@@ -503,6 +513,8 @@
return (dt.weekday() + 1) % 7
cnx.create_function("WEEKDAY", 1, weekday)
+ cnx.cursor().execute("pragma foreign_keys = on")
+
import yams.constraints
yams.constraints.patch_sqlite_decimal()
--- a/server/ssplanner.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/ssplanner.py Tue Jul 19 16:13:12 2016 +0200
@@ -249,7 +249,7 @@
raise QueryError('can not assign to %r relation'
% relation.r_type)
lhs, rhs = relation.get_variable_parts()
- lhskey = lhs.as_string('utf-8')
+ lhskey = lhs.as_string()
if not lhskey in selectedidx:
if lhs.variable in eidconsts:
eid = eidconsts[lhs.variable]
@@ -262,7 +262,7 @@
selectedidx[lhskey] = lhsinfo
else:
lhsinfo = selectedidx[lhskey][:-1] + (None,)
- rhskey = rhs.as_string('utf-8')
+ rhskey = rhs.as_string()
if not rhskey in selectedidx:
if isinstance(rhs, Constant):
rhsinfo = (_CONSTANT, rhs.eval(plan.args), residx)
--- a/server/test/data-cwep002/schema.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/data-cwep002/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -32,4 +32,5 @@
class has_employee(ComputedRelation):
rule = 'O works_for S'
+ __permissions__ = {'read': ('managers',)}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/bootstrap_cubes Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+card,comment,tag,basket,file,localperms,fakeemail
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/cubes/__init__.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+__import__('pkg_resources').declare_namespace(__name__)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/cubes/fakeemail/__pkginfo__.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,53 @@
+# pylint: disable-msg=W0622
+"""cubicweb-fakeemail packaging information"""
+
+modname = 'fakeemail'
+distname = "cubicweb-%s" % modname
+
+numversion = (1, 10, 0)
+version = '.'.join(str(num) for num in numversion)
+
+license = 'LGPL'
+author = "Logilab"
+author_email = "contact@logilab.fr"
+web = 'http://www.cubicweb.org/project/%s' % distname
+description = "email component for the CubicWeb framework"
+classifiers = [
+ 'Environment :: Web Environment',
+ 'Framework :: CubicWeb',
+ 'Programming Language :: Python',
+ 'Programming Language :: JavaScript',
+]
+
+# used packages
+__depends__ = {'cubicweb': '>= 3.19.0',
+ 'cubicweb-file': '>= 1.9.0',
+ 'logilab-common': '>= 0.58.3',
+ }
+__recommends__ = {'cubicweb-comment': None}
+
+
+# packaging ###
+
+from os import listdir as _listdir
+from os.path import join, isdir
+from glob import glob
+
+THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
+
+def listdir(dirpath):
+ return [join(dirpath, fname) for fname in _listdir(dirpath)
+ if fname[0] != '.' and not fname.endswith('.pyc')
+ and not fname.endswith('~')
+ and not isdir(join(dirpath, fname))]
+
+data_files = [
+ # common files
+ [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
+ ]
+# check for possible extended cube layout
+for dirname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n', 'migration', 'wdoc'):
+ if isdir(dirname):
+ data_files.append([join(THIS_CUBE_DIR, dirname), listdir(dirname)])
+# Note: here, you'll need to add subdirectories if you want
+# them to be included in the debian package
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/cubes/fakeemail/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,88 @@
+"""entity/relation schemas to store email in an cubicweb instance
+
+:organization: Logilab
+:copyright: 2006-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+# pylint: disable-msg=E0611,F0401
+from yams.buildobjs import (SubjectRelation, RelationType, EntityType,
+ String, Datetime, Int, RelationDefinition)
+from yams.reader import context
+
+from cubicweb.schema import ERQLExpression
+
+
+class Email(EntityType):
+ """electronic mail"""
+ subject = String(fulltextindexed=True)
+ date = Datetime(description=_('UTC time on which the mail was sent'))
+ messageid = String(required=True, indexed=True)
+ headers = String(description=_('raw headers'))
+
+ sender = SubjectRelation('EmailAddress', cardinality='?*')
+ # an email with only Bcc is acceptable, don't require any recipients
+ recipients = SubjectRelation('EmailAddress')
+ cc = SubjectRelation('EmailAddress')
+
+ parts = SubjectRelation('EmailPart', cardinality='*1', composite='subject')
+ attachment = SubjectRelation('File')
+
+ reply_to = SubjectRelation('Email', cardinality='?*')
+ cites = SubjectRelation('Email')
+ in_thread = SubjectRelation('EmailThread', cardinality='?*')
+
+
+class EmailPart(EntityType):
+ """an email attachment"""
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests',), # XXX if E parts X, U has_read_permission E
+ 'add': ('managers', ERQLExpression('E parts X, U has_update_permission E'),),
+ 'delete': ('managers', ERQLExpression('E parts X, U has_update_permission E')),
+ 'update': ('managers', 'owners',),
+ }
+
+ content = String(fulltextindexed=True)
+ content_format = String(required=True, maxsize=50)
+ ordernum = Int(required=True)
+ alternative = SubjectRelation('EmailPart', symmetric=True)
+
+
+class EmailThread(EntityType):
+ """discussion thread"""
+ title = String(required=True, indexed=True, fulltextindexed=True)
+ see_also = SubjectRelation('EmailThread')
+ forked_from = SubjectRelation('EmailThread', cardinality='?*')
+
+class parts(RelationType):
+ """ """
+ fulltext_container = 'subject'
+
+class sender(RelationType):
+ """ """
+ inlined = True
+
+class in_thread(RelationType):
+ """ """
+ inlined = True
+
+class reply_to(RelationType):
+ """ """
+ inlined = True
+
+class generated_by(RelationType):
+ """mark an entity as generated from an email"""
+ cardinality = '?*'
+ subject = ('TrInfo',)
+ object = 'Email'
+
+# if comment is installed
+if 'Comment' in context.defined:
+ class comment_generated_by(RelationDefinition):
+ subject = 'Comment'
+ name = 'generated_by'
+ object = 'Email'
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/migratedapp/__init__.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,17 @@
+# 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/>.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/migratedapp/bootstrap_cubes Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+card,comment,tag,basket,fakeemail,file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/migratedapp/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,212 @@
+# copyright 2003-2013 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/>.
+"""cw.server.migraction test"""
+import datetime as dt
+from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
+ SubjectRelation, Bytes,
+ RichString, String, Int, Boolean, Datetime, Date, Float)
+from yams.constraints import SizeConstraint, UniqueConstraint
+from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
+ RQLVocabularyConstraint,
+ ERQLExpression, RRQLExpression)
+
+class Affaire(EntityType):
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers', ERQLExpression('X concerne S, S owned_by U')),
+ 'update': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+ 'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+ }
+
+ ref = String(fulltextindexed=True, indexed=True,
+ constraints=[SizeConstraint(16)])
+ sujet = String(fulltextindexed=True,
+ constraints=[SizeConstraint(256)])
+ concerne = SubjectRelation('Societe')
+ opt_attr = Bytes()
+
+class Societe(WorkflowableEntityType):
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'update': ('managers', 'owners'),
+ 'delete': ('managers', 'owners'),
+ 'add': ('managers', 'users',)
+ }
+ nom = String(maxsize=64, fulltextindexed=True)
+ web = String(maxsize=128)
+ tel = Float()
+ fax = Int()
+ rncs = String(maxsize=128)
+ ad1 = String(maxsize=128)
+ ad2 = String(maxsize=128)
+ ad3 = String(maxsize=128)
+ cp = String(maxsize=12)
+ ville= String(maxsize=32)
+
+# Division and SubDivision are gone
+
+# New
+class Para(EntityType):
+ para = String(maxsize=512)
+ newattr = String()
+ newinlined = SubjectRelation('Affaire', cardinality='?*', inlined=True)
+ newnotinlined = SubjectRelation('Affaire', cardinality='?*')
+
+class Note(Para):
+ __specializes_schema__ = True
+
+ __permissions__ = {'read': ('managers', 'users', 'guests',),
+ 'update': ('managers', 'owners',),
+ 'delete': ('managers', ),
+ 'add': ('managers',
+ ERQLExpression('X ecrit_part PE, U in_group G, '
+ 'PE require_permission P, P name "add_note", '
+ 'P require_group G'),)}
+
+ whatever = Int(default=0) # keep it before `date` for unittest_migraction.test_add_attribute_int
+ yesno = Boolean(default=False)
+ date = Datetime()
+ type = String(maxsize=1)
+ unique_id = String(maxsize=1, required=True, unique=True)
+ mydate = Date(default='TODAY')
+ oldstyledefaultdate = Date(default='2013/01/01')
+ newstyledefaultdate = Date(default=dt.date(2013, 1, 1))
+ shortpara = String(maxsize=64, default='hop')
+ ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
+ attachment = SubjectRelation('File')
+
+
+class Frozable(EntityType):
+ __permissions__ = {
+ 'read': ('managers', 'users'),
+ 'add': ('managers', 'users'),
+ 'update': ('managers', ERQLExpression('X frozen False'),),
+ 'delete': ('managers', ERQLExpression('X frozen False'),)
+ }
+ name = String()
+ frozen = Boolean(default=False,
+ __permissions__ = {
+ 'read': ('managers', 'users'),
+ 'add': ('managers', 'users'),
+ 'update': ('managers', 'owners')
+ })
+
+
+class Personne(EntityType):
+ __permissions__ = {
+ 'read': ('managers', 'users'), # 'guests' was removed
+ 'add': ('managers', 'users'),
+ 'update': ('managers', 'owners'),
+ 'delete': ('managers', 'owners')
+ }
+ __unique_together__ = [('nom', 'prenom', 'datenaiss')]
+ nom = String(fulltextindexed=True, required=True, maxsize=64)
+ prenom = String(fulltextindexed=True, maxsize=64)
+ civility = String(maxsize=1, default='M', fulltextindexed=True)
+ promo = String(vocabulary=('bon','pasbon'))
+ titre = String(fulltextindexed=True, maxsize=128)
+ adel = String(maxsize=128)
+ ass = String(maxsize=128)
+ web = String(maxsize=128)
+ tel = Int()
+ fax = Int()
+ datenaiss = Datetime()
+ test = Boolean()
+
+ travaille = SubjectRelation('Societe')
+ concerne = SubjectRelation('Affaire')
+ concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*')
+ connait = SubjectRelation('Personne', symmetric=True)
+
+class concerne(RelationType):
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers', RRQLExpression('U has_update_permission S')),
+ 'delete': ('managers', RRQLExpression('O owned_by U')),
+ }
+
+# `Old` entity type is gonce
+# `comments` is gone
+# `fiche` is gone
+# `multisource_*` rdefs are gone
+# `see_also_*` rdefs are gone
+
+class evaluee(RelationDefinition):
+ subject = ('Personne', 'CWUser', 'Societe')
+ object = ('Note')
+ constraints = [RQLVocabularyConstraint('S owned_by U')]
+
+class ecrit_par(RelationType):
+ __permissions__ = {'read': ('managers', 'users', 'guests',),
+ 'delete': ('managers', ),
+ 'add': ('managers',
+ RRQLExpression('O require_permission P, P name "add_note", '
+ 'U in_group G, P require_group G'),)
+ }
+ inlined = True
+ cardinality = '?*'
+
+# `copain` rdef is gone
+# `tags` rdef is gone
+# `filed_under` rdef is gone
+# `require_permission` rdef is gone
+# `require_state` rdef is gone
+# `personne_composite` rdef is gone
+# `personne_inlined` rdef is gone
+# `login_user` rdef is gone
+# `ambiguous_inlined` rdef is gone
+
+class Folder(EntityType):
+ """folders are used to classify entities. They may be defined as a tree.
+ """
+ name = String(required=True, indexed=True, internationalizable=True,
+ maxsize=64)
+ description = RichString(fulltextindexed=True)
+ filed_under = SubjectRelation('Folder', description=_('parent folder'))
+
+
+# New
+class Text(Para):
+ __specializes_schema__ = True
+ summary = String(maxsize=512)
+
+
+# New
+class Folder2(EntityType):
+ """folders are used to classify entities. They may be defined as a tree.
+ When you include the Folder entity, all application specific entities
+ may then be classified using the "filed_under" relation.
+ """
+ name = String(required=True, indexed=True, internationalizable=True,
+ constraints=[UniqueConstraint(), SizeConstraint(64)])
+ description = RichString(fulltextindexed=True)
+
+# New
+class filed_under2(RelationDefinition):
+ subject ='*'
+ object = 'Folder2'
+
+
+# New
+class New(EntityType):
+ new_name = String()
+
+# New
+class same_as(RelationDefinition):
+ subject = ('Societe',)
+ object = 'ExternalUri'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-migractions/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,288 @@
+# copyright 2003-2014 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 yams.buildobjs import (EntityType, RelationType, RelationDefinition, ComputedRelation,
+ SubjectRelation, RichString, String, Int, Float,
+ Boolean, Datetime, TZDatetime, Bytes)
+from yams.constraints import SizeConstraint
+from cubicweb.schema import (WorkflowableEntityType,
+ RQLConstraint, RQLUniqueConstraint,
+ RQLVocabularyConstraint,
+ ERQLExpression, RRQLExpression)
+
+class Affaire(WorkflowableEntityType):
+ __permissions__ = {
+ 'read': ('managers',
+ ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')),
+ 'add': ('managers', ERQLExpression('X concerne S, S owned_by U')),
+ 'update': ('managers', 'owners', ERQLExpression('X in_state S, S name in ("pitetre", "en cours")')),
+ 'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+ }
+
+ ref = String(fulltextindexed=True, indexed=True,
+ constraints=[SizeConstraint(16)])
+ sujet = String(fulltextindexed=True,
+ constraints=[SizeConstraint(256)])
+ descr = RichString(fulltextindexed=True,
+ description=_('more detailed description'))
+
+ duration = Int()
+ invoiced = Float()
+ opt_attr = Bytes()
+
+ depends_on = SubjectRelation('Affaire')
+ require_permission = SubjectRelation('CWPermission')
+ concerne = SubjectRelation(('Societe', 'Note'))
+ todo_by = SubjectRelation('Personne', cardinality='?*')
+ documented_by = SubjectRelation('Card')
+
+
+class Societe(EntityType):
+ __unique_together__ = [('nom', 'type', 'cp')]
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'update': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+ 'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+ 'add': ('managers', 'users',)
+ }
+
+ nom = String(maxsize=64, fulltextindexed=True)
+ web = String(maxsize=128)
+ type = String(maxsize=128) # attribute in common with Note
+ tel = Int()
+ fax = Int()
+ rncs = String(maxsize=128)
+ ad1 = String(maxsize=128)
+ ad2 = String(maxsize=128)
+ ad3 = String(maxsize=128)
+ cp = String(maxsize=12)
+ ville= String(maxsize=32)
+
+
+class Division(Societe):
+ __specializes_schema__ = True
+
+class SubDivision(Division):
+ __specializes_schema__ = True
+
+class travaille_subdivision(RelationDefinition):
+ subject = 'Personne'
+ object = 'SubDivision'
+
+from cubicweb.schemas.base import CWUser
+CWUser.get_relations('login').next().fulltextindexed = True
+
+class Note(WorkflowableEntityType):
+ date = String(maxsize=10)
+ type = String(vocabulary=[u'todo', u'a', u'b', u'T', u'lalala'])
+ para = String(maxsize=512,
+ __permissions__ = {
+ 'add': ('managers', ERQLExpression('X in_state S, S name "todo"')),
+ 'read': ('managers', 'users', 'guests'),
+ 'update': ('managers', ERQLExpression('X in_state S, S name "todo"')),
+ })
+ something = String(maxsize=1,
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': (ERQLExpression('NOT X para NULL'),),
+ 'update': ('managers', 'owners')
+ })
+ migrated_from = SubjectRelation('Note')
+ attachment = SubjectRelation('File')
+ inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*',
+ constraints=[RQLUniqueConstraint('S type T, S inline1 A1, A1 todo_by C, '
+ 'Y type T, Y inline1 A2, A2 todo_by C',
+ 'S,Y')])
+ todo_by = SubjectRelation('CWUser')
+
+
+class Frozable(EntityType):
+ __permissions__ = {
+ 'read': ('managers', 'users'),
+ 'add': ('managers', 'users'),
+ 'update': ('managers', ERQLExpression('X frozen False'),),
+ 'delete': ('managers', ERQLExpression('X frozen False'),)
+ }
+ name = String()
+ frozen = Boolean(default=False,
+ __permissions__ = {
+ 'read': ('managers', 'users'),
+ 'add': ('managers', 'users'),
+ 'update': ('managers', 'owners')
+ })
+
+
+class Personne(EntityType):
+ __unique_together__ = [('nom', 'prenom', 'inline2')]
+ nom = String(fulltextindexed=True, required=True, maxsize=64)
+ prenom = String(fulltextindexed=True, maxsize=64)
+ sexe = String(maxsize=1, default='M', fulltextindexed=True)
+ promo = String(vocabulary=('bon','pasbon'))
+ titre = String(fulltextindexed=True, maxsize=128)
+ adel = String(maxsize=128)
+ ass = String(maxsize=128)
+ web = String(maxsize=128)
+ tel = Int()
+ fax = Int()
+ datenaiss = Datetime()
+ tzdatenaiss = TZDatetime()
+ test = Boolean(__permissions__={
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers',),
+ 'update': ('managers',),
+ })
+ description = String()
+ firstname = String(fulltextindexed=True, maxsize=64)
+
+ concerne = SubjectRelation('Affaire')
+ connait = SubjectRelation('Personne')
+ inline2 = SubjectRelation('Affaire', inlined=True, cardinality='?*')
+
+
+class Old(EntityType):
+ name = String(__permissions__ = {
+ 'read' : ('managers', 'users', 'guests'),
+ 'add' : ('managers', 'users', 'guests'),
+ 'update' : ()
+ })
+
+
+class connait(RelationType):
+ symmetric = True
+
+class concerne(RelationType):
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers', RRQLExpression('U has_update_permission S')),
+ 'delete': ('managers', RRQLExpression('O owned_by U')),
+ }
+
+class travaille(RelationDefinition):
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers', RRQLExpression('U has_update_permission S')),
+ 'delete': ('managers', RRQLExpression('O owned_by U')),
+ }
+ subject = 'Personne'
+ object = 'Societe'
+ constraints = [RQLVocabularyConstraint('S owned_by U'),
+ RQLVocabularyConstraint('S created_by U')]
+
+class comments(RelationDefinition):
+ subject = 'Comment'
+ object = 'Personne'
+
+class fiche(RelationDefinition):
+ inlined = True
+ subject = 'Personne'
+ object = 'Card'
+ cardinality = '??'
+
+class multisource_inlined_rel(RelationDefinition):
+ inlined = True
+ cardinality = '?*'
+ subject = ('Card', 'Note')
+ object = ('Affaire', 'Note')
+
+
+class see_also_1(RelationDefinition):
+ name = 'see_also'
+ subject = object = 'Folder'
+
+class see_also_2(RelationDefinition):
+ name = 'see_also'
+ subject = ('Bookmark', 'Note')
+ object = ('Bookmark', 'Note')
+
+class evaluee(RelationDefinition):
+ subject = ('Personne', 'CWUser', 'Societe')
+ object = ('Note')
+ constraints = [
+ RQLVocabularyConstraint('S created_by U'),
+ RQLVocabularyConstraint('S owned_by U'),
+ ]
+
+class ecrit_par(RelationType):
+ inlined = True
+
+class ecrit_par_1(RelationDefinition):
+ name = 'ecrit_par'
+ subject = 'Note'
+ object ='Personne'
+ cardinality = '?*'
+
+class ecrit_par_2(RelationDefinition):
+ name = 'ecrit_par'
+ subject = 'Note'
+ object ='CWUser'
+ cardinality='?*'
+
+
+class copain(RelationDefinition):
+ subject = object = 'CWUser'
+
+class tags(RelationDefinition):
+ subject = 'Tag'
+ object = ('CWUser', 'CWGroup', 'State', 'Note', 'Card', 'Affaire')
+
+class Folder(EntityType):
+ """folders are used to classify entities. They may be defined as a tree.
+ """
+ name = String(required=True, indexed=True, internationalizable=True,
+ maxsize=64)
+ description = RichString(fulltextindexed=True)
+ filed_under = SubjectRelation('Folder', description=_('parent folder'))
+
+class filed_under(RelationDefinition):
+ subject = ('Note', 'Affaire')
+ object = 'Folder'
+
+class require_permission(RelationDefinition):
+ subject = ('Card', 'Note', 'Personne')
+ object = 'CWPermission'
+
+class require_state(RelationDefinition):
+ subject = 'CWPermission'
+ object = 'State'
+
+class personne_composite(RelationDefinition):
+ subject='Personne'
+ object='Personne'
+ composite='subject'
+
+class personne_inlined(RelationDefinition):
+ subject='Personne'
+ object='Personne'
+ cardinality='?*'
+ inlined=True
+
+
+class login_user(RelationDefinition):
+ subject = 'Personne'
+ object = 'CWUser'
+ cardinality = '??'
+
+class ambiguous_inlined(RelationDefinition):
+ subject = ('Affaire', 'Note')
+ object = 'CWUser'
+ inlined = True
+ cardinality = '?*'
+
+
+class user_login(ComputedRelation):
+ rule = 'O login_user S'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/Company.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,67 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+from yams.buildobjs import EntityType, RelationType, RelationDefinition, \
+ SubjectRelation, String
+
+class Company(EntityType):
+ name = String()
+
+class Subcompany(Company):
+ __specializes_schema__ = True
+ subcompany_of = SubjectRelation('Company')
+
+class Division(Company):
+ __specializes_schema__ = True
+ division_of = SubjectRelation('Company')
+
+class Subdivision(Division):
+ __specializes_schema__ = True
+ subdivision_of = SubjectRelation('Company')
+
+class Employee(EntityType):
+ works_for = SubjectRelation('Company')
+
+class require_permission(RelationType):
+ """link a permission to the entity. This permission should be used in the
+ security definition of the entity's type to be useful.
+ """
+ fulltext_container = 'subject'
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers',),
+ 'delete': ('managers',),
+ }
+
+
+class missing_require_permission(RelationDefinition):
+ name = 'require_permission'
+ subject = 'Company'
+ object = 'EPermission'
+
+class EPermission(EntityType):
+ """entity type that may be used to construct some advanced security configuration
+ """
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests',),
+ 'add': ('managers',),
+ 'delete': ('managers',),
+ 'update': ('managers', 'owners',),
+ }
+ name = String(required=True, indexed=True, internationalizable=True,
+ fulltextindexed=True, maxsize=100,
+ description=_('name or identifier of the permission'))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/Dates.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,29 @@
+# copyright 2004-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+from datetime import time, date
+from yams.buildobjs import EntityType, Datetime, Date, Time
+from yams.constraints import TODAY, BoundaryConstraint
+
+class Datetest(EntityType):
+ dt1 = Datetime(default=u'now')
+ dt2 = Datetime(default=u'today')
+ d1 = Date(default=u'today', constraints=[BoundaryConstraint('<=', TODAY())])
+ d2 = Date(default=date(2007, 12, 11))
+ t1 = Time(default=time(8, 40))
+ t2 = Time(default=time(9, 45))
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/State.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,81 @@
+# copyright 2004-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
+ SubjectRelation, Int, String, Boolean)
+from yams.constraints import SizeConstraint, UniqueConstraint
+
+from __init__ import RESTRICTED_RTYPE_PERMS
+
+class State(EntityType):
+ """used to associate simple states to an entity
+ type and/or to define workflows
+ """
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests',),
+ 'add': ('managers', 'users',),
+ 'delete': ('managers', 'owners',),
+ 'update': ('managers', 'owners',),
+ }
+
+ # attributes
+ eid = Int(required=True, uid=True)
+ name = String(required=True,
+ indexed=True, internationalizable=True,
+ constraints=[SizeConstraint(256)])
+ description = String(fulltextindexed=True)
+ # relations
+ state_of = SubjectRelation('Eetype', cardinality='+*')
+ next_state = SubjectRelation('State', cardinality='**')
+
+
+class state_of(RelationType):
+ """link a state to one or more entity type"""
+ __permissions__ = RESTRICTED_RTYPE_PERMS
+
+class next_state(RelationType):
+ """define a workflow by associating a state to possible following states
+ """
+ __permissions__ = RESTRICTED_RTYPE_PERMS
+
+class initial_state(RelationType):
+ """indicate which state should be used by default when an entity using states
+ is created
+ """
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests',),
+ 'add': ('managers', 'users',),
+ 'delete': ('managers', 'users',),
+ }
+ subject = 'Eetype'
+ object = 'State'
+ cardinality = '?*'
+ inlined = True
+
+class Eetype(EntityType):
+ """define an entity type, used to build the application schema"""
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests',),
+ 'add': ('managers',),
+ 'delete': ('managers',),
+ 'update': ('managers', 'owners',),
+ }
+ name = String(required=True, indexed=True, internationalizable=True,
+ constraints=[UniqueConstraint(), SizeConstraint(64)])
+ description = String(fulltextindexed=True)
+ meta = Boolean()
+ final = Boolean()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/__init__.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,23 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+"""test schema"""
+RESTRICTED_RTYPE_PERMS = {
+ 'read': ('managers', 'users', 'guests',),
+ 'add': ('managers',),
+ 'delete': ('managers',),
+ }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,113 @@
+# copyright 2004-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of yams.
+#
+# yams 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.
+#
+# yams 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 yams. If not, see <http://www.gnu.org/licenses/>.
+from yams.buildobjs import (EntityType, RelationDefinition, RelationType,
+ SubjectRelation, String, Int, Float, Date, Boolean)
+from yams.constraints import Attribute, BoundaryConstraint
+
+class Affaire(EntityType):
+ sujet = String(maxsize=128)
+ ref = String(maxsize=12)
+
+ concerne = SubjectRelation('Societe')
+ obj_wildcard = SubjectRelation('*')
+ sym_rel = SubjectRelation('Person', symmetric=True)
+ inline_rel = SubjectRelation('Person', inlined=True, cardinality='?*')
+
+class subj_wildcard(RelationDefinition):
+ subject = '*'
+ object = 'Affaire'
+
+
+class Person(EntityType):
+ __unique_together__ = [('nom', 'prenom')]
+ nom = String(maxsize=64, fulltextindexed=True, required=True)
+ prenom = String(maxsize=64, fulltextindexed=True)
+ sexe = String(maxsize=1, default='M')
+ promo = String(vocabulary=('bon','pasbon'))
+ titre = String(maxsize=128, fulltextindexed=True)
+ adel = String(maxsize=128)
+ ass = String(maxsize=128)
+ web = String(maxsize=128)
+ tel = Int(__permissions__={'read': (),
+ 'add': ('managers',),
+ 'update': ('managers',)})
+ fax = Int()
+ datenaiss = Date()
+ test = Boolean()
+ salary = Float()
+ travaille = SubjectRelation('Societe',
+ __permissions__={'read': (),
+ 'add': (),
+ 'delete': ('managers',),
+ })
+
+ evaluee = SubjectRelation('Note')
+
+class Salaried(Person):
+ __specializes_schema__ = True
+
+class Societe(EntityType):
+ nom = String(maxsize=64, fulltextindexed=True)
+ web = String(maxsize=128)
+ tel = Int()
+ fax = Int(constraints=[BoundaryConstraint('<=', Attribute('tel'))])
+ rncs = String(maxsize=32)
+ ad1 = String(maxsize=128)
+ ad2 = String(maxsize=128)
+ ad3 = String(maxsize=128)
+ cp = String(maxsize=12)
+ ville = String(maxsize=32)
+
+ evaluee = SubjectRelation('Note')
+
+
+class Note(EntityType):
+ date = String(maxsize=10)
+ type = String(maxsize=1)
+ para = String(maxsize=512)
+
+
+class pkginfo(EntityType):
+ modname = String(maxsize=30, required=True)
+ version = String(maxsize=10, required=True, default='0.1')
+ copyright = String(required=True)
+ license = String(vocabulary=('GPL', 'ZPL'))
+ short_desc = String(maxsize=80, required=True)
+ long_desc = String(required=True, fulltextindexed=True)
+ author = String(maxsize=100, required=True)
+ author_email = String(maxsize=100, required=True)
+ mailinglist = String(maxsize=100)
+ debian_handler = String(vocabulary=('machin', 'bidule'))
+
+
+class evaluee(RelationType):
+ __permissions__ = {
+ 'read': ('managers',),
+ 'add': ('managers',),
+ 'delete': ('managers',),
+ }
+
+class concerne(RelationDefinition):
+ subject = 'Person'
+ object = 'Affaire'
+ __permissions__ = {
+ 'read': ('managers',),
+ 'add': ('managers',),
+ 'delete': ('managers',),
+ }
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-schema2sql/schema/toignore Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+coucou
--- a/server/test/data/bootstrap_cubes Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/data/bootstrap_cubes Tue Jul 19 16:13:12 2016 +0200
@@ -1,1 +1,1 @@
-card,comment,folder,tag,basket,email,file,localperms
+card,comment,tag,basket,file,localperms
--- a/server/test/data/migratedapp/__init__.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +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/>.
--- a/server/test/data/migratedapp/bootstrap_cubes Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-card,comment,folder,tag,basket,email,file
--- a/server/test/data/migratedapp/schema.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,203 +0,0 @@
-# copyright 2003-2013 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/>.
-"""cw.server.migraction test"""
-import datetime as dt
-from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
- SubjectRelation, Bytes,
- RichString, String, Int, Boolean, Datetime, Date)
-from yams.constraints import SizeConstraint, UniqueConstraint
-from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
- RQLVocabularyConstraint,
- ERQLExpression, RRQLExpression)
-
-class Affaire(EntityType):
- __permissions__ = {
- 'read': ('managers', 'users', 'guests'),
- 'add': ('managers', ERQLExpression('X concerne S, S owned_by U')),
- 'update': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
- 'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
- }
-
- ref = String(fulltextindexed=True, indexed=True,
- constraints=[SizeConstraint(16)])
- sujet = String(fulltextindexed=True,
- constraints=[SizeConstraint(256)])
- concerne = SubjectRelation('Societe')
- opt_attr = Bytes()
-
-class Societe(WorkflowableEntityType):
- __permissions__ = {
- 'read': ('managers', 'users', 'guests'),
- 'update': ('managers', 'owners'),
- 'delete': ('managers', 'owners'),
- 'add': ('managers', 'users',)
- }
- nom = String(maxsize=64, fulltextindexed=True)
- web = String(maxsize=128)
- tel = Int()
- fax = Int()
- rncs = String(maxsize=128)
- ad1 = String(maxsize=128)
- ad2 = String(maxsize=128)
- ad3 = String(maxsize=128)
- cp = String(maxsize=12)
- ville= String(maxsize=32)
-
-# Division and SubDivision are gone
-
-# New
-class Para(EntityType):
- para = String(maxsize=512)
- newattr = String()
- newinlined = SubjectRelation('Affaire', cardinality='?*', inlined=True)
- newnotinlined = SubjectRelation('Affaire', cardinality='?*')
-
-class Note(Para):
- __specializes_schema__ = True
-
- __permissions__ = {'read': ('managers', 'users', 'guests',),
- 'update': ('managers', 'owners',),
- 'delete': ('managers', ),
- 'add': ('managers',
- ERQLExpression('X ecrit_part PE, U in_group G, '
- 'PE require_permission P, P name "add_note", '
- 'P require_group G'),)}
-
- whatever = Int(default=0) # keep it before `date` for unittest_migraction.test_add_attribute_int
- yesno = Boolean(default=False)
- date = Datetime()
- type = String(maxsize=1)
- unique_id = String(maxsize=1, required=True, unique=True)
- mydate = Date(default='TODAY')
- oldstyledefaultdate = Date(default='2013/01/01')
- newstyledefaultdate = Date(default=dt.date(2013, 1, 1))
- shortpara = String(maxsize=64, default='hop')
- ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
- attachment = SubjectRelation('File')
-
-
-class Frozable(EntityType):
- __permissions__ = {
- 'read': ('managers', 'users'),
- 'add': ('managers', 'users'),
- 'update': ('managers', ERQLExpression('X frozen False'),),
- 'delete': ('managers', ERQLExpression('X frozen False'),)
- }
- name = String()
- frozen = Boolean(default=False,
- __permissions__ = {
- 'read': ('managers', 'users'),
- 'add': ('managers', 'users'),
- 'update': ('managers', 'owners')
- })
-
-
-class Personne(EntityType):
- __permissions__ = {
- 'read': ('managers', 'users'), # 'guests' was removed
- 'add': ('managers', 'users'),
- 'update': ('managers', 'owners'),
- 'delete': ('managers', 'owners')
- }
- __unique_together__ = [('nom', 'prenom', 'datenaiss')]
- nom = String(fulltextindexed=True, required=True, maxsize=64)
- prenom = String(fulltextindexed=True, maxsize=64)
- civility = String(maxsize=1, default='M', fulltextindexed=True)
- promo = String(vocabulary=('bon','pasbon'))
- titre = String(fulltextindexed=True, maxsize=128)
- adel = String(maxsize=128)
- ass = String(maxsize=128)
- web = String(maxsize=128)
- tel = Int()
- fax = Int()
- datenaiss = Datetime()
- test = Boolean()
-
- travaille = SubjectRelation('Societe')
- concerne = SubjectRelation('Affaire')
- concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*')
- connait = SubjectRelation('Personne', symmetric=True)
-
-class concerne(RelationType):
- __permissions__ = {
- 'read': ('managers', 'users', 'guests'),
- 'add': ('managers', RRQLExpression('U has_update_permission S')),
- 'delete': ('managers', RRQLExpression('O owned_by U')),
- }
-
-# `Old` entity type is gonce
-# `comments` is gone
-# `fiche` is gone
-# `multisource_*` rdefs are gone
-# `see_also_*` rdefs are gone
-
-class evaluee(RelationDefinition):
- subject = ('Personne', 'CWUser', 'Societe')
- object = ('Note')
- constraints = [RQLVocabularyConstraint('S owned_by U')]
-
-class ecrit_par(RelationType):
- __permissions__ = {'read': ('managers', 'users', 'guests',),
- 'delete': ('managers', ),
- 'add': ('managers',
- RRQLExpression('O require_permission P, P name "add_note", '
- 'U in_group G, P require_group G'),)
- }
- inlined = True
- cardinality = '?*'
-
-# `copain` rdef is gone
-# `tags` rdef is gone
-# `filed_under` rdef is gone
-# `require_permission` rdef is gone
-# `require_state` rdef is gone
-# `personne_composite` rdef is gone
-# `personne_inlined` rdef is gone
-# `login_user` rdef is gone
-# `ambiguous_inlined` rdef is gone
-
-# New
-class Text(Para):
- __specializes_schema__ = True
- summary = String(maxsize=512)
-
-
-# New
-class Folder2(EntityType):
- """folders are used to classify entities. They may be defined as a tree.
- When you include the Folder entity, all application specific entities
- may then be classified using the "filed_under" relation.
- """
- name = String(required=True, indexed=True, internationalizable=True,
- constraints=[UniqueConstraint(), SizeConstraint(64)])
- description = RichString(fulltextindexed=True)
-
-# New
-class filed_under2(RelationDefinition):
- subject ='*'
- object = 'Folder2'
-
-
-# New
-class New(EntityType):
- new_name = String()
-
-# New
-class same_as(RelationDefinition):
- subject = ('Societe',)
- object = 'ExternalUri'
--- a/server/test/data/schema.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/data/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -89,7 +89,7 @@
class Note(WorkflowableEntityType):
date = String(maxsize=10)
- type = String(maxsize=6)
+ type = String(vocabulary=[u'todo', u'a', u'b', u'T', u'lalala'])
para = String(maxsize=512,
__permissions__ = {
'add': ('managers', ERQLExpression('X in_state S, S name "todo"')),
@@ -168,6 +168,22 @@
})
+class Email(EntityType):
+ subject = String(fulltextindexed=True)
+ messageid = String(required=True, indexed=True, unique=True)
+ sender = SubjectRelation('EmailAddress', cardinality='?*')
+ recipients = SubjectRelation('EmailAddress')
+ attachment = SubjectRelation('File')
+
+
+class EmailPart(EntityType):
+ pass
+
+
+class EmailThread(EntityType):
+ see_also = SubjectRelation('EmailThread')
+
+
class connait(RelationType):
symmetric = True
@@ -246,6 +262,14 @@
subject = 'Tag'
object = ('CWUser', 'CWGroup', 'State', 'Note', 'Card', 'Affaire')
+class Folder(EntityType):
+ """folders are used to classify entities. They may be defined as a tree.
+ """
+ name = String(required=True, indexed=True, internationalizable=True,
+ maxsize=64)
+ description = RichString(fulltextindexed=True)
+ filed_under = SubjectRelation('Folder', description=_('parent folder'))
+
class filed_under(RelationDefinition):
subject = ('Note', 'Affaire')
object = 'Folder'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,7 @@
+psycopg2
+cubicweb-basket
+cubicweb-card
+cubicweb-comment
+cubicweb-file
+cubicweb-localperms
+cubicweb-tag
--- a/server/test/unittest_ldapsource.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_ldapsource.py Tue Jul 19 16:13:12 2016 +0200
@@ -25,8 +25,6 @@
import subprocess
import tempfile
-from logilab.common.testlib import TestCase, unittest_main, mock_object, Tags
-
from cubicweb import AuthenticationError
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.devtools.repotest import RQLGeneratorTC
@@ -480,4 +478,5 @@
if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
unittest_main()
--- a/server/test/unittest_migractions.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_migractions.py Tue Jul 19 16:13:12 2016 +0200
@@ -18,7 +18,7 @@
"""unit tests for module cubicweb.server.migractions"""
from datetime import date
-import os.path as osp
+import os, os.path as osp
from contextlib import contextmanager
from logilab.common.testlib import unittest_main, Tags, tag
@@ -26,6 +26,7 @@
from yams.constraints import UniqueConstraint
from cubicweb import ConfigurationError, ValidationError, ExecutionError
+from cubicweb.devtools import startpgcluster, stoppgcluster
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.server.sqlutils import SQL_PREFIX
from cubicweb.server.migractions import ServerMigrationHelper
@@ -35,6 +36,11 @@
HERE = osp.dirname(osp.abspath(__file__))
+
+def setUpModule():
+ startpgcluster(__file__)
+
+
migrschema = None
def tearDownModule(*args):
global migrschema
@@ -43,10 +49,19 @@
del MigrationCommandsTC.origschema
if hasattr(MigrationCommandsComputedTC, 'origschema'):
del MigrationCommandsComputedTC.origschema
+ stoppgcluster(__file__)
+
+
+class MigrationConfig(cubicweb.devtools.TestServerConfiguration):
+ default_sources = cubicweb.devtools.DEFAULT_PSQL_SOURCES
+ CUBES_PATH = [osp.join(HERE, 'data-migractions', 'cubes')]
+
class MigrationTC(CubicWebTC):
- configcls = cubicweb.devtools.TestServerConfiguration
+ appid = 'data-migractions'
+
+ configcls = MigrationConfig
tags = CubicWebTC.tags | Tags(('server', 'migration', 'migractions'))
@@ -64,31 +79,32 @@
config._apphome = osp.join(HERE, self.appid)
def setUp(self):
- CubicWebTC.setUp(self)
+ self.configcls.cls_adjust_sys_path()
+ super(MigrationTC, self).setUp()
def tearDown(self):
- CubicWebTC.tearDown(self)
+ super(MigrationTC, self).tearDown()
self.repo.vreg['etypes'].clear_caches()
@contextmanager
def mh(self):
- with self.admin_access.client_cnx() as cnx:
+ with self.admin_access.repo_cnx() as cnx:
yield cnx, ServerMigrationHelper(self.repo.config, migrschema,
repo=self.repo, cnx=cnx,
interactive=False)
def table_sql(self, mh, tablename):
- result = mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
- "and name=%(table)s", {'table': tablename})
+ result = mh.sqlexec("SELECT table_name FROM information_schema.tables WHERE LOWER(table_name)=%(table)s",
+ {'table': tablename.lower()})
if result:
return result[0][0]
return None # no such table
def table_schema(self, mh, tablename):
- sql = self.table_sql(mh, tablename)
- assert sql, 'no table %s' % tablename
- return dict(x.split()[:2]
- for x in sql.split('(', 1)[1].rsplit(')', 1)[0].split(','))
+ result = mh.sqlexec("SELECT column_name, data_type, character_maximum_length FROM information_schema.columns "
+ "WHERE LOWER(table_name) = %(table)s", {'table': tablename.lower()})
+ assert result, 'no table %s' % tablename
+ return dict((x[0], (x[1], x[2])) for x in result)
class MigrationCommandsTC(MigrationTC):
@@ -160,7 +176,7 @@
self.assertEqual(self.schema['shortpara'].objects(), ('String', ))
# test created column is actually a varchar(64)
fields = self.table_schema(mh, '%sNote' % SQL_PREFIX)
- self.assertEqual(fields['%sshortpara' % SQL_PREFIX], 'varchar(64)')
+ self.assertEqual(fields['%sshortpara' % SQL_PREFIX], ('character varying', 64))
# test default value set on existing entities
self.assertEqual(cnx.execute('Note X').get_entity(0, 0).shortpara, 'hop')
# test default value set for next entities
@@ -212,6 +228,7 @@
droprequired=True):
mh.cmd_add_attribute('Note', 'unique_id')
mh.rqlexec('INSERT Note N')
+ mh.rqlexec('SET N unique_id "x"')
# make sure the required=True was restored
self.assertRaises(ValidationError, mh.rqlexec, 'INSERT Note N')
mh.rollback()
@@ -259,8 +276,8 @@
'filed_under2', 'has_text',
'identity', 'in_basket', 'is', 'is_instance_of',
'modification_date', 'name', 'owned_by'])
- self.assertEqual([str(rs) for rs in self.schema['Folder2'].object_relations()],
- ['filed_under2', 'identity'])
+ self.assertCountEqual([str(rs) for rs in self.schema['Folder2'].object_relations()],
+ ['filed_under2', 'identity'])
# Old will be missing as it has been renamed into 'New' in the migrated
# schema while New hasn't been added here.
self.assertEqual(sorted(str(e) for e in self.schema['filed_under2'].subjects()),
@@ -567,15 +584,15 @@
('Bookmark', 'Bookmark'), ('Bookmark', 'Note'),
('Note', 'Note'), ('Note', 'Bookmark')]))
try:
- mh.cmd_drop_cube('email', removedeps=True)
+ mh.cmd_drop_cube('fakeemail', removedeps=True)
# file was there because it's an email dependancy, should have been removed
- self.assertNotIn('email', self.config.cubes())
- self.assertNotIn(self.config.cube_dir('email'), self.config.cubes_path())
+ self.assertNotIn('fakeemail', self.config.cubes())
+ self.assertNotIn(self.config.cube_dir('fakeemail'), self.config.cubes_path())
self.assertNotIn('file', self.config.cubes())
self.assertNotIn(self.config.cube_dir('file'), self.config.cubes_path())
for ertype in ('Email', 'EmailThread', 'EmailPart', 'File',
'sender', 'in_thread', 'reply_to', 'data_format'):
- self.assertFalse(ertype in schema, ertype)
+ self.assertNotIn(ertype, schema)
self.assertEqual(sorted(schema['see_also'].rdefs.iterkeys()),
sorted([('Folder', 'Folder'),
('Bookmark', 'Bookmark'),
@@ -584,17 +601,17 @@
('Note', 'Bookmark')]))
self.assertEqual(sorted(schema['see_also'].subjects()), ['Bookmark', 'Folder', 'Note'])
self.assertEqual(sorted(schema['see_also'].objects()), ['Bookmark', 'Folder', 'Note'])
- self.assertEqual(cnx.execute('Any X WHERE X pkey "system.version.email"').rowcount, 0)
+ self.assertEqual(cnx.execute('Any X WHERE X pkey "system.version.fakeemail"').rowcount, 0)
self.assertEqual(cnx.execute('Any X WHERE X pkey "system.version.file"').rowcount, 0)
finally:
- mh.cmd_add_cube('email')
- self.assertIn('email', self.config.cubes())
- self.assertIn(self.config.cube_dir('email'), self.config.cubes_path())
+ mh.cmd_add_cube('fakeemail')
+ self.assertIn('fakeemail', self.config.cubes())
+ self.assertIn(self.config.cube_dir('fakeemail'), self.config.cubes_path())
self.assertIn('file', self.config.cubes())
self.assertIn(self.config.cube_dir('file'), self.config.cubes_path())
for ertype in ('Email', 'EmailThread', 'EmailPart', 'File',
'sender', 'in_thread', 'reply_to', 'data_format'):
- self.assertTrue(ertype in schema, ertype)
+ self.assertIn(ertype, schema)
self.assertEqual(sorted(schema['see_also'].rdefs.iterkeys()),
sorted([('EmailThread', 'EmailThread'), ('Folder', 'Folder'),
('Bookmark', 'Bookmark'),
@@ -603,9 +620,9 @@
('Note', 'Bookmark')]))
self.assertEqual(sorted(schema['see_also'].subjects()), ['Bookmark', 'EmailThread', 'Folder', 'Note'])
self.assertEqual(sorted(schema['see_also'].objects()), ['Bookmark', 'EmailThread', 'Folder', 'Note'])
- from cubes.email.__pkginfo__ import version as email_version
+ from cubes.fakeemail.__pkginfo__ import version as email_version
from cubes.file.__pkginfo__ import version as file_version
- self.assertEqual(cnx.execute('Any V WHERE X value V, X pkey "system.version.email"')[0][0],
+ self.assertEqual(cnx.execute('Any V WHERE X value V, X pkey "system.version.fakeemail"')[0][0],
email_version)
self.assertEqual(cnx.execute('Any V WHERE X value V, X pkey "system.version.file"')[0][0],
file_version)
@@ -623,16 +640,16 @@
cubes = set(self.config.cubes())
schema = self.repo.schema
try:
- mh.cmd_drop_cube('email')
- cubes.remove('email')
- self.assertNotIn('email', self.config.cubes())
+ mh.cmd_drop_cube('fakeemail')
+ cubes.remove('fakeemail')
+ self.assertNotIn('fakeemail', self.config.cubes())
self.assertIn('file', self.config.cubes())
for ertype in ('Email', 'EmailThread', 'EmailPart',
'sender', 'in_thread', 'reply_to'):
- self.assertFalse(ertype in schema, ertype)
+ self.assertNotIn(ertype, schema)
finally:
- mh.cmd_add_cube('email')
- self.assertIn('email', self.config.cubes())
+ mh.cmd_add_cube('fakeemail')
+ self.assertIn('fakeemail', self.config.cubes())
# trick: overwrite self.maxeid to avoid deletion of just reintroduced
# types (and their associated tables!)
self.maxeid = cnx.execute('Any MAX(X)')[0][0] # XXXXXXX KILL KENNY
@@ -690,6 +707,18 @@
mh.cmd_add_relation_type('same_as')
self.assertTrue(self.table_sql(mh, 'same_as_relation'))
+ def test_change_attribute_type(self):
+ with self.mh() as (cnx, mh):
+ mh.cmd_create_entity('Societe', tel=1)
+ mh.commit()
+ mh.change_attribute_type('Societe', 'tel', 'Float')
+ self.assertNotIn(('Societe', 'Int'), self.schema['tel'].rdefs)
+ self.assertIn(('Societe', 'Float'), self.schema['tel'].rdefs)
+ self.assertEqual(self.schema['tel'].rdefs[('Societe', 'Float')].object, 'Float')
+ tel = mh.rqlexec('Any T WHERE X tel T')[0][0]
+ self.assertEqual(tel, 1.0)
+ self.assertIsInstance(tel, float)
+
class MigrationCommandsComputedTC(MigrationTC):
""" Unit tests for computed relations and attributes
@@ -787,7 +816,7 @@
self.assertEqual(self.schema['score'].rdefs['Company', 'Float'].formula,
'Any AVG(NN) WHERE X employees E, N concerns E, N note NN')
fields = self.table_schema(mh, '%sCompany' % SQL_PREFIX)
- self.assertEqual(fields['%sscore' % SQL_PREFIX], 'float')
+ self.assertEqual(fields['%sscore' % SQL_PREFIX], ('double precision', None))
self.assertEqual([[3.0]],
mh.rqlexec('Any CS WHERE C score CS, C is Company').rows)
@@ -811,10 +840,9 @@
def assert_computed_attribute_dropped(self):
self.assertNotIn('note20', self.schema)
- # DROP COLUMN not supported by sqlite
- #with self.mh() as (cnx, mh):
- # fields = self.table_schema(mh, '%sNote' % SQL_PREFIX)
- #self.assertNotIn('%snote20' % SQL_PREFIX, fields)
+ with self.mh() as (cnx, mh):
+ fields = self.table_schema(mh, '%sNote' % SQL_PREFIX)
+ self.assertNotIn('%snote20' % SQL_PREFIX, fields)
def test_computed_attribute_drop_type(self):
self.assertIn('note20', self.schema)
--- a/server/test/unittest_postgres.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_postgres.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -21,17 +21,29 @@
from logilab.common.testlib import SkipTest
-from cubicweb.devtools import PostgresApptestConfiguration
+from cubicweb import ValidationError
+from cubicweb.devtools import PostgresApptestConfiguration, startpgcluster, stoppgcluster
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.predicates import is_instance
from cubicweb.entities.adapters import IFTIndexableAdapter
from unittest_querier import FixedOffset
+
+def setUpModule():
+ startpgcluster(__file__)
+
+
+def tearDownModule():
+ stoppgcluster(__file__)
+
+
class PostgresTimeoutConfiguration(PostgresApptestConfiguration):
- default_sources = PostgresApptestConfiguration.default_sources.copy()
- default_sources['system'] = PostgresApptestConfiguration.default_sources['system'].copy()
- default_sources['system']['db-statement-timeout'] = 200
+ def __init__(self, *args, **kwargs):
+ self.default_sources = PostgresApptestConfiguration.default_sources.copy()
+ self.default_sources['system'] = PostgresApptestConfiguration.default_sources['system'].copy()
+ self.default_sources['system']['db-statement-timeout'] = 200
+ super(PostgresTimeoutConfiguration, self).__init__(*args, **kwargs)
class PostgresFTITC(CubicWebTC):
@@ -119,6 +131,24 @@
self.assertEqual(datenaiss.tzinfo, None)
self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 2, 0))
+ def test_constraint_validationerror(self):
+ with self.admin_access.repo_cnx() as cnx:
+ with cnx.allow_all_hooks_but('integrity'):
+ with self.assertRaises(ValidationError) as cm:
+ cnx.execute("INSERT Note N: N type 'nogood'")
+ self.assertEqual(cm.exception.errors,
+ {'type-subject': u'invalid value %(KEY-value)s, it must be one of %(KEY-choices)s'})
+ self.assertEqual(cm.exception.msgargs,
+ {'type-subject-value': u'"nogood"',
+ 'type-subject-choices': u'"todo", "a", "b", "T", "lalala"'})
+
+ def test_statement_timeout(self):
+ with self.admin_access.repo_cnx() as cnx:
+ cnx.system_sql('select pg_sleep(0.1)')
+ with self.assertRaises(Exception):
+ cnx.system_sql('select pg_sleep(0.3)')
+
+
class PostgresLimitSizeTC(CubicWebTC):
configcls = PostgresApptestConfiguration
@@ -137,12 +167,6 @@
yield self.assertEqual, sql("SELECT limit_size('<span>a>b</span>', 'text/html', 2)"), \
'a>...'
- def test_statement_timeout(self):
- with self.admin_access.repo_cnx() as cnx:
- cnx.system_sql('select pg_sleep(0.1)')
- with self.assertRaises(Exception):
- cnx.system_sql('select pg_sleep(0.3)')
-
if __name__ == '__main__':
from logilab.common.testlib import unittest_main
--- a/server/test/unittest_querier.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_querier.py Tue Jul 19 16:13:12 2016 +0200
@@ -69,7 +69,6 @@
def tearDownClass(cls, *args):
global repo, cnx
- cnx.close()
repo.shutdown()
del repo, cnx
@@ -1173,11 +1172,10 @@
def test_delete_3(self):
s = self.user_groups_session('users')
with s.new_cnx() as cnx:
- with cnx.ensure_cnx_set:
- peid, = self.o.execute(cnx, "INSERT Personne P: P nom 'toto'")[0]
- seid, = self.o.execute(cnx, "INSERT Societe S: S nom 'logilab'")[0]
- self.o.execute(cnx, "SET P travaille S")
- cnx.commit()
+ peid, = self.o.execute(cnx, "INSERT Personne P: P nom 'toto'")[0]
+ seid, = self.o.execute(cnx, "INSERT Societe S: S nom 'logilab'")[0]
+ self.o.execute(cnx, "SET P travaille S")
+ cnx.commit()
rset = self.qexecute('Personne P WHERE P travaille S')
self.assertEqual(len(rset.rows), 1)
self.qexecute("DELETE X travaille Y WHERE X eid %s, Y eid %s" % (peid, seid))
@@ -1212,12 +1210,11 @@
'X sender Y, X recipients Y WHERE Y is EmailAddress')[0]
self.qexecute("DELETE Email X")
with self.session.new_cnx() as cnx:
- with cnx.ensure_cnx_set:
- sqlc = cnx.cnxset.cu
- sqlc.execute('SELECT * FROM recipients_relation')
- self.assertEqual(len(sqlc.fetchall()), 0)
- sqlc.execute('SELECT * FROM owned_by_relation WHERE eid_from=%s'%eeid)
- self.assertEqual(len(sqlc.fetchall()), 0)
+ sqlc = cnx.cnxset.cu
+ sqlc.execute('SELECT * FROM recipients_relation')
+ self.assertEqual(len(sqlc.fetchall()), 0)
+ sqlc.execute('SELECT * FROM owned_by_relation WHERE eid_from=%s'%eeid)
+ self.assertEqual(len(sqlc.fetchall()), 0)
def test_nonregr_delete_cache2(self):
eid = self.qexecute("INSERT Folder T: T name 'toto'")[0][0]
@@ -1364,12 +1361,11 @@
self.assertRaises(Unauthorized,
self.qexecute, "Any P WHERE X is CWUser, X login 'bob', X upassword P")
with self.session.new_cnx() as cnx:
- with cnx.ensure_cnx_set:
- cursor = cnx.cnxset.cu
- cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
- % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
- passwd = str(cursor.fetchone()[0])
- self.assertEqual(passwd, crypt_password('toto', passwd))
+ cursor = cnx.cnxset.cu
+ cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
+ % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
+ passwd = str(cursor.fetchone()[0])
+ self.assertEqual(passwd, crypt_password('toto', passwd))
rset = self.qexecute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
{'pwd': Binary(passwd)})
self.assertEqual(len(rset.rows), 1)
@@ -1377,21 +1373,20 @@
def test_update_upassword(self):
with self.session.new_cnx() as cnx:
- with cnx.ensure_cnx_set:
- rset = cnx.execute("INSERT CWUser X: X login 'bob', X upassword %(pwd)s",
- {'pwd': 'toto'})
- self.assertEqual(rset.description[0][0], 'CWUser')
- rset = cnx.execute("SET X upassword %(pwd)s WHERE X is CWUser, X login 'bob'",
- {'pwd': 'tutu'})
- cursor = cnx.cnxset.cu
- cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
- % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
- passwd = str(cursor.fetchone()[0])
- self.assertEqual(passwd, crypt_password('tutu', passwd))
- rset = cnx.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
- {'pwd': Binary(passwd)})
- self.assertEqual(len(rset.rows), 1)
- self.assertEqual(rset.description, [('CWUser',)])
+ rset = cnx.execute("INSERT CWUser X: X login 'bob', X upassword %(pwd)s",
+ {'pwd': 'toto'})
+ self.assertEqual(rset.description[0][0], 'CWUser')
+ rset = cnx.execute("SET X upassword %(pwd)s WHERE X is CWUser, X login 'bob'",
+ {'pwd': 'tutu'})
+ cursor = cnx.cnxset.cu
+ cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
+ % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
+ passwd = str(cursor.fetchone()[0])
+ self.assertEqual(passwd, crypt_password('tutu', passwd))
+ rset = cnx.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
+ {'pwd': Binary(passwd)})
+ self.assertEqual(len(rset.rows), 1)
+ self.assertEqual(rset.description, [('CWUser',)])
# ZT datetime tests ########################################################
@@ -1402,6 +1397,13 @@
self.assertEqual(datenaiss.tzinfo, None)
self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 1, 0))
+ def test_tz_datetime_cache_nonregr(self):
+ datenaiss = datetime(1977, 6, 7, 2, 0, tzinfo=FixedOffset(1))
+ self.qexecute("INSERT Personne X: X nom 'bob', X tzdatenaiss %(date)s",
+ {'date': datenaiss})
+ self.assertTrue(self.qexecute("Any X WHERE X tzdatenaiss %(d)s", {'d': datenaiss}))
+ self.assertFalse(self.qexecute("Any X WHERE X tzdatenaiss %(d)s", {'d': datenaiss - timedelta(1)}))
+
# non regression tests #####################################################
def test_nonregr_1(self):
@@ -1512,9 +1514,9 @@
def test_nonregr_has_text_cache(self):
eid1 = self.qexecute("INSERT Personne X: X nom 'bidule'")[0][0]
eid2 = self.qexecute("INSERT Personne X: X nom 'tag'")[0][0]
- rset = self.qexecute("Any X WHERE X has_text %(text)s", {'text': 'bidule'})
+ rset = self.qexecute("Any X WHERE X has_text %(text)s", {'text': u'bidule'})
self.assertEqual(rset.rows, [[eid1]])
- rset = self.qexecute("Any X WHERE X has_text %(text)s", {'text': 'tag'})
+ rset = self.qexecute("Any X WHERE X has_text %(text)s", {'text': u'tag'})
self.assertEqual(rset.rows, [[eid2]])
def test_nonregr_sortterm_management(self):
@@ -1625,9 +1627,9 @@
aff2 = cnx.create_entity('Societe', nom=u'aff2')
cnx.commit()
with self.new_access('user').repo_cnx() as cnx:
- res = cnx.execute('Any X WHERE X has_text %(text)s', {'text': 'aff1'})
+ res = cnx.execute('Any X WHERE X has_text %(text)s', {'text': u'aff1'})
self.assertEqual(res.rows, [[aff1.eid]])
- res = cnx.execute('Any X WHERE X has_text %(text)s', {'text': 'aff2'})
+ res = cnx.execute('Any X WHERE X has_text %(text)s', {'text': u'aff2'})
self.assertEqual(res.rows, [[aff2.eid]])
def test_set_relations_eid(self):
--- a/server/test/unittest_repository.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_repository.py Tue Jul 19 16:13:12 2016 +0200
@@ -31,10 +31,9 @@
UnknownEid, AuthenticationError, Unauthorized, QueryError)
from cubicweb.predicates import is_instance
from cubicweb.schema import RQLConstraint
-from cubicweb.dbapi import connect, multiple_connections_unfix
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.devtools.repotest import tuplify
-from cubicweb.server import repository, hook
+from cubicweb.server import hook
from cubicweb.server.sqlutils import SQL_PREFIX
from cubicweb.server.hook import Hook
from cubicweb.server.sources import native
@@ -93,35 +92,17 @@
self.assertRaises(AuthenticationError,
self.repo.connect, None)
- def test_execute(self):
+ def test_login_upassword_accent(self):
+ with self.admin_access.repo_cnx() as cnx:
+ cnx.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, '
+ 'X in_group G WHERE G name "users"',
+ {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')})
+ cnx.commit()
repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- repo.execute(cnxid, 'Any X')
- repo.execute(cnxid, 'Any X where X is Personne')
- repo.execute(cnxid, 'Any X where X is Personne, X nom ~= "to"')
- repo.execute(cnxid, 'Any X WHERE X has_text %(text)s', {'text': u'\xe7a'})
- repo.close(cnxid)
-
- def test_login_upassword_accent(self):
- repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_group G WHERE G name "users"',
- {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')})
- repo.commit(cnxid)
- repo.close(cnxid)
cnxid = repo.connect(u"barnabé", password=u"héhéhé".encode('UTF8'))
self.assert_(cnxid)
repo.close(cnxid)
- def test_rollback_on_commit_error(self):
- cnxid = self.repo.connect(self.admlogin, password=self.admpassword)
- self.repo.execute(cnxid,
- 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s',
- {'login': u"tutetute", 'passwd': 'tutetute'})
- self.assertRaises(ValidationError, self.repo.commit, cnxid)
- self.assertFalse(self.repo.execute(cnxid, 'CWUser X WHERE X login "tutetute"'))
- self.repo.close(cnxid)
-
def test_rollback_on_execute_validation_error(self):
class ValidationErrorAfterHook(Hook):
__regid__ = 'valerror-after-hook'
@@ -166,31 +147,6 @@
cnxid = repo.connect(self.admlogin, password=self.admpassword)
self.assert_(cnxid)
repo.close(cnxid)
- self.assertRaises(BadConnectionId, repo.execute, cnxid, 'Any X')
-
- def test_invalid_cnxid(self):
- self.assertRaises(BadConnectionId, self.repo.execute, 0, 'Any X')
- self.assertRaises(BadConnectionId, self.repo.close, None)
-
- def test_shared_data(self):
- repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- repo.set_shared_data(cnxid, 'data', 4)
- cnxid2 = repo.connect(self.admlogin, password=self.admpassword)
- self.assertEqual(repo.get_shared_data(cnxid, 'data'), 4)
- self.assertEqual(repo.get_shared_data(cnxid2, 'data'), None)
- repo.set_shared_data(cnxid2, 'data', 5)
- self.assertEqual(repo.get_shared_data(cnxid, 'data'), 4)
- self.assertEqual(repo.get_shared_data(cnxid2, 'data'), 5)
- repo.get_shared_data(cnxid2, 'data', pop=True)
- self.assertEqual(repo.get_shared_data(cnxid, 'data'), 4)
- self.assertEqual(repo.get_shared_data(cnxid2, 'data'), None)
- repo.close(cnxid)
- repo.close(cnxid2)
- self.assertRaises(BadConnectionId, repo.get_shared_data, cnxid, 'data')
- self.assertRaises(BadConnectionId, repo.get_shared_data, cnxid2, 'data')
- self.assertRaises(BadConnectionId, repo.set_shared_data, cnxid, 'data', 1)
- self.assertRaises(BadConnectionId, repo.set_shared_data, cnxid2, 'data', 1)
def test_check_session(self):
repo = self.repo
@@ -199,76 +155,6 @@
repo.close(cnxid)
self.assertRaises(BadConnectionId, repo.check_session, cnxid)
- def test_transaction_base(self):
- repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- # check db state
- result = repo.execute(cnxid, 'Personne X')
- self.assertEqual(result.rowcount, 0)
- # rollback entity insertion
- repo.execute(cnxid, "INSERT Personne X: X nom 'bidule'")
- result = repo.execute(cnxid, 'Personne X')
- self.assertEqual(result.rowcount, 1)
- repo.rollback(cnxid)
- result = repo.execute(cnxid, 'Personne X')
- self.assertEqual(result.rowcount, 0, result.rows)
- # commit
- repo.execute(cnxid, "INSERT Personne X: X nom 'bidule'")
- repo.commit(cnxid)
- result = repo.execute(cnxid, 'Personne X')
- self.assertEqual(result.rowcount, 1)
- repo.close(cnxid)
-
- def test_transaction_base2(self):
- repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- # rollback relation insertion
- repo.execute(cnxid, "SET U in_group G WHERE U login 'admin', G name 'guests'")
- result = repo.execute(cnxid, "Any U WHERE U in_group G, U login 'admin', G name 'guests'")
- self.assertEqual(result.rowcount, 1)
- repo.rollback(cnxid)
- result = repo.execute(cnxid, "Any U WHERE U in_group G, U login 'admin', G name 'guests'")
- self.assertEqual(result.rowcount, 0, result.rows)
- repo.close(cnxid)
-
- def test_transaction_base3(self):
- repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- # rollback state change which trigger TrInfo insertion
- session = repo._get_session(cnxid)
- user = session.user
- user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
- rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
- self.assertEqual(len(rset), 1)
- repo.rollback(cnxid)
- rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
- self.assertEqual(len(rset), 0)
- repo.close(cnxid)
-
- def test_close_kill_processing_request(self):
- repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- repo.execute(cnxid, 'INSERT CWUser X: X login "toto", X upassword "tutu", X in_group G WHERE G name "users"')
- repo.commit(cnxid)
- lock = threading.Lock()
- lock.acquire()
- # close has to be in the thread due to sqlite limitations
- def close_in_a_few_moment():
- lock.acquire()
- repo.close(cnxid)
- t = threading.Thread(target=close_in_a_few_moment)
- t.start()
- def run_transaction():
- lock.release()
- repo.execute(cnxid, 'DELETE CWUser X WHERE X login "toto"')
- repo.commit(cnxid)
- try:
- with self.assertRaises(SessionClosedError) as cm:
- run_transaction()
- self.assertEqual(str(cm.exception), 'try to access connections set on a closed session %s' % cnxid)
- finally:
- t.join()
-
def test_initial_schema(self):
schema = self.repo.schema
# check order of attributes is respected
@@ -312,118 +198,14 @@
ownedby = schema.rschema('owned_by')
self.assertEqual(ownedby.objects('CWEType'), ('CWUser',))
- def test_pyro(self):
- import Pyro
- Pyro.config.PYRO_MULTITHREADED = 0
- done = []
- self.repo.config.global_set_option('pyro-ns-host', 'NO_PYRONS')
- daemon = self.repo.pyro_register()
- try:
- uri = self.repo.pyro_uri.replace('PYRO', 'pyroloc')
- # the client part has to be in the thread due to sqlite limitations
- t = threading.Thread(target=self._pyro_client, args=(uri, done))
- t.start()
- while not done:
- daemon.handleRequests(1.0)
- t.join(1)
- if t.isAlive():
- self.fail('something went wrong, thread still alive')
- finally:
- repository.pyro_unregister(self.repo.config)
- from logilab.common import pyro_ext
- pyro_ext._DAEMONS.clear()
-
-
- def _pyro_client(self, uri, done):
- cnx = connect(uri,
- u'admin', password='gingkow',
- initlog=False) # don't reset logging configuration
- try:
- cnx.load_appobjects(subpath=('entities',))
- # check we can get the schema
- schema = cnx.get_schema()
- self.assertTrue(cnx.vreg)
- self.assertTrue('etypes'in cnx.vreg)
- cu = cnx.cursor()
- rset = cu.execute('Any U,G WHERE U in_group G')
- user = iter(rset.entities()).next()
- self.assertTrue(user._cw)
- self.assertTrue(user._cw.vreg)
- from cubicweb.entities import authobjs
- self.assertIsInstance(user._cw.user, authobjs.CWUser)
- # make sure the tcp connection is closed properly; yes, it's disgusting.
- adapter = cnx._repo.adapter
- cnx.close()
- adapter.release()
- done.append(True)
- finally:
- # connect monkey patch some method by default, remove them
- multiple_connections_unfix()
-
-
- def test_zmq(self):
- try:
- import zmq
- except ImportError:
- self.skipTest("zmq in not available")
- done = []
- from cubicweb.devtools import TestServerConfiguration as ServerConfiguration
- from cubicweb.server.cwzmq import ZMQRepositoryServer
- # the client part has to be in a thread due to sqlite limitations
- t = threading.Thread(target=self._zmq_client, args=(done,))
- t.start()
-
- zmq_server = ZMQRepositoryServer(self.repo)
- zmq_server.connect('zmqpickle-tcp://127.0.0.1:41415')
-
- t2 = threading.Thread(target=self._zmq_quit, args=(done, zmq_server,))
- t2.start()
-
- zmq_server.run()
-
- t2.join(1)
- t.join(1)
-
- if t.isAlive():
- self.fail('something went wrong, thread still alive')
-
- def _zmq_quit(self, done, srv):
- while not done:
- time.sleep(0.1)
- srv.quit()
-
- def _zmq_client(self, done):
- try:
- cnx = connect('zmqpickle-tcp://127.0.0.1:41415', u'admin', password=u'gingkow',
- initlog=False) # don't reset logging configuration
- try:
- cnx.load_appobjects(subpath=('entities',))
- # check we can get the schema
- schema = cnx.get_schema()
- self.assertTrue(cnx.vreg)
- self.assertTrue('etypes'in cnx.vreg)
- cu = cnx.cursor()
- rset = cu.execute('Any U,G WHERE U in_group G')
- user = iter(rset.entities()).next()
- self.assertTrue(user._cw)
- self.assertTrue(user._cw.vreg)
- from cubicweb.entities import authobjs
- self.assertIsInstance(user._cw.user, authobjs.CWUser)
- cnx.close()
- done.append(True)
- finally:
- # connect monkey patch some method by default, remove them
- multiple_connections_unfix()
- finally:
- done.append(False)
-
def test_internal_api(self):
repo = self.repo
cnxid = repo.connect(self.admlogin, password=self.admpassword)
- session = repo._get_session(cnxid, setcnxset=True)
- self.assertEqual(repo.type_and_source_from_eid(2, session),
- ('CWGroup', None, 'system'))
- self.assertEqual(repo.type_from_eid(2, session), 'CWGroup')
+ session = repo._get_session(cnxid)
+ with session.new_cnx() as cnx:
+ self.assertEqual(repo.type_and_source_from_eid(2, cnx),
+ ('CWGroup', None, 'system'))
+ self.assertEqual(repo.type_from_eid(2, cnx), 'CWGroup')
repo.close(cnxid)
def test_public_api(self):
@@ -435,45 +217,11 @@
# .properties() return a result set
self.assertEqual(self.repo.properties().rql, 'Any K,V WHERE P is CWProperty,P pkey K, P value V, NOT P for_user U')
- def test_session_api(self):
- repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- self.assertEqual(repo.user_info(cnxid), (6, 'admin', set([u'managers']), {}))
- self.assertEqual({'type': u'CWGroup', 'extid': None, 'source': 'system'},
- repo.entity_metas(cnxid, 2))
- self.assertEqual(repo.describe(cnxid, 2), (u'CWGroup', 'system', None, 'system'))
- repo.close(cnxid)
- self.assertRaises(BadConnectionId, repo.user_info, cnxid)
- self.assertRaises(BadConnectionId, repo.describe, cnxid, 1)
-
- def test_shared_data_api(self):
- repo = self.repo
- cnxid = repo.connect(self.admlogin, password=self.admpassword)
- self.assertEqual(repo.get_shared_data(cnxid, 'data'), None)
- repo.set_shared_data(cnxid, 'data', 4)
- self.assertEqual(repo.get_shared_data(cnxid, 'data'), 4)
- repo.get_shared_data(cnxid, 'data', pop=True)
- repo.get_shared_data(cnxid, 'whatever', pop=True)
- self.assertEqual(repo.get_shared_data(cnxid, 'data'), None)
- repo.close(cnxid)
- self.assertRaises(BadConnectionId, repo.set_shared_data, cnxid, 'data', 0)
- self.assertRaises(BadConnectionId, repo.get_shared_data, cnxid, 'data')
-
def test_schema_is_relation(self):
with self.admin_access.repo_cnx() as cnx:
no_is_rset = cnx.execute('Any X WHERE NOT X is ET')
self.assertFalse(no_is_rset, no_is_rset.description)
-# def test_perfo(self):
-# self.set_debug(True)
-# from time import time, clock
-# t, c = time(), clock()
-# try:
-# self.create_user('toto')
-# finally:
-# self.set_debug(False)
-# print 'test time: %.3f (time) %.3f (cpu)' % ((time() - t), clock() - c)
-
def test_delete_if_singlecard1(self):
with self.admin_access.repo_cnx() as cnx:
note = cnx.create_entity('Affaire')
@@ -626,38 +374,37 @@
namecol = SQL_PREFIX + 'name'
finalcol = SQL_PREFIX + 'final'
with self.admin_access.repo_cnx() as cnx:
- with cnx.ensure_cnx_set:
- cu = cnx.system_sql('SELECT %s FROM %s WHERE %s is NULL'
- % (namecol, table, finalcol))
- self.assertEqual(cu.fetchall(), [])
- cu = cnx.system_sql('SELECT %s FROM %s '
- 'WHERE %s=%%(final)s ORDER BY %s'
- % (namecol, table, finalcol, namecol),
- {'final': True})
- self.assertEqual(cu.fetchall(),
- [(u'BabarTestType',),
- (u'BigInt',), (u'Boolean',), (u'Bytes',),
- (u'Date',), (u'Datetime',),
- (u'Decimal',),(u'Float',),
- (u'Int',),
- (u'Interval',), (u'Password',),
- (u'String',),
- (u'TZDatetime',), (u'TZTime',), (u'Time',)])
- sql = ("SELECT etype.cw_eid, etype.cw_name, cstr.cw_eid, rel.eid_to "
- "FROM cw_CWUniqueTogetherConstraint as cstr, "
- " relations_relation as rel, "
- " cw_CWEType as etype "
- "WHERE cstr.cw_eid = rel.eid_from "
- " AND cstr.cw_constraint_of = etype.cw_eid "
- " AND etype.cw_name = 'Personne' "
- ";")
- cu = cnx.system_sql(sql)
- rows = cu.fetchall()
- self.assertEqual(len(rows), 3)
- person = self.repo.schema.eschema('Personne')
- self.assertEqual(len(person._unique_together), 1)
- self.assertItemsEqual(person._unique_together[0],
- ('nom', 'prenom', 'inline2'))
+ cu = cnx.system_sql('SELECT %s FROM %s WHERE %s is NULL'
+ % (namecol, table, finalcol))
+ self.assertEqual(cu.fetchall(), [])
+ cu = cnx.system_sql('SELECT %s FROM %s '
+ 'WHERE %s=%%(final)s ORDER BY %s'
+ % (namecol, table, finalcol, namecol),
+ {'final': True})
+ self.assertEqual(cu.fetchall(),
+ [(u'BabarTestType',),
+ (u'BigInt',), (u'Boolean',), (u'Bytes',),
+ (u'Date',), (u'Datetime',),
+ (u'Decimal',),(u'Float',),
+ (u'Int',),
+ (u'Interval',), (u'Password',),
+ (u'String',),
+ (u'TZDatetime',), (u'TZTime',), (u'Time',)])
+ sql = ("SELECT etype.cw_eid, etype.cw_name, cstr.cw_eid, rel.eid_to "
+ "FROM cw_CWUniqueTogetherConstraint as cstr, "
+ " relations_relation as rel, "
+ " cw_CWEType as etype "
+ "WHERE cstr.cw_eid = rel.eid_from "
+ " AND cstr.cw_constraint_of = etype.cw_eid "
+ " AND etype.cw_name = 'Personne' "
+ ";")
+ cu = cnx.system_sql(sql)
+ rows = cu.fetchall()
+ self.assertEqual(len(rows), 3)
+ person = self.repo.schema.eschema('Personne')
+ self.assertEqual(len(person._unique_together), 1)
+ self.assertItemsEqual(person._unique_together[0],
+ ('nom', 'prenom', 'inline2'))
finally:
self.repo.set_schema(origshema)
@@ -680,30 +427,26 @@
def test_type_from_eid(self):
with self.admin_access.repo_cnx() as cnx:
- with cnx.ensure_cnx_set:
- self.assertEqual(self.repo.type_from_eid(2, cnx), 'CWGroup')
+ self.assertEqual(self.repo.type_from_eid(2, cnx), 'CWGroup')
def test_type_from_eid_raise(self):
with self.admin_access.repo_cnx() as cnx:
- with cnx.ensure_cnx_set:
- self.assertRaises(UnknownEid, self.repo.type_from_eid, -2, cnx)
+ self.assertRaises(UnknownEid, self.repo.type_from_eid, -2, cnx)
def test_add_delete_info(self):
with self.admin_access.repo_cnx() as cnx:
- with cnx.ensure_cnx_set:
- cnx.mode = 'write'
- entity = self.repo.vreg['etypes'].etype_class('Personne')(cnx)
- entity.eid = -1
- entity.complete = lambda x: None
- self.repo.add_info(cnx, entity, self.repo.system_source)
- cu = cnx.system_sql('SELECT * FROM entities WHERE eid = -1')
- data = cu.fetchall()
- self.assertEqual(tuplify(data), [(-1, 'Personne', 'system', None)])
- self.repo.delete_info(cnx, entity, 'system')
- #self.repo.commit()
- cu = cnx.system_sql('SELECT * FROM entities WHERE eid = -1')
- data = cu.fetchall()
- self.assertEqual(data, [])
+ entity = self.repo.vreg['etypes'].etype_class('Personne')(cnx)
+ entity.eid = -1
+ entity.complete = lambda x: None
+ self.repo.add_info(cnx, entity, self.repo.system_source)
+ cu = cnx.system_sql('SELECT * FROM entities WHERE eid = -1')
+ data = cu.fetchall()
+ self.assertEqual(tuplify(data), [(-1, 'Personne', 'system', None)])
+ self.repo._delete_cascade_multi(cnx, [entity])
+ self.repo.system_source.delete_info_multi(cnx, [entity])
+ cu = cnx.system_sql('SELECT * FROM entities WHERE eid = -1')
+ data = cu.fetchall()
+ self.assertEqual(data, [])
class FTITC(CubicWebTC):
@@ -757,9 +500,7 @@
u'system.version.card',
u'system.version.comment',
u'system.version.cubicweb',
- u'system.version.email',
u'system.version.file',
- u'system.version.folder',
u'system.version.localperms',
u'system.version.tag'])
--- a/server/test/unittest_rql2sql.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_rql2sql.py Tue Jul 19 16:13:12 2016 +0200
@@ -396,13 +396,13 @@
ORDER BY 1'''),
# DISTINCT, can use relation under exists scope as principal
- ('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
+ ('DISTINCT Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
'''SELECT DISTINCT _X.cw_eid, rel_read_permission0.eid_to
FROM cw_CWEType AS _X, read_permission_relation AS rel_read_permission0
WHERE _X.cw_name=CWGroup AND rel_read_permission0.eid_to IN(1, 2, 3) AND EXISTS(SELECT 1 WHERE rel_read_permission0.eid_from=_X.cw_eid)'''),
# no distinct, Y can't be invariant
- ('Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
+ ('Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
'''SELECT _X.cw_eid, _Y.cw_eid
FROM cw_CWEType AS _X, cw_CWGroup AS _Y
WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid)
@@ -412,7 +412,7 @@
WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid)'''),
# DISTINCT but NEGED exists, can't be invariant
- ('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)',
+ ('DISTINCT Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)',
'''SELECT DISTINCT _X.cw_eid, _Y.cw_eid
FROM cw_CWEType AS _X, cw_CWGroup AS _Y
WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))
@@ -422,7 +422,7 @@
WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))'''),
# should generate the same query as above
- ('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT X read_permission Y',
+ ('DISTINCT Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), NOT X read_permission Y',
'''SELECT DISTINCT _X.cw_eid, _Y.cw_eid
FROM cw_CWEType AS _X, cw_CWGroup AS _Y
WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))
@@ -432,7 +432,7 @@
WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))'''),
# neged relation, can't be inveriant
- ('Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), NOT X read_permission Y',
+ ('Any X,Y WHERE X name "CWGroup", X is CWEType, Y eid IN(1, 2, 3), NOT X read_permission Y',
'''SELECT _X.cw_eid, _Y.cw_eid
FROM cw_CWEType AS _X, cw_CWGroup AS _Y
WHERE _X.cw_name=CWGroup AND _Y.cw_eid IN(1, 2, 3) AND NOT (EXISTS(SELECT 1 FROM read_permission_relation AS rel_read_permission0 WHERE rel_read_permission0.eid_from=_X.cw_eid AND rel_read_permission0.eid_to=_Y.cw_eid))
@@ -563,6 +563,17 @@
'''SELECT _X.cw_eid
FROM cw_Note AS _X
WHERE _X.cw_eid IN(999998, 999999) AND NOT (EXISTS(SELECT 1 FROM cw_source_relation AS rel_cw_source0 WHERE rel_cw_source0.eid_from=_X.cw_eid))'''),
+
+ # Test for https://www.cubicweb.org/ticket/5503548
+ ('''Any X
+ WHERE X is CWSourceSchemaConfig,
+ EXISTS(X created_by U, U login L),
+ X cw_schema X_CW_SCHEMA,
+ X owned_by X_OWNED_BY?
+ ''', '''SELECT _X.cw_eid
+FROM cw_CWSourceSchemaConfig AS _X LEFT OUTER JOIN owned_by_relation AS rel_owned_by1 ON (rel_owned_by1.eid_from=_X.cw_eid)
+WHERE EXISTS(SELECT 1 FROM created_by_relation AS rel_created_by0, cw_CWUser AS _U WHERE rel_created_by0.eid_from=_X.cw_eid AND rel_created_by0.eid_to=_U.cw_eid) AND _X.cw_cw_schema IS NOT NULL
+''')
]
ADVANCED_WITH_GROUP_CONCAT = [
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_schema2sql.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,285 @@
+# copyright 2004-2014 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/>.
+"""unit tests for module cubicweb.server.schema2sql
+"""
+
+import os.path as osp
+
+from logilab.common.testlib import TestCase, unittest_main
+from logilab.database import get_db_helper
+
+from yams.reader import SchemaLoader
+from cubicweb.server import schema2sql
+
+schema2sql.SET_DEFAULT = True
+
+DATADIR = osp.abspath(osp.join(osp.dirname(__file__), 'data-schema2sql'))
+
+schema = SchemaLoader().load([DATADIR])
+
+
+EXPECTED_DATA_NO_DROP = """
+CREATE TABLE Affaire(
+ sujet varchar(128),
+ ref varchar(12),
+ inline_rel integer REFERENCES entities (eid)
+);
+CREATE INDEX affaire_inline_rel_idx ON Affaire(inline_rel);
+
+CREATE TABLE Company(
+ name text
+);
+
+CREATE TABLE Datetest(
+ dt1 timestamp,
+ dt2 timestamp,
+ d1 date,
+ d2 date,
+ t1 time,
+ t2 time
+, CONSTRAINT cstredd407706bdfbd2285714dd689e8fcc0 CHECK(d1 <= CAST(clock_timestamp() AS DATE))
+);
+
+CREATE TABLE Division(
+ name text
+);
+
+CREATE TABLE EPermission(
+ name varchar(100) NOT NULL
+);
+CREATE INDEX epermission_name_idx ON EPermission(name);
+
+CREATE TABLE Eetype(
+ name varchar(64) UNIQUE NOT NULL,
+ description text,
+ meta boolean,
+ final boolean,
+ initial_state integer REFERENCES entities (eid)
+);
+CREATE INDEX eetype_name_idx ON Eetype(name);
+CREATE INDEX eetype_initial_state_idx ON Eetype(initial_state);
+
+CREATE TABLE Employee(
+);
+
+CREATE TABLE Note(
+ date varchar(10),
+ type varchar(1),
+ para varchar(512)
+);
+
+CREATE TABLE Person(
+ nom varchar(64) NOT NULL,
+ prenom varchar(64),
+ sexe varchar(1) DEFAULT 'M',
+ promo varchar(6),
+ titre varchar(128),
+ adel varchar(128),
+ ass varchar(128),
+ web varchar(128),
+ tel integer,
+ fax integer,
+ datenaiss date,
+ test boolean,
+ salary float
+, CONSTRAINT cstr41fe7db9ce1d5be95de2477e26590386 CHECK(promo IN ('bon', 'pasbon'))
+);
+CREATE UNIQUE INDEX unique_e6c2d219772dbf1715597f7d9a6b3892 ON Person(nom,prenom);
+
+CREATE TABLE Salaried(
+ nom varchar(64) NOT NULL,
+ prenom varchar(64),
+ sexe varchar(1) DEFAULT 'M',
+ promo varchar(6),
+ titre varchar(128),
+ adel varchar(128),
+ ass varchar(128),
+ web varchar(128),
+ tel integer,
+ fax integer,
+ datenaiss date,
+ test boolean,
+ salary float
+, CONSTRAINT cstrc8556fcc665865217761cdbcd220cae0 CHECK(promo IN ('bon', 'pasbon'))
+);
+CREATE UNIQUE INDEX unique_98da0f9de8588baa8966f0b1a6f850a3 ON Salaried(nom,prenom);
+
+CREATE TABLE Societe(
+ nom varchar(64),
+ web varchar(128),
+ tel integer,
+ fax integer,
+ rncs varchar(32),
+ ad1 varchar(128),
+ ad2 varchar(128),
+ ad3 varchar(128),
+ cp varchar(12),
+ ville varchar(32)
+, CONSTRAINT cstrc51dd462e9f6115506a0fe468d4c8114 CHECK(fax <= tel)
+);
+
+CREATE TABLE State(
+ eid integer PRIMARY KEY REFERENCES entities (eid),
+ name varchar(256) NOT NULL,
+ description text
+);
+CREATE INDEX state_name_idx ON State(name);
+
+CREATE TABLE Subcompany(
+ name text
+);
+
+CREATE TABLE Subdivision(
+ name text
+);
+
+CREATE TABLE pkginfo(
+ modname varchar(30) NOT NULL,
+ version varchar(10) DEFAULT '0.1' NOT NULL,
+ copyright text NOT NULL,
+ license varchar(3),
+ short_desc varchar(80) NOT NULL,
+ long_desc text NOT NULL,
+ author varchar(100) NOT NULL,
+ author_email varchar(100) NOT NULL,
+ mailinglist varchar(100),
+ debian_handler varchar(6)
+, CONSTRAINT cstr70f766f834557c715815d76f0a0db956 CHECK(license IN ('GPL', 'ZPL'))
+, CONSTRAINT cstr831a117424d0007ae0278cc15f344f5e CHECK(debian_handler IN ('machin', 'bidule'))
+);
+
+
+CREATE TABLE concerne_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT concerne_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX concerne_relation_from_idx ON concerne_relation(eid_from);
+CREATE INDEX concerne_relation_to_idx ON concerne_relation(eid_to);
+
+CREATE TABLE division_of_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT division_of_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX division_of_relation_from_idx ON division_of_relation(eid_from);
+CREATE INDEX division_of_relation_to_idx ON division_of_relation(eid_to);
+
+CREATE TABLE evaluee_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT evaluee_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX evaluee_relation_from_idx ON evaluee_relation(eid_from);
+CREATE INDEX evaluee_relation_to_idx ON evaluee_relation(eid_to);
+
+CREATE TABLE next_state_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT next_state_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX next_state_relation_from_idx ON next_state_relation(eid_from);
+CREATE INDEX next_state_relation_to_idx ON next_state_relation(eid_to);
+
+CREATE TABLE obj_wildcard_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT obj_wildcard_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX obj_wildcard_relation_from_idx ON obj_wildcard_relation(eid_from);
+CREATE INDEX obj_wildcard_relation_to_idx ON obj_wildcard_relation(eid_to);
+
+CREATE TABLE require_permission_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT require_permission_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX require_permission_relation_from_idx ON require_permission_relation(eid_from);
+CREATE INDEX require_permission_relation_to_idx ON require_permission_relation(eid_to);
+
+CREATE TABLE state_of_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT state_of_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX state_of_relation_from_idx ON state_of_relation(eid_from);
+CREATE INDEX state_of_relation_to_idx ON state_of_relation(eid_to);
+
+CREATE TABLE subcompany_of_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT subcompany_of_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX subcompany_of_relation_from_idx ON subcompany_of_relation(eid_from);
+CREATE INDEX subcompany_of_relation_to_idx ON subcompany_of_relation(eid_to);
+
+CREATE TABLE subdivision_of_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT subdivision_of_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX subdivision_of_relation_from_idx ON subdivision_of_relation(eid_from);
+CREATE INDEX subdivision_of_relation_to_idx ON subdivision_of_relation(eid_to);
+
+CREATE TABLE subj_wildcard_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT subj_wildcard_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX subj_wildcard_relation_from_idx ON subj_wildcard_relation(eid_from);
+CREATE INDEX subj_wildcard_relation_to_idx ON subj_wildcard_relation(eid_to);
+
+CREATE TABLE sym_rel_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT sym_rel_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX sym_rel_relation_from_idx ON sym_rel_relation(eid_from);
+CREATE INDEX sym_rel_relation_to_idx ON sym_rel_relation(eid_to);
+
+CREATE TABLE travaille_relation (
+ eid_from INTEGER NOT NULL REFERENCES entities (eid),
+ eid_to INTEGER NOT NULL REFERENCES entities (eid),
+ CONSTRAINT travaille_relation_p_key PRIMARY KEY(eid_from, eid_to)
+);
+
+CREATE INDEX travaille_relation_from_idx ON travaille_relation(eid_from);
+CREATE INDEX travaille_relation_to_idx ON travaille_relation(eid_to);
+"""
+
+class SQLSchemaTC(TestCase):
+
+ def test_known_values(self):
+ dbhelper = get_db_helper('postgres')
+ output = schema2sql.schema2sql(dbhelper, schema, skip_relations=('works_for',))
+ self.assertMultiLineEqual(EXPECTED_DATA_NO_DROP.strip(), output.strip())
+
+
+if __name__ == '__main__':
+ unittest_main()
--- a/server/test/unittest_schemaserial.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_schemaserial.py Tue Jul 19 16:13:12 2016 +0200
@@ -17,9 +17,6 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unit tests for schema rql (de)serialization"""
-import sys
-from cStringIO import StringIO
-
from logilab.common.testlib import TestCase, unittest_main
from cubicweb import Binary
@@ -437,6 +434,8 @@
self.repo.set_schema(self.repo.deserialize_schema(), resetvreg=False)
schema = self.repo.schema
self.assertEqual([('Company', 'Person')], list(schema['has_employee'].rdefs))
+ self.assertEqual(schema['has_employee'].rdef('Company', 'Person').permissions['read'],
+ (u'managers',))
self.assertEqual('O works_for S',
schema['has_employee'].rule)
self.assertEqual([('Company', 'Int')], list(schema['total_salary'].rdefs))
--- a/server/test/unittest_security.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_security.py Tue Jul 19 16:13:12 2016 +0200
@@ -31,9 +31,9 @@
def setup_database(self):
super(BaseSecurityTC, self).setup_database()
with self.admin_access.client_cnx() as cnx:
- self.create_user(cnx, 'iaminusersgrouponly')
+ self.create_user(cnx, u'iaminusersgrouponly')
hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
- self.create_user(cnx, 'oldpassword', password=Binary(hash))
+ self.create_user(cnx, u'oldpassword', password=Binary(hash))
class LowLevelSecurityFunctionTC(BaseSecurityTC):
@@ -45,7 +45,7 @@
with self.admin_access.repo_cnx() as cnx:
self.repo.vreg.solutions(cnx, rqlst, None)
check_relations_read_access(cnx, rqlst, {})
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
self.assertRaises(Unauthorized,
check_relations_read_access,
cnx, rqlst, {})
@@ -60,7 +60,7 @@
solution = rqlst.solutions[0]
localchecks = get_local_checks(cnx, rqlst, solution)
self.assertEqual({}, localchecks)
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
self.assertRaises(Unauthorized,
get_local_checks,
cnx, rqlst, solution)
@@ -70,7 +70,7 @@
with self.admin_access.repo_cnx() as cnx:
self.assertRaises(Unauthorized,
cnx.execute, 'Any X,P WHERE X is CWUser, X upassword P')
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
self.assertRaises(Unauthorized,
cnx.execute, 'Any X,P WHERE X is CWUser, X upassword P')
@@ -104,7 +104,7 @@
super(SecurityRewritingTC, self).tearDown()
def test_not_relation_read_security(self):
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
self.hijack_source_execute()
cnx.execute('Any U WHERE NOT A todo_by U, A is Affaire')
self.assertEqual(self.query[0][1].as_string(),
@@ -126,13 +126,13 @@
cnx.commit()
def test_insert_security(self):
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
cnx.execute("INSERT Personne X: X nom 'bidule'")
self.assertRaises(Unauthorized, cnx.commit)
self.assertEqual(cnx.execute('Personne X').rowcount, 1)
def test_insert_security_2(self):
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
cnx.execute("INSERT Affaire X")
self.assertRaises(Unauthorized, cnx.commit)
# anon has no read permission on Affaire entities, so
@@ -141,20 +141,20 @@
def test_insert_rql_permission(self):
# test user can only add une affaire related to a societe he owns
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute("INSERT Affaire X: X sujet 'cool'")
self.assertRaises(Unauthorized, cnx.commit)
# test nothing has actually been inserted
with self.admin_access.repo_cnx() as cnx:
self.assertEqual(cnx.execute('Affaire X').rowcount, 1)
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute("INSERT Affaire X: X sujet 'cool'")
cnx.execute("INSERT Societe X: X nom 'chouette'")
cnx.execute("SET A concerne S WHERE A sujet 'cool', S nom 'chouette'")
cnx.commit()
def test_update_security_1(self):
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
# local security check
cnx.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
self.assertRaises(Unauthorized, cnx.commit)
@@ -164,7 +164,7 @@
def test_update_security_2(self):
with self.temporary_permissions(Personne={'read': ('users', 'managers'),
'add': ('guests', 'users', 'managers')}):
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
self.assertRaises(Unauthorized, cnx.execute,
"SET X nom 'bidulechouette' WHERE X is Personne")
# test nothing has actually been inserted
@@ -172,7 +172,7 @@
self.assertEqual(cnx.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
def test_update_security_3(self):
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute("INSERT Personne X: X nom 'biduuule'")
cnx.execute("INSERT Societe X: X nom 'looogilab'")
cnx.execute("SET X travaille S WHERE X nom 'biduuule', S nom 'looogilab'")
@@ -191,7 +191,7 @@
cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
cnx.commit()
# test user can only update une affaire related to a societe he owns
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute("SET X sujet 'pascool' WHERE X is Affaire")
# this won't actually do anything since the selection query won't return anything
cnx.commit()
@@ -212,7 +212,7 @@
#self.assertRaises(Unauthorized,
# self.o.execute, user, "DELETE CWUser X WHERE X login 'bidule'")
# check local security
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
self.assertRaises(Unauthorized, cnx.execute, "DELETE CWGroup Y WHERE Y name 'staff'")
def test_delete_rql_permission(self):
@@ -220,7 +220,7 @@
cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
cnx.commit()
# test user can only dele une affaire related to a societe he owns
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
# this won't actually do anything since the selection query won't return anything
cnx.execute("DELETE Affaire X")
cnx.commit()
@@ -239,7 +239,7 @@
cnx.commit()
def test_insert_relation_rql_permission(self):
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
# should raise Unauthorized since user don't own S though this won't
# actually do anything since the selection query won't return
@@ -266,7 +266,7 @@
with self.admin_access.repo_cnx() as cnx:
cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
# this won't actually do anything since the selection query won't return anything
cnx.execute("DELETE A concerne S")
cnx.commit()
@@ -277,7 +277,7 @@
{'x': eid})
cnx.execute("SET A concerne S WHERE A sujet 'pascool', S is Societe")
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
self.assertRaises(Unauthorized, cnx.execute, "DELETE A concerne S")
self.assertRaises(QueryError, cnx.commit) # can't commit anymore
cnx.rollback()
@@ -290,8 +290,8 @@
def test_user_can_change_its_upassword(self):
with self.admin_access.repo_cnx() as cnx:
- ueid = self.create_user(cnx, 'user').eid
- with self.new_access('user').repo_cnx() as cnx:
+ ueid = self.create_user(cnx, u'user').eid
+ with self.new_access(u'user').repo_cnx() as cnx:
cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
{'x': ueid, 'passwd': 'newpwd'})
cnx.commit()
@@ -299,8 +299,8 @@
def test_user_cant_change_other_upassword(self):
with self.admin_access.repo_cnx() as cnx:
- ueid = self.create_user(cnx, 'otheruser').eid
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ ueid = self.create_user(cnx, u'otheruser').eid
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
{'x': ueid, 'passwd': 'newpwd'})
self.assertRaises(Unauthorized, cnx.commit)
@@ -309,7 +309,7 @@
def test_read_base(self):
with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
self.assertRaises(Unauthorized,
cnx.execute, 'Personne U where U nom "managers"')
@@ -317,7 +317,7 @@
with self.admin_access.repo_cnx() as cnx:
eid = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
rset = cnx.execute('Affaire X')
self.assertEqual(rset.rows, [])
self.assertRaises(Unauthorized, cnx.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
@@ -342,7 +342,7 @@
def test_entity_created_in_transaction(self):
affschema = self.schema['Affaire']
with self.temporary_permissions(Affaire={'read': affschema.permissions['add']}):
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
# entity created in transaction are readable *by eid*
self.assertTrue(cnx.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
@@ -358,7 +358,7 @@
cnx.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"',
{'x': card1})
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
soc1 = cnx.execute("INSERT Societe X: X nom 'chouette'")[0][0]
cnx.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1})
@@ -376,7 +376,7 @@
cnx.execute("INSERT Societe X: X nom 'bidule'")
cnx.commit()
with self.temporary_permissions(Personne={'read': ('managers',)}):
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
rset = cnx.execute('Any N WHERE N has_text "bidule"')
self.assertEqual(len(rset.rows), 1, rset.rows)
rset = cnx.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
@@ -388,7 +388,7 @@
cnx.execute("INSERT Societe X: X nom 'bidule'")
cnx.commit()
with self.temporary_permissions(Personne={'read': ('managers',)}):
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
rset = cnx.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
self.assertEqual(len(rset.rows), 1, rset.rows)
@@ -396,7 +396,7 @@
with self.admin_access.repo_cnx() as cnx:
cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
rset = cnx.execute('Any COUNT(X) WHERE X is Affaire')
self.assertEqual(rset.rows, [[0]])
aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
@@ -424,7 +424,7 @@
"X web 'http://www.debian.org', X test TRUE")[0][0]
cnx.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute("INSERT Personne X: X nom 'bidule', "
"X web 'http://www.debian.org', X test TRUE")
self.assertRaises(Unauthorized, cnx.commit)
@@ -440,7 +440,7 @@
self.assertRaises(Unauthorized, cnx.commit)
cnx.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute('INSERT Frozable F: F name "Foo"')
cnx.commit()
cnx.execute('SET F name "Bar" WHERE F is Frozable')
@@ -464,7 +464,7 @@
note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
cnx.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid})
self.assertRaises(Unauthorized, cnx.commit)
note2 = cnx.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
@@ -496,10 +496,11 @@
login_rdef = self.repo.schema['CWUser'].rdef('login')
with self.temporary_permissions((login_rdef, {'read': ('users', 'managers')}),
CWUser={'read': ('guests', 'users', 'managers')}):
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
rset = cnx.execute('CWUser X')
self.assertTrue(rset)
x = rset.get_entity(0, 0)
+ x.complete()
self.assertEqual(x.login, None)
self.assertTrue(x.creation_date)
x = rset.get_entity(1, 0)
@@ -510,7 +511,7 @@
def test_yams_inheritance_and_security_bug(self):
with self.temporary_permissions(Division={'read': ('managers',
ERQLExpression('X owned_by U'))}):
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
querier = cnx.repo.querier
rqlst = querier.parse('Any X WHERE X is_instance_of Societe')
querier.solutions(cnx, rqlst, {})
@@ -519,7 +520,7 @@
plan.preprocess(rqlst)
self.assertEqual(
rqlst.as_string(),
- '(Any X WHERE X is IN(SubDivision, Societe)) UNION '
+ '(Any X WHERE X is IN(Societe, SubDivision)) UNION '
'(Any X WHERE X is Division, EXISTS(X owned_by %(B)s))')
@@ -528,7 +529,7 @@
def test_user_can_delete_object_he_created(self):
# even if some other user have changed object'state
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
# due to security test, affaire has to concerne a societe the user owns
cnx.execute('INSERT Societe X: X nom "ARCTIA"')
cnx.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
@@ -542,7 +543,7 @@
self.assertEqual(len(cnx.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
'X owned_by U, U login "admin"')),
1) # TrInfo at the above state change
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
cnx.execute('DELETE Affaire X WHERE X ref "ARCT01"')
cnx.commit()
self.assertFalse(cnx.execute('Affaire X'))
@@ -550,7 +551,7 @@
def test_users_and_groups_non_readable_by_guests(self):
with self.repo.internal_cnx() as cnx:
admineid = cnx.execute('CWUser U WHERE U login "admin"').rows[0][0]
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
anon = cnx.user
# anonymous user can only read itself
rset = cnx.execute('Any L WHERE X owned_by U, U login L')
@@ -569,7 +570,7 @@
self.assertRaises(Unauthorized, cnx.commit)
def test_in_group_relation(self):
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
rql = u"DELETE U in_group G WHERE U login 'admin'"
self.assertRaises(Unauthorized, cnx.execute, rql)
rql = u"SET U in_group G WHERE U login 'admin', G name 'users'"
@@ -579,7 +580,7 @@
with self.admin_access.repo_cnx() as cnx:
cnx.execute("INSERT Personne X: X nom 'bidule'")
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
rql = u"SET X owned_by U WHERE U login 'iaminusersgrouponly', X is Personne"
self.assertRaises(Unauthorized, cnx.execute, rql)
@@ -589,7 +590,7 @@
beid2 = cnx.execute('INSERT Bookmark B: B path "?vid=index", B title "index", '
'B bookmarked_by U WHERE U login "anon"')[0][0]
cnx.commit()
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
anoneid = cnx.user.eid
self.assertEqual(cnx.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
'B bookmarked_by U, U eid %s' % anoneid).rows,
@@ -606,7 +607,7 @@
{'x': anoneid, 'b': beid1})
def test_ambigous_ordered(self):
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
names = [t for t, in cnx.execute('Any N ORDERBY lower(N) WHERE X name N')]
self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
@@ -617,7 +618,7 @@
with self.admin_access.repo_cnx() as cnx:
eid = cnx.execute('INSERT Affaire X: X ref "ARCT01"')[0][0]
cnx.commit()
- with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+ with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
# needed to remove rql expr granting update perm to the user
affschema = self.schema['Affaire']
with self.temporary_permissions(Affaire={'update': affschema.get_groups('update'),
@@ -675,7 +676,7 @@
'U use_email X WHERE U login "anon"').get_entity(0, 0)
cnx.commit()
self.assertEqual(len(cnx.execute('Any X WHERE X is EmailAddress')), 2)
- with self.new_access('anon').repo_cnx() as cnx:
+ with self.new_access(u'anon').repo_cnx() as cnx:
self.assertEqual(len(cnx.execute('Any X WHERE X is EmailAddress')), 1)
if __name__ == '__main__':
--- a/server/test/unittest_session.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,124 +0,0 @@
-# copyright 2003-2014 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 cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.session import HOOKS_ALLOW_ALL, HOOKS_DENY_ALL
-from cubicweb.server import hook
-from cubicweb.predicates import is_instance
-
-class InternalSessionTC(CubicWebTC):
- def test_dbapi_query(self):
- session = self.repo.internal_session()
- self.assertFalse(session.running_dbapi_query)
- session.close()
-
- def test_integrity_hooks(self):
- with self.repo.internal_session() as session:
- self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode)
- self.assertEqual(set(('integrity', 'security')), session.disabled_hook_categories)
- self.assertEqual(set(), session.enabled_hook_categories)
- session.commit()
- self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode)
- self.assertEqual(set(('integrity', 'security')), session.disabled_hook_categories)
- self.assertEqual(set(), session.enabled_hook_categories)
-
-class SessionTC(CubicWebTC):
-
- def test_hooks_control(self):
- session = self.session
- # this test check the "old" behavior of session with automatic connection management
- # close the default cnx, we do nto want it to interfer with the test
- self.cnx.close()
- # open a dedicated one
- session.set_cnx('Some-random-cnx-unrelated-to-the-default-one')
- # go test go
- self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode)
- self.assertEqual(set(), session.disabled_hook_categories)
- self.assertEqual(set(), session.enabled_hook_categories)
- self.assertEqual(1, len(session._cnxs))
- with session.deny_all_hooks_but('metadata'):
- self.assertEqual(HOOKS_DENY_ALL, session.hooks_mode)
- self.assertEqual(set(), session.disabled_hook_categories)
- self.assertEqual(set(('metadata',)), session.enabled_hook_categories)
- session.commit()
- self.assertEqual(HOOKS_DENY_ALL, session.hooks_mode)
- self.assertEqual(set(), session.disabled_hook_categories)
- self.assertEqual(set(('metadata',)), session.enabled_hook_categories)
- session.rollback()
- self.assertEqual(HOOKS_DENY_ALL, session.hooks_mode)
- self.assertEqual(set(), session.disabled_hook_categories)
- self.assertEqual(set(('metadata',)), session.enabled_hook_categories)
- with session.allow_all_hooks_but('integrity'):
- self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode)
- self.assertEqual(set(('integrity',)), session.disabled_hook_categories)
- self.assertEqual(set(('metadata',)), session.enabled_hook_categories) # not changed in such case
- self.assertEqual(HOOKS_DENY_ALL, session.hooks_mode)
- self.assertEqual(set(), session.disabled_hook_categories)
- self.assertEqual(set(('metadata',)), session.enabled_hook_categories)
- # leaving context manager with no transaction running should reset the
- # transaction local storage (and associated cnxset)
- self.assertEqual({}, session._cnxs)
- self.assertEqual(None, session.cnxset)
- self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode, session.HOOKS_ALLOW_ALL)
- self.assertEqual(set(), session.disabled_hook_categories)
- self.assertEqual(set(), session.enabled_hook_categories)
-
- def test_explicit_connection(self):
- with self.session.new_cnx() as cnx:
- rset = cnx.execute('Any X LIMIT 1 WHERE X is CWUser')
- self.assertEqual(1, len(rset))
- user = rset.get_entity(0, 0)
- user.cw_delete()
- cnx.rollback()
- new_user = cnx.entity_from_eid(user.eid)
- self.assertIsNotNone(new_user.login)
- self.assertFalse(cnx._open)
-
- def test_internal_cnx(self):
- with self.repo.internal_cnx() as cnx:
- rset = cnx.execute('Any X LIMIT 1 WHERE X is CWUser')
- self.assertEqual(1, len(rset))
- user = rset.get_entity(0, 0)
- user.cw_delete()
- cnx.rollback()
- new_user = cnx.entity_from_eid(user.eid)
- self.assertIsNotNone(new_user.login)
- self.assertFalse(cnx._open)
-
- def test_connection_exit(self):
- """exiting a connection should roll back the transaction, including any
- pending operations"""
- self.rollbacked = False
- class RollbackOp(hook.Operation):
- _test = self
- def rollback_event(self):
- self._test.rollbacked = True
- class RollbackHook(hook.Hook):
- __regid__ = 'rollback'
- events = ('after_update_entity',)
- __select__ = hook.Hook.__select__ & is_instance('CWGroup')
- def __call__(self):
- RollbackOp(self._cw)
- with self.temporary_appobjects(RollbackHook):
- with self.admin_access.client_cnx() as cnx:
- cnx.execute('SET G name "foo" WHERE G is CWGroup, G name "managers"')
- self.assertTrue(self.rollbacked)
-
-if __name__ == '__main__':
- from logilab.common.testlib import unittest_main
- unittest_main()
--- a/server/test/unittest_storage.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_storage.py Tue Jul 19 16:13:12 2016 +0200
@@ -87,27 +87,31 @@
'managed attribute. Is FSPATH() argument BFSS managed?')
def test_bfss_storage(self):
- with self.admin_access.repo_cnx() as cnx:
- f1 = self.create_file(cnx)
+ with self.admin_access.web_request() as req:
+ cnx = req.cnx
+ f1 = self.create_file(req)
filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
self.assertEqual(len(filepaths), 1, filepaths)
expected_filepath = filepaths[0]
# file should be read only
self.assertFalse(os.access(expected_filepath, os.W_OK))
- self.assertEqual(file(expected_filepath).read(), 'the-data')
+ self.assertEqual(open(expected_filepath).read(), 'the-data')
cnx.rollback()
self.assertFalse(osp.isfile(expected_filepath))
filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
self.assertEqual(len(filepaths), 0, filepaths)
- f1 = self.create_file(cnx)
+ f1 = self.create_file(req)
cnx.commit()
filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
self.assertEqual(len(filepaths), 1, filepaths)
expected_filepath = filepaths[0]
- self.assertEqual(file(expected_filepath).read(), 'the-data')
+ self.assertEqual(open(expected_filepath).read(), 'the-data')
+
+ # add f1 back to the entity cache with req as _cw
+ f1 = req.entity_from_eid(f1.eid)
f1.cw_set(data=Binary('the new data'))
cnx.rollback()
- self.assertEqual(file(expected_filepath).read(), 'the-data')
+ self.assertEqual(open(expected_filepath).read(), 'the-data')
f1.cw_delete()
self.assertTrue(osp.isfile(expected_filepath))
cnx.rollback()
--- a/server/test/unittest_tools.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_tools.py Tue Jul 19 16:13:12 2016 +0200
@@ -23,7 +23,6 @@
class ImportTC(TestCase):
def test(self):
# the minimal test: module is importable...
- import cubicweb.server.server
import cubicweb.server.checkintegrity
import cubicweb.server.serverctl
--- a/server/test/unittest_undo.py Tue Jul 19 15:59:02 2016 +0200
+++ b/server/test/unittest_undo.py Tue Jul 19 16:13:12 2016 +0200
@@ -48,7 +48,6 @@
def tearDown(self):
cubicweb.server.session.Connection = OldConnection
- self.restore_connection()
super(UndoableTransactionTC, self).tearDown()
def check_transaction_deleted(self, cnx, txuuid):
@@ -210,13 +209,12 @@
['CWUser'])
# undoing shouldn't be visble in undoable transaction, and the undone
# transaction should be removed
- txs = self.cnx.undoable_transactions()
+ txs = cnx.undoable_transactions()
self.assertEqual(len(txs), 2)
self.assertRaises(NoSuchTransaction,
- self.cnx.transaction_info, txuuid)
+ cnx.transaction_info, txuuid)
with self.admin_access.repo_cnx() as cnx:
- with cnx.ensure_cnx_set:
- self.check_transaction_deleted(cnx, txuuid)
+ self.check_transaction_deleted(cnx, txuuid)
# the final test: check we can login with the previously deleted user
with self.new_access('toto').client_cnx():
pass
@@ -238,6 +236,8 @@
cnx.commit()
p.cw_clear_all_caches()
self.assertEqual(p.fiche[0].eid, c2.eid)
+ # we restored the card
+ self.assertTrue(cnx.entity_from_eid(c.eid))
def test_undo_deletion_integrity_2(self):
with self.admin_access.client_cnx() as cnx:
@@ -272,18 +272,17 @@
self.assertFalse(cnx.execute('Any X WHERE X eid %(x)s', {'x': p.eid}))
self.assertFalse(cnx.execute('Any X,Y WHERE X fiche Y'))
with self.admin_access.repo_cnx() as cnx:
- with cnx.ensure_cnx_set:
- for eid in (p.eid, c.eid):
- self.assertFalse(cnx.system_sql(
- 'SELECT * FROM entities WHERE eid=%s' % eid).fetchall())
- self.assertFalse(cnx.system_sql(
- 'SELECT 1 FROM owned_by_relation WHERE eid_from=%s' % eid).fetchall())
- # added by sql in hooks (except when using dataimport)
- self.assertFalse(cnx.system_sql(
- 'SELECT 1 FROM is_relation WHERE eid_from=%s' % eid).fetchall())
- self.assertFalse(cnx.system_sql(
- 'SELECT 1 FROM is_instance_of_relation WHERE eid_from=%s' % eid).fetchall())
- self.check_transaction_deleted(cnx, txuuid)
+ for eid in (p.eid, c.eid):
+ self.assertFalse(cnx.system_sql(
+ 'SELECT * FROM entities WHERE eid=%s' % eid).fetchall())
+ self.assertFalse(cnx.system_sql(
+ 'SELECT 1 FROM owned_by_relation WHERE eid_from=%s' % eid).fetchall())
+ # added by sql in hooks (except when using dataimport)
+ self.assertFalse(cnx.system_sql(
+ 'SELECT 1 FROM is_relation WHERE eid_from=%s' % eid).fetchall())
+ self.assertFalse(cnx.system_sql(
+ 'SELECT 1 FROM is_instance_of_relation WHERE eid_from=%s' % eid).fetchall())
+ self.check_transaction_deleted(cnx, txuuid)
def test_undo_creation_integrity_1(self):
with self.admin_access.client_cnx() as cnx:
@@ -356,9 +355,8 @@
p.cw_clear_all_caches()
self.assertFalse(p.fiche)
with self.admin_access.repo_cnx() as cnx:
- with cnx.ensure_cnx_set:
- self.assertIsNone(cnx.system_sql(
- 'SELECT cw_fiche FROM cw_Personne WHERE cw_eid=%s' % p.eid).fetchall()[0][0])
+ self.assertIsNone(cnx.system_sql(
+ 'SELECT cw_fiche FROM cw_Personne WHERE cw_eid=%s' % p.eid).fetchall()[0][0])
def test_undo_inline_rel_add_ok(self):
"""Undo add relation Personne (?) fiche (?) Card
@@ -375,6 +373,17 @@
p.cw_clear_all_caches()
self.assertFalse(p.fiche)
+ def test_undo_inline_rel_delete_ko(self):
+ with self.admin_access.client_cnx() as cnx:
+ c = cnx.create_entity('Card', title=u'hop', content=u'hop')
+ txuuid = cnx.commit()
+ p = cnx.create_entity('Personne', nom=u'louis', fiche=c)
+ cnx.commit()
+ integrityerror = self.repo.sources_by_uri['system'].dbhelper.dbapi_module.IntegrityError
+ with self.assertRaises(integrityerror):
+ cnx.undo_transaction(txuuid)
+
+
def test_undo_inline_rel_add_ko(self):
"""Undo add relation Personne (?) fiche (?) Card
--- a/skeleton/__pkginfo__.py.tmpl Tue Jul 19 15:59:02 2016 +0200
+++ b/skeleton/__pkginfo__.py.tmpl Tue Jul 19 16:13:12 2016 +0200
@@ -13,7 +13,7 @@
description = '%(shortdesc)s'
web = 'http://www.cubicweb.org/project/%%s' %% distname
-__depends__ = %(dependencies)s
+__depends__ = %(dependencies)s
__recommends__ = {}
classifiers = [
@@ -29,6 +29,7 @@
THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
+
def listdir(dirpath):
return [join(dirpath, fname) for fname in _listdir(dirpath)
if fname[0] != '.' and not fname.endswith('.pyc')
@@ -40,9 +41,9 @@
[THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
]
# check for possible extended cube layout
-for dname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'wdoc', 'i18n', 'migration'):
+for dname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data',
+ 'wdoc', 'i18n', 'migration'):
if isdir(dname):
data_files.append([join(THIS_CUBE_DIR, dname), listdir(dname)])
# Note: here, you'll need to add subdirectories if you want
# them to be included in the debian package
-
--- a/skeleton/migration/postcreate.py.tmpl Tue Jul 19 15:59:02 2016 +0200
+++ b/skeleton/migration/postcreate.py.tmpl Tue Jul 19 16:13:12 2016 +0200
@@ -11,4 +11,3 @@
# Example of site property change
#set_property('ui.site-title', "<sitename>")
-
--- a/skeleton/setup.py Tue Jul 19 15:59:02 2016 +0200
+++ b/skeleton/setup.py Tue Jul 19 16:13:12 2016 +0200
@@ -16,8 +16,8 @@
# 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/>.
+# You should have received a copy of the GNU Lesser General Public License
+# along with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""Generic Setup script, takes package info from __pkginfo__.py file
"""
__docformat__ = "restructuredtext en"
@@ -25,11 +25,11 @@
import os
import sys
import shutil
-from os.path import isdir, exists, join, walk
+from os.path import exists, join, walk
try:
if os.environ.get('NO_SETUPTOOLS'):
- raise ImportError() # do as there is no setuptools
+ raise ImportError() # do as there is no setuptools
from setuptools import setup
from setuptools.command import install_lib
USE_SETUPTOOLS = True
@@ -41,7 +41,7 @@
# import required features
from __pkginfo__ import modname, version, license, description, web, \
- author, author_email, classifiers
+ author, author_email, classifiers
if exists('README'):
long_description = file('README').read()
@@ -52,10 +52,10 @@
import __pkginfo__
if USE_SETUPTOOLS:
requires = {}
- for entry in ("__depends__",): # "__recommends__"):
+ for entry in ("__depends__",): # "__recommends__"):
requires.update(getattr(__pkginfo__, entry, {}))
install_requires = [("%s %s" % (d, v and v or "")).strip()
- for d, v in requires.iteritems()]
+ for d, v in requires.iteritems()]
else:
install_requires = []
@@ -82,6 +82,7 @@
scripts_ = linux_scripts
return scripts_
+
def export(from_dir, to_dir,
blacklist=BASE_BLACKLIST,
ignore_ext=IGNORED_EXTENSIONS,
@@ -150,13 +151,15 @@
old_install_data.run(self)
self.install_dir = _old_install_dir
try:
- import setuptools.command.easy_install # only if easy_install available
+ # only if easy_install available
+ import setuptools.command.easy_install # noqa
# monkey patch: Crack SandboxViolation verification
from setuptools.sandbox import DirectorySandbox as DS
old_ok = DS._ok
+
def _ok(self, path):
"""Return True if ``path`` can be written during installation."""
- out = old_ok(self, path) # here for side effect from setuptools
+ out = old_ok(self, path) # here for side effect from setuptools
realpath = os.path.normcase(os.path.realpath(path))
allowed_path = os.path.normcase(sys.prefix)
if realpath.startswith(allowed_path):
@@ -166,6 +169,7 @@
except ImportError:
pass
+
def install(**kwargs):
"""setup entry point"""
if USE_SETUPTOOLS:
@@ -181,21 +185,22 @@
kwargs['zip_safe'] = False
cmdclass['install_data'] = MyInstallData
- return setup(name = distname,
- version = version,
- license = license,
- description = description,
- long_description = long_description,
- author = author,
- author_email = author_email,
- url = web,
- scripts = ensure_scripts(scripts),
- data_files = data_files,
- ext_modules = ext_modules,
- cmdclass = cmdclass,
- classifiers = classifiers,
+ return setup(name=distname,
+ version=version,
+ license=license,
+ description=description,
+ long_description=long_description,
+ author=author,
+ author_email=author_email,
+ url=web,
+ scripts=ensure_scripts(scripts),
+ data_files=data_files,
+ ext_modules=ext_modules,
+ cmdclass=cmdclass,
+ classifiers=classifiers,
**kwargs
)
-if __name__ == '__main__' :
+
+if __name__ == '__main__':
install()
--- a/skeleton/test/pytestconf.py Tue Jul 19 15:59:02 2016 +0200
+++ b/skeleton/test/pytestconf.py Tue Jul 19 16:13:12 2016 +0200
@@ -13,8 +13,8 @@
# 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/>.
+# You should have received a copy of the GNU Lesser General Public License
+# along with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""
"""
@@ -23,6 +23,7 @@
from logilab.common.pytest import PyTester
+
def getlogin():
"""avoid usinng os.getlogin() because of strange tty / stdin problems
(man 3 getlogin)
--- a/skeleton/test/realdb_test_CUBENAME.py Tue Jul 19 15:59:02 2016 +0200
+++ b/skeleton/test/realdb_test_CUBENAME.py Tue Jul 19 16:13:12 2016 +0200
@@ -13,14 +13,15 @@
# 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/>.
+# 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 cubicweb.devtools.testlib import CubicWebTC
from cubicweb.devtools.realdbtest import buildconfig, loadconfig
+
def setUpModule(options):
if options.source:
configcls = loadconfig(options.source)
@@ -28,13 +29,13 @@
raise Exception('either <sourcefile> or <dbname> options are required')
else:
configcls = buildconfig(options.dbuser, options.dbpassword,
- options.dbname, options.euser,
- options.epassword)
+ options.dbname,
+ options.euser, options.epassword)
RealDatabaseTC.configcls = configcls
class RealDatabaseTC(CubicWebTC):
- configcls = None # set by setUpModule()
+ configcls = None # set by setUpModule()
def test_all_primaries(self):
for rset in self.iter_individual_rsets(limit=50):
--- a/skeleton/test/test_CUBENAME.py.tmpl Tue Jul 19 15:59:02 2016 +0200
+++ b/skeleton/test/test_CUBENAME.py.tmpl Tue Jul 19 16:13:12 2016 +0200
@@ -27,6 +27,7 @@
from cubicweb.devtools import testlib
+
class DefaultTC(testlib.CubicWebTC):
def test_something(self):
self.skipTest('this cube has no test')
--- a/sobjects/cwxmlparser.py Tue Jul 19 15:59:02 2016 +0200
+++ b/sobjects/cwxmlparser.py Tue Jul 19 16:13:12 2016 +0200
@@ -32,7 +32,8 @@
"""
from datetime import datetime, time
-from cgi import parse_qs # in urlparse with python >= 2.6
+import urlparse
+import urllib
from logilab.common.date import todate, totime
from logilab.common.textutils import splitstrip, text_to_dict
@@ -238,20 +239,17 @@
attrs = extract_typed_attrs(entity.e_schema, sourceparams['item'])
entity.cw_edited.update(attrs)
-
def normalize_url(self, url):
- """overriden to add vid=xml"""
+ """overridden to add vid=xml if vid is not set in the qs"""
url = super(CWEntityXMLParser, self).normalize_url(url)
- if url.startswith('http'):
- try:
- url, qs = url.split('?', 1)
- except ValueError:
- params = {}
- else:
- params = parse_qs(qs)
- if not 'vid' in params:
+ purl = urlparse.urlparse(url)
+ if purl.scheme in ('http', 'https'):
+ params = urlparse.parse_qs(purl.query)
+ if 'vid' not in params:
params['vid'] = ['xml']
- return url + '?' + self._cw.build_url_params(**params)
+ purl = list(purl)
+ purl[4] = urllib.urlencode(params, doseq=True)
+ return urlparse.urlunparse(purl)
return url
def complete_url(self, url, etype=None, known_relations=None):
@@ -265,29 +263,22 @@
If `known_relations` is given, it should be a dictionary of already
known relations, so they don't get queried again.
"""
- try:
- url, qs = url.split('?', 1)
- except ValueError:
- qs = ''
- # XXX vid will be added by later call to normalize_url (in parent class)
- params = parse_qs(qs)
+ purl = urlparse.urlparse(url)
+ params = urlparse.parse_qs(purl.query)
if etype is None:
- try:
- etype = url.rsplit('/', 1)[1]
- except ValueError:
- return url + '?' + self._cw.build_url_params(**params)
- try:
- etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
- except KeyError:
- return url + '?' + self._cw.build_url_params(**params)
- relations = params.setdefault('relation', [])
+ etype = purl.path.split('/')[-1]
+ try:
+ etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
+ except KeyError:
+ return url
+ relations = params['relation'] = set(params.get('relation', ()))
for rtype, role, _ in self.source.mapping.get(etype, ()):
if known_relations and rtype in known_relations.get('role', ()):
continue
- reldef = '%s-%s' % (rtype, role)
- if not reldef in relations:
- relations.append(reldef)
- return url + '?' + self._cw.build_url_params(**params)
+ relations.add('%s-%s' % (rtype, role))
+ purl = list(purl)
+ purl[4] = urllib.urlencode(params, doseq=True)
+ return urlparse.urlunparse(purl)
def complete_item(self, item, rels):
try:
--- a/sobjects/notification.py Tue Jul 19 15:59:02 2016 +0200
+++ b/sobjects/notification.py Tue Jul 19 16:13:12 2016 +0200
@@ -270,7 +270,7 @@
"""
__abstract__ = True
__regid__ = 'notif_entity_updated'
- msgid_timestamp = False
+ msgid_timestamp = True
message = _('updated')
no_detailed_change_attrs = ()
content = """
--- a/sobjects/services.py Tue Jul 19 15:59:02 2016 +0200
+++ b/sobjects/services.py Tue Jul 19 16:13:12 2016 +0200
@@ -43,7 +43,7 @@
(len(source._cache), repo.config['rql-cache-size'],
source.cache_hit, source.cache_miss, 'sql'),
):
- results['%s_cache_size' % title] = '%s / %s' % (size, maxsize)
+ results['%s_cache_size' % title] = {'size': size, 'maxsize': maxsize}
results['%s_cache_hit' % title] = hits
results['%s_cache_miss' % title] = misses
results['%s_cache_hit_percent' % title] = (hits * 100) / (hits + misses)
@@ -53,9 +53,9 @@
results['nb_open_sessions'] = len(repo._sessions)
results['nb_active_threads'] = threading.activeCount()
looping_tasks = repo._tasks_manager._looping_tasks
- results['looping_tasks'] = ', '.join(str(t) for t in looping_tasks)
+ results['looping_tasks'] = [(t.name, t.interval) for t in looping_tasks]
results['available_cnxsets'] = repo._cnxsets_pool.qsize()
- results['threads'] = ', '.join(sorted(str(t) for t in threading.enumerate()))
+ results['threads'] = [t.name for t in threading.enumerate()]
return results
class GcStatsService(Service):
@@ -79,13 +79,11 @@
from cubicweb._gcdebug import gc_info
from cubicweb.appobject import AppObject
from cubicweb.rset import ResultSet
- from cubicweb.dbapi import Connection, Cursor
from cubicweb.web.request import CubicWebRequestBase
from rql.stmts import Union
lookupclasses = (AppObject,
Union, ResultSet,
- Connection, Cursor,
CubicWebRequestBase)
try:
from cubicweb.server.session import Session, InternalSession
@@ -100,7 +98,7 @@
results['lookupclasses'] = values
values = sorted(ocounters.iteritems(), key=lambda x: x[1], reverse=True)[:nmax]
results['referenced'] = values
- results['unreachable'] = len(garbage)
+ results['unreachable'] = garbage
return results
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,2 @@
+cubicweb-card
+cubicweb-comment
--- a/sobjects/test/unittest_cwxmlparser.py Tue Jul 19 15:59:02 2016 +0200
+++ b/sobjects/test/unittest_cwxmlparser.py Tue Jul 19 16:13:12 2016 +0200
@@ -17,6 +17,7 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
+from urlparse import urlsplit, parse_qsl
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.sobjects.cwxmlparser import CWEntityXMLParser
@@ -133,6 +134,16 @@
"""
test_db_id = 'xmlparser'
+ def assertURLEquiv(self, first, second):
+ # ignore ordering differences in query params
+ parsed_first = urlsplit(first)
+ parsed_second = urlsplit(second)
+ self.assertEqual(parsed_first.scheme, parsed_second.scheme)
+ self.assertEqual(parsed_first.netloc, parsed_second.netloc)
+ self.assertEqual(parsed_first.path, parsed_second.path)
+ self.assertEqual(parsed_first.fragment, parsed_second.fragment)
+ self.assertCountEqual(parse_qsl(parsed_first.query), parse_qsl(parsed_second.query))
+
@classmethod
def pre_setup_database(cls, cnx, config):
myfeed = cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
@@ -161,16 +172,16 @@
dfsource = self.repo.sources_by_uri['myfeed']
with self.admin_access.repo_cnx() as cnx:
parser = dfsource._get_parser(cnx)
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/CWUser'),
- 'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser'),
- 'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
- 'http://www.cubicweb.org/cwuser?relation=hop&relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=rdf')
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&vid=rdf&relation=hop'),
- 'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=rdf')
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&relation=hop'),
- 'http://www.cubicweb.org/?rql=cwuser&relation=hop')
+ self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/CWUser'),
+ 'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
+ self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/cwuser'),
+ 'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
+ self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
+ 'http://www.cubicweb.org/cwuser?relation=hop&relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=rdf')
+ self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&vid=rdf&relation=hop'),
+ 'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=rdf')
+ self.assertURLEquiv(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&relation=hop'),
+ 'http://www.cubicweb.org/?rql=cwuser&relation=hop')
def test_actions(self):
@@ -256,7 +267,11 @@
self.assertEqual(e.cw_source[0].name, 'system')
self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
# test everything is still fine after source synchronization
+ # clear caches to make sure we look at the moved_entities table
+ self.repo._type_source_cache.clear()
+ self.repo._extid_cache.clear()
stats = dfsource.pull_data(cnx, force=True, raise_on_error=True)
+ self.assertEqual(stats['updated'], set((email.eid,)))
rset = cnx.execute('EmailAddress X WHERE X address "syt@logilab.fr"')
self.assertEqual(len(rset), 1)
e = rset.get_entity(0, 0)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/statsd_logger.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,135 @@
+# copyright 2015 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/>.
+
+"""Simple statsd_ logger for cubicweb.
+
+This module is meant to be configured by setting a couple of global variables:
+
+- ``bucket`` global variable will be used as statsd bucket in every
+statsd_ UDP sent packet.
+
+`- `address`` is a pair (IP, port) specifying the address of the
+statsd_ server
+
+
+There are 3 kinds of statds_ message::
+
+- ``statsd_c(context, n)`` is a simple function to send statsd_
+ counter-type of messages like::
+
+ <bucket>.<context>:<n>|c\n
+
+- ``statsd_g(context, value)`` to send statsd_ gauge-type of messages
+ like::
+
+ <bucket>.<context>:<n>|g\n
+
+- ``statsd_t(context, ms)`` to send statsd_ time-type of messages
+ like::
+
+ <bucket>.<context>:<ms>|ms\n
+
+There is also a decorator (``statsd_timeit``) that may be used to
+measure and send to the statsd_ server the time passed in a function
+or a method and the number of calls. It will send a message like::
+
+ <bucket>.<funcname>:<ms>|ms\n<bucket>.<funcname>:1|c\n
+
+
+.. _statsd: https://github.com/etsy/statsd
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+import time
+import socket
+
+_bucket = 'cubicweb'
+_address = None
+_socket = None
+
+
+def setup(bucket, address):
+ """Configure the statsd endpoint
+
+ :param bucket: the name of the statsd bucket that will be used to
+ build messages.
+
+ :param address: the UDP endpoint of the statsd server. Must a
+ couple (ip, port).
+ """
+ global _bucket, _address, _socket
+ packed = None
+ for family in (socket.AF_INET6, socket.AF_INET):
+ try:
+ packed = socket.inet_pton(family, address[0])
+ break
+ except socket.error:
+ continue
+ if packed is None:
+ return
+ _bucket, _address = bucket, address
+ _socket = socket.socket(family, socket.SOCK_DGRAM)
+
+
+def statsd_c(context, n=1):
+ if _address is not None:
+ _socket.sendto('{0}.{1}:{2}|c\n'.format(_bucket, context, n), _address)
+
+
+def statsd_g(context, value):
+ if _address is not None:
+ _socket.sendto('{0}.{1}:{2}|g\n'.format(_bucket, context, value), _address)
+
+
+def statsd_t(context, value):
+ if _address is not None:
+ _socket.sendto('{0}.{1}:{2:.4f}|ms\n'.format(_bucket, context, value), _address)
+
+
+class statsd_timeit(object):
+ __slots__ = ('callable',)
+
+ def __init__(self, callableobj):
+ self.callable = callableobj
+
+ @property
+ def __doc__(self):
+ return self.callable.__doc__
+ @property
+ def __name__(self):
+ return self.callable.__name__
+
+ def __call__(self, *args, **kw):
+ if _address is None:
+ return self.callable(*args, **kw)
+ t0 = time.time()
+ try:
+ return self.callable(*args, **kw)
+ finally:
+ dt = 1000*(time.time()-t0)
+ msg = '{0}.{1}:{2:.4f}|ms\n{0}.{1}:1|c\n'.format(_bucket, self.__name__, dt)
+ _socket.sendto(msg, _address)
+
+ def __get__(self, obj, objtype):
+ """Support instance methods."""
+ if obj is None: # class method or some already wrapped method
+ return self
+ import functools
+ return functools.partial(self.__call__, obj)
--- a/test/data/bootstrap_cubes Tue Jul 19 15:59:02 2016 +0200
+++ b/test/data/bootstrap_cubes Tue Jul 19 16:13:12 2016 +0200
@@ -1,1 +1,1 @@
-card, file, tag, localperms
+card, tag, localperms
--- a/test/data/entities.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/data/entities.py Tue Jul 19 16:13:12 2016 +0200
@@ -16,7 +16,9 @@
# 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 cubicweb.entities import AnyEntity, fetch_config
+from cubicweb.entities import AnyEntity, fetch_config, adapters
+from cubicweb.predicates import is_instance
+
class Societe(AnyEntity):
__regid__ = 'Societe'
@@ -34,3 +36,7 @@
class Note(AnyEntity):
__regid__ = 'Note'
+
+
+class FakeFileIDownloadableAdapter(adapters.IDownloadableAdapter):
+ __select__ = is_instance('FakeFile')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/rqlexpr_on_computedrel.py Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,18 @@
+from yams.buildobjs import ComputedRelation, EntityType, RelationDefinition
+from cubicweb.schema import RRQLExpression
+
+class Subject(EntityType):
+ pass
+
+class Object(EntityType):
+ pass
+
+class relation(RelationDefinition):
+ subject = 'Subject'
+ object = 'Object'
+
+class computed(ComputedRelation):
+ rule = 'S relation O'
+ __permissions__ = {'read': (RRQLExpression('S is ET'),)}
+
+
--- a/test/data/schema.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/data/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -16,13 +16,16 @@
# 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 (EntityType, String, SubjectRelation,
- RelationDefinition)
+from yams.buildobjs import (EntityType, String, RichString, Bytes,
+ SubjectRelation, RelationDefinition)
from cubicweb.schema import (WorkflowableEntityType,
RQLConstraint, RQLVocabularyConstraint)
+_ = unicode
+
+
class Personne(EntityType):
nom = String(required=True)
prenom = String()
@@ -94,3 +97,17 @@
class Reference(EntityType):
nom = String(unique=True)
ean = String(unique=True, required=True)
+
+
+class FakeFile(EntityType):
+ title = String(fulltextindexed=True, maxsize=256)
+ data = Bytes(required=True, fulltextindexed=True, description=_('file to upload'))
+ data_format = String(required=True, maxsize=128,
+ description=_('MIME type of the file. Should be dynamically set at upload time.'))
+ data_encoding = String(maxsize=32,
+ description=_('encoding of the file when it applies (e.g. text). '
+ 'Should be dynamically set at upload time.'))
+ data_name = String(required=True, fulltextindexed=True,
+ description=_('name of the file. Should be dynamically set at upload time.'))
+ description = RichString(fulltextindexed=True, internationalizable=True,
+ default_format='text/rest')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,6 @@
+Pygments
+#fyzz XXX pip install fails
+cubicweb-card
+cubicweb-file
+cubicweb-localperms
+cubicweb-tag
--- a/test/unittest_cwconfig.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/unittest_cwconfig.py Tue Jul 19 16:13:12 2016 +0200
@@ -104,11 +104,14 @@
def test_appobjects_path(self):
self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
self.config.adjust_sys_path()
- self.assertEqual([unabsolutize(p) for p in self.config.appobjects_path()],
- ['entities', 'web/views', 'sobjects', 'hooks',
- 'file/entities', 'file/views.py', 'file/hooks',
- 'email/entities.py', 'email/views', 'email/hooks.py',
- 'test/data/entities.py', 'test/data/views.py'])
+ path = [unabsolutize(p) for p in self.config.appobjects_path()]
+ self.assertEqual(path[0], 'entities')
+ self.assertCountEqual(path[1:4], ['web/views', 'sobjects', 'hooks'])
+ self.assertEqual(path[4], 'file/entities')
+ self.assertCountEqual(path[5:7], ['file/views.py', 'file/hooks'])
+ self.assertEqual(path[7], 'email/entities.py')
+ self.assertCountEqual(path[8:10], ['email/views', 'email/hooks.py'])
+ self.assertEqual(path[10:], ['test/data/entities.py', 'test/data/views.py'])
def test_cubes_path(self):
# make sure we don't import the email cube, but the stdlib email package
--- a/test/unittest_cwctl.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/unittest_cwctl.py Tue Jul 19 16:13:12 2016 +0200
@@ -44,7 +44,7 @@
def test_process_script_args_context(self):
repo = self.repo
- with self.admin_access.client_cnx() as cnx:
+ with self.admin_access.repo_cnx() as cnx:
mih = ServerMigrationHelper(None, repo=repo, cnx=cnx,
interactive=False,
# hack so it don't try to load fs schema
--- a/test/unittest_dataimport.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import datetime as DT
-from StringIO import StringIO
-
-from logilab.common.testlib import TestCase, unittest_main
-
-from cubicweb import dataimport
-from cubicweb.devtools.testlib import CubicWebTC
-
-
-class RQLObjectStoreTC(CubicWebTC):
-
- def test_all(self):
- with self.admin_access.repo_cnx() as cnx:
- store = dataimport.RQLObjectStore(cnx)
- group_eid = store.create_entity('CWGroup', name=u'grp').eid
- user_eid = store.create_entity('CWUser', login=u'lgn', upassword=u'pwd').eid
- store.relate(user_eid, 'in_group', group_eid)
- cnx.commit()
-
- with self.admin_access.repo_cnx() as cnx:
- users = cnx.execute('CWUser X WHERE X login "lgn"')
- self.assertEqual(1, len(users))
- self.assertEqual(user_eid, users.one().eid)
- groups = cnx.execute('CWGroup X WHERE U in_group X, U login "lgn"')
- self.assertEqual(1, len(users))
- self.assertEqual(group_eid, groups.one().eid)
-
-
-class CreateCopyFromBufferTC(TestCase):
-
- # test converters
-
- def test_convert_none(self):
- cnvt = dataimport._copyfrom_buffer_convert_None
- self.assertEqual('NULL', cnvt(None))
-
- def test_convert_number(self):
- cnvt = dataimport._copyfrom_buffer_convert_number
- self.assertEqual('42', cnvt(42))
- self.assertEqual('42', cnvt(42L))
- self.assertEqual('42.42', cnvt(42.42))
-
- def test_convert_string(self):
- cnvt = dataimport._copyfrom_buffer_convert_string
- # simple
- self.assertEqual('babar', cnvt('babar'))
- # unicode
- self.assertEqual('\xc3\xa9l\xc3\xa9phant', cnvt(u'éléphant'))
- self.assertEqual('\xe9l\xe9phant', cnvt(u'éléphant', encoding='latin1'))
- self.assertEqual('babar#', cnvt('babar\t', replace_sep='#'))
- self.assertRaises(ValueError, cnvt, 'babar\t')
-
- def test_convert_date(self):
- cnvt = dataimport._copyfrom_buffer_convert_date
- self.assertEqual('0666-01-13', cnvt(DT.date(666, 1, 13)))
-
- def test_convert_time(self):
- cnvt = dataimport._copyfrom_buffer_convert_time
- self.assertEqual('06:06:06.000100', cnvt(DT.time(6, 6, 6, 100)))
-
- def test_convert_datetime(self):
- cnvt = dataimport._copyfrom_buffer_convert_datetime
- self.assertEqual('0666-06-13 06:06:06.000000', cnvt(DT.datetime(666, 6, 13, 6, 6, 6)))
-
- # test buffer
- def test_create_copyfrom_buffer_tuple(self):
- cnvt = dataimport._create_copyfrom_buffer
- data = ((42, 42L, 42.42, u'éléphant', DT.date(666, 1, 13), DT.time(6, 6, 6), DT.datetime(666, 6, 13, 6, 6, 6)),
- (6, 6L, 6.6, u'babar', DT.date(2014, 1, 14), DT.time(4, 2, 1), DT.datetime(2014, 1, 1, 0, 0, 0)))
- results = dataimport._create_copyfrom_buffer(data)
- # all columns
- expected = '''42\t42\t42.42\téléphant\t0666-01-13\t06:06:06.000000\t0666-06-13 06:06:06.000000
-6\t6\t6.6\tbabar\t2014-01-14\t04:02:01.000000\t2014-01-01 00:00:00.000000'''
- self.assertMultiLineEqual(expected, results.getvalue())
- # selected columns
- results = dataimport._create_copyfrom_buffer(data, columns=(1, 3, 6))
- expected = '''42\téléphant\t0666-06-13 06:06:06.000000
-6\tbabar\t2014-01-01 00:00:00.000000'''
- self.assertMultiLineEqual(expected, results.getvalue())
-
- def test_create_copyfrom_buffer_dict(self):
- cnvt = dataimport._create_copyfrom_buffer
- data = (dict(integer=42, double=42.42, text=u'éléphant', date=DT.datetime(666, 6, 13, 6, 6, 6)),
- dict(integer=6, double=6.6, text=u'babar', date=DT.datetime(2014, 1, 1, 0, 0, 0)))
- results = dataimport._create_copyfrom_buffer(data, ('integer', 'text'))
- expected = '''42\téléphant\n6\tbabar'''
- self.assertMultiLineEqual(expected, results.getvalue())
-
-
-class UcsvreaderTC(TestCase):
-
- def test_empty_lines_skipped(self):
- stream = StringIO('''a,b,c,d,
-1,2,3,4,
-,,,,
-,,,,
-''')
- self.assertEqual([[u'a', u'b', u'c', u'd', u''],
- [u'1', u'2', u'3', u'4', u''],
- ],
- list(dataimport.ucsvreader(stream)))
- stream.seek(0)
- self.assertEqual([[u'a', u'b', u'c', u'd', u''],
- [u'1', u'2', u'3', u'4', u''],
- [u'', u'', u'', u'', u''],
- [u'', u'', u'', u'', u'']
- ],
- list(dataimport.ucsvreader(stream, skip_empty=False)))
-
- def test_skip_first(self):
- stream = StringIO('a,b,c,d,\n'
- '1,2,3,4,\n')
- reader = dataimport.ucsvreader(stream, skipfirst=True,
- ignore_errors=True)
- self.assertEqual(list(reader),
- [[u'1', u'2', u'3', u'4', u'']])
-
- stream.seek(0)
- reader = dataimport.ucsvreader(stream, skipfirst=True,
- ignore_errors=False)
- self.assertEqual(list(reader),
- [[u'1', u'2', u'3', u'4', u'']])
-
- stream.seek(0)
- reader = dataimport.ucsvreader(stream, skipfirst=False,
- ignore_errors=True)
- self.assertEqual(list(reader),
- [[u'a', u'b', u'c', u'd', u''],
- [u'1', u'2', u'3', u'4', u'']])
-
- stream.seek(0)
- reader = dataimport.ucsvreader(stream, skipfirst=False,
- ignore_errors=False)
- self.assertEqual(list(reader),
- [[u'a', u'b', u'c', u'd', u''],
- [u'1', u'2', u'3', u'4', u'']])
-
-
-class MetaGeneratorTC(CubicWebTC):
-
- def test_dont_generate_relation_to_internal_manager(self):
- with self.admin_access.repo_cnx() as cnx:
- metagen = dataimport.MetaGenerator(cnx)
- self.assertIn('created_by', metagen.etype_rels)
- self.assertIn('owned_by', metagen.etype_rels)
- with self.repo.internal_cnx() as cnx:
- metagen = dataimport.MetaGenerator(cnx)
- self.assertNotIn('created_by', metagen.etype_rels)
- self.assertNotIn('owned_by', metagen.etype_rels)
-
- def test_dont_generate_specified_values(self):
- with self.admin_access.repo_cnx() as cnx:
- metagen = dataimport.MetaGenerator(cnx)
- # hijack gen_modification_date to ensure we don't go through it
- metagen.gen_modification_date = None
- md = DT.datetime.now() - DT.timedelta(days=1)
- entity, rels = metagen.base_etype_dicts('CWUser')
- entity.cw_edited.update(dict(modification_date=md))
- with cnx.ensure_cnx_set:
- metagen.init_entity(entity)
- self.assertEqual(entity.cw_edited['modification_date'], md)
-
-
-if __name__ == '__main__':
- unittest_main()
--- a/test/unittest_dbapi.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +0,0 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# 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/>.
-"""unittest for cubicweb.dbapi"""
-
-from copy import copy
-
-from logilab.common import tempattr
-
-from cubicweb import ConnectionError, cwconfig, NoSelectableObject
-from cubicweb.dbapi import ProgrammingError, _repo_connect
-from cubicweb.devtools.testlib import CubicWebTC
-
-
-class DBAPITC(CubicWebTC):
-
- def test_public_repo_api(self):
- cnx = _repo_connect(self.repo, login='anon', password='anon')
- self.assertEqual(cnx.get_schema(), self.repo.schema)
- self.assertEqual(cnx.source_defs(), {'system': {'type': 'native', 'uri': 'system',
- 'use-cwuri-as-url': False}})
- cnx.close()
- self.assertRaises(ProgrammingError, cnx.get_schema)
- self.assertRaises(ProgrammingError, cnx.source_defs)
-
- def test_db_api(self):
- cnx = _repo_connect(self.repo, login='anon', password='anon')
- self.assertEqual(cnx.rollback(), None)
- self.assertEqual(cnx.commit(), None)
- cnx.close()
- self.assertRaises(ProgrammingError, cnx.rollback)
- self.assertRaises(ProgrammingError, cnx.commit)
- self.assertRaises(ProgrammingError, cnx.close)
-
- def test_api(self):
- cnx = _repo_connect(self.repo, login='anon', password='anon')
- self.assertEqual(cnx.user(None).login, 'anon')
- self.assertEqual({'type': u'CWSource', 'source': u'system', 'extid': None},
- cnx.entity_metas(1))
- self.assertEqual(cnx.describe(1), (u'CWSource', u'system', None))
- cnx.close()
- self.assertRaises(ProgrammingError, cnx.user, None)
- self.assertRaises(ProgrammingError, cnx.entity_metas, 1)
- self.assertRaises(ProgrammingError, cnx.describe, 1)
-
- def test_shared_data_api(self):
- cnx = _repo_connect(self.repo, login='anon', password='anon')
- self.assertEqual(cnx.get_shared_data('data'), None)
- cnx.set_shared_data('data', 4)
- self.assertEqual(cnx.get_shared_data('data'), 4)
- cnx.get_shared_data('data', pop=True)
- cnx.get_shared_data('whatever', pop=True)
- self.assertEqual(cnx.get_shared_data('data'), None)
- cnx.set_shared_data('data', 4)
- self.assertEqual(cnx.get_shared_data('data'), 4)
- cnx.close()
- self.assertRaises(ProgrammingError, cnx.check)
- self.assertRaises(ProgrammingError, cnx.set_shared_data, 'data', 0)
- self.assertRaises(ProgrammingError, cnx.get_shared_data, 'data')
-
- def test_web_compatible_request(self):
- config = cwconfig.CubicWebNoAppConfiguration()
- cnx = _repo_connect(self.repo, login='admin', password='gingkow')
- with tempattr(cnx.vreg, 'config', config):
- cnx.use_web_compatible_requests('http://perdu.com')
- req = cnx.request()
- self.assertEqual(req.base_url(), 'http://perdu.com/')
- self.assertEqual(req.from_controller(), 'view')
- self.assertEqual(req.relative_path(), '')
- req.ajax_replace_url('domid') # don't crash
- req.user.cw_adapt_to('IBreadCrumbs') # don't crash
-
- def test_call_service(self):
- ServiceClass = self.vreg['services']['test_service'][0]
- for _cw in (self.request(), self.session):
- ret_value = _cw.call_service('test_service', msg='coucou')
- self.assertEqual('coucou', ServiceClass.passed_here.pop())
- self.assertEqual('babar', ret_value)
- with self.login('anon') as ctm:
- for _cw in (self.request(), self.session):
- with self.assertRaises(NoSelectableObject):
- _cw.call_service('test_service', msg='toto')
- self.rollback()
- self.assertEqual([], ServiceClass.passed_here)
-
-
-if __name__ == '__main__':
- from logilab.common.testlib import unittest_main
- unittest_main()
--- a/test/unittest_entity.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/unittest_entity.py Tue Jul 19 16:13:12 2016 +0200
@@ -140,13 +140,24 @@
with self.admin_access.web_request() as req:
user = req.execute('Any X WHERE X eid %(x)s', {'x':req.user.eid}).get_entity(0, 0)
adeleid = req.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
+ self.assertEqual({}, user._cw_related_cache)
req.cnx.commit()
- self.assertEqual(user._cw_related_cache, {})
+ self.assertEqual(['primary_email_subject', 'use_email_subject', 'wf_info_for_object'],
+ sorted(user._cw_related_cache))
email = user.primary_email[0]
- self.assertEqual(sorted(user._cw_related_cache), ['primary_email_subject'])
- self.assertEqual(list(email._cw_related_cache), ['primary_email_object'])
+ self.assertEqual(u'toto@logilab.org', email.address)
+ self.assertEqual(['created_by_subject',
+ 'cw_source_subject',
+ 'is_instance_of_subject',
+ 'is_subject',
+ 'owned_by_subject',
+ 'prefered_form_object',
+ 'prefered_form_subject',
+ 'primary_email_object',
+ 'use_email_object'],
+ sorted(email._cw_related_cache))
+ self.assertEqual('admin', email._cw_related_cache['primary_email_object'][1][0].login)
groups = user.in_group
- self.assertEqual(sorted(user._cw_related_cache), ['in_group_subject', 'primary_email_subject'])
for group in groups:
self.assertNotIn('in_group_subject', group._cw_related_cache)
user.cw_clear_all_caches()
@@ -223,8 +234,8 @@
user = req.user
# testing basic fetch_attrs attribute
self.assertEqual(Personne.fetch_rql(user),
- 'Any X,AA,AB,AC ORDERBY AA '
- 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X modification_date AC')
+ 'Any X,AA,AB,AC ORDERBY AB '
+ 'WHERE X is_instance_of Personne, X modification_date AA, X nom AB, X prenom AC')
# testing unknown attributes
Personne.fetch_attrs = ('bloug', 'beep')
self.assertEqual(Personne.fetch_rql(user), 'Any X WHERE X is_instance_of Personne')
@@ -236,21 +247,20 @@
# testing two non final relations
Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee')
self.assertEqual(Personne.fetch_rql(user),
- 'Any X,AA,AB,AC,AD,AE ORDERBY AA '
- 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, '
- 'X evaluee AE?')
+ 'Any X,AA,AB,AC,AD,AE ORDERBY AB '
+ 'WHERE X is_instance_of Personne, X evaluee AA?, X nom AB, X prenom AC, X travaille AD?, '
+ 'AD nom AE')
# testing one non final relation with recursion
Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
Societe.fetch_attrs = ('nom', 'evaluee')
self.assertEqual(Personne.fetch_rql(user),
- 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA,AF DESC '
- 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, '
- 'AC evaluee AE?, AE modification_date AF'
- )
+ 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA,AE DESC '
+ 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, '
+ 'AC evaluee AD?, AD modification_date AE, AC nom AF')
# testing symmetric relation
Personne.fetch_attrs = ('nom', 'connait')
- self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA '
- 'WHERE X is_instance_of Personne, X nom AA, X connait AB?')
+ self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AB '
+ 'WHERE X is_instance_of Personne, X connait AA?, X nom AB')
# testing optional relation
peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '?*'
Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
@@ -278,8 +288,8 @@
with self.admin_access.web_request() as req:
p = req.create_entity('Personne', nom=u'pouet')
self.assertEqual(p.cw_related_rql('evaluee'),
- 'Any X,AA,AB ORDERBY AA WHERE E eid %(x)s, E evaluee X, '
- 'X type AA, X modification_date AB')
+ 'Any X,AA,AB ORDERBY AB WHERE E eid %(x)s, E evaluee X, '
+ 'X modification_date AA, X type AB')
n = req.create_entity('Note')
self.assertEqual(n.cw_related_rql('evaluee', role='object',
targettypes=('Societe', 'Personne')),
@@ -297,9 +307,9 @@
'Any X,AA ORDERBY AA DESC '
'WHERE E eid %(x)s, E tags X, X modification_date AA')
self.assertEqual(tag.cw_related_rql('tags', 'subject', ('Personne',)),
- 'Any X,AA,AB ORDERBY AA '
- 'WHERE E eid %(x)s, E tags X, X is Personne, X nom AA, '
- 'X modification_date AB')
+ 'Any X,AA,AB ORDERBY AB '
+ 'WHERE E eid %(x)s, E tags X, X is Personne, X modification_date AA, '
+ 'X nom AB')
def test_related_rql_ambiguous_cant_use_fetch_order(self):
with self.admin_access.web_request() as req:
@@ -363,9 +373,9 @@
with self.admin_access.web_request() as req:
email = req.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
- self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+ self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AB '
'WHERE NOT S use_email O, O eid %(x)s, S is_instance_of CWUser, '
- 'S login AA, S firstname AB, S surname AC, S modification_date AD')
+ 'S firstname AA, S login AB, S modification_date AC, S surname AD')
req.cnx.commit()
rperms = self.schema['EmailAddress'].permissions['read']
clear_cache(self.schema['EmailAddress'], 'get_groups')
@@ -375,9 +385,9 @@
with self.new_access('anon').web_request() as req:
email = req.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
- self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+ self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AB '
'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, '
- 'S login AA, S firstname AB, S surname AC, S modification_date AD, '
+ 'S firstname AA, S login AB, S modification_date AC, S surname AD, '
'AE eid %(AF)s, EXISTS(S identity AE, NOT AE in_group AG, AG name "guests", AG is CWGroup)')
finally:
clear_cache(self.schema['EmailAddress'], 'get_groups')
@@ -388,17 +398,17 @@
with self.admin_access.web_request() as req:
email = req.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
rql = email.cw_linkable_rql('use_email', 'CWUser', 'object')[0]
- self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+ self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AB '
'WHERE O eid %(x)s, S is_instance_of CWUser, '
- 'S login AA, S firstname AB, S surname AC, S modification_date AD')
+ 'S firstname AA, S login AB, S modification_date AC, S surname AD')
def test_unrelated_rql_security_nonexistant(self):
with self.new_access('anon').web_request() as req:
email = self.vreg['etypes'].etype_class('EmailAddress')(req)
rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
- self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+ self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AB '
'WHERE S is CWUser, '
- 'S login AA, S firstname AB, S surname AC, S modification_date AD, '
+ 'S firstname AA, S login AB, S modification_date AC, S surname AD, '
'AE eid %(AF)s, EXISTS(S identity AE, NOT AE in_group AG, AG name "guests", AG is CWGroup)')
def test_unrelated_rql_constraints_creation_subject(self):
@@ -406,16 +416,16 @@
person = self.vreg['etypes'].etype_class('Personne')(req)
rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
self.assertEqual(
- rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
- 'O is_instance_of Personne, O nom AA, O prenom AB, O modification_date AC')
+ rql, 'Any O,AA,AB,AC ORDERBY AA DESC WHERE '
+ 'O is_instance_of Personne, O modification_date AA, O nom AB, O prenom AC')
def test_unrelated_rql_constraints_creation_object(self):
with self.admin_access.web_request() as req:
person = self.vreg['etypes'].etype_class('Personne')(req)
rql = person.cw_unrelated_rql('connait', 'Personne', 'object')[0]
self.assertEqual(
- rql, 'Any S,AA,AB,AC ORDERBY AC DESC WHERE '
- 'S is Personne, S nom AA, S prenom AB, S modification_date AC, '
+ rql, 'Any S,AA,AB,AC ORDERBY AA DESC WHERE '
+ 'S is Personne, S modification_date AA, S nom AB, S prenom AC, '
'NOT (S connait AD, AD nom "toto"), AD is Personne, '
'EXISTS(S travaille AE, AE nom "tutu")')
@@ -428,18 +438,18 @@
with self.admin_access.web_request() as req:
person = self.vreg['etypes'].etype_class('Personne')(req)
rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
- self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
- 'O is_instance_of Personne, O nom AA, O prenom AB, '
- 'O modification_date AC')
+ self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AA DESC WHERE '
+ 'O is_instance_of Personne, O modification_date AA, O nom AB, '
+ 'O prenom AC')
def test_unrelated_rql_constraints_edition_subject(self):
with self.admin_access.web_request() as req:
person = req.create_entity('Personne', nom=u'sylvain')
rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
self.assertEqual(
- rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
+ rql, 'Any O,AA,AB,AC ORDERBY AA DESC WHERE '
'NOT S connait O, S eid %(x)s, O is Personne, '
- 'O nom AA, O prenom AB, O modification_date AC, '
+ 'O modification_date AA, O nom AB, O prenom AC, '
'NOT S identity O')
def test_unrelated_rql_constraints_edition_object(self):
@@ -447,9 +457,9 @@
person = req.create_entity('Personne', nom=u'sylvain')
rql = person.cw_unrelated_rql('connait', 'Personne', 'object')[0]
self.assertEqual(
- rql, 'Any S,AA,AB,AC ORDERBY AC DESC WHERE '
+ rql, 'Any S,AA,AB,AC ORDERBY AA DESC WHERE '
'NOT S connait O, O eid %(x)s, S is Personne, '
- 'S nom AA, S prenom AB, S modification_date AC, '
+ 'S modification_date AA, S nom AB, S prenom AC, '
'NOT S identity O, NOT (S connait AD, AD nom "toto"), '
'EXISTS(S travaille AE, AE nom "tutu")')
@@ -634,7 +644,7 @@
def test_printable_value_bytes(self):
with self.admin_access.web_request() as req:
- e = req.create_entity('File', data=Binary('lambda x: 1'), data_format=u'text/x-python',
+ e = req.create_entity('FakeFile', data=Binary('lambda x: 1'), data_format=u'text/x-python',
data_encoding=u'ascii', data_name=u'toto.py')
from cubicweb import mttransforms
if mttransforms.HAS_PYGMENTS_TRANSFORMS:
@@ -653,7 +663,7 @@
<span style="color: #C00000;">lambda</span> <span style="color: #000000;">x</span><span style="color: #0000C0;">:</span> <span style="color: #0080C0;">1</span>
</pre>''')
- e = req.create_entity('File', data=Binary('*héhéhé*'), data_format=u'text/rest',
+ e = req.create_entity('FakeFile', data=Binary('*héhéhé*'), data_format=u'text/rest',
data_encoding=u'utf-8', data_name=u'toto.txt')
self.assertEqual(e.printable_value('data'),
u'<p><em>héhéhé</em></p>')
@@ -704,7 +714,7 @@
def test_fulltextindex(self):
with self.admin_access.web_request() as req:
- e = self.vreg['etypes'].etype_class('File')(req)
+ e = self.vreg['etypes'].etype_class('FakeFile')(req)
e.cw_attr_cache['description'] = 'du <em>html</em>'
e.cw_attr_cache['description_format'] = 'text/html'
e.cw_attr_cache['data'] = Binary('some <em>data</em>')
--- a/test/unittest_predicates.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/unittest_predicates.py Tue Jul 19 16:13:12 2016 +0200
@@ -27,7 +27,7 @@
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.predicates import (is_instance, adaptable, match_kwargs, match_user_groups,
multi_lines_rset, score_entity, is_in_state,
- rql_condition, relation_possible)
+ rql_condition, relation_possible, match_form_params)
from cubicweb.selectors import on_transition # XXX on_transition is deprecated
from cubicweb.view import EntityAdapter
from cubicweb.web import action
@@ -37,12 +37,13 @@
class ImplementsTC(CubicWebTC):
def test_etype_priority(self):
with self.admin_access.web_request() as req:
- f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
+ f = req.create_entity('FakeFile', data_name=u'hop.txt', data=Binary('hop'),
+ data_format=u'text/plain')
rset = f.as_rset()
anyscore = is_instance('Any')(f.__class__, req, rset=rset)
idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
self.assertTrue(idownscore > anyscore, (idownscore, anyscore))
- filescore = is_instance('File')(f.__class__, req, rset=rset)
+ filescore = is_instance('FakeFile')(f.__class__, req, rset=rset)
self.assertTrue(filescore > idownscore, (filescore, idownscore))
def test_etype_inheritance_no_yams_inheritance(self):
@@ -391,6 +392,102 @@
rset = req.execute('Any X WHERE X is IN(CWGroup, CWUser)')
self.assertTrue(selector(None, req, rset=rset))
+
+class MatchFormParamsTC(CubicWebTC):
+ """tests for match_form_params predicate"""
+
+ def test_keyonly_match(self):
+ """test standard usage: ``match_form_params('param1', 'param2')``
+
+ ``param1`` and ``param2`` must be specified in request's form.
+ """
+ web_request = self.admin_access.web_request
+ vid_selector = match_form_params('vid')
+ vid_subvid_selector = match_form_params('vid', 'subvid')
+ # no parameter => KO,KO
+ with web_request() as req:
+ self.assertEqual(vid_selector(None, req), 0)
+ self.assertEqual(vid_subvid_selector(None, req), 0)
+ # one expected parameter found => OK,KO
+ with web_request(vid='foo') as req:
+ self.assertEqual(vid_selector(None, req), 1)
+ self.assertEqual(vid_subvid_selector(None, req), 0)
+ # all expected parameters found => OK,OK
+ with web_request(vid='foo', subvid='bar') as req:
+ self.assertEqual(vid_selector(None, req), 1)
+ self.assertEqual(vid_subvid_selector(None, req), 2)
+
+ def test_keyvalue_match_one_parameter(self):
+ """test dict usage: ``match_form_params(param1=value1)``
+
+ ``param1`` must be specified in the request's form and its value
+ must be ``value1``.
+ """
+ web_request = self.admin_access.web_request
+ # test both positional and named parameters
+ vid_selector = match_form_params(vid='foo')
+ # no parameter => should fail
+ with web_request() as req:
+ self.assertEqual(vid_selector(None, req), 0)
+ # expected parameter found with expected value => OK
+ with web_request(vid='foo', subvid='bar') as req:
+ self.assertEqual(vid_selector(None, req), 1)
+ # expected parameter found but value is incorrect => KO
+ with web_request(vid='bar') as req:
+ self.assertEqual(vid_selector(None, req), 0)
+
+ def test_keyvalue_match_two_parameters(self):
+ """test dict usage: ``match_form_params(param1=value1, param2=value2)``
+
+ ``param1`` and ``param2`` must be specified in the request's form and
+ their respective value must be ``value1`` and ``value2``.
+ """
+ web_request = self.admin_access.web_request
+ vid_subvid_selector = match_form_params(vid='list', subvid='tsearch')
+ # missing one expected parameter => KO
+ with web_request(vid='list') as req:
+ self.assertEqual(vid_subvid_selector(None, req), 0)
+ # expected parameters found but values are incorrect => KO
+ with web_request(vid='list', subvid='foo') as req:
+ self.assertEqual(vid_subvid_selector(None, req), 0)
+ # expected parameters found and values are correct => OK
+ with web_request(vid='list', subvid='tsearch') as req:
+ self.assertEqual(vid_subvid_selector(None, req), 2)
+
+ def test_keyvalue_multiple_match(self):
+ """test dict usage with multiple values
+
+ i.e. as in ``match_form_params(param1=('value1', 'value2'))``
+
+ ``param1`` must be specified in the request's form and its value
+ must be either ``value1`` or ``value2``.
+ """
+ web_request = self.admin_access.web_request
+ vid_subvid_selector = match_form_params(vid='list', subvid=('tsearch', 'listitem'))
+ # expected parameters found and values correct => OK
+ with web_request(vid='list', subvid='tsearch') as req:
+ self.assertEqual(vid_subvid_selector(None, req), 2)
+ with web_request(vid='list', subvid='listitem') as req:
+ self.assertEqual(vid_subvid_selector(None, req), 2)
+ # expected parameters found but values are incorrect => OK
+ with web_request(vid='list', subvid='foo') as req:
+ self.assertEqual(vid_subvid_selector(None, req), 0)
+
+ def test_invalid_calls(self):
+ """checks invalid calls raise a ValueError"""
+ # mixing named and positional arguments should fail
+ with self.assertRaises(ValueError) as cm:
+ match_form_params('list', x='1', y='2')
+ self.assertEqual(str(cm.exception),
+ "match_form_params() can't be called with both "
+ "positional and named arguments")
+ # using a dict as first and unique argument should fail
+ with self.assertRaises(ValueError) as cm:
+ match_form_params({'x': 1})
+ self.assertEqual(str(cm.exception),
+ "match_form_params() positional arguments must be strings")
+
+
if __name__ == '__main__':
unittest_main()
--- a/test/unittest_repoapi.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/unittest_repoapi.py Tue Jul 19 16:13:12 2016 +0200
@@ -15,18 +15,18 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""unittest for cubicweb.dbapi"""
+"""unittest for cubicweb.repoapi"""
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb import ProgrammingError
-from cubicweb.repoapi import ClientConnection, connect, anonymous_cnx
+from cubicweb.repoapi import Connection, connect, anonymous_cnx
class REPOAPITC(CubicWebTC):
- def test_clt_cnx_basic_usage(self):
+ def test_cnx_basic_usage(self):
"""Test that a client connection can be used to access the database"""
with self.admin_access.client_cnx() as cltcnx:
# (1) some RQL request
@@ -52,11 +52,11 @@
''')
self.assertTrue(rset)
- def test_clt_cnx_life_cycle(self):
+ def test_cnx_life_cycle(self):
"""Check that ClientConnection requires explicit open and close
"""
access = self.admin_access
- cltcnx = ClientConnection(access._session)
+ cltcnx = Connection(access._session)
# connection not open yet
with self.assertRaises(ProgrammingError):
cltcnx.execute('Any X WHERE X is CWUser')
@@ -69,18 +69,18 @@
def test_connect(self):
"""check that repoapi.connect works and returns a usable connection"""
- clt_cnx = connect(self.repo, login='admin', password='gingkow')
- self.assertEqual('admin', clt_cnx.user.login)
- with clt_cnx:
- rset = clt_cnx.execute('Any X WHERE X is CWUser')
+ cnx = connect(self.repo, login='admin', password='gingkow')
+ self.assertEqual('admin', cnx.user.login)
+ with cnx:
+ rset = cnx.execute('Any X WHERE X is CWUser')
self.assertTrue(rset)
def test_anonymous_connect(self):
"""check that you can get anonymous connection when the data exist"""
- clt_cnx = anonymous_cnx(self.repo)
- self.assertEqual('anon', clt_cnx.user.login)
- with clt_cnx:
- rset = clt_cnx.execute('Any X WHERE X is CWUser')
+ cnx = anonymous_cnx(self.repo)
+ self.assertEqual('anon', cnx.user.login)
+ with cnx:
+ rset = cnx.execute('Any X WHERE X is CWUser')
self.assertTrue(rset)
--- a/test/unittest_schema.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/unittest_schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -169,7 +169,7 @@
'CWRelation', 'CWPermission', 'CWProperty', 'CWRType',
'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig',
'CWUniqueTogetherConstraint', 'CWUser',
- 'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
+ 'ExternalUri', 'FakeFile', 'Float', 'Int', 'Interval', 'Note',
'Password', 'Personne', 'Produit',
'RQLExpression', 'Reference',
'Service', 'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint',
@@ -221,8 +221,6 @@
'value',
'wf_info_for', 'wikiid', 'workflow_of', 'tr_count']
- if config.cube_version('file') >= (1, 14, 0):
- expected_relations.append('data_sha1hex')
self.assertListEqual(sorted(expected_relations), relations)
@@ -360,8 +358,8 @@
schema['produces_and_buys'].rdefs.keys())
self.assertEqual([('Person','Service')],
schema['produces_and_buys2'].rdefs.keys())
- self.assertEqual([('Company', 'Service'), ('Person', 'Service')],
- schema['reproduce'].rdefs.keys())
+ self.assertCountEqual([('Company', 'Service'), ('Person', 'Service')],
+ schema['reproduce'].rdefs.keys())
# check relation definitions are marked infered
rdef = schema['produces_and_buys'].rdefs[('Person','Service')]
self.assertTrue(rdef.infered)
@@ -419,6 +417,10 @@
self._test('rrqlexpr_on_attr.py',
"can't use RRQLExpression on attribute ToTo.attr[String], use an ERQLExpression")
+ def test_rqlexpr_on_computedrel(self):
+ self._test('rqlexpr_on_computedrel.py',
+ "can't use rql expression for read permission of relation Subject computed Object")
+
class NormalizeExpressionTC(TestCase):
@@ -484,6 +486,7 @@
('cw_schema', 'CWSourceSchemaConfig', 'CWRelation', 'object'),
('delete_permission', 'CWRelation', 'RQLExpression', 'subject'),
('read_permission', 'CWRelation', 'RQLExpression', 'subject')],
+ 'CWComputedRType': [('read_permission', 'CWComputedRType', 'RQLExpression', 'subject')],
'CWSource': [('cw_for_source', 'CWSourceSchemaConfig', 'CWSource', 'object'),
('cw_host_config_of', 'CWSourceHostConfig', 'CWSource', 'object'),
('cw_import_of', 'CWDataImport', 'CWSource', 'object'),
@@ -510,7 +513,7 @@
('cw_source', 'Card', 'CWSource', 'object'),
('cw_source', 'EmailAddress', 'CWSource', 'object'),
('cw_source', 'ExternalUri', 'CWSource', 'object'),
- ('cw_source', 'File', 'CWSource', 'object'),
+ ('cw_source', 'FakeFile', 'CWSource', 'object'),
('cw_source', 'Note', 'CWSource', 'object'),
('cw_source', 'Personne', 'CWSource', 'object'),
('cw_source', 'Produit', 'CWSource', 'object'),
--- a/test/unittest_utils.py Tue Jul 19 15:59:02 2016 +0200
+++ b/test/unittest_utils.py Tue Jul 19 16:13:12 2016 +0200
@@ -58,12 +58,6 @@
parse_repo_uri('myapp'))
self.assertEqual(('inmemory', None, 'myapp'),
parse_repo_uri('inmemory://myapp'))
- self.assertEqual(('pyro', 'pyro-ns-host:pyro-ns-port', '/myapp'),
- parse_repo_uri('pyro://pyro-ns-host:pyro-ns-port/myapp'))
- self.assertEqual(('pyroloc', 'host:port', '/appkey'),
- parse_repo_uri('pyroloc://host:port/appkey'))
- self.assertEqual(('zmqpickle-tcp', '127.0.0.1:666', ''),
- parse_repo_uri('zmqpickle-tcp://127.0.0.1:666'))
with self.assertRaises(NotImplementedError):
parse_repo_uri('foo://bar')
--- a/toolsutils.py Tue Jul 19 15:59:02 2016 +0200
+++ b/toolsutils.py Tue Jul 19 16:13:12 2016 +0200
@@ -257,18 +257,6 @@
}),
)
-def config_connect(appid, optconfig):
- from cubicweb.dbapi import connect
- from getpass import getpass
- user = optconfig.user
- if not user:
- user = raw_input('login: ')
- password = optconfig.password
- if not password:
- password = getpass('password: ')
- return connect(login=user, password=password, host=optconfig.host, database=appid)
-
-
## cwshell helpers #############################################################
class AbstractMatcher(object):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tox.ini Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,55 @@
+[tox]
+env = py27
+
+[testenv]
+sitepackages = True
+commands = pytest -t {envname}/test {posargs}
+
+[testenv:cubicweb]
+deps =
+ -r{toxinidir}/test/requirements.txt
+commands = pytest -t test {posargs}
+
+[testenv:dataimport]
+
+[testenv:devtools]
+deps =
+ -r{toxinidir}/devtools/test/requirements.txt
+
+[testenv:entities]
+deps =
+ -r{toxinidir}/entities/test/requirements.txt
+
+[testenv:etwist]
+deps =
+ -r{toxinidir}/etwist/test/requirements.txt
+
+[testenv:ext]
+deps =
+ -r{toxinidir}/ext/test/requirements.txt
+
+[testenv:hooks]
+
+[testenv:server]
+deps =
+ -r{toxinidir}/server/test/requirements.txt
+
+[testenv:sobjects]
+deps =
+ -r{toxinidir}/sobjects/test/requirements.txt
+
+[testenv:web]
+deps =
+ -r{toxinidir}/web/test/requirements.txt
+
+[testenv:wsgi]
+deps =
+ -r{toxinidir}/wsgi/test/requirements.txt
+
+[testenv:doc]
+changedir = doc
+whitelist_externals =
+ sphinx-build
+deps =
+ sphinx
+commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
--- a/transaction.py Tue Jul 19 15:59:02 2016 +0200
+++ b/transaction.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,13 +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/>.
-"""undoable transaction objects.
-
-
-This module is in the cubicweb package and not in cubicweb.server because those
-objects should be accessible to client through pyro, where the cubicweb.server
-package may not be installed.
-"""
+""" undoable transaction objects. """
__docformat__ = "restructuredtext en"
_ = unicode
@@ -42,27 +36,21 @@
msg = _("there is no transaction #%s")
def __init__(self, txuuid):
- super(RepositoryError, self).__init__(txuuid)
+ super(NoSuchTransaction, self).__init__(txuuid)
self.txuuid = txuuid
class Transaction(object):
"""an undoable transaction"""
- def __init__(self, uuid, time, ueid):
+ def __init__(self, cnx, uuid, time, ueid):
+ self.cnx = cnx
self.uuid = uuid
self.datetime = time
self.user_eid = ueid
- # should be set by the dbapi connection
- self.req = None # old style
- self.cnx = None # new style
def _execute(self, *args, **kwargs):
"""execute a query using either the req or the cnx"""
- if self.req is None:
- execute = self.cnx.execute
- else:
- execute = self.req
- return execute(*args, **kwargs)
+ return self.cnx.execute(*args, **kwargs)
def __repr__(self):
@@ -73,8 +61,7 @@
"""return the user entity which has done the transaction,
none if not found.
"""
- return self._execute('Any X WHERE X eid %(x)s',
- {'x': self.user_eid}).get_entity(0, 0)
+ return self.cnx.find('CWUser', eid=self.user_eid).one()
def actions_list(self, public=True):
"""return an ordered list of action effectued during that transaction
@@ -82,14 +69,11 @@
if public is true, return only 'public' action, eg not ones triggered
under the cover by hooks.
"""
- if self.req is not None:
- cnx = self.req.cnx
- else:
- cnx = self.cnx
- return cnx.transaction_actions(self.uuid, public)
+ return self.cnx.transaction_actions(self.uuid, public)
class AbstractAction(object):
+
def __init__(self, action, public, order):
self.action = action
self.public = public
@@ -106,8 +90,9 @@
class EntityAction(AbstractAction):
+
def __init__(self, action, public, order, etype, eid, changes):
- AbstractAction.__init__(self, action, public, order)
+ super(EntityAction, self).__init__(action, public, order)
self.etype = etype
self.eid = eid
self.changes = changes
@@ -124,8 +109,9 @@
class RelationAction(AbstractAction):
+
def __init__(self, action, public, order, rtype, eidfrom, eidto):
- AbstractAction.__init__(self, action, public, order)
+ super(RelationAction, self).__init__(action, public, order)
self.rtype = rtype
self.eid_from = eidfrom
self.eid_to = eidto
--- a/utils.py Tue Jul 19 15:59:02 2016 +0200
+++ b/utils.py Tue Jul 19 16:13:12 2016 +0200
@@ -21,7 +21,6 @@
__docformat__ = "restructuredtext en"
-import sys
import decimal
import datetime
import random
@@ -553,8 +552,12 @@
"""
def _dict2js(d, predictable=False):
+ if predictable:
+ it = sorted(d.iteritems())
+ else:
+ it = d.iteritems()
res = [key + ': ' + js_dumps(val, predictable)
- for key, val in d.iteritems()]
+ for key, val in it]
return '{%s}' % ', '.join(res)
def _list2js(l, predictable=False):
@@ -578,7 +581,7 @@
return _list2js(something, predictable)
if isinstance(something, JSString):
return something
- return json_dumps(something)
+ return json_dumps(something, sort_keys=predictable)
PERCENT_IN_URLQUOTE_RE = re.compile(r'%(?=[0-9a-fA-F]{2})')
def js_href(javascript_code):
@@ -608,8 +611,6 @@
""" transform a command line uri into a (protocol, hostport, appid), e.g:
<myapp> -> 'inmemory', None, '<myapp>'
inmemory://<myapp> -> 'inmemory', None, '<myapp>'
- pyro://[host][:port] -> 'pyro', 'host:port', None
- zmqpickle://[host][:port] -> 'zmqpickle', 'host:port', None
"""
parseduri = urlparse(uri)
scheme = parseduri.scheme
@@ -617,8 +618,6 @@
return ('inmemory', None, parseduri.path)
if scheme == 'inmemory':
return (scheme, None, parseduri.netloc)
- if scheme in ('pyro', 'pyroloc') or scheme.startswith('zmqpickle-'):
- return (scheme, parseduri.netloc, parseduri.path)
raise NotImplementedError('URI protocol not implemented for `%s`' % uri)
@@ -647,7 +646,7 @@
Occasional elements can be buggy requests (server-side) or
end-user (web-ui provided) requests. These have to be cleaned up
- when they fill the cache, without evicting the usefull, frequently
+ when they fill the cache, without evicting the useful, frequently
used entries.
"""
# quite arbitrary, but we want to never
--- a/view.py Tue Jul 19 15:59:02 2016 +0200
+++ b/view.py Tue Jul 19 16:13:12 2016 +0200
@@ -20,7 +20,7 @@
__docformat__ = "restructuredtext en"
_ = unicode
-from cStringIO import StringIO
+from io import BytesIO
from warnings import warn
from functools import partial
@@ -101,7 +101,7 @@
return
if w is None:
if self.binary:
- self._stream = stream = StringIO()
+ self._stream = stream = BytesIO()
else:
self._stream = stream = UStringIO()
w = stream.write
@@ -471,7 +471,7 @@
return
if w is None:
if self.binary:
- self._stream = stream = StringIO()
+ self._stream = stream = BytesIO()
else:
self._stream = stream = HTMLStream(self._cw)
w = stream.write
--- a/web/application.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/application.py Tue Jul 19 16:13:12 2016 +0200
@@ -52,103 +52,14 @@
@contextmanager
def anonymized_request(req):
orig_cnx = req.cnx
- anon_clt_cnx = anonymous_cnx(orig_cnx._session.repo)
- req.set_cnx(anon_clt_cnx)
+ anon_cnx = anonymous_cnx(orig_cnx.session.repo)
+ req.set_cnx(anon_cnx)
try:
- with anon_clt_cnx:
+ with anon_cnx:
yield req
finally:
req.set_cnx(orig_cnx)
-class AbstractSessionManager(component.Component):
- """manage session data associated to a session identifier"""
- __regid__ = 'sessionmanager'
-
- def __init__(self, repo):
- vreg = repo.vreg
- self.session_time = vreg.config['http-session-time'] or None
- self.authmanager = vreg['components'].select('authmanager', repo=repo)
- interval = (self.session_time or 0) / 2.
- if vreg.config.anonymous_user()[0] is not None:
- self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60
- assert self.cleanup_anon_session_time > 0
- if self.session_time is not None:
- self.cleanup_anon_session_time = min(self.session_time,
- self.cleanup_anon_session_time)
- interval = self.cleanup_anon_session_time / 2.
- # we don't want to check session more than once every 5 minutes
- self.clean_sessions_interval = max(5 * 60, interval)
-
- def clean_sessions(self):
- """cleanup sessions which has not been unused since a given amount of
- time. Return the number of sessions which have been closed.
- """
- self.debug('cleaning http sessions')
- session_time = self.session_time
- closed, total = 0, 0
- for session in self.current_sessions():
- total += 1
- last_usage_time = session.mtime
- no_use_time = (time() - last_usage_time)
- if session.anonymous_session:
- if no_use_time >= self.cleanup_anon_session_time:
- self.close_session(session)
- closed += 1
- elif session_time is not None and no_use_time >= session_time:
- self.close_session(session)
- closed += 1
- return closed, total - closed
-
- def current_sessions(self):
- """return currently open sessions"""
- raise NotImplementedError()
-
- def get_session(self, req, sessionid):
- """return existing session for the given session identifier"""
- raise NotImplementedError()
-
- def open_session(self, req):
- """open and return a new session for the given request.
-
- raise :exc:`cubicweb.AuthenticationError` if authentication failed
- (no authentication info found or wrong user/password)
- """
- raise NotImplementedError()
-
- def close_session(self, session):
- """close session on logout or on invalid session detected (expired out,
- corrupted...)
- """
- raise NotImplementedError()
-
-
-class AbstractAuthenticationManager(component.Component):
- """authenticate user associated to a request and check session validity"""
- __regid__ = 'authmanager'
-
- def __init__(self, repo):
- self.vreg = repo.vreg
-
- 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).
-
- raise :exc:`InvalidSession` if session is corrupted for a reason or
- another and should be closed
- """
- raise NotImplementedError()
-
- 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.
-
- raise :exc:`cubicweb.AuthenticationError` if authentication failed
- (no authentication info found or wrong user/password)
- """
- raise NotImplementedError()
class CookieSessionHandler(object):
@@ -350,7 +261,7 @@
try:
session = self.get_session(req)
from cubicweb import repoapi
- cnx = repoapi.ClientConnection(session)
+ cnx = repoapi.Connection(session)
req.set_cnx(cnx)
except AuthenticationError:
# Keep the dummy session set at initialisation.
@@ -365,12 +276,6 @@
# several cubes like registration or forgotten password rely on
# this principle.
- # DENY https acces for anonymous_user
- if (req.https
- and req.session.anonymous_session
- and self.vreg.config['https-deny-anonymous']):
- # don't allow anonymous on https connection
- raise AuthenticationError()
# nested try to allow LogOut to delegate logic to AuthenticationError
# handler
try:
--- a/web/component.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/component.py Tue Jul 19 16:13:12 2016 +0200
@@ -353,7 +353,7 @@
has some content to display. If not, you can still raise
:exc:`EmptyComponent` to inform it should be skipped.
- Also, :exc:`Unauthorized` will be catched, logged, then the component
+ Also, :exc:`Unauthorized` will be caught, logged, then the component
will be skipped.
"""
self.items = []
--- a/web/data/cubicweb.ajax.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/data/cubicweb.ajax.js Tue Jul 19 16:13:12 2016 +0200
@@ -39,7 +39,7 @@
},
addCallback: function(callback) {
- if ((this._req.readyState == 4) && this._result) {
+ if (this._req && (this._req.readyState == 4) && this._result) {
var args = [this._result, this._req];
jQuery.merge(args, cw.utils.sliceList(arguments, 1));
callback.apply(null, args);
@@ -51,7 +51,7 @@
},
addErrback: function(callback) {
- if (this._req.readyState == 4 && this._error) {
+ if (this._req && this._req.readyState == 4 && this._error) {
callback.apply(null, [this._error, this._req]);
}
else {
@@ -275,9 +275,6 @@
if (typeof roundedCorners != 'undefined') {
roundedCorners(node);
}
- if (typeof setFormsTarget != 'undefined') {
- setFormsTarget(node);
- }
_loadDynamicFragments(node);
jQuery(cw).trigger('server-response', [true, node]);
jQuery(node).trigger('server-response', [true, node]);
@@ -503,7 +500,7 @@
function unloadPageData() {
// NOTE: do not make async calls on unload if you want to avoid
// strange bugs
- loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'GET', true);
+ loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'POST', true);
}
function removeBookmark(beid) {
@@ -518,59 +515,6 @@
});
}
-userCallback = cw.utils.deprecatedFunction(
- '[3.19] use a plain ajaxfunc instead of user callbacks',
- function userCallback(cbname) {
- setProgressCursor();
- var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('user_callback', null, cbname));
- d.addCallback(resetCursor);
- d.addErrback(resetCursor);
- d.addErrback(remoteCallFailed);
- return d;
-});
-
-userCallbackThenUpdateUI = cw.utils.deprecatedFunction(
- '[3.19] use a plain ajaxfunc instead of user callbacks',
- function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
- var d = userCallback(cbname);
- d.addCallback(function() {
- $('#' + nodeid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {'rql': rql},
- registry, compid), null, 'swap');
- if (msg) {
- updateMessage(msg);
- }
- });
-});
-
-userCallbackThenReloadPage = cw.utils.deprecatedFunction(
- '[3.19] use a plain ajaxfunc instead of user callbacks',
- function userCallbackThenReloadPage(cbname, msg) {
- var d = userCallback(cbname);
- d.addCallback(function() {
- window.location.reload();
- if (msg) {
- updateMessage(msg);
- }
- });
-});
-
-/**
- * .. function:: unregisterUserCallback(cbname)
- *
- * unregisters the python function registered on the server's side
- * while the page was generated.
- */
-unregisterUserCallback = cw.utils.deprecatedFunction(
- '[3.19] use a plain ajaxfunc instead of user callbacks',
- function unregisterUserCallback(cbname) {
- setProgressCursor();
- var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unregister_user_callback',
- null, cbname));
- d.addCallback(resetCursor);
- d.addErrback(resetCursor);
- d.addErrback(remoteCallFailed);
-});
-
//============= XXX move those functions? ====================================//
function openHash() {
--- a/web/data/cubicweb.edition.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/data/cubicweb.edition.js Tue Jul 19 16:13:12 2016 +0200
@@ -171,7 +171,7 @@
var entityForm = jQuery('#entityForm');
var oid = optionNode.id.substring(2); // option id is prefixed by "id"
loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null,
- [oid.split(':')]), 'GET', true);
+ [oid.split(':')]), 'POST', true);
var selectNode = optionNode.parentNode;
// remove option node
selectNode.removeChild(optionNode);
@@ -537,53 +537,6 @@
}
/**
- * .. function:: setFormsTarget(node)
- *
- * called on load to set target and iframeso object.
- *
- * .. note::
- *
- * This was a hack to make form loop handling XHTML compliant.
- * Since we do not care about xhtml any longer, this may go away.
- *
- * .. note::
- *
- * `object` nodes might be a potential replacement for iframes
- *
- * .. note::
- *
- * The form's `target` attribute should probably become a simple data-target
- * immediately generated server-side.
- * Since we don't do xhtml any longer, the iframe should probably be either
- * reconsidered or at least emitted server-side.
- */
-function setFormsTarget(node) {
- var $node = jQuery(node || document.body);
- $node.find('form').each(function() {
- var form = jQuery(this);
- var target = form.attr('cubicweb:target');
- if (target) {
- form.attr('target', target);
- /* do not use display: none because some browsers ignore iframe
- * with no display */
- form.append(IFRAME({
- name: target,
- id: target,
- src: 'javascript: void(0)',
- width: '0px',
- height: '0px'
- }));
- form.removeAttr('cubicweb:target'); // useles from now on, pop it
- // to make IE9 happy
- }
- });
-}
-
-jQuery(document).ready(function() {
- setFormsTarget();
-});
-
-/**
* .. function:: validateForm(formid, action, onsuccess, onfailure)
*
* called on traditionnal form submission : the idea is to try
--- a/web/data/cubicweb.htmlhelpers.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/data/cubicweb.htmlhelpers.js Tue Jul 19 16:13:12 2016 +0200
@@ -39,7 +39,7 @@
*/
function resetCursor(result) {
var body = document.getElementsByTagName('body')[0];
- body.style.cursor = 'default';
+ body.style.cursor = '';
// pass result to next callback in the callback chain
return result;
}
--- a/web/data/cubicweb.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/data/cubicweb.js Tue Jul 19 16:13:12 2016 +0200
@@ -16,7 +16,7 @@
detachEvent: function() {},
log: function log() {
- if (typeof(window) != "undefined" && window.console && window.console.log) {
+ if (typeof window !== "undefined" && window.console && window.console.log) {
// NOTE console.log requires "console" to be the console to be "this"
window.console.log.apply(console, arguments);
}
@@ -33,7 +33,7 @@
* safe version of jQuery('#nodeid') because we use ':' in nodeids
* which messes with jQuery selection mechanism
*/
- if (typeof(node) == 'string') {
+ if (typeof node === 'string') {
node = document.getElementById(node);
}
if (node) {
@@ -44,7 +44,7 @@
// escapes string selectors (e.g. "foo.[subject]:42" -> "foo\.\[subject\]\:42"
escape: function(selector) {
- if (typeof(selector) == 'string') {
+ if (typeof selector === 'string') {
return selector.replace( /(:|\.|\[|\])/g, "\\$1" );
}
// cw.log('non string selector', selector);
@@ -52,7 +52,7 @@
},
getNode: function (node) {
- if (typeof(node) == 'string') {
+ if (typeof node === 'string') {
return document.getElementById(node);
}
return node;
@@ -69,7 +69,7 @@
},
urlEncode: function (str) {
- if (typeof(encodeURIComponent) != "undefined") {
+ if (typeof encodeURIComponent !== "undefined") {
return encodeURIComponent(str).replace(/\'/g, '%27');
} else {
return escape(str).replace(/\+/g, '%2B').replace(/\"/g, '%22').
@@ -93,7 +93,7 @@
var $node = $(node);
var sortvalue = $node.attr('cubicweb:sortvalue');
// No metadata found, use cell content as sort key
- if (sortvalue === undefined) {
+ if (typeof sortvalue === 'undefined') {
return $node.text();
}
return cw.evalJSON(sortvalue);
@@ -117,9 +117,9 @@
var node = document.createElement(tag);
for (key in params) {
var value = params[key];
- if (key.substring(0, 2) == 'on') {
+ if (key.substring(0, 2) === 'on') {
// this is an event handler definition
- if (typeof value == 'string') {
+ if (typeof value === 'string') {
// litteral definition
value = new Function(value);
}
@@ -142,7 +142,7 @@
}
for (var i = 0; i < children.length; i++) {
var child = children[i];
- if (typeof child == "string" || typeof child == "number") {
+ if (typeof child === "string" || typeof child === "number") {
child = document.createTextNode(child);
}
node.appendChild(child);
@@ -158,7 +158,7 @@
*
*/
toISOTimestamp: function (date) {
- if (typeof(date) == "undefined" || date === null) {
+ if (date == null) {
return null;
}
@@ -188,11 +188,11 @@
},
isArray: function (it) { // taken from dojo
- return it && (it instanceof Array || typeof it == "array");
+ return it && (it instanceof Array || typeof it === "array");
},
isString: function (it) { // taken from dojo
- return !!arguments.length && it != null && (typeof it == "string" || it instanceof String);
+ return !!arguments.length && it != null && (typeof it === "string" || it instanceof String);
},
isArrayLike: function (it) { // taken from dojo
@@ -405,11 +405,11 @@
var node = document.createElement('iframe');
}
for (key in params) {
- if (key != 'name') {
+ if (key !== 'name') {
var value = params[key];
- if (key.substring(0, 2) == 'on') {
+ if (key.substring(0, 2) === 'on') {
// this is an event handler definition
- if (typeof value == 'string') {
+ if (typeof value === 'string') {
// litteral definition
value = new Function(value);
}
--- a/web/data/cubicweb.preferences.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/data/cubicweb.preferences.js Tue Jul 19 16:13:12 2016 +0200
@@ -45,7 +45,7 @@
function validatePrefsForm(formid) {
clearPreviousMessages();
_clearPreviousErrors(formid);
- return validateForm(formid, null, submitSucces, submitFailure);
+ return validateForm(formid, null, submitSuccess, submitFailure);
}
function submitFailure(result, formid, cbargs) {
@@ -59,13 +59,13 @@
return false; // so handleFormValidationResponse doesn't try to display error
}
-function submitSucces(result, formid, cbargs) {
+function submitSuccess(result, formid, cbargs) {
var $form = jQuery('#' + formid);
setCurrentValues($form);
var dom = DIV({'class': 'msg'}, _("changes applied"));
$form.find('div.formsg').empty().append(dom);
$form.find('input').removeClass('changed');
- checkValues(form, true);
+ checkValues($form, true);
return;
}
--- a/web/data/cubicweb.timeline-bundle.js Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,10129 +0,0 @@
-/**
- * This file contains timeline utilities
- * :organization: Logilab
- */
-
-var SimileAjax_urlPrefix = BASE_URL + 'data/';
-var Timeline_urlPrefix = BASE_URL + 'data/';
-
-/*
- * Simile Ajax API
- *
- * Include this file in your HTML file as follows::
- *
- * <script src="http://simile.mit.edu/ajax/api/simile-ajax-api.js" type="text/javascript"></script>
- *
- *
- */
-
-if (typeof SimileAjax == "undefined") {
- var SimileAjax = {
- loaded: false,
- loadingScriptsCount: 0,
- error: null,
- params: { bundle:"true" }
- };
-
- SimileAjax.Platform = new Object();
- /*
- HACK: We need these 2 things here because we cannot simply append
- a <script> element containing code that accesses SimileAjax.Platform
- to initialize it because IE executes that <script> code first
- before it loads ajax.js and platform.js.
- */
-
- var getHead = function(doc) {
- return doc.getElementsByTagName("head")[0];
- };
-
- SimileAjax.findScript = function(doc, substring) {
- var heads = doc.documentElement.getElementsByTagName("head");
- for (var h = 0; h < heads.length; h++) {
- var node = heads[h].firstChild;
- while (node != null) {
- if (node.nodeType == 1 && node.tagName.toLowerCase() == "script") {
- var url = node.src;
- var i = url.indexOf(substring);
- if (i >= 0) {
- return url;
- }
- }
- node = node.nextSibling;
- }
- }
- return null;
- };
- SimileAjax.includeJavascriptFile = function(doc, url, onerror, charset) {
- onerror = onerror || "";
- if (doc.body == null) {
- try {
- var q = "'" + onerror.replace( /'/g, '&apos' ) + "'"; // "
- doc.write("<script src='" + url + "' onerror="+ q +
- (charset ? " charset='"+ charset +"'" : "") +
- " type='text/javascript'>"+ onerror + "</script>");
- return;
- } catch (e) {
- // fall through
- }
- }
-
- var script = doc.createElement("script");
- if (onerror) {
- try { script.innerHTML = onerror; } catch(e) {}
- script.setAttribute("onerror", onerror);
- }
- if (charset) {
- script.setAttribute("charset", charset);
- }
- script.type = "text/javascript";
- script.language = "JavaScript";
- script.src = url;
- return getHead(doc).appendChild(script);
- };
- SimileAjax.includeJavascriptFiles = function(doc, urlPrefix, filenames) {
- for (var i = 0; i < filenames.length; i++) {
- SimileAjax.includeJavascriptFile(doc, urlPrefix + filenames[i]);
- }
- SimileAjax.loadingScriptsCount += filenames.length;
- // XXX adim SimileAjax.includeJavascriptFile(doc, SimileAjax.urlPrefix + "scripts/signal.js?" + filenames.length);
- };
- SimileAjax.includeCssFile = function(doc, url) {
- if (doc.body == null) {
- try {
- doc.write("<link rel='stylesheet' href='" + url + "' type='text/css'/>");
- return;
- } catch (e) {
- // fall through
- }
- }
-
- var link = doc.createElement("link");
- link.setAttribute("rel", "stylesheet");
- link.setAttribute("type", "text/css");
- link.setAttribute("href", url);
- getHead(doc).appendChild(link);
- };
- SimileAjax.includeCssFiles = function(doc, urlPrefix, filenames) {
- for (var i = 0; i < filenames.length; i++) {
- SimileAjax.includeCssFile(doc, urlPrefix + filenames[i]);
- }
- };
-
- /**
- * Append into urls each string in suffixes after prefixing it with urlPrefix.
- * @param {Array} urls
- * @param {String} urlPrefix
- * @param {Array} suffixes
- */
- SimileAjax.prefixURLs = function(urls, urlPrefix, suffixes) {
- for (var i = 0; i < suffixes.length; i++) {
- urls.push(urlPrefix + suffixes[i]);
- }
- };
-
- /**
- * Parse out the query parameters from a URL
- * @param {String} url the url to parse, or location.href if undefined
- * @param {Object} to optional object to extend with the parameters
- * @param {Object} types optional object mapping keys to value types
- * (String, Number, Boolean or Array, String by default)
- * @return a key/value Object whose keys are the query parameter names
- * @type Object
- */
- SimileAjax.parseURLParameters = function(url, to, types) {
- to = to || {};
- types = types || {};
-
- if (typeof url == "undefined") {
- url = location.href;
- }
- var q = url.indexOf("?");
- if (q < 0) {
- return to;
- }
- url = (url+"#").slice(q+1, url.indexOf("#")); // toss the URL fragment
-
- var params = url.split("&"), param, parsed = {};
- var decode = window.decodeURIComponent || unescape;
- for (var i = 0; param = params[i]; i++) {
- var eq = param.indexOf("=");
- var name = decode(param.slice(0,eq));
- var old = parsed[name];
- if (typeof old == "undefined") {
- old = [];
- } else if (!(old instanceof Array)) {
- old = [old];
- }
- parsed[name] = old.concat(decode(param.slice(eq+1)));
- }
- for (var i in parsed) {
- if (!parsed.hasOwnProperty(i)) continue;
- var type = types[i] || String;
- var data = parsed[i];
- if (!(data instanceof Array)) {
- data = [data];
- }
- if (type === Boolean && data[0] == "false") {
- to[i] = false; // because Boolean("false") === true
- } else {
- to[i] = type.apply(this, data);
- }
- }
- return to;
- };
-
- (function() {
- var javascriptFiles = [
- "jquery-1.2.6.js",
- "platform.js",
- "debug.js",
- "xmlhttp.js",
- "json.js",
- "dom.js",
- "graphics.js",
- "date-time.js",
- "string.js",
- "html.js",
- "data-structure.js",
- "units.js",
-
- "ajax.js",
- "history.js",
- "window-manager.js"
- ];
- var cssFiles = [
- "graphics.css"
- ];
-
- if (typeof SimileAjax_urlPrefix == "string") {
- SimileAjax.urlPrefix = SimileAjax_urlPrefix;
- } else {
- var url = SimileAjax.findScript(document, "simile-ajax-api.js");
- if (url == null) {
- SimileAjax.error = new Error("Failed to derive URL prefix for Simile Ajax API code files");
- return;
- }
-
- SimileAjax.urlPrefix = url.substr(0, url.indexOf("simile-ajax-api.js"));
- }
-
- SimileAjax.parseURLParameters(url, SimileAjax.params, {bundle:Boolean});
-// if (SimileAjax.params.bundle) {
-// SimileAjax.includeJavascriptFiles(document, SimileAjax.urlPrefix, [ "simile-ajax-bundle.js" ]);
-// } else {
-// SimileAjax.includeJavascriptFiles(document, SimileAjax.urlPrefix + "scripts/", javascriptFiles);
-// }
-// SimileAjax.includeCssFiles(document, SimileAjax.urlPrefix + "styles/", cssFiles);
-
- SimileAjax.loaded = true;
- })();
-}
-/*
- * Platform Utility Functions and Constants
- *
- */
-
-/* This must be called after our jQuery has been loaded
- but before control returns to user-code.
-*/
-SimileAjax.jQuery = jQuery;
-// SimileAjax.jQuery = jQuery.noConflict(true);
-if (typeof window["$"] == "undefined") {
- window.$ = SimileAjax.jQuery;
-}
-
-SimileAjax.Platform.os = {
- isMac: false,
- isWin: false,
- isWin32: false,
- isUnix: false
-};
-SimileAjax.Platform.browser = {
- isIE: false,
- isNetscape: false,
- isMozilla: false,
- isFirefox: false,
- isOpera: false,
- isSafari: false,
-
- majorVersion: 0,
- minorVersion: 0
-};
-
-(function() {
- var an = navigator.appName.toLowerCase();
- var ua = navigator.userAgent.toLowerCase();
-
- /*
- * Operating system
- */
- SimileAjax.Platform.os.isMac = (ua.indexOf('mac') != -1);
- SimileAjax.Platform.os.isWin = (ua.indexOf('win') != -1);
- SimileAjax.Platform.os.isWin32 = SimileAjax.Platform.isWin && (
- ua.indexOf('95') != -1 ||
- ua.indexOf('98') != -1 ||
- ua.indexOf('nt') != -1 ||
- ua.indexOf('win32') != -1 ||
- ua.indexOf('32bit') != -1
- );
- SimileAjax.Platform.os.isUnix = (ua.indexOf('x11') != -1);
-
- /*
- * Browser
- */
- SimileAjax.Platform.browser.isIE = (an.indexOf("microsoft") != -1);
- SimileAjax.Platform.browser.isNetscape = (an.indexOf("netscape") != -1);
- SimileAjax.Platform.browser.isMozilla = (ua.indexOf("mozilla") != -1);
- SimileAjax.Platform.browser.isFirefox = (ua.indexOf("firefox") != -1);
- SimileAjax.Platform.browser.isOpera = (an.indexOf("opera") != -1);
- SimileAjax.Platform.browser.isSafari = (an.indexOf("safari") != -1);
-
- var parseVersionString = function(s) {
- var a = s.split(".");
- SimileAjax.Platform.browser.majorVersion = parseInt(a[0]);
- SimileAjax.Platform.browser.minorVersion = parseInt(a[1]);
- };
- var indexOf = function(s, sub, start) {
- var i = s.indexOf(sub, start);
- return i >= 0 ? i : s.length;
- };
-
- if (SimileAjax.Platform.browser.isMozilla) {
- var offset = ua.indexOf("mozilla/");
- if (offset >= 0) {
- parseVersionString(ua.substring(offset + 8, indexOf(ua, " ", offset)));
- }
- }
- if (SimileAjax.Platform.browser.isIE) {
- var offset = ua.indexOf("msie ");
- if (offset >= 0) {
- parseVersionString(ua.substring(offset + 5, indexOf(ua, ";", offset)));
- }
- }
- if (SimileAjax.Platform.browser.isNetscape) {
- var offset = ua.indexOf("rv:");
- if (offset >= 0) {
- parseVersionString(ua.substring(offset + 3, indexOf(ua, ")", offset)));
- }
- }
- if (SimileAjax.Platform.browser.isFirefox) {
- var offset = ua.indexOf("firefox/");
- if (offset >= 0) {
- parseVersionString(ua.substring(offset + 8, indexOf(ua, " ", offset)));
- }
- }
-
- if (!("localeCompare" in String.prototype)) {
- String.prototype.localeCompare = function (s) {
- if (this < s) return -1;
- else if (this > s) return 1;
- else return 0;
- };
- }
-})();
-
-SimileAjax.Platform.getDefaultLocale = function() {
- return SimileAjax.Platform.clientLocale;
-};
-/*
- * Debug Utility Functions
- *
- */
-
-SimileAjax.Debug = {
- silent: false
-};
-
-SimileAjax.Debug.log = function(msg) {
- var f;
- if ("console" in window && "log" in window.console) { // FireBug installed
- f = function(msg2) {
- console.log(msg2);
- }
- } else {
- f = function(msg2) {
- if (!SimileAjax.Debug.silent) {
- alert(msg2);
- }
- }
- }
- SimileAjax.Debug.log = f;
- f(msg);
-};
-
-SimileAjax.Debug.warn = function(msg) {
- var f;
- if ("console" in window && "warn" in window.console) { // FireBug installed
- f = function(msg2) {
- console.warn(msg2);
- }
- } else {
- f = function(msg2) {
- if (!SimileAjax.Debug.silent) {
- alert(msg2);
- }
- }
- }
- SimileAjax.Debug.warn = f;
- f(msg);
-};
-
-SimileAjax.Debug.exception = function(e, msg) {
- var f, params = SimileAjax.parseURLParameters();
- if (params.errors == "throw" || SimileAjax.params.errors == "throw") {
- f = function(e2, msg2) {
- throw(e2); // do not hide from browser's native debugging features
- };
- } else if ("console" in window && "error" in window.console) { // FireBug installed
- f = function(e2, msg2) {
- if (msg2 != null) {
- console.error(msg2 + " %o", e2);
- } else {
- console.error(e2);
- }
- throw(e2); // do not hide from browser's native debugging features
- };
- } else {
- f = function(e2, msg2) {
- if (!SimileAjax.Debug.silent) {
- alert("Caught exception: " + msg2 + "\n\nDetails: " + ("description" in e2 ? e2.description : e2));
- }
- throw(e2); // do not hide from browser's native debugging features
- };
- }
- SimileAjax.Debug.exception = f;
- f(e, msg);
-};
-
-SimileAjax.Debug.objectToString = function(o) {
- return SimileAjax.Debug._objectToString(o, "");
-};
-
-SimileAjax.Debug._objectToString = function(o, indent) {
- var indent2 = indent + " ";
- if (typeof o == "object") {
- var s = "{";
- for (n in o) {
- s += indent2 + n + ": " + SimileAjax.Debug._objectToString(o[n], indent2) + "\n";
- }
- s += indent + "}";
- return s;
- } else if (typeof o == "array") {
- var s = "[";
- for (var n = 0; n < o.length; n++) {
- s += SimileAjax.Debug._objectToString(o[n], indent2) + "\n";
- }
- s += indent + "]";
- return s;
- } else {
- return o;
- }
-};
-/**
- * @fileOverview XmlHttp utility functions
- * @name SimileAjax.XmlHttp
- */
-
-SimileAjax.XmlHttp = new Object();
-
-/**
- * Callback for XMLHttp onRequestStateChange.
- */
-SimileAjax.XmlHttp._onReadyStateChange = function(xmlhttp, fError, fDone) {
- switch (xmlhttp.readyState) {
- // 1: Request not yet made
- // 2: Contact established with server but nothing downloaded yet
- // 3: Called multiple while downloading in progress
-
- // Download complete
- case 4:
- try {
- if (xmlhttp.status == 0 // file:// urls, works on Firefox
- || xmlhttp.status == 200 // http:// urls
- ) {
- if (fDone) {
- fDone(xmlhttp);
- }
- } else {
- if (fError) {
- fError(
- xmlhttp.statusText,
- xmlhttp.status,
- xmlhttp
- );
- }
- }
- } catch (e) {
- SimileAjax.Debug.exception("XmlHttp: Error handling onReadyStateChange", e);
- }
- break;
- }
-};
-
-/**
- * Creates an XMLHttpRequest object. On the first run, this
- * function creates a platform-specific function for
- * instantiating an XMLHttpRequest object and then replaces
- * itself with that function.
- */
-SimileAjax.XmlHttp._createRequest = function() {
- if (SimileAjax.Platform.browser.isIE) {
- var programIDs = [
- "Msxml2.XMLHTTP",
- "Microsoft.XMLHTTP",
- "Msxml2.XMLHTTP.4.0"
- ];
- for (var i = 0; i < programIDs.length; i++) {
- try {
- var programID = programIDs[i];
- var f = function() {
- return new ActiveXObject(programID);
- };
- var o = f();
-
- // We are replacing the SimileAjax._createXmlHttpRequest
- // function with this inner function as we've
- // found out that it works. This is so that we
- // don't have to do all the testing over again
- // on subsequent calls.
- SimileAjax.XmlHttp._createRequest = f;
-
- return o;
- } catch (e) {
- // silent
- }
- }
- // fall through to try new XMLHttpRequest();
- }
-
- try {
- var f = function() {
- return new XMLHttpRequest();
- };
- var o = f();
-
- // We are replacing the SimileAjax._createXmlHttpRequest
- // function with this inner function as we've
- // found out that it works. This is so that we
- // don't have to do all the testing over again
- // on subsequent calls.
- SimileAjax.XmlHttp._createRequest = f;
-
- return o;
- } catch (e) {
- throw new Error("Failed to create an XMLHttpRequest object");
- }
-};
-
-/**
- * Performs an asynchronous HTTP GET.
- *
- * @param {Function} fError a function of the form
- function(statusText, statusCode, xmlhttp)
- * @param {Function} fDone a function of the form function(xmlhttp)
- */
-SimileAjax.XmlHttp.get = function(url, fError, fDone) {
- var xmlhttp = SimileAjax.XmlHttp._createRequest();
-
- xmlhttp.open("GET", url, true);
- xmlhttp.onreadystatechange = function() {
- SimileAjax.XmlHttp._onReadyStateChange(xmlhttp, fError, fDone);
- };
- xmlhttp.send(null);
-};
-
-/**
- * Performs an asynchronous HTTP POST.
- *
- * @param {Function} fError a function of the form
- function(statusText, statusCode, xmlhttp)
- * @param {Function} fDone a function of the form function(xmlhttp)
- */
-SimileAjax.XmlHttp.post = function(url, body, fError, fDone) {
- var xmlhttp = SimileAjax.XmlHttp._createRequest();
-
- xmlhttp.open("POST", url, true);
- xmlhttp.onreadystatechange = function() {
- SimileAjax.XmlHttp._onReadyStateChange(xmlhttp, fError, fDone);
- };
- xmlhttp.send(body);
-};
-
-SimileAjax.XmlHttp._forceXML = function(xmlhttp) {
- try {
- xmlhttp.overrideMimeType("text/xml");
- } catch (e) {
- xmlhttp.setrequestheader("Content-Type", "text/xml");
- }
-};/*
- * Copied directly from http://www.json.org/json.js.
- */
-
-/*
- json.js
- 2006-04-28
-
- This file adds these methods to JavaScript:
-
- object.toJSONString()
-
- This method produces a JSON text from an object. The
- object must not contain any cyclical references.
-
- array.toJSONString()
-
- This method produces a JSON text from an array. The
- array must not contain any cyclical references.
-
- string.parseJSON()
-
- This method parses a JSON text to produce an object or
- array. It will return false if there is an error.
-*/
-
-SimileAjax.JSON = new Object();
-
-(function () {
- var m = {
- '\b': '\\b',
- '\t': '\\t',
- '\n': '\\n',
- '\f': '\\f',
- '\r': '\\r',
- '"' : '\\"',
- '\\': '\\\\'
- };
- var s = {
- array: function (x) {
- var a = ['['], b, f, i, l = x.length, v;
- for (i = 0; i < l; i += 1) {
- v = x[i];
- f = s[typeof v];
- if (f) {
- v = f(v);
- if (typeof v == 'string') {
- if (b) {
- a[a.length] = ',';
- }
- a[a.length] = v;
- b = true;
- }
- }
- }
- a[a.length] = ']';
- return a.join('');
- },
- 'boolean': function (x) {
- return String(x);
- },
- 'null': function (x) {
- return "null";
- },
- number: function (x) {
- return isFinite(x) ? String(x) : 'null';
- },
- object: function (x) {
- if (x) {
- if (x instanceof Array) {
- return s.array(x);
- }
- var a = ['{'], b, f, i, v;
- for (i in x) {
- v = x[i];
- f = s[typeof v];
- if (f) {
- v = f(v);
- if (typeof v == 'string') {
- if (b) {
- a[a.length] = ',';
- }
- a.push(s.string(i), ':', v);
- b = true;
- }
- }
- }
- a[a.length] = '}';
- return a.join('');
- }
- return 'null';
- },
- string: function (x) {
- if (/["\\\x00-\x1f]/.test(x)) {
- x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
- var c = m[b];
- if (c) {
- return c;
- }
- c = b.charCodeAt();
- return '\\u00' +
- Math.floor(c / 16).toString(16) +
- (c % 16).toString(16);
- });
- }
- return '"' + x + '"';
- }
- };
-
- SimileAjax.JSON.toJSONString = function(o) {
- if (o instanceof Object) {
- return s.object(o);
- } else if (o instanceof Array) {
- return s.array(o);
- } else {
- return o.toString();
- }
- };
-
- SimileAjax.JSON.parseJSON = function () {
- try {
- return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
- this.replace(/"(\\.|[^"\\])*"/g, ''))) &&
- eval('(' + this + ')');
- } catch (e) {
- return false;
- }
- };
-})();
-/*
- * DOM Utility Functions
- *
- */
-
-SimileAjax.DOM = new Object();
-
-SimileAjax.DOM.registerEventWithObject = function(elmt, eventName, obj, handlerName) {
- SimileAjax.DOM.registerEvent(elmt, eventName, function(elmt2, evt, target) {
- return obj[handlerName].call(obj, elmt2, evt, target);
- });
-};
-
-SimileAjax.DOM.registerEvent = function(elmt, eventName, handler) {
- var handler2 = function(evt) {
- evt = (evt) ? evt : ((event) ? event : null);
- if (evt) {
- var target = (evt.target) ?
- evt.target : ((evt.srcElement) ? evt.srcElement : null);
- if (target) {
- target = (target.nodeType == 1 || target.nodeType == 9) ?
- target : target.parentNode;
- }
-
- return handler(elmt, evt, target);
- }
- return true;
- }
-
- if (SimileAjax.Platform.browser.isIE) {
- elmt.attachEvent("on" + eventName, handler2);
- } else {
- elmt.addEventListener(eventName, handler2, false);
- }
-};
-
-SimileAjax.DOM.getPageCoordinates = function(elmt) {
- var left = 0;
- var top = 0;
-
- if (elmt.nodeType != 1) {
- elmt = elmt.parentNode;
- }
-
- var elmt2 = elmt;
- while (elmt2 != null) {
- left += elmt2.offsetLeft;
- top += elmt2.offsetTop;
- elmt2 = elmt2.offsetParent;
- }
-
- var body = document.body;
- while (elmt != null && elmt != body) {
- if ("scrollLeft" in elmt) {
- left -= elmt.scrollLeft;
- top -= elmt.scrollTop;
- }
- elmt = elmt.parentNode;
- }
-
- return { left: left, top: top };
-};
-
-SimileAjax.DOM.getSize = function(elmt) {
- var w = this.getStyle(elmt,"width");
- var h = this.getStyle(elmt,"height");
- if (w.indexOf("px") > -1) w = w.replace("px","");
- if (h.indexOf("px") > -1) h = h.replace("px","");
- return {
- w: w,
- h: h
- }
-}
-
-SimileAjax.DOM.getStyle = function(elmt, styleProp) {
- if (elmt.currentStyle) { // IE
- var style = elmt.currentStyle[styleProp];
- } else if (window.getComputedStyle) { // standard DOM
- var style = document.defaultView.getComputedStyle(elmt, null).getPropertyValue(styleProp);
- } else {
- var style = "";
- }
- return style;
-}
-
-SimileAjax.DOM.getEventRelativeCoordinates = function(evt, elmt) {
- if (SimileAjax.Platform.browser.isIE) {
- if (evt.type == "mousewheel") {
- var coords = SimileAjax.DOM.getPageCoordinates(elmt);
- return {
- x: evt.clientX - coords.left,
- y: evt.clientY - coords.top
- };
- } else {
- return {
- x: evt.offsetX,
- y: evt.offsetY
- };
- }
- } else {
- var coords = SimileAjax.DOM.getPageCoordinates(elmt);
-
- if ((evt.type == "DOMMouseScroll") &&
- SimileAjax.Platform.browser.isFirefox &&
- (SimileAjax.Platform.browser.majorVersion == 2)) {
- // Due to: https://bugzilla.mozilla.org/show_bug.cgi?id=352179
-
- return {
- x: evt.screenX - coords.left,
- y: evt.screenY - coords.top
- };
- } else {
- return {
- x: evt.pageX - coords.left,
- y: evt.pageY - coords.top
- };
- }
- }
-};
-
-SimileAjax.DOM.getEventPageCoordinates = function(evt) {
- if (SimileAjax.Platform.browser.isIE) {
- return {
- x: evt.clientX + document.body.scrollLeft,
- y: evt.clientY + document.body.scrollTop
- };
- } else {
- return {
- x: evt.pageX,
- y: evt.pageY
- };
- }
-};
-
-SimileAjax.DOM.hittest = function(x, y, except) {
- return SimileAjax.DOM._hittest(document.body, x, y, except);
-};
-
-SimileAjax.DOM._hittest = function(elmt, x, y, except) {
- var childNodes = elmt.childNodes;
- outer: for (var i = 0; i < childNodes.length; i++) {
- var childNode = childNodes[i];
- for (var j = 0; j < except.length; j++) {
- if (childNode == except[j]) {
- continue outer;
- }
- }
-
- if (childNode.offsetWidth == 0 && childNode.offsetHeight == 0) {
- /*
- * Sometimes SPAN elements have zero width and height but
- * they have children like DIVs that cover non-zero areas.
- */
- var hitNode = SimileAjax.DOM._hittest(childNode, x, y, except);
- if (hitNode != childNode) {
- return hitNode;
- }
- } else {
- var top = 0;
- var left = 0;
-
- var node = childNode;
- while (node) {
- top += node.offsetTop;
- left += node.offsetLeft;
- node = node.offsetParent;
- }
-
- if (left <= x && top <= y && (x - left) < childNode.offsetWidth && (y - top) < childNode.offsetHeight) {
- return SimileAjax.DOM._hittest(childNode, x, y, except);
- } else if (childNode.nodeType == 1 && childNode.tagName == "TR") {
- /*
- * Table row might have cells that span several rows.
- */
- var childNode2 = SimileAjax.DOM._hittest(childNode, x, y, except);
- if (childNode2 != childNode) {
- return childNode2;
- }
- }
- }
- }
- return elmt;
-};
-
-SimileAjax.DOM.cancelEvent = function(evt) {
- evt.returnValue = false;
- evt.cancelBubble = true;
- if ("preventDefault" in evt) {
- evt.preventDefault();
- }
-};
-
-SimileAjax.DOM.appendClassName = function(elmt, className) {
- var classes = elmt.className.split(" ");
- for (var i = 0; i < classes.length; i++) {
- if (classes[i] == className) {
- return;
- }
- }
- classes.push(className);
- elmt.className = classes.join(" ");
-};
-
-SimileAjax.DOM.createInputElement = function(type) {
- var div = document.createElement("div");
- div.innerHTML = "<input type='" + type + "' />";
-
- return div.firstChild;
-};
-
-SimileAjax.DOM.createDOMFromTemplate = function(template) {
- var result = {};
- result.elmt = SimileAjax.DOM._createDOMFromTemplate(template, result, null);
-
- return result;
-};
-
-SimileAjax.DOM._createDOMFromTemplate = function(templateNode, result, parentElmt) {
- if (templateNode == null) {
- /*
- var node = doc.createTextNode("--null--");
- if (parentElmt != null) {
- parentElmt.appendChild(node);
- }
- return node;
- */
- return null;
- } else if (typeof templateNode != "object") {
- var node = document.createTextNode(templateNode);
- if (parentElmt != null) {
- parentElmt.appendChild(node);
- }
- return node;
- } else {
- var elmt = null;
- if ("tag" in templateNode) {
- var tag = templateNode.tag;
- if (parentElmt != null) {
- if (tag == "tr") {
- elmt = parentElmt.insertRow(parentElmt.rows.length);
- } else if (tag == "td") {
- elmt = parentElmt.insertCell(parentElmt.cells.length);
- }
- }
- if (elmt == null) {
- elmt = tag == "input" ?
- SimileAjax.DOM.createInputElement(templateNode.type) :
- document.createElement(tag);
-
- if (parentElmt != null) {
- parentElmt.appendChild(elmt);
- }
- }
- } else {
- elmt = templateNode.elmt;
- if (parentElmt != null) {
- parentElmt.appendChild(elmt);
- }
- }
-
- for (var attribute in templateNode) {
- var value = templateNode[attribute];
-
- if (attribute == "field") {
- result[value] = elmt;
-
- } else if (attribute == "className") {
- elmt.className = value;
- } else if (attribute == "id") {
- elmt.id = value;
- } else if (attribute == "title") {
- elmt.title = value;
- } else if (attribute == "type" && elmt.tagName == "input") {
- // do nothing
- } else if (attribute == "style") {
- for (n in value) {
- var v = value[n];
- if (n == "float") {
- n = SimileAjax.Platform.browser.isIE ? "styleFloat" : "cssFloat";
- }
- elmt.style[n] = v;
- }
- } else if (attribute == "children") {
- for (var i = 0; i < value.length; i++) {
- SimileAjax.DOM._createDOMFromTemplate(value[i], result, elmt);
- }
- } else if (attribute != "tag" && attribute != "elmt") {
- elmt.setAttribute(attribute, value);
- }
- }
- return elmt;
- }
-}
-
-SimileAjax.DOM._cachedParent = null;
-SimileAjax.DOM.createElementFromString = function(s) {
- if (SimileAjax.DOM._cachedParent == null) {
- SimileAjax.DOM._cachedParent = document.createElement("div");
- }
- SimileAjax.DOM._cachedParent.innerHTML = s;
- return SimileAjax.DOM._cachedParent.firstChild;
-};
-
-SimileAjax.DOM.createDOMFromString = function(root, s, fieldElmts) {
- var elmt = typeof root == "string" ? document.createElement(root) : root;
- elmt.innerHTML = s;
-
- var dom = { elmt: elmt };
- SimileAjax.DOM._processDOMChildrenConstructedFromString(dom, elmt, fieldElmts != null ? fieldElmts : {} );
-
- return dom;
-};
-
-SimileAjax.DOM._processDOMConstructedFromString = function(dom, elmt, fieldElmts) {
- var id = elmt.id;
- if (id != null && id.length > 0) {
- elmt.removeAttribute("id");
- if (id in fieldElmts) {
- var parentElmt = elmt.parentNode;
- parentElmt.insertBefore(fieldElmts[id], elmt);
- parentElmt.removeChild(elmt);
-
- dom[id] = fieldElmts[id];
- return;
- } else {
- dom[id] = elmt;
- }
- }
-
- if (elmt.hasChildNodes()) {
- SimileAjax.DOM._processDOMChildrenConstructedFromString(dom, elmt, fieldElmts);
- }
-};
-
-SimileAjax.DOM._processDOMChildrenConstructedFromString = function(dom, elmt, fieldElmts) {
- var node = elmt.firstChild;
- while (node != null) {
- var node2 = node.nextSibling;
- if (node.nodeType == 1) {
- SimileAjax.DOM._processDOMConstructedFromString(dom, node, fieldElmts);
- }
- node = node2;
- }
-};
-/**
- * @fileOverview Graphics utility functions and constants
- * @name SimileAjax.Graphics
- */
-
-SimileAjax.Graphics = new Object();
-
-/**
- * A boolean value indicating whether PNG translucency is supported on the
- * user's browser or not.
- *
- * @type Boolean
- */
-SimileAjax.Graphics.pngIsTranslucent = (!SimileAjax.Platform.browser.isIE) || (SimileAjax.Platform.browser.majorVersion > 6);
-if (!SimileAjax.Graphics.pngIsTranslucent) {
- SimileAjax.includeCssFile(document, SimileAjax.urlPrefix + "styles/graphics-ie6.css");
-}
-
-/*
- * Opacity, translucency
- *
- */
-SimileAjax.Graphics._createTranslucentImage1 = function(url, verticalAlign) {
- var elmt = document.createElement("img");
- elmt.setAttribute("src", url);
- if (verticalAlign != null) {
- elmt.style.verticalAlign = verticalAlign;
- }
- return elmt;
-};
-SimileAjax.Graphics._createTranslucentImage2 = function(url, verticalAlign) {
- var elmt = document.createElement("img");
- elmt.style.width = "1px"; // just so that IE will calculate the size property
- elmt.style.height = "1px";
- elmt.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url +"', sizingMethod='image')";
- elmt.style.verticalAlign = (verticalAlign != null) ? verticalAlign : "middle";
- return elmt;
-};
-
-/**
- * Creates a DOM element for an <code>img</code> tag using the URL given. This
- * is a convenience method that automatically includes the necessary CSS to
- * allow for translucency, even on IE.
- *
- * @function
- * @param {String} url the URL to the image
- * @param {String} verticalAlign the CSS value for the image's vertical-align
- * @return {Element} a DOM element containing the <code>img</code> tag
- */
-SimileAjax.Graphics.createTranslucentImage = SimileAjax.Graphics.pngIsTranslucent ?
- SimileAjax.Graphics._createTranslucentImage1 :
- SimileAjax.Graphics._createTranslucentImage2;
-
-SimileAjax.Graphics._createTranslucentImageHTML1 = function(url, verticalAlign) {
- return "<img src=\"" + url + "\"" +
- (verticalAlign != null ? " style=\"vertical-align: " + verticalAlign + ";\"" : "") +
- " />";
-};
-SimileAjax.Graphics._createTranslucentImageHTML2 = function(url, verticalAlign) {
- var style =
- "width: 1px; height: 1px; " +
- "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url +"', sizingMethod='image');" +
- (verticalAlign != null ? " vertical-align: " + verticalAlign + ";" : "");
-
- return "<img src='" + url + "' style=\"" + style + "\" />";
-};
-
-/**
- * Creates an HTML string for an <code>img</code> tag using the URL given.
- * This is a convenience method that automatically includes the necessary CSS
- * to allow for translucency, even on IE.
- *
- * @function
- * @param {String} url the URL to the image
- * @param {String} verticalAlign the CSS value for the image's vertical-align
- * @return {String} a string containing the <code>img</code> tag
- */
-SimileAjax.Graphics.createTranslucentImageHTML = SimileAjax.Graphics.pngIsTranslucent ?
- SimileAjax.Graphics._createTranslucentImageHTML1 :
- SimileAjax.Graphics._createTranslucentImageHTML2;
-
-/**
- * Sets the opacity on the given DOM element.
- *
- * @param {Element} elmt the DOM element to set the opacity on
- * @param {Number} opacity an integer from 0 to 100 specifying the opacity
- */
-SimileAjax.Graphics.setOpacity = function(elmt, opacity) {
- if (SimileAjax.Platform.browser.isIE) {
- elmt.style.filter = "progid:DXImageTransform.Microsoft.Alpha(Style=0,Opacity=" + opacity + ")";
- } else {
- var o = (opacity / 100).toString();
- elmt.style.opacity = o;
- elmt.style.MozOpacity = o;
- }
-};
-
-/*
- * Bubble
- *
- */
-
-SimileAjax.Graphics.bubbleConfig = {
- containerCSSClass: "simileAjax-bubble-container",
- innerContainerCSSClass: "simileAjax-bubble-innerContainer",
- contentContainerCSSClass: "simileAjax-bubble-contentContainer",
-
- borderGraphicSize: 50,
- borderGraphicCSSClassPrefix: "simileAjax-bubble-border-",
-
- arrowGraphicTargetOffset: 33, // from tip of arrow to the side of the graphic that touches the content of the bubble
- arrowGraphicLength: 100, // dimension of arrow graphic along the direction that the arrow points
- arrowGraphicWidth: 49, // dimension of arrow graphic perpendicular to the direction that the arrow points
- arrowGraphicCSSClassPrefix: "simileAjax-bubble-arrow-",
-
- closeGraphicCSSClass: "simileAjax-bubble-close",
-
- extraPadding: 20
-};
-
-/**
- * Creates a nice, rounded bubble popup with the given content in a div,
- * page coordinates and a suggested width. The bubble will point to the
- * location on the page as described by pageX and pageY. All measurements
- * should be given in pixels.
- *
- * @param {Element} the content div
- * @param {Number} pageX the x coordinate of the point to point to
- * @param {Number} pageY the y coordinate of the point to point to
- * @param {Number} contentWidth a suggested width of the content
- * @param {String} orientation a string ("top", "bottom", "left", or "right")
- * that describes the orientation of the arrow on the bubble
- * @param {Number} maxHeight. Add a scrollbar div if bubble would be too tall.
- * Default of 0 or null means no maximum
- */
-SimileAjax.Graphics.createBubbleForContentAndPoint = function(
- div, pageX, pageY, contentWidth, orientation, maxHeight) {
- if (typeof contentWidth != "number") {
- contentWidth = 300;
- }
- if (typeof maxHeight != "number") {
- maxHeight = 0;
- }
-
- div.style.position = "absolute";
- div.style.left = "-5000px";
- div.style.top = "0px";
- div.style.width = contentWidth + "px";
- document.body.appendChild(div);
-
- window.setTimeout(function() {
- var width = div.scrollWidth + 10;
- var height = div.scrollHeight + 10;
- var scrollDivW = 0; // width of the possible inner container when we want vertical scrolling
- if (maxHeight > 0 && height > maxHeight) {
- height = maxHeight;
- scrollDivW = width - 25;
- }
-
- var bubble = SimileAjax.Graphics.createBubbleForPoint(pageX, pageY, width, height, orientation);
-
- document.body.removeChild(div);
- div.style.position = "static";
- div.style.left = "";
- div.style.top = "";
-
- // create a scroll div if needed
- if (scrollDivW > 0) {
- var scrollDiv = document.createElement("div");
- div.style.width = "";
- scrollDiv.style.width = scrollDivW + "px";
- scrollDiv.appendChild(div);
- bubble.content.appendChild(scrollDiv);
- } else {
- div.style.width = width + "px";
- bubble.content.appendChild(div);
- }
- }, 200);
-};
-
-/**
- * Creates a nice, rounded bubble popup with the given page coordinates and
- * content dimensions. The bubble will point to the location on the page
- * as described by pageX and pageY. All measurements should be given in
- * pixels.
- *
- * @param {Number} pageX the x coordinate of the point to point to
- * @param {Number} pageY the y coordinate of the point to point to
- * @param {Number} contentWidth the width of the content box in the bubble
- * @param {Number} contentHeight the height of the content box in the bubble
- * @param {String} orientation a string ("top", "bottom", "left", or "right")
- * that describes the orientation of the arrow on the bubble
- * @return {Element} a DOM element for the newly created bubble
- */
-SimileAjax.Graphics.createBubbleForPoint = function(pageX, pageY, contentWidth, contentHeight, orientation) {
- contentWidth = parseInt(contentWidth, 10); // harden against bad input bugs
- contentHeight = parseInt(contentHeight, 10); // getting numbers-as-strings
-
- var bubbleConfig = SimileAjax.Graphics.bubbleConfig;
- var pngTransparencyClassSuffix =
- SimileAjax.Graphics.pngIsTranslucent ? "pngTranslucent" : "pngNotTranslucent";
-
- var bubbleWidth = contentWidth + 2 * bubbleConfig.borderGraphicSize;
- var bubbleHeight = contentHeight + 2 * bubbleConfig.borderGraphicSize;
-
- var generatePngSensitiveClass = function(className) {
- return className + " " + className + "-" + pngTransparencyClassSuffix;
- };
-
- /*
- * Render container divs
- */
- var div = document.createElement("div");
- div.className = generatePngSensitiveClass(bubbleConfig.containerCSSClass);
- div.style.width = contentWidth + "px";
- div.style.height = contentHeight + "px";
-
- var divInnerContainer = document.createElement("div");
- divInnerContainer.className = generatePngSensitiveClass(bubbleConfig.innerContainerCSSClass);
- div.appendChild(divInnerContainer);
-
- /*
- * Create layer for bubble
- */
- var close = function() {
- if (!bubble._closed) {
- document.body.removeChild(bubble._div);
- bubble._doc = null;
- bubble._div = null;
- bubble._content = null;
- bubble._closed = true;
- }
- }
- var bubble = { _closed: false };
- var layer = SimileAjax.WindowManager.pushLayer(close, true, div);
- bubble._div = div;
- bubble.close = function() { SimileAjax.WindowManager.popLayer(layer); }
-
- /*
- * Render border graphics
- */
- var createBorder = function(classNameSuffix) {
- var divBorderGraphic = document.createElement("div");
- divBorderGraphic.className = generatePngSensitiveClass(bubbleConfig.borderGraphicCSSClassPrefix + classNameSuffix);
- divInnerContainer.appendChild(divBorderGraphic);
- };
- createBorder("top-left");
- createBorder("top-right");
- createBorder("bottom-left");
- createBorder("bottom-right");
- createBorder("left");
- createBorder("right");
- createBorder("top");
- createBorder("bottom");
-
- /*
- * Render content
- */
- var divContentContainer = document.createElement("div");
- divContentContainer.className = generatePngSensitiveClass(bubbleConfig.contentContainerCSSClass);
- divInnerContainer.appendChild(divContentContainer);
- bubble.content = divContentContainer;
-
- /*
- * Render close button
- */
- var divClose = document.createElement("div");
- divClose.className = generatePngSensitiveClass(bubbleConfig.closeGraphicCSSClass);
- divInnerContainer.appendChild(divClose);
- SimileAjax.WindowManager.registerEventWithObject(divClose, "click", bubble, "close");
-
- (function() {
- var dims = SimileAjax.Graphics.getWindowDimensions();
- var docWidth = dims.w;
- var docHeight = dims.h;
-
- var halfArrowGraphicWidth = Math.ceil(bubbleConfig.arrowGraphicWidth / 2);
-
- var createArrow = function(classNameSuffix) {
- var divArrowGraphic = document.createElement("div");
- divArrowGraphic.className = generatePngSensitiveClass(bubbleConfig.arrowGraphicCSSClassPrefix + "point-" + classNameSuffix);
- divInnerContainer.appendChild(divArrowGraphic);
- return divArrowGraphic;
- };
-
- if (pageX - halfArrowGraphicWidth - bubbleConfig.borderGraphicSize - bubbleConfig.extraPadding > 0 &&
- pageX + halfArrowGraphicWidth + bubbleConfig.borderGraphicSize + bubbleConfig.extraPadding < docWidth) {
-
- /*
- * Bubble can be positioned above or below the target point.
- */
-
- var left = pageX - Math.round(contentWidth / 2);
- left = pageX < (docWidth / 2) ?
- Math.max(left, bubbleConfig.extraPadding + bubbleConfig.borderGraphicSize) :
- Math.min(left, docWidth - bubbleConfig.extraPadding - bubbleConfig.borderGraphicSize - contentWidth);
-
- if ((orientation && orientation == "top") ||
- (!orientation &&
- (pageY
- - bubbleConfig.arrowGraphicTargetOffset
- - contentHeight
- - bubbleConfig.borderGraphicSize
- - bubbleConfig.extraPadding > 0))) {
-
- /*
- * Position bubble above the target point.
- */
-
- var divArrow = createArrow("down");
- divArrow.style.left = (pageX - halfArrowGraphicWidth - left) + "px";
-
- div.style.left = left + "px";
- div.style.top = (pageY - bubbleConfig.arrowGraphicTargetOffset - contentHeight) + "px";
-
- return;
- } else if ((orientation && orientation == "bottom") ||
- (!orientation &&
- (pageY
- + bubbleConfig.arrowGraphicTargetOffset
- + contentHeight
- + bubbleConfig.borderGraphicSize
- + bubbleConfig.extraPadding < docHeight))) {
-
- /*
- * Position bubble below the target point.
- */
-
- var divArrow = createArrow("up");
- divArrow.style.left = (pageX - halfArrowGraphicWidth - left) + "px";
-
- div.style.left = left + "px";
- div.style.top = (pageY + bubbleConfig.arrowGraphicTargetOffset) + "px";
-
- return;
- }
- }
-
- var top = pageY - Math.round(contentHeight / 2);
- top = pageY < (docHeight / 2) ?
- Math.max(top, bubbleConfig.extraPadding + bubbleConfig.borderGraphicSize) :
- Math.min(top, docHeight - bubbleConfig.extraPadding - bubbleConfig.borderGraphicSize - contentHeight);
-
- if ((orientation && orientation == "left") ||
- (!orientation &&
- (pageX
- - bubbleConfig.arrowGraphicTargetOffset
- - contentWidth
- - bubbleConfig.borderGraphicSize
- - bubbleConfig.extraPadding > 0))) {
-
- /*
- * Position bubble left of the target point.
- */
-
- var divArrow = createArrow("right");
- divArrow.style.top = (pageY - halfArrowGraphicWidth - top) + "px";
-
- div.style.top = top + "px";
- div.style.left = (pageX - bubbleConfig.arrowGraphicTargetOffset - contentWidth) + "px";
- } else {
-
- /*
- * Position bubble right of the target point, as the last resort.
- */
-
- var divArrow = createArrow("left");
- divArrow.style.top = (pageY - halfArrowGraphicWidth - top) + "px";
-
- div.style.top = top + "px";
- div.style.left = (pageX + bubbleConfig.arrowGraphicTargetOffset) + "px";
- }
- })();
-
- document.body.appendChild(div);
-
- return bubble;
-};
-
-SimileAjax.Graphics.getWindowDimensions = function() {
- if (typeof window.innerHeight == 'number') {
- return { w:window.innerWidth, h:window.innerHeight }; // Non-IE
- } else if (document.documentElement && document.documentElement.clientHeight) {
- return { // IE6+, in "standards compliant mode"
- w:document.documentElement.clientWidth,
- h:document.documentElement.clientHeight
- };
- } else if (document.body && document.body.clientHeight) {
- return { // IE 4 compatible
- w:document.body.clientWidth,
- h:document.body.clientHeight
- };
- }
-};
-
-
-/**
- * Creates a floating, rounded message bubble in the center of the window for
- * displaying modal information, e.g. "Loading..."
- *
- * @param {Document} doc the root document for the page to render on
- * @param {Object} an object with two properties, contentDiv and containerDiv,
- * consisting of the newly created DOM elements
- */
-SimileAjax.Graphics.createMessageBubble = function(doc) {
- var containerDiv = doc.createElement("div");
- if (SimileAjax.Graphics.pngIsTranslucent) {
- var topDiv = doc.createElement("div");
- topDiv.style.height = "33px";
- topDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-top-left.png) top left no-repeat";
- topDiv.style.paddingLeft = "44px";
- containerDiv.appendChild(topDiv);
-
- var topRightDiv = doc.createElement("div");
- topRightDiv.style.height = "33px";
- topRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-top-right.png) top right no-repeat";
- topDiv.appendChild(topRightDiv);
-
- var middleDiv = doc.createElement("div");
- middleDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-left.png) top left repeat-y";
- middleDiv.style.paddingLeft = "44px";
- containerDiv.appendChild(middleDiv);
-
- var middleRightDiv = doc.createElement("div");
- middleRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-right.png) top right repeat-y";
- middleRightDiv.style.paddingRight = "44px";
- middleDiv.appendChild(middleRightDiv);
-
- var contentDiv = doc.createElement("div");
- middleRightDiv.appendChild(contentDiv);
-
- var bottomDiv = doc.createElement("div");
- bottomDiv.style.height = "55px";
- bottomDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-bottom-left.png) bottom left no-repeat";
- bottomDiv.style.paddingLeft = "44px";
- containerDiv.appendChild(bottomDiv);
-
- var bottomRightDiv = doc.createElement("div");
- bottomRightDiv.style.height = "55px";
- bottomRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-bottom-right.png) bottom right no-repeat";
- bottomDiv.appendChild(bottomRightDiv);
- } else {
- containerDiv.style.border = "2px solid #7777AA";
- containerDiv.style.padding = "20px";
- containerDiv.style.background = "white";
- SimileAjax.Graphics.setOpacity(containerDiv, 90);
-
- var contentDiv = doc.createElement("div");
- containerDiv.appendChild(contentDiv);
- }
-
- return {
- containerDiv: containerDiv,
- contentDiv: contentDiv
- };
-};
-
-/*
- * Animation
- *
- */
-
-/**
- * Creates an animation for a function, and an interval of values. The word
- * "animation" here is used in the sense of repeatedly calling a function with
- * a current value from within an interval, and a delta value.
- *
- * @param {Function} f a function to be called every 50 milliseconds throughout
- * the animation duration, of the form f(current, delta), where current is
- * the current value within the range and delta is the current change.
- * @param {Number} from a starting value
- * @param {Number} to an ending value
- * @param {Number} duration the duration of the animation in milliseconds
- * @param {Function} [cont] an optional function that is called at the end of
- * the animation, i.e. a continuation.
- * @return {SimileAjax.Graphics._Animation} a new animation object
- */
-SimileAjax.Graphics.createAnimation = function(f, from, to, duration, cont) {
- return new SimileAjax.Graphics._Animation(f, from, to, duration, cont);
-};
-
-SimileAjax.Graphics._Animation = function(f, from, to, duration, cont) {
- this.f = f;
- this.cont = (typeof cont == "function") ? cont : function() {};
-
- this.from = from;
- this.to = to;
- this.current = from;
-
- this.duration = duration;
- this.start = new Date().getTime();
- this.timePassed = 0;
-};
-
-/**
- * Runs this animation.
- */
-SimileAjax.Graphics._Animation.prototype.run = function() {
- var a = this;
- window.setTimeout(function() { a.step(); }, 50);
-};
-
-/**
- * Increments this animation by one step, and then continues the animation with
- * <code>run()</code>.
- */
-SimileAjax.Graphics._Animation.prototype.step = function() {
- this.timePassed += 50;
-
- var timePassedFraction = this.timePassed / this.duration;
- var parameterFraction = -Math.cos(timePassedFraction * Math.PI) / 2 + 0.5;
- var current = parameterFraction * (this.to - this.from) + this.from;
-
- try {
- this.f(current, current - this.current);
- } catch (e) {
- }
- this.current = current;
-
- if (this.timePassed < this.duration) {
- this.run();
- } else {
- this.f(this.to, 0);
- this["cont"]();
- }
-};
-
-/*
- * CopyPasteButton
- *
- * Adapted from http://spaces.live.com/editorial/rayozzie/demo/liveclip/liveclipsample/techPreview.html.
- *
- */
-
-/**
- * Creates a button and textarea for displaying structured data and copying it
- * to the clipboard. The data is dynamically generated by the given
- * createDataFunction parameter.
- *
- * @param {String} image an image URL to use as the background for the
- * generated box
- * @param {Number} width the width in pixels of the generated box
- * @param {Number} height the height in pixels of the generated box
- * @param {Function} createDataFunction a function that is called with no
- * arguments to generate the structured data
- * @return a new DOM element
- */
-SimileAjax.Graphics.createStructuredDataCopyButton = function(image, width, height, createDataFunction) {
- var div = document.createElement("div");
- div.style.position = "relative";
- div.style.display = "inline";
- div.style.width = width + "px";
- div.style.height = height + "px";
- div.style.overflow = "hidden";
- div.style.margin = "2px";
-
- if (SimileAjax.Graphics.pngIsTranslucent) {
- div.style.background = "url(" + image + ") no-repeat";
- } else {
- div.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + image +"', sizingMethod='image')";
- }
-
- var style;
- if (SimileAjax.Platform.browser.isIE) {
- style = "filter:alpha(opacity=0)";
- } else {
- style = "opacity: 0";
- }
- div.innerHTML = "<textarea rows='1' autocomplete='off' value='none' style='" + style + "' />";
-
- var textarea = div.firstChild;
- textarea.style.width = width + "px";
- textarea.style.height = height + "px";
- textarea.onmousedown = function(evt) {
- evt = (evt) ? evt : ((event) ? event : null);
- if (evt.button == 2) {
- textarea.value = createDataFunction();
- textarea.select();
- }
- };
-
- return div;
-};
-
-/*
- * getWidthHeight
- *
- */
-SimileAjax.Graphics.getWidthHeight = function(el) {
- // RETURNS hash {width: w, height: h} in pixels
-
- var w, h;
- // offsetWidth rounds on FF, so doesn't work for us.
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=458617
- if (el.getBoundingClientRect == null) {
- // use offsetWidth
- w = el.offsetWidth;
- h = el.offsetHeight;
- } else {
- // use getBoundingClientRect
- var rect = el.getBoundingClientRect();
- w = Math.ceil(rect.right - rect.left);
- h = Math.ceil(rect.bottom - rect.top);
- }
- return {
- width: w,
- height: h
- };
-};
-
-
-/*
- * FontRenderingContext
- *
- */
-SimileAjax.Graphics.getFontRenderingContext = function(elmt, width) {
- return new SimileAjax.Graphics._FontRenderingContext(elmt, width);
-};
-
-SimileAjax.Graphics._FontRenderingContext = function(elmt, width) {
- this._elmt = elmt;
- this._elmt.style.visibility = "hidden";
- if (typeof width == "string") {
- this._elmt.style.width = width;
- } else if (typeof width == "number") {
- this._elmt.style.width = width + "px";
- }
-};
-
-SimileAjax.Graphics._FontRenderingContext.prototype.dispose = function() {
- this._elmt = null;
-};
-
-SimileAjax.Graphics._FontRenderingContext.prototype.update = function() {
- this._elmt.innerHTML = "A";
- this._lineHeight = this._elmt.offsetHeight;
-};
-
-SimileAjax.Graphics._FontRenderingContext.prototype.computeSize = function(text, className) {
- // className arg is optional
- var el = this._elmt;
- el.innerHTML = text;
- el.className = className === undefined ? '' : className;
- var wh = SimileAjax.Graphics.getWidthHeight(el);
- el.className = ''; // reset for the next guy
-
- return wh;
-};
-
-SimileAjax.Graphics._FontRenderingContext.prototype.getLineHeight = function() {
- return this._lineHeight;
-};
-
-/**
- * @fileOverview A collection of date/time utility functions
- * @name SimileAjax.DateTime
- */
-
-SimileAjax.DateTime = new Object();
-
-SimileAjax.DateTime.MILLISECOND = 0;
-SimileAjax.DateTime.SECOND = 1;
-SimileAjax.DateTime.MINUTE = 2;
-SimileAjax.DateTime.HOUR = 3;
-SimileAjax.DateTime.DAY = 4;
-SimileAjax.DateTime.WEEK = 5;
-SimileAjax.DateTime.MONTH = 6;
-SimileAjax.DateTime.YEAR = 7;
-SimileAjax.DateTime.DECADE = 8;
-SimileAjax.DateTime.CENTURY = 9;
-SimileAjax.DateTime.MILLENNIUM = 10;
-
-SimileAjax.DateTime.EPOCH = -1;
-SimileAjax.DateTime.ERA = -2;
-
-/**
- * An array of unit lengths, expressed in milliseconds, of various lengths of
- * time. The array indices are predefined and stored as properties of the
- * SimileAjax.DateTime object, e.g. SimileAjax.DateTime.YEAR.
- * @type Array
- */
-SimileAjax.DateTime.gregorianUnitLengths = [];
- (function() {
- var d = SimileAjax.DateTime;
- var a = d.gregorianUnitLengths;
-
- a[d.MILLISECOND] = 1;
- a[d.SECOND] = 1000;
- a[d.MINUTE] = a[d.SECOND] * 60;
- a[d.HOUR] = a[d.MINUTE] * 60;
- a[d.DAY] = a[d.HOUR] * 24;
- a[d.WEEK] = a[d.DAY] * 7;
- a[d.MONTH] = a[d.DAY] * 31;
- a[d.YEAR] = a[d.DAY] * 365;
- a[d.DECADE] = a[d.YEAR] * 10;
- a[d.CENTURY] = a[d.YEAR] * 100;
- a[d.MILLENNIUM] = a[d.YEAR] * 1000;
- })();
-
-SimileAjax.DateTime._dateRegexp = new RegExp(
- "^(-?)([0-9]{4})(" + [
- "(-?([0-9]{2})(-?([0-9]{2}))?)", // -month-dayOfMonth
- "(-?([0-9]{3}))", // -dayOfYear
- "(-?W([0-9]{2})(-?([1-7]))?)" // -Wweek-dayOfWeek
- ].join("|") + ")?$"
-);
-SimileAjax.DateTime._timezoneRegexp = new RegExp(
- "Z|(([-+])([0-9]{2})(:?([0-9]{2}))?)$"
-);
-SimileAjax.DateTime._timeRegexp = new RegExp(
- "^([0-9]{2})(:?([0-9]{2})(:?([0-9]{2})(\.([0-9]+))?)?)?$"
-);
-
-/**
- * Takes a date object and a string containing an ISO 8601 date and sets the
- * the date using information parsed from the string. Note that this method
- * does not parse any time information.
- *
- * @param {Date} dateObject the date object to modify
- * @param {String} string an ISO 8601 string to parse
- * @return {Date} the modified date object
- */
-SimileAjax.DateTime.setIso8601Date = function(dateObject, string) {
- /*
- * This function has been adapted from dojo.date, v.0.3.0
- * http://dojotoolkit.org/.
- */
-
- var d = string.match(SimileAjax.DateTime._dateRegexp);
- if(!d) {
- throw new Error("Invalid date string: " + string);
- }
-
- var sign = (d[1] == "-") ? -1 : 1; // BC or AD
- var year = sign * d[2];
- var month = d[5];
- var date = d[7];
- var dayofyear = d[9];
- var week = d[11];
- var dayofweek = (d[13]) ? d[13] : 1;
-
- dateObject.setUTCFullYear(year);
- if (dayofyear) {
- dateObject.setUTCMonth(0);
- dateObject.setUTCDate(Number(dayofyear));
- } else if (week) {
- dateObject.setUTCMonth(0);
- dateObject.setUTCDate(1);
- var gd = dateObject.getUTCDay();
- var day = (gd) ? gd : 7;
- var offset = Number(dayofweek) + (7 * Number(week));
-
- if (day <= 4) {
- dateObject.setUTCDate(offset + 1 - day);
- } else {
- dateObject.setUTCDate(offset + 8 - day);
- }
- } else {
- if (month) {
- dateObject.setUTCDate(1);
- dateObject.setUTCMonth(month - 1);
- }
- if (date) {
- dateObject.setUTCDate(date);
- }
- }
-
- return dateObject;
-};
-
-/**
- * Takes a date object and a string containing an ISO 8601 time and sets the
- * the time using information parsed from the string. Note that this method
- * does not parse any date information.
- *
- * @param {Date} dateObject the date object to modify
- * @param {String} string an ISO 8601 string to parse
- * @return {Date} the modified date object
- */
-SimileAjax.DateTime.setIso8601Time = function (dateObject, string) {
- /*
- * This function has been adapted from dojo.date, v.0.3.0
- * http://dojotoolkit.org/.
- */
-
- var d = string.match(SimileAjax.DateTime._timeRegexp);
- if(!d) {
- SimileAjax.Debug.warn("Invalid time string: " + string);
- return false;
- }
- var hours = d[1];
- var mins = Number((d[3]) ? d[3] : 0);
- var secs = (d[5]) ? d[5] : 0;
- var ms = d[7] ? (Number("0." + d[7]) * 1000) : 0;
-
- dateObject.setUTCHours(hours);
- dateObject.setUTCMinutes(mins);
- dateObject.setUTCSeconds(secs);
- dateObject.setUTCMilliseconds(ms);
-
- return dateObject;
-};
-
-/**
- * The timezone offset in minutes in the user's browser.
- * @type Number
- */
-SimileAjax.DateTime.timezoneOffset = new Date().getTimezoneOffset();
-
-/**
- * Takes a date object and a string containing an ISO 8601 date and time and
- * sets the date object using information parsed from the string.
- *
- * @param {Date} dateObject the date object to modify
- * @param {String} string an ISO 8601 string to parse
- * @return {Date} the modified date object
- */
-SimileAjax.DateTime.setIso8601 = function (dateObject, string){
- /*
- * This function has been adapted from dojo.date, v.0.3.0
- * http://dojotoolkit.org/.
- */
-
- var offset = null;
- var comps = (string.indexOf("T") == -1) ? string.split(" ") : string.split("T");
-
- SimileAjax.DateTime.setIso8601Date(dateObject, comps[0]);
- if (comps.length == 2) {
- // first strip timezone info from the end
- var d = comps[1].match(SimileAjax.DateTime._timezoneRegexp);
- if (d) {
- if (d[0] == 'Z') {
- offset = 0;
- } else {
- offset = (Number(d[3]) * 60) + Number(d[5]);
- offset *= ((d[2] == '-') ? 1 : -1);
- }
- comps[1] = comps[1].substr(0, comps[1].length - d[0].length);
- }
-
- SimileAjax.DateTime.setIso8601Time(dateObject, comps[1]);
- }
- if (offset == null) {
- offset = dateObject.getTimezoneOffset(); // local time zone if no tz info
- }
- dateObject.setTime(dateObject.getTime() + offset * 60000);
-
- return dateObject;
-};
-
-/**
- * Takes a string containing an ISO 8601 date and returns a newly instantiated
- * date object with the parsed date and time information from the string.
- *
- * @param {String} string an ISO 8601 string to parse
- * @return {Date} a new date object created from the string
- */
-SimileAjax.DateTime.parseIso8601DateTime = function (string) {
- try {
- return SimileAjax.DateTime.setIso8601(new Date(0), string);
- } catch (e) {
- return null;
- }
-};
-
-/**
- * Takes a string containing a Gregorian date and time and returns a newly
- * instantiated date object with the parsed date and time information from the
- * string. If the param is actually an instance of Date instead of a string,
- * simply returns the given date instead.
- *
- * @param {Object} o an object, to either return or parse as a string
- * @return {Date} the date object
- */
-SimileAjax.DateTime.parseGregorianDateTime = function(o) {
- if (o == null) {
- return null;
- } else if (o instanceof Date) {
- return o;
- }
-
- var s = o.toString();
- if (s.length > 0 && s.length < 8) {
- var space = s.indexOf(" ");
- if (space > 0) {
- var year = parseInt(s.substr(0, space));
- var suffix = s.substr(space + 1);
- if (suffix.toLowerCase() == "bc") {
- year = 1 - year;
- }
- } else {
- var year = parseInt(s);
- }
-
- var d = new Date(0);
- d.setUTCFullYear(year);
-
- return d;
- }
-
- try {
- return new Date(Date.parse(s));
- } catch (e) {
- return null;
- }
-};
-
-/**
- * Rounds date objects down to the nearest interval or multiple of an interval.
- * This method modifies the given date object, converting it to the given
- * timezone if specified.
- *
- * @param {Date} date the date object to round
- * @param {Number} intervalUnit a constant, integer index specifying an
- * interval, e.g. SimileAjax.DateTime.HOUR
- * @param {Number} timeZone a timezone shift, given in hours
- * @param {Number} multiple a multiple of the interval to round by
- * @param {Number} firstDayOfWeek an integer specifying the first day of the
- * week, 0 corresponds to Sunday, 1 to Monday, etc.
- */
-SimileAjax.DateTime.roundDownToInterval = function(date, intervalUnit, timeZone, multiple, firstDayOfWeek) {
- var timeShift = timeZone *
- SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR];
-
- var date2 = new Date(date.getTime() + timeShift);
- var clearInDay = function(d) {
- d.setUTCMilliseconds(0);
- d.setUTCSeconds(0);
- d.setUTCMinutes(0);
- d.setUTCHours(0);
- };
- var clearInYear = function(d) {
- clearInDay(d);
- d.setUTCDate(1);
- d.setUTCMonth(0);
- };
-
- switch(intervalUnit) {
- case SimileAjax.DateTime.MILLISECOND:
- var x = date2.getUTCMilliseconds();
- date2.setUTCMilliseconds(x - (x % multiple));
- break;
- case SimileAjax.DateTime.SECOND:
- date2.setUTCMilliseconds(0);
-
- var x = date2.getUTCSeconds();
- date2.setUTCSeconds(x - (x % multiple));
- break;
- case SimileAjax.DateTime.MINUTE:
- date2.setUTCMilliseconds(0);
- date2.setUTCSeconds(0);
-
- var x = date2.getUTCMinutes();
- date2.setTime(date2.getTime() -
- (x % multiple) * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.MINUTE]);
- break;
- case SimileAjax.DateTime.HOUR:
- date2.setUTCMilliseconds(0);
- date2.setUTCSeconds(0);
- date2.setUTCMinutes(0);
-
- var x = date2.getUTCHours();
- date2.setUTCHours(x - (x % multiple));
- break;
- case SimileAjax.DateTime.DAY:
- clearInDay(date2);
- break;
- case SimileAjax.DateTime.WEEK:
- clearInDay(date2);
- var d = (date2.getUTCDay() + 7 - firstDayOfWeek) % 7;
- date2.setTime(date2.getTime() -
- d * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.DAY]);
- break;
- case SimileAjax.DateTime.MONTH:
- clearInDay(date2);
- date2.setUTCDate(1);
-
- var x = date2.getUTCMonth();
- date2.setUTCMonth(x - (x % multiple));
- break;
- case SimileAjax.DateTime.YEAR:
- clearInYear(date2);
-
- var x = date2.getUTCFullYear();
- date2.setUTCFullYear(x - (x % multiple));
- break;
- case SimileAjax.DateTime.DECADE:
- clearInYear(date2);
- date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 10) * 10);
- break;
- case SimileAjax.DateTime.CENTURY:
- clearInYear(date2);
- date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 100) * 100);
- break;
- case SimileAjax.DateTime.MILLENNIUM:
- clearInYear(date2);
- date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 1000) * 1000);
- break;
- }
-
- date.setTime(date2.getTime() - timeShift);
-};
-
-/**
- * Rounds date objects up to the nearest interval or multiple of an interval.
- * This method modifies the given date object, converting it to the given
- * timezone if specified.
- *
- * @param {Date} date the date object to round
- * @param {Number} intervalUnit a constant, integer index specifying an
- * interval, e.g. SimileAjax.DateTime.HOUR
- * @param {Number} timeZone a timezone shift, given in hours
- * @param {Number} multiple a multiple of the interval to round by
- * @param {Number} firstDayOfWeek an integer specifying the first day of the
- * week, 0 corresponds to Sunday, 1 to Monday, etc.
- * @see SimileAjax.DateTime.roundDownToInterval
- */
-SimileAjax.DateTime.roundUpToInterval = function(date, intervalUnit, timeZone, multiple, firstDayOfWeek) {
- var originalTime = date.getTime();
- SimileAjax.DateTime.roundDownToInterval(date, intervalUnit, timeZone, multiple, firstDayOfWeek);
- if (date.getTime() < originalTime) {
- date.setTime(date.getTime() +
- SimileAjax.DateTime.gregorianUnitLengths[intervalUnit] * multiple);
- }
-};
-
-/**
- * Increments a date object by a specified interval, taking into
- * consideration the timezone.
- *
- * @param {Date} date the date object to increment
- * @param {Number} intervalUnit a constant, integer index specifying an
- * interval, e.g. SimileAjax.DateTime.HOUR
- * @param {Number} timeZone the timezone offset in hours
- */
-SimileAjax.DateTime.incrementByInterval = function(date, intervalUnit, timeZone) {
- timeZone = (typeof timeZone == 'undefined') ? 0 : timeZone;
-
- var timeShift = timeZone *
- SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR];
-
- var date2 = new Date(date.getTime() + timeShift);
-
- switch(intervalUnit) {
- case SimileAjax.DateTime.MILLISECOND:
- date2.setTime(date2.getTime() + 1)
- break;
- case SimileAjax.DateTime.SECOND:
- date2.setTime(date2.getTime() + 1000);
- break;
- case SimileAjax.DateTime.MINUTE:
- date2.setTime(date2.getTime() +
- SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.MINUTE]);
- break;
- case SimileAjax.DateTime.HOUR:
- date2.setTime(date2.getTime() +
- SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR]);
- break;
- case SimileAjax.DateTime.DAY:
- date2.setUTCDate(date2.getUTCDate() + 1);
- break;
- case SimileAjax.DateTime.WEEK:
- date2.setUTCDate(date2.getUTCDate() + 7);
- break;
- case SimileAjax.DateTime.MONTH:
- date2.setUTCMonth(date2.getUTCMonth() + 1);
- break;
- case SimileAjax.DateTime.YEAR:
- date2.setUTCFullYear(date2.getUTCFullYear() + 1);
- break;
- case SimileAjax.DateTime.DECADE:
- date2.setUTCFullYear(date2.getUTCFullYear() + 10);
- break;
- case SimileAjax.DateTime.CENTURY:
- date2.setUTCFullYear(date2.getUTCFullYear() + 100);
- break;
- case SimileAjax.DateTime.MILLENNIUM:
- date2.setUTCFullYear(date2.getUTCFullYear() + 1000);
- break;
- }
-
- date.setTime(date2.getTime() - timeShift);
-};
-
-/**
- * Returns a new date object with the given time offset removed.
- *
- * @param {Date} date the starting date
- * @param {Number} timeZone a timezone specified in an hour offset to remove
- * @return {Date} a new date object with the offset removed
- */
-SimileAjax.DateTime.removeTimeZoneOffset = function(date, timeZone) {
- return new Date(date.getTime() +
- timeZone * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR]);
-};
-
-/**
- * Returns the timezone of the user's browser.
- *
- * @return {Number} the timezone in the user's locale in hours
- */
-SimileAjax.DateTime.getTimezone = function() {
- var d = new Date().getTimezoneOffset();
- return d / -60;
-};
-/*
- * String Utility Functions and Constants
- *
- */
-
-String.prototype.trim = function() {
- return this.replace(/^\s+|\s+$/g, '');
-};
-
-String.prototype.startsWith = function(prefix) {
- return this.length >= prefix.length && this.substr(0, prefix.length) == prefix;
-};
-
-String.prototype.endsWith = function(suffix) {
- return this.length >= suffix.length && this.substr(this.length - suffix.length) == suffix;
-};
-
-String.substitute = function(s, objects) {
- var result = "";
- var start = 0;
- while (start < s.length - 1) {
- var percent = s.indexOf("%", start);
- if (percent < 0 || percent == s.length - 1) {
- break;
- } else if (percent > start && s.charAt(percent - 1) == "\\") {
- result += s.substring(start, percent - 1) + "%";
- start = percent + 1;
- } else {
- var n = parseInt(s.charAt(percent + 1));
- if (isNaN(n) || n >= objects.length) {
- result += s.substring(start, percent + 2);
- } else {
- result += s.substring(start, percent) + objects[n].toString();
- }
- start = percent + 2;
- }
- }
-
- if (start < s.length) {
- result += s.substring(start);
- }
- return result;
-};
-/*
- * HTML Utility Functions
- *
- */
-
-SimileAjax.HTML = new Object();
-
-SimileAjax.HTML._e2uHash = {};
-(function() {
- var e2uHash = SimileAjax.HTML._e2uHash;
- e2uHash['nbsp']= '\u00A0[space]';
- e2uHash['iexcl']= '\u00A1';
- e2uHash['cent']= '\u00A2';
- e2uHash['pound']= '\u00A3';
- e2uHash['curren']= '\u00A4';
- e2uHash['yen']= '\u00A5';
- e2uHash['brvbar']= '\u00A6';
- e2uHash['sect']= '\u00A7';
- e2uHash['uml']= '\u00A8';
- e2uHash['copy']= '\u00A9';
- e2uHash['ordf']= '\u00AA';
- e2uHash['laquo']= '\u00AB';
- e2uHash['not']= '\u00AC';
- e2uHash['shy']= '\u00AD';
- e2uHash['reg']= '\u00AE';
- e2uHash['macr']= '\u00AF';
- e2uHash['deg']= '\u00B0';
- e2uHash['plusmn']= '\u00B1';
- e2uHash['sup2']= '\u00B2';
- e2uHash['sup3']= '\u00B3';
- e2uHash['acute']= '\u00B4';
- e2uHash['micro']= '\u00B5';
- e2uHash['para']= '\u00B6';
- e2uHash['middot']= '\u00B7';
- e2uHash['cedil']= '\u00B8';
- e2uHash['sup1']= '\u00B9';
- e2uHash['ordm']= '\u00BA';
- e2uHash['raquo']= '\u00BB';
- e2uHash['frac14']= '\u00BC';
- e2uHash['frac12']= '\u00BD';
- e2uHash['frac34']= '\u00BE';
- e2uHash['iquest']= '\u00BF';
- e2uHash['Agrave']= '\u00C0';
- e2uHash['Aacute']= '\u00C1';
- e2uHash['Acirc']= '\u00C2';
- e2uHash['Atilde']= '\u00C3';
- e2uHash['Auml']= '\u00C4';
- e2uHash['Aring']= '\u00C5';
- e2uHash['AElig']= '\u00C6';
- e2uHash['Ccedil']= '\u00C7';
- e2uHash['Egrave']= '\u00C8';
- e2uHash['Eacute']= '\u00C9';
- e2uHash['Ecirc']= '\u00CA';
- e2uHash['Euml']= '\u00CB';
- e2uHash['Igrave']= '\u00CC';
- e2uHash['Iacute']= '\u00CD';
- e2uHash['Icirc']= '\u00CE';
- e2uHash['Iuml']= '\u00CF';
- e2uHash['ETH']= '\u00D0';
- e2uHash['Ntilde']= '\u00D1';
- e2uHash['Ograve']= '\u00D2';
- e2uHash['Oacute']= '\u00D3';
- e2uHash['Ocirc']= '\u00D4';
- e2uHash['Otilde']= '\u00D5';
- e2uHash['Ouml']= '\u00D6';
- e2uHash['times']= '\u00D7';
- e2uHash['Oslash']= '\u00D8';
- e2uHash['Ugrave']= '\u00D9';
- e2uHash['Uacute']= '\u00DA';
- e2uHash['Ucirc']= '\u00DB';
- e2uHash['Uuml']= '\u00DC';
- e2uHash['Yacute']= '\u00DD';
- e2uHash['THORN']= '\u00DE';
- e2uHash['szlig']= '\u00DF';
- e2uHash['agrave']= '\u00E0';
- e2uHash['aacute']= '\u00E1';
- e2uHash['acirc']= '\u00E2';
- e2uHash['atilde']= '\u00E3';
- e2uHash['auml']= '\u00E4';
- e2uHash['aring']= '\u00E5';
- e2uHash['aelig']= '\u00E6';
- e2uHash['ccedil']= '\u00E7';
- e2uHash['egrave']= '\u00E8';
- e2uHash['eacute']= '\u00E9';
- e2uHash['ecirc']= '\u00EA';
- e2uHash['euml']= '\u00EB';
- e2uHash['igrave']= '\u00EC';
- e2uHash['iacute']= '\u00ED';
- e2uHash['icirc']= '\u00EE';
- e2uHash['iuml']= '\u00EF';
- e2uHash['eth']= '\u00F0';
- e2uHash['ntilde']= '\u00F1';
- e2uHash['ograve']= '\u00F2';
- e2uHash['oacute']= '\u00F3';
- e2uHash['ocirc']= '\u00F4';
- e2uHash['otilde']= '\u00F5';
- e2uHash['ouml']= '\u00F6';
- e2uHash['divide']= '\u00F7';
- e2uHash['oslash']= '\u00F8';
- e2uHash['ugrave']= '\u00F9';
- e2uHash['uacute']= '\u00FA';
- e2uHash['ucirc']= '\u00FB';
- e2uHash['uuml']= '\u00FC';
- e2uHash['yacute']= '\u00FD';
- e2uHash['thorn']= '\u00FE';
- e2uHash['yuml']= '\u00FF';
- e2uHash['quot']= '\u0022';
- e2uHash['amp']= '\u0026';
- e2uHash['lt']= '\u003C';
- e2uHash['gt']= '\u003E';
- e2uHash['OElig']= '';
- e2uHash['oelig']= '\u0153';
- e2uHash['Scaron']= '\u0160';
- e2uHash['scaron']= '\u0161';
- e2uHash['Yuml']= '\u0178';
- e2uHash['circ']= '\u02C6';
- e2uHash['tilde']= '\u02DC';
- e2uHash['ensp']= '\u2002';
- e2uHash['emsp']= '\u2003';
- e2uHash['thinsp']= '\u2010';
- e2uHash['zwnj']= '\u200C';
- e2uHash['zwj']= '\u200D';
- e2uHash['lrm']= '\u200E';
- e2uHash['rlm']= '\u200F';
- e2uHash['ndash']= '\u2013';
- e2uHash['mdash']= '\u2014';
- e2uHash['lsquo']= '\u2018';
- e2uHash['rsquo']= '\u2019';
- e2uHash['sbquo']= '\u201A';
- e2uHash['ldquo']= '\u201C';
- e2uHash['rdquo']= '\u201D';
- e2uHash['bdquo']= '\u201E';
- e2uHash['dagger']= '\u2020';
- e2uHash['Dagger']= '\u2021';
- e2uHash['permil']= '\u2030';
- e2uHash['lsaquo']= '\u2039';
- e2uHash['rsaquo']= '\u203A';
- e2uHash['euro']= '\u20AC';
- e2uHash['fnof']= '\u0192';
- e2uHash['Alpha']= '\u0391';
- e2uHash['Beta']= '\u0392';
- e2uHash['Gamma']= '\u0393';
- e2uHash['Delta']= '\u0394';
- e2uHash['Epsilon']= '\u0395';
- e2uHash['Zeta']= '\u0396';
- e2uHash['Eta']= '\u0397';
- e2uHash['Theta']= '\u0398';
- e2uHash['Iota']= '\u0399';
- e2uHash['Kappa']= '\u039A';
- e2uHash['Lambda']= '\u039B';
- e2uHash['Mu']= '\u039C';
- e2uHash['Nu']= '\u039D';
- e2uHash['Xi']= '\u039E';
- e2uHash['Omicron']= '\u039F';
- e2uHash['Pi']= '\u03A0';
- e2uHash['Rho']= '\u03A1';
- e2uHash['Sigma']= '\u03A3';
- e2uHash['Tau']= '\u03A4';
- e2uHash['Upsilon']= '\u03A5';
- e2uHash['Phi']= '\u03A6';
- e2uHash['Chi']= '\u03A7';
- e2uHash['Psi']= '\u03A8';
- e2uHash['Omega']= '\u03A9';
- e2uHash['alpha']= '\u03B1';
- e2uHash['beta']= '\u03B2';
- e2uHash['gamma']= '\u03B3';
- e2uHash['delta']= '\u03B4';
- e2uHash['epsilon']= '\u03B5';
- e2uHash['zeta']= '\u03B6';
- e2uHash['eta']= '\u03B7';
- e2uHash['theta']= '\u03B8';
- e2uHash['iota']= '\u03B9';
- e2uHash['kappa']= '\u03BA';
- e2uHash['lambda']= '\u03BB';
- e2uHash['mu']= '\u03BC';
- e2uHash['nu']= '\u03BD';
- e2uHash['xi']= '\u03BE';
- e2uHash['omicron']= '\u03BF';
- e2uHash['pi']= '\u03C0';
- e2uHash['rho']= '\u03C1';
- e2uHash['sigmaf']= '\u03C2';
- e2uHash['sigma']= '\u03C3';
- e2uHash['tau']= '\u03C4';
- e2uHash['upsilon']= '\u03C5';
- e2uHash['phi']= '\u03C6';
- e2uHash['chi']= '\u03C7';
- e2uHash['psi']= '\u03C8';
- e2uHash['omega']= '\u03C9';
- e2uHash['thetasym']= '\u03D1';
- e2uHash['upsih']= '\u03D2';
- e2uHash['piv']= '\u03D6';
- e2uHash['bull']= '\u2022';
- e2uHash['hellip']= '\u2026';
- e2uHash['prime']= '\u2032';
- e2uHash['Prime']= '\u2033';
- e2uHash['oline']= '\u203E';
- e2uHash['frasl']= '\u2044';
- e2uHash['weierp']= '\u2118';
- e2uHash['image']= '\u2111';
- e2uHash['real']= '\u211C';
- e2uHash['trade']= '\u2122';
- e2uHash['alefsym']= '\u2135';
- e2uHash['larr']= '\u2190';
- e2uHash['uarr']= '\u2191';
- e2uHash['rarr']= '\u2192';
- e2uHash['darr']= '\u2193';
- e2uHash['harr']= '\u2194';
- e2uHash['crarr']= '\u21B5';
- e2uHash['lArr']= '\u21D0';
- e2uHash['uArr']= '\u21D1';
- e2uHash['rArr']= '\u21D2';
- e2uHash['dArr']= '\u21D3';
- e2uHash['hArr']= '\u21D4';
- e2uHash['forall']= '\u2200';
- e2uHash['part']= '\u2202';
- e2uHash['exist']= '\u2203';
- e2uHash['empty']= '\u2205';
- e2uHash['nabla']= '\u2207';
- e2uHash['isin']= '\u2208';
- e2uHash['notin']= '\u2209';
- e2uHash['ni']= '\u220B';
- e2uHash['prod']= '\u220F';
- e2uHash['sum']= '\u2211';
- e2uHash['minus']= '\u2212';
- e2uHash['lowast']= '\u2217';
- e2uHash['radic']= '\u221A';
- e2uHash['prop']= '\u221D';
- e2uHash['infin']= '\u221E';
- e2uHash['ang']= '\u2220';
- e2uHash['and']= '\u2227';
- e2uHash['or']= '\u2228';
- e2uHash['cap']= '\u2229';
- e2uHash['cup']= '\u222A';
- e2uHash['int']= '\u222B';
- e2uHash['there4']= '\u2234';
- e2uHash['sim']= '\u223C';
- e2uHash['cong']= '\u2245';
- e2uHash['asymp']= '\u2248';
- e2uHash['ne']= '\u2260';
- e2uHash['equiv']= '\u2261';
- e2uHash['le']= '\u2264';
- e2uHash['ge']= '\u2265';
- e2uHash['sub']= '\u2282';
- e2uHash['sup']= '\u2283';
- e2uHash['nsub']= '\u2284';
- e2uHash['sube']= '\u2286';
- e2uHash['supe']= '\u2287';
- e2uHash['oplus']= '\u2295';
- e2uHash['otimes']= '\u2297';
- e2uHash['perp']= '\u22A5';
- e2uHash['sdot']= '\u22C5';
- e2uHash['lceil']= '\u2308';
- e2uHash['rceil']= '\u2309';
- e2uHash['lfloor']= '\u230A';
- e2uHash['rfloor']= '\u230B';
- e2uHash['lang']= '\u2329';
- e2uHash['rang']= '\u232A';
- e2uHash['loz']= '\u25CA';
- e2uHash['spades']= '\u2660';
- e2uHash['clubs']= '\u2663';
- e2uHash['hearts']= '\u2665';
- e2uHash['diams']= '\u2666';
-})();
-
-SimileAjax.HTML.deEntify = function(s) {
- var e2uHash = SimileAjax.HTML._e2uHash;
-
- var re = /&(\w+?);/;
- while (re.test(s)) {
- var m = s.match(re);
- s = s.replace(re, e2uHash[m[1]]);
- }
- return s;
-};/**
- * A basic set (in the mathematical sense) data structure
- *
- * @constructor
- * @param {Array or SimileAjax.Set} [a] an initial collection
- */
-SimileAjax.Set = function(a) {
- this._hash = {};
- this._count = 0;
-
- if (a instanceof Array) {
- for (var i = 0; i < a.length; i++) {
- this.add(a[i]);
- }
- } else if (a instanceof SimileAjax.Set) {
- this.addSet(a);
- }
-}
-
-/**
- * Adds the given object to this set, assuming there it does not already exist
- *
- * @param {Object} o the object to add
- * @return {Boolean} true if the object was added, false if not
- */
-SimileAjax.Set.prototype.add = function(o) {
- if (!(o in this._hash)) {
- this._hash[o] = true;
- this._count++;
- return true;
- }
- return false;
-}
-
-/**
- * Adds each element in the given set to this set
- *
- * @param {SimileAjax.Set} set the set of elements to add
- */
-SimileAjax.Set.prototype.addSet = function(set) {
- for (var o in set._hash) {
- this.add(o);
- }
-}
-
-/**
- * Removes the given element from this set
- *
- * @param {Object} o the object to remove
- * @return {Boolean} true if the object was successfully removed,
- * false otherwise
- */
-SimileAjax.Set.prototype.remove = function(o) {
- if (o in this._hash) {
- delete this._hash[o];
- this._count--;
- return true;
- }
- return false;
-}
-
-/**
- * Removes the elements in this set that correspond to the elements in the
- * given set
- *
- * @param {SimileAjax.Set} set the set of elements to remove
- */
-SimileAjax.Set.prototype.removeSet = function(set) {
- for (var o in set._hash) {
- this.remove(o);
- }
-}
-
-/**
- * Removes all elements in this set that are not present in the given set, i.e.
- * modifies this set to the intersection of the two sets
- *
- * @param {SimileAjax.Set} set the set to intersect
- */
-SimileAjax.Set.prototype.retainSet = function(set) {
- for (var o in this._hash) {
- if (!set.contains(o)) {
- delete this._hash[o];
- this._count--;
- }
- }
-}
-
-/**
- * Returns whether or not the given element exists in this set
- *
- * @param {SimileAjax.Set} o the object to test for
- * @return {Boolean} true if the object is present, false otherwise
- */
-SimileAjax.Set.prototype.contains = function(o) {
- return (o in this._hash);
-}
-
-/**
- * Returns the number of elements in this set
- *
- * @return {Number} the number of elements in this set
- */
-SimileAjax.Set.prototype.size = function() {
- return this._count;
-}
-
-/**
- * Returns the elements of this set as an array
- *
- * @return {Array} a new array containing the elements of this set
- */
-SimileAjax.Set.prototype.toArray = function() {
- var a = [];
- for (var o in this._hash) {
- a.push(o);
- }
- return a;
-}
-
-/**
- * Iterates through the elements of this set, order unspecified, executing the
- * given function on each element until the function returns true
- *
- * @param {Function} f a function of form f(element)
- */
-SimileAjax.Set.prototype.visit = function(f) {
- for (var o in this._hash) {
- if (f(o) == true) {
- break;
- }
- }
-}
-
-/**
- * A sorted array data structure
- *
- * @constructor
- */
-SimileAjax.SortedArray = function(compare, initialArray) {
- this._a = (initialArray instanceof Array) ? initialArray : [];
- this._compare = compare;
-};
-
-SimileAjax.SortedArray.prototype.add = function(elmt) {
- var sa = this;
- var index = this.find(function(elmt2) {
- return sa._compare(elmt2, elmt);
- });
-
- if (index < this._a.length) {
- this._a.splice(index, 0, elmt);
- } else {
- this._a.push(elmt);
- }
-};
-
-SimileAjax.SortedArray.prototype.remove = function(elmt) {
- var sa = this;
- var index = this.find(function(elmt2) {
- return sa._compare(elmt2, elmt);
- });
-
- while (index < this._a.length && this._compare(this._a[index], elmt) == 0) {
- if (this._a[index] == elmt) {
- this._a.splice(index, 1);
- return true;
- } else {
- index++;
- }
- }
- return false;
-};
-
-SimileAjax.SortedArray.prototype.removeAll = function() {
- this._a = [];
-};
-
-SimileAjax.SortedArray.prototype.elementAt = function(index) {
- return this._a[index];
-};
-
-SimileAjax.SortedArray.prototype.length = function() {
- return this._a.length;
-};
-
-SimileAjax.SortedArray.prototype.find = function(compare) {
- var a = 0;
- var b = this._a.length;
-
- while (a < b) {
- var mid = Math.floor((a + b) / 2);
- var c = compare(this._a[mid]);
- if (mid == a) {
- return c < 0 ? a+1 : a;
- } else if (c < 0) {
- a = mid;
- } else {
- b = mid;
- }
- }
- return a;
-};
-
-SimileAjax.SortedArray.prototype.getFirst = function() {
- return (this._a.length > 0) ? this._a[0] : null;
-};
-
-SimileAjax.SortedArray.prototype.getLast = function() {
- return (this._a.length > 0) ? this._a[this._a.length - 1] : null;
-};
-
-/*
- * Event Index
- *
- */
-
-SimileAjax.EventIndex = function(unit) {
- var eventIndex = this;
-
- this._unit = (unit != null) ? unit : SimileAjax.NativeDateUnit;
- this._events = new SimileAjax.SortedArray(
- function(event1, event2) {
- return eventIndex._unit.compare(event1.getStart(), event2.getStart());
- }
- );
- this._idToEvent = {};
- this._indexed = true;
-};
-
-SimileAjax.EventIndex.prototype.getUnit = function() {
- return this._unit;
-};
-
-SimileAjax.EventIndex.prototype.getEvent = function(id) {
- return this._idToEvent[id];
-};
-
-SimileAjax.EventIndex.prototype.add = function(evt) {
- this._events.add(evt);
- this._idToEvent[evt.getID()] = evt;
- this._indexed = false;
-};
-
-SimileAjax.EventIndex.prototype.removeAll = function() {
- this._events.removeAll();
- this._idToEvent = {};
- this._indexed = false;
-};
-
-SimileAjax.EventIndex.prototype.getCount = function() {
- return this._events.length();
-};
-
-SimileAjax.EventIndex.prototype.getIterator = function(startDate, endDate) {
- if (!this._indexed) {
- this._index();
- }
- return new SimileAjax.EventIndex._Iterator(this._events, startDate, endDate, this._unit);
-};
-
-SimileAjax.EventIndex.prototype.getReverseIterator = function(startDate, endDate) {
- if (!this._indexed) {
- this._index();
- }
- return new SimileAjax.EventIndex._ReverseIterator(this._events, startDate, endDate, this._unit);
-};
-
-SimileAjax.EventIndex.prototype.getAllIterator = function() {
- return new SimileAjax.EventIndex._AllIterator(this._events);
-};
-
-SimileAjax.EventIndex.prototype.getEarliestDate = function() {
- var evt = this._events.getFirst();
- return (evt == null) ? null : evt.getStart();
-};
-
-SimileAjax.EventIndex.prototype.getLatestDate = function() {
- var evt = this._events.getLast();
- if (evt == null) {
- return null;
- }
-
- if (!this._indexed) {
- this._index();
- }
-
- var index = evt._earliestOverlapIndex;
- var date = this._events.elementAt(index).getEnd();
- for (var i = index + 1; i < this._events.length(); i++) {
- date = this._unit.later(date, this._events.elementAt(i).getEnd());
- }
-
- return date;
-};
-
-SimileAjax.EventIndex.prototype._index = function() {
- /*
- * For each event, we want to find the earliest preceding
- * event that overlaps with it, if any.
- */
-
- var l = this._events.length();
- for (var i = 0; i < l; i++) {
- var evt = this._events.elementAt(i);
- evt._earliestOverlapIndex = i;
- }
-
- var toIndex = 1;
- for (var i = 0; i < l; i++) {
- var evt = this._events.elementAt(i);
- var end = evt.getEnd();
-
- toIndex = Math.max(toIndex, i + 1);
- while (toIndex < l) {
- var evt2 = this._events.elementAt(toIndex);
- var start2 = evt2.getStart();
-
- if (this._unit.compare(start2, end) < 0) {
- evt2._earliestOverlapIndex = i;
- toIndex++;
- } else {
- break;
- }
- }
- }
- this._indexed = true;
-};
-
-SimileAjax.EventIndex._Iterator = function(events, startDate, endDate, unit) {
- this._events = events;
- this._startDate = startDate;
- this._endDate = endDate;
- this._unit = unit;
-
- this._currentIndex = events.find(function(evt) {
- return unit.compare(evt.getStart(), startDate);
- });
- if (this._currentIndex - 1 >= 0) {
- this._currentIndex = this._events.elementAt(this._currentIndex - 1)._earliestOverlapIndex;
- }
- this._currentIndex--;
-
- this._maxIndex = events.find(function(evt) {
- return unit.compare(evt.getStart(), endDate);
- });
-
- this._hasNext = false;
- this._next = null;
- this._findNext();
-};
-
-SimileAjax.EventIndex._Iterator.prototype = {
- hasNext: function() { return this._hasNext; },
- next: function() {
- if (this._hasNext) {
- var next = this._next;
- this._findNext();
-
- return next;
- } else {
- return null;
- }
- },
- _findNext: function() {
- var unit = this._unit;
- while ((++this._currentIndex) < this._maxIndex) {
- var evt = this._events.elementAt(this._currentIndex);
- if (unit.compare(evt.getStart(), this._endDate) < 0 &&
- unit.compare(evt.getEnd(), this._startDate) > 0) {
-
- this._next = evt;
- this._hasNext = true;
- return;
- }
- }
- this._next = null;
- this._hasNext = false;
- }
-};
-
-SimileAjax.EventIndex._ReverseIterator = function(events, startDate, endDate, unit) {
- this._events = events;
- this._startDate = startDate;
- this._endDate = endDate;
- this._unit = unit;
-
- this._minIndex = events.find(function(evt) {
- return unit.compare(evt.getStart(), startDate);
- });
- if (this._minIndex - 1 >= 0) {
- this._minIndex = this._events.elementAt(this._minIndex - 1)._earliestOverlapIndex;
- }
-
- this._maxIndex = events.find(function(evt) {
- return unit.compare(evt.getStart(), endDate);
- });
-
- this._currentIndex = this._maxIndex;
- this._hasNext = false;
- this._next = null;
- this._findNext();
-};
-
-SimileAjax.EventIndex._ReverseIterator.prototype = {
- hasNext: function() { return this._hasNext; },
- next: function() {
- if (this._hasNext) {
- var next = this._next;
- this._findNext();
-
- return next;
- } else {
- return null;
- }
- },
- _findNext: function() {
- var unit = this._unit;
- while ((--this._currentIndex) >= this._minIndex) {
- var evt = this._events.elementAt(this._currentIndex);
- if (unit.compare(evt.getStart(), this._endDate) < 0 &&
- unit.compare(evt.getEnd(), this._startDate) > 0) {
-
- this._next = evt;
- this._hasNext = true;
- return;
- }
- }
- this._next = null;
- this._hasNext = false;
- }
-};
-
-SimileAjax.EventIndex._AllIterator = function(events) {
- this._events = events;
- this._index = 0;
-};
-
-SimileAjax.EventIndex._AllIterator.prototype = {
- hasNext: function() {
- return this._index < this._events.length();
- },
- next: function() {
- return this._index < this._events.length() ?
- this._events.elementAt(this._index++) : null;
- }
-};/*
- * Default Unit
- *
- */
-
-SimileAjax.NativeDateUnit = new Object();
-
-SimileAjax.NativeDateUnit.makeDefaultValue = function() {
- return new Date();
-};
-
-SimileAjax.NativeDateUnit.cloneValue = function(v) {
- return new Date(v.getTime());
-};
-
-SimileAjax.NativeDateUnit.getParser = function(format) {
- if (typeof format == "string") {
- format = format.toLowerCase();
- }
- return (format == "iso8601" || format == "iso 8601") ?
- SimileAjax.DateTime.parseIso8601DateTime :
- SimileAjax.DateTime.parseGregorianDateTime;
-};
-
-SimileAjax.NativeDateUnit.parseFromObject = function(o) {
- return SimileAjax.DateTime.parseGregorianDateTime(o);
-};
-
-SimileAjax.NativeDateUnit.toNumber = function(v) {
- return v.getTime();
-};
-
-SimileAjax.NativeDateUnit.fromNumber = function(n) {
- return new Date(n);
-};
-
-SimileAjax.NativeDateUnit.compare = function(v1, v2) {
- var n1, n2;
- if (typeof v1 == "object") {
- n1 = v1.getTime();
- } else {
- n1 = Number(v1);
- }
- if (typeof v2 == "object") {
- n2 = v2.getTime();
- } else {
- n2 = Number(v2);
- }
-
- return n1 - n2;
-};
-
-SimileAjax.NativeDateUnit.earlier = function(v1, v2) {
- return SimileAjax.NativeDateUnit.compare(v1, v2) < 0 ? v1 : v2;
-};
-
-SimileAjax.NativeDateUnit.later = function(v1, v2) {
- return SimileAjax.NativeDateUnit.compare(v1, v2) > 0 ? v1 : v2;
-};
-
-SimileAjax.NativeDateUnit.change = function(v, n) {
- return new Date(v.getTime() + n);
-};
-
-/*
- * General, miscellaneous SimileAjax stuff
- *
- */
-
-SimileAjax.ListenerQueue = function(wildcardHandlerName) {
- this._listeners = [];
- this._wildcardHandlerName = wildcardHandlerName;
-};
-
-SimileAjax.ListenerQueue.prototype.add = function(listener) {
- this._listeners.push(listener);
-};
-
-SimileAjax.ListenerQueue.prototype.remove = function(listener) {
- var listeners = this._listeners;
- for (var i = 0; i < listeners.length; i++) {
- if (listeners[i] == listener) {
- listeners.splice(i, 1);
- break;
- }
- }
-};
-
-SimileAjax.ListenerQueue.prototype.fire = function(handlerName, args) {
- var listeners = [].concat(this._listeners);
- for (var i = 0; i < listeners.length; i++) {
- var listener = listeners[i];
- if (handlerName in listener) {
- try {
- listener[handlerName].apply(listener, args);
- } catch (e) {
- SimileAjax.Debug.exception("Error firing event of name " + handlerName, e);
- }
- } else if (this._wildcardHandlerName != null &&
- this._wildcardHandlerName in listener) {
- try {
- listener[this._wildcardHandlerName].apply(listener, [ handlerName ]);
- } catch (e) {
- SimileAjax.Debug.exception("Error firing event of name " + handlerName + " to wildcard handler", e);
- }
- }
- }
-};
-
-/*
- * History
- *
- * This is a singleton that keeps track of undoable user actions and
- * performs undos and redos in response to the browser's Back and
- * Forward buttons.
- *
- * Call addAction(action) to register an undoable user action. action
- * must have 4 fields:
- *
- * perform: an argument-less function that carries out the action
- * undo: an argument-less function that undos the action
- * label: a short, user-friendly string describing the action
- * uiLayer: the UI layer on which the action takes place
- *
- * By default, the history keeps track of upto 10 actions. You can
- * configure this behavior by setting
- * SimileAjax.History.maxHistoryLength
- * to a different number.
- *
- * An iframe is inserted into the document's body element to track
- * onload events.
- *
- */
-
-SimileAjax.History = {
- maxHistoryLength: 10,
- historyFile: "__history__.html",
- enabled: true,
-
- _initialized: false,
- _listeners: new SimileAjax.ListenerQueue(),
-
- _actions: [],
- _baseIndex: 0,
- _currentIndex: 0,
-
- _plainDocumentTitle: document.title
-};
-
-SimileAjax.History.formatHistoryEntryTitle = function(actionLabel) {
- return SimileAjax.History._plainDocumentTitle + " {" + actionLabel + "}";
-};
-
-SimileAjax.History.initialize = function() {
- if (SimileAjax.History._initialized) {
- return;
- }
-
- if (SimileAjax.History.enabled) {
- var iframe = document.createElement("iframe");
- iframe.id = "simile-ajax-history";
- iframe.style.position = "absolute";
- iframe.style.width = "10px";
- iframe.style.height = "10px";
- iframe.style.top = "0px";
- iframe.style.left = "0px";
- iframe.style.visibility = "hidden";
- iframe.src = SimileAjax.History.historyFile + "?0";
-
- document.body.appendChild(iframe);
- SimileAjax.DOM.registerEvent(iframe, "load", SimileAjax.History._handleIFrameOnLoad);
-
- SimileAjax.History._iframe = iframe;
- }
- SimileAjax.History._initialized = true;
-};
-
-SimileAjax.History.addListener = function(listener) {
- SimileAjax.History.initialize();
-
- SimileAjax.History._listeners.add(listener);
-};
-
-SimileAjax.History.removeListener = function(listener) {
- SimileAjax.History.initialize();
-
- SimileAjax.History._listeners.remove(listener);
-};
-
-SimileAjax.History.addAction = function(action) {
- SimileAjax.History.initialize();
-
- SimileAjax.History._listeners.fire("onBeforePerform", [ action ]);
- window.setTimeout(function() {
- try {
- action.perform();
- SimileAjax.History._listeners.fire("onAfterPerform", [ action ]);
-
- if (SimileAjax.History.enabled) {
- SimileAjax.History._actions = SimileAjax.History._actions.slice(
- 0, SimileAjax.History._currentIndex - SimileAjax.History._baseIndex);
-
- SimileAjax.History._actions.push(action);
- SimileAjax.History._currentIndex++;
-
- var diff = SimileAjax.History._actions.length - SimileAjax.History.maxHistoryLength;
- if (diff > 0) {
- SimileAjax.History._actions = SimileAjax.History._actions.slice(diff);
- SimileAjax.History._baseIndex += diff;
- }
-
- try {
- SimileAjax.History._iframe.contentWindow.location.search =
- "?" + SimileAjax.History._currentIndex;
- } catch (e) {
- /*
- * We can't modify location.search most probably because it's a file:// url.
- * We'll just going to modify the document's title.
- */
- var title = SimileAjax.History.formatHistoryEntryTitle(action.label);
- document.title = title;
- }
- }
- } catch (e) {
- SimileAjax.Debug.exception(e, "Error adding action {" + action.label + "} to history");
- }
- }, 0);
-};
-
-SimileAjax.History.addLengthyAction = function(perform, undo, label) {
- SimileAjax.History.addAction({
- perform: perform,
- undo: undo,
- label: label,
- uiLayer: SimileAjax.WindowManager.getBaseLayer(),
- lengthy: true
- });
-};
-
-SimileAjax.History._handleIFrameOnLoad = function() {
- /*
- * This function is invoked when the user herself
- * navigates backward or forward. We need to adjust
- * the application's state accordingly.
- */
-
- try {
- var q = SimileAjax.History._iframe.contentWindow.location.search;
- var c = (q.length == 0) ? 0 : Math.max(0, parseInt(q.substr(1)));
-
- var finishUp = function() {
- var diff = c - SimileAjax.History._currentIndex;
- SimileAjax.History._currentIndex += diff;
- SimileAjax.History._baseIndex += diff;
-
- SimileAjax.History._iframe.contentWindow.location.search = "?" + c;
- };
-
- if (c < SimileAjax.History._currentIndex) { // need to undo
- SimileAjax.History._listeners.fire("onBeforeUndoSeveral", []);
- window.setTimeout(function() {
- while (SimileAjax.History._currentIndex > c &&
- SimileAjax.History._currentIndex > SimileAjax.History._baseIndex) {
-
- SimileAjax.History._currentIndex--;
-
- var action = SimileAjax.History._actions[SimileAjax.History._currentIndex - SimileAjax.History._baseIndex];
-
- try {
- action.undo();
- } catch (e) {
- SimileAjax.Debug.exception(e, "History: Failed to undo action {" + action.label + "}");
- }
- }
-
- SimileAjax.History._listeners.fire("onAfterUndoSeveral", []);
- finishUp();
- }, 0);
- } else if (c > SimileAjax.History._currentIndex) { // need to redo
- SimileAjax.History._listeners.fire("onBeforeRedoSeveral", []);
- window.setTimeout(function() {
- while (SimileAjax.History._currentIndex < c &&
- SimileAjax.History._currentIndex - SimileAjax.History._baseIndex < SimileAjax.History._actions.length) {
-
- var action = SimileAjax.History._actions[SimileAjax.History._currentIndex - SimileAjax.History._baseIndex];
-
- try {
- action.perform();
- } catch (e) {
- SimileAjax.Debug.exception(e, "History: Failed to redo action {" + action.label + "}");
- }
-
- SimileAjax.History._currentIndex++;
- }
-
- SimileAjax.History._listeners.fire("onAfterRedoSeveral", []);
- finishUp();
- }, 0);
- } else {
- var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex - 1;
- var title = (index >= 0 && index < SimileAjax.History._actions.length) ?
- SimileAjax.History.formatHistoryEntryTitle(SimileAjax.History._actions[index].label) :
- SimileAjax.History._plainDocumentTitle;
-
- SimileAjax.History._iframe.contentWindow.document.title = title;
- document.title = title;
- }
- } catch (e) {
- // silent
- }
-};
-
-SimileAjax.History.getNextUndoAction = function() {
- try {
- var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex - 1;
- return SimileAjax.History._actions[index];
- } catch (e) {
- return null;
- }
-};
-
-SimileAjax.History.getNextRedoAction = function() {
- try {
- var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex;
- return SimileAjax.History._actions[index];
- } catch (e) {
- return null;
- }
-};
-/**
- * @fileOverview UI layers and window-wide dragging
- * @name SimileAjax.WindowManager
- */
-
-/**
- * This is a singleton that keeps track of UI layers (modal and
- * modeless) and enables/disables UI elements based on which layers
- * they belong to. It also provides window-wide dragging
- * implementation.
- */
-SimileAjax.WindowManager = {
- _initialized: false,
- _listeners: [],
-
- _draggedElement: null,
- _draggedElementCallback: null,
- _dropTargetHighlightElement: null,
- _lastCoords: null,
- _ghostCoords: null,
- _draggingMode: "",
- _dragging: false,
-
- _layers: []
-};
-
-SimileAjax.WindowManager.initialize = function() {
- if (SimileAjax.WindowManager._initialized) {
- return;
- }
-
- SimileAjax.DOM.registerEvent(document.body, "mousedown", SimileAjax.WindowManager._onBodyMouseDown);
- SimileAjax.DOM.registerEvent(document.body, "mousemove", SimileAjax.WindowManager._onBodyMouseMove);
- SimileAjax.DOM.registerEvent(document.body, "mouseup", SimileAjax.WindowManager._onBodyMouseUp);
- SimileAjax.DOM.registerEvent(document, "keydown", SimileAjax.WindowManager._onBodyKeyDown);
- SimileAjax.DOM.registerEvent(document, "keyup", SimileAjax.WindowManager._onBodyKeyUp);
-
- SimileAjax.WindowManager._layers.push({index: 0});
-
- SimileAjax.WindowManager._historyListener = {
- onBeforeUndoSeveral: function() {},
- onAfterUndoSeveral: function() {},
- onBeforeUndo: function() {},
- onAfterUndo: function() {},
-
- onBeforeRedoSeveral: function() {},
- onAfterRedoSeveral: function() {},
- onBeforeRedo: function() {},
- onAfterRedo: function() {}
- };
- SimileAjax.History.addListener(SimileAjax.WindowManager._historyListener);
-
- SimileAjax.WindowManager._initialized = true;
-};
-
-SimileAjax.WindowManager.getBaseLayer = function() {
- SimileAjax.WindowManager.initialize();
- return SimileAjax.WindowManager._layers[0];
-};
-
-SimileAjax.WindowManager.getHighestLayer = function() {
- SimileAjax.WindowManager.initialize();
- return SimileAjax.WindowManager._layers[SimileAjax.WindowManager._layers.length - 1];
-};
-
-SimileAjax.WindowManager.registerEventWithObject = function(elmt, eventName, obj, handlerName, layer) {
- SimileAjax.WindowManager.registerEvent(
- elmt,
- eventName,
- function(elmt2, evt, target) {
- return obj[handlerName].call(obj, elmt2, evt, target);
- },
- layer
- );
-};
-
-SimileAjax.WindowManager.registerEvent = function(elmt, eventName, handler, layer) {
- if (layer == null) {
- layer = SimileAjax.WindowManager.getHighestLayer();
- }
-
- var handler2 = function(elmt, evt, target) {
- if (SimileAjax.WindowManager._canProcessEventAtLayer(layer)) {
- SimileAjax.WindowManager._popToLayer(layer.index);
- try {
- handler(elmt, evt, target);
- } catch (e) {
- SimileAjax.Debug.exception(e);
- }
- }
- SimileAjax.DOM.cancelEvent(evt);
- return false;
- }
-
- SimileAjax.DOM.registerEvent(elmt, eventName, handler2);
-};
-
-SimileAjax.WindowManager.pushLayer = function(f, ephemeral, elmt) {
- var layer = { onPop: f, index: SimileAjax.WindowManager._layers.length, ephemeral: (ephemeral), elmt: elmt };
- SimileAjax.WindowManager._layers.push(layer);
-
- return layer;
-};
-
-SimileAjax.WindowManager.popLayer = function(layer) {
- for (var i = 1; i < SimileAjax.WindowManager._layers.length; i++) {
- if (SimileAjax.WindowManager._layers[i] == layer) {
- SimileAjax.WindowManager._popToLayer(i - 1);
- break;
- }
- }
-};
-
-SimileAjax.WindowManager.popAllLayers = function() {
- SimileAjax.WindowManager._popToLayer(0);
-};
-
-SimileAjax.WindowManager.registerForDragging = function(elmt, callback, layer) {
- SimileAjax.WindowManager.registerEvent(
- elmt,
- "mousedown",
- function(elmt, evt, target) {
- SimileAjax.WindowManager._handleMouseDown(elmt, evt, callback);
- },
- layer
- );
-};
-
-SimileAjax.WindowManager._popToLayer = function(level) {
- while (level+1 < SimileAjax.WindowManager._layers.length) {
- try {
- var layer = SimileAjax.WindowManager._layers.pop();
- if (layer.onPop != null) {
- layer.onPop();
- }
- } catch (e) {
- }
- }
-};
-
-SimileAjax.WindowManager._canProcessEventAtLayer = function(layer) {
- if (layer.index == (SimileAjax.WindowManager._layers.length - 1)) {
- return true;
- }
- for (var i = layer.index + 1; i < SimileAjax.WindowManager._layers.length; i++) {
- if (!SimileAjax.WindowManager._layers[i].ephemeral) {
- return false;
- }
- }
- return true;
-};
-
-SimileAjax.WindowManager.cancelPopups = function(evt) {
- var evtCoords = (evt) ? SimileAjax.DOM.getEventPageCoordinates(evt) : { x: -1, y: -1 };
-
- var i = SimileAjax.WindowManager._layers.length - 1;
- while (i > 0 && SimileAjax.WindowManager._layers[i].ephemeral) {
- var layer = SimileAjax.WindowManager._layers[i];
- if (layer.elmt != null) { // if event falls within main element of layer then don't cancel
- var elmt = layer.elmt;
- var elmtCoords = SimileAjax.DOM.getPageCoordinates(elmt);
- if (evtCoords.x >= elmtCoords.left && evtCoords.x < (elmtCoords.left + elmt.offsetWidth) &&
- evtCoords.y >= elmtCoords.top && evtCoords.y < (elmtCoords.top + elmt.offsetHeight)) {
- break;
- }
- }
- i--;
- }
- SimileAjax.WindowManager._popToLayer(i);
-};
-
-SimileAjax.WindowManager._onBodyMouseDown = function(elmt, evt, target) {
- if (!("eventPhase" in evt) || evt.eventPhase == evt.BUBBLING_PHASE) {
- SimileAjax.WindowManager.cancelPopups(evt);
- }
-};
-
-SimileAjax.WindowManager._handleMouseDown = function(elmt, evt, callback) {
- SimileAjax.WindowManager._draggedElement = elmt;
- SimileAjax.WindowManager._draggedElementCallback = callback;
- SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
-
- SimileAjax.DOM.cancelEvent(evt);
- return false;
-};
-
-SimileAjax.WindowManager._onBodyKeyDown = function(elmt, evt, target) {
- if (SimileAjax.WindowManager._dragging) {
- if (evt.keyCode == 27) { // esc
- SimileAjax.WindowManager._cancelDragging();
- } else if ((evt.keyCode == 17 || evt.keyCode == 16) && SimileAjax.WindowManager._draggingMode != "copy") {
- SimileAjax.WindowManager._draggingMode = "copy";
-
- var img = SimileAjax.Graphics.createTranslucentImage(SimileAjax.urlPrefix + "images/copy.png");
- img.style.position = "absolute";
- img.style.left = (SimileAjax.WindowManager._ghostCoords.left - 16) + "px";
- img.style.top = (SimileAjax.WindowManager._ghostCoords.top) + "px";
- document.body.appendChild(img);
-
- SimileAjax.WindowManager._draggingModeIndicatorElmt = img;
- }
- }
-};
-
-SimileAjax.WindowManager._onBodyKeyUp = function(elmt, evt, target) {
- if (SimileAjax.WindowManager._dragging) {
- if (evt.keyCode == 17 || evt.keyCode == 16) {
- SimileAjax.WindowManager._draggingMode = "";
- if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
- document.body.removeChild(SimileAjax.WindowManager._draggingModeIndicatorElmt);
- SimileAjax.WindowManager._draggingModeIndicatorElmt = null;
- }
- }
- }
-};
-
-SimileAjax.WindowManager._onBodyMouseMove = function(elmt, evt, target) {
- if (SimileAjax.WindowManager._draggedElement != null) {
- var callback = SimileAjax.WindowManager._draggedElementCallback;
-
- var lastCoords = SimileAjax.WindowManager._lastCoords;
- var diffX = evt.clientX - lastCoords.x;
- var diffY = evt.clientY - lastCoords.y;
-
- if (!SimileAjax.WindowManager._dragging) {
- if (Math.abs(diffX) > 5 || Math.abs(diffY) > 5) {
- try {
- if ("onDragStart" in callback) {
- callback.onDragStart();
- }
-
- if ("ghost" in callback && callback.ghost) {
- var draggedElmt = SimileAjax.WindowManager._draggedElement;
-
- SimileAjax.WindowManager._ghostCoords = SimileAjax.DOM.getPageCoordinates(draggedElmt);
- SimileAjax.WindowManager._ghostCoords.left += diffX;
- SimileAjax.WindowManager._ghostCoords.top += diffY;
-
- var ghostElmt = draggedElmt.cloneNode(true);
- ghostElmt.style.position = "absolute";
- ghostElmt.style.left = SimileAjax.WindowManager._ghostCoords.left + "px";
- ghostElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
- ghostElmt.style.zIndex = 1000;
- SimileAjax.Graphics.setOpacity(ghostElmt, 50);
-
- document.body.appendChild(ghostElmt);
- callback._ghostElmt = ghostElmt;
- }
-
- SimileAjax.WindowManager._dragging = true;
- SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
-
- document.body.focus();
- } catch (e) {
- SimileAjax.Debug.exception("WindowManager: Error handling mouse down", e);
- SimileAjax.WindowManager._cancelDragging();
- }
- }
- } else {
- try {
- SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
-
- if ("onDragBy" in callback) {
- callback.onDragBy(diffX, diffY);
- }
-
- if ("_ghostElmt" in callback) {
- var ghostElmt = callback._ghostElmt;
-
- SimileAjax.WindowManager._ghostCoords.left += diffX;
- SimileAjax.WindowManager._ghostCoords.top += diffY;
-
- ghostElmt.style.left = SimileAjax.WindowManager._ghostCoords.left + "px";
- ghostElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
- if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
- var indicatorElmt = SimileAjax.WindowManager._draggingModeIndicatorElmt;
-
- indicatorElmt.style.left = (SimileAjax.WindowManager._ghostCoords.left - 16) + "px";
- indicatorElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
- }
-
- if ("droppable" in callback && callback.droppable) {
- var coords = SimileAjax.DOM.getEventPageCoordinates(evt);
- var target = SimileAjax.DOM.hittest(
- coords.x, coords.y,
- [ SimileAjax.WindowManager._ghostElmt,
- SimileAjax.WindowManager._dropTargetHighlightElement
- ]
- );
- target = SimileAjax.WindowManager._findDropTarget(target);
-
- if (target != SimileAjax.WindowManager._potentialDropTarget) {
- if (SimileAjax.WindowManager._dropTargetHighlightElement != null) {
- document.body.removeChild(SimileAjax.WindowManager._dropTargetHighlightElement);
-
- SimileAjax.WindowManager._dropTargetHighlightElement = null;
- SimileAjax.WindowManager._potentialDropTarget = null;
- }
-
- var droppable = false;
- if (target != null) {
- if ((!("canDropOn" in callback) || callback.canDropOn(target)) &&
- (!("canDrop" in target) || target.canDrop(SimileAjax.WindowManager._draggedElement))) {
-
- droppable = true;
- }
- }
-
- if (droppable) {
- var border = 4;
- var targetCoords = SimileAjax.DOM.getPageCoordinates(target);
- var highlight = document.createElement("div");
- highlight.style.border = border + "px solid yellow";
- highlight.style.backgroundColor = "yellow";
- highlight.style.position = "absolute";
- highlight.style.left = targetCoords.left + "px";
- highlight.style.top = targetCoords.top + "px";
- highlight.style.width = (target.offsetWidth - border * 2) + "px";
- highlight.style.height = (target.offsetHeight - border * 2) + "px";
- SimileAjax.Graphics.setOpacity(highlight, 30);
- document.body.appendChild(highlight);
-
- SimileAjax.WindowManager._potentialDropTarget = target;
- SimileAjax.WindowManager._dropTargetHighlightElement = highlight;
- }
- }
- }
- }
- } catch (e) {
- SimileAjax.Debug.exception("WindowManager: Error handling mouse move", e);
- SimileAjax.WindowManager._cancelDragging();
- }
- }
-
- SimileAjax.DOM.cancelEvent(evt);
- return false;
- }
-};
-
-SimileAjax.WindowManager._onBodyMouseUp = function(elmt, evt, target) {
- if (SimileAjax.WindowManager._draggedElement != null) {
- try {
- if (SimileAjax.WindowManager._dragging) {
- var callback = SimileAjax.WindowManager._draggedElementCallback;
- if ("onDragEnd" in callback) {
- callback.onDragEnd();
- }
- if ("droppable" in callback && callback.droppable) {
- var dropped = false;
-
- var target = SimileAjax.WindowManager._potentialDropTarget;
- if (target != null) {
- if ((!("canDropOn" in callback) || callback.canDropOn(target)) &&
- (!("canDrop" in target) || target.canDrop(SimileAjax.WindowManager._draggedElement))) {
-
- if ("onDropOn" in callback) {
- callback.onDropOn(target);
- }
- target.ondrop(SimileAjax.WindowManager._draggedElement, SimileAjax.WindowManager._draggingMode);
-
- dropped = true;
- }
- }
-
- if (!dropped) {
- // TODO: do holywood explosion here
- }
- }
- }
- } finally {
- SimileAjax.WindowManager._cancelDragging();
- }
-
- SimileAjax.DOM.cancelEvent(evt);
- return false;
- }
-};
-
-SimileAjax.WindowManager._cancelDragging = function() {
- var callback = SimileAjax.WindowManager._draggedElementCallback;
- if ("_ghostElmt" in callback) {
- var ghostElmt = callback._ghostElmt;
- document.body.removeChild(ghostElmt);
-
- delete callback._ghostElmt;
- }
- if (SimileAjax.WindowManager._dropTargetHighlightElement != null) {
- document.body.removeChild(SimileAjax.WindowManager._dropTargetHighlightElement);
- SimileAjax.WindowManager._dropTargetHighlightElement = null;
- }
- if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
- document.body.removeChild(SimileAjax.WindowManager._draggingModeIndicatorElmt);
- SimileAjax.WindowManager._draggingModeIndicatorElmt = null;
- }
-
- SimileAjax.WindowManager._draggedElement = null;
- SimileAjax.WindowManager._draggedElementCallback = null;
- SimileAjax.WindowManager._potentialDropTarget = null;
- SimileAjax.WindowManager._dropTargetHighlightElement = null;
- SimileAjax.WindowManager._lastCoords = null;
- SimileAjax.WindowManager._ghostCoords = null;
- SimileAjax.WindowManager._draggingMode = "";
- SimileAjax.WindowManager._dragging = false;
-};
-
-SimileAjax.WindowManager._findDropTarget = function(elmt) {
- while (elmt != null) {
- if ("ondrop" in elmt && (typeof elmt.ondrop) == "function") {
- break;
- }
- elmt = elmt.parentNode;
- }
- return elmt;
-};
-/*
- * Timeline API
- *
- * This file will load all the Javascript files
- * necessary to make the standard timeline work.
- * It also detects the default locale.
- *
- * To run from the MIT copy of Timeline:
- * Include this file in your HTML file as follows:
- *
- * <script src="http://api.simile-widgets.org/timeline/2.3.1/timeline-api.js"
- * type="text/javascript"></script>
- *
- *
- * To host the Timeline files on your own server:
- * 1) Install the Timeline and Simile-Ajax files onto your webserver using
- * timeline_libraries.zip or timeline_source.zip
- *
- * 2) Set global js variables used to send parameters to this script:
- * var Timeline_ajax_url -- url for simile-ajax-api.js
- * var Timeline_urlPrefix -- url for the *directory* that contains timeline-api.js
- * Include trailing slash
- * var Timeline_parameters='bundle=true'; // you must set bundle to true if you are using
- * // timeline_libraries.zip since only the
- * // bundled libraries are included
- *
- * eg your html page would include
- *
- * <script>
- * var Timeline_ajax_url="http://YOUR_SERVER/javascripts/timeline/timeline_ajax/simile-ajax-api.js";
- * var Timeline_urlPrefix='http://YOUR_SERVER/javascripts/timeline/timeline_js/';
- * var Timeline_parameters='bundle=true';
- * </script>
- * <script src="http://YOUR_SERVER/javascripts/timeline/timeline_js/timeline-api.js"
- * type="text/javascript">
- * </script>
- *
- * SCRIPT PARAMETERS
- * This script auto-magically figures out locale and has defaults for other parameters
- * To set parameters explicity, set js global variable Timeline_parameters or include as
- * parameters on the url using GET style. Eg the two next lines pass the same parameters:
- * Timeline_parameters='bundle=true'; // pass parameter via js variable
- * <script src="http://....timeline-api.js?bundle=true" // pass parameter via url
- *
- * Parameters
- * timeline-use-local-resources --
- * bundle -- true: use the single js bundle file; false: load individual files (for debugging)
- * locales --
- * defaultLocale --
- * forceLocale -- force locale to be a particular value--used for debugging. Normally locale is determined
- * by browser's and server's locale settings.
- *
- * DEBUGGING
- * If you have a problem with Timeline, the first step is to use the unbundled Javascript files. To do so:
- * To use the unbundled Timeline and Ajax libraries
- * Change
- * <script src="http://api.simile-widgets.org/timeline/2.3.1/api/timeline-api.js?bundle=true" type="text/javascript"></script>
- * To
- * <script>var Timeline_ajax_url = "http://api.simile-widgets.org/ajax/2.2.1/simile-ajax-api.js?bundle=false"</script>
- * <script src="http://api.simile-widgets.org/timeline/2.3.1/api/timeline-api.js?bundle=false" type="text/javascript"></script>
- *
- * Note that the Ajax version is usually NOT the same as the Timeline version.
- * See variable simile_ajax_ver below for the current version
- *
- *
- */
-
-(function() {
-
- var simile_ajax_ver = "2.2.1"; // ===========>>> current Simile-Ajax version
-
- var useLocalResources = false;
- if (document.location.search.length > 0) {
- var params = document.location.search.substr(1).split("&");
- for (var i = 0; i < params.length; i++) {
- if (params[i] == "timeline-use-local-resources") {
- useLocalResources = true;
- }
- }
- };
-
- var loadMe = function() {
- if ("Timeline" in window) {
- return;
- }
-
- window.Timeline = new Object();
- window.Timeline.DateTime = window.SimileAjax.DateTime; // for backward compatibility
-
- var bundle = false;
- var javascriptFiles = [
- "timeline.js",
- "band.js",
- "themes.js",
- "ethers.js",
- "ether-painters.js",
- "event-utils.js",
- "labellers.js",
- "sources.js",
- "original-painter.js",
- "detailed-painter.js",
- "overview-painter.js",
- "compact-painter.js",
- "decorators.js",
- "units.js"
- ];
- var cssFiles = [
- "timeline.css",
- "ethers.css",
- "events.css"
- ];
-
- var localizedJavascriptFiles = [
- "timeline.js",
- "labellers.js"
- ];
- var localizedCssFiles = [
- ];
-
- // ISO-639 language codes, ISO-3166 country codes (2 characters)
- var supportedLocales = [
- "cs", // Czech
- "de", // German
- "en", // English
- "es", // Spanish
- "fr", // French
- "it", // Italian
- "nl", // Dutch (The Netherlands)
- "ru", // Russian
- "se", // Swedish
- "tr", // Turkish
- "vi", // Vietnamese
- "zh" // Chinese
- ];
-
- try {
- var desiredLocales = [ "en" ],
- defaultServerLocale = "en",
- forceLocale = null;
-
- var parseURLParameters = function(parameters) {
- var params = parameters.split("&");
- for (var p = 0; p < params.length; p++) {
- var pair = params[p].split("=");
- if (pair[0] == "locales") {
- desiredLocales = desiredLocales.concat(pair[1].split(","));
- } else if (pair[0] == "defaultLocale") {
- defaultServerLocale = pair[1];
- } else if (pair[0] == "forceLocale") {
- forceLocale = pair[1];
- desiredLocales = desiredLocales.concat(pair[1].split(","));
- } else if (pair[0] == "bundle") {
- bundle = pair[1] != "false";
- }
- }
- };
-
- (function() {
- if (typeof Timeline_urlPrefix == "string") {
- Timeline.urlPrefix = Timeline_urlPrefix;
- if (typeof Timeline_parameters == "string") {
- parseURLParameters(Timeline_parameters);
- }
- } else {
- var heads = document.documentElement.getElementsByTagName("head");
- for (var h = 0; h < heads.length; h++) {
- var scripts = heads[h].getElementsByTagName("script");
- for (var s = 0; s < scripts.length; s++) {
- var url = scripts[s].src;
- var i = url.indexOf("timeline-api.js");
- if (i >= 0) {
- Timeline.urlPrefix = url.substr(0, i);
- var q = url.indexOf("?");
- if (q > 0) {
- parseURLParameters(url.substr(q + 1));
- }
- return;
- }
- }
- }
- throw new Error("Failed to derive URL prefix for Timeline API code files");
- }
- })();
-
- var includeJavascriptFiles = function(urlPrefix, filenames) {
- SimileAjax.includeJavascriptFiles(document, urlPrefix, filenames);
- }
- var includeCssFiles = function(urlPrefix, filenames) {
- SimileAjax.includeCssFiles(document, urlPrefix, filenames);
- }
-
- /*
- * Include non-localized files
- */
- if (bundle) {
- includeJavascriptFiles(Timeline.urlPrefix, [ "timeline-bundle.js" ]);
- includeCssFiles(Timeline.urlPrefix, [ "timeline-bundle.css" ]);
- } else {
- // XXX adim includeJavascriptFiles(Timeline.urlPrefix + "scripts/", javascriptFiles);
- // XXX adim includeCssFiles(Timeline.urlPrefix + "styles/", cssFiles);
- }
-
- /*
- * Include localized files
- */
- var loadLocale = [];
- loadLocale[defaultServerLocale] = true;
-
- var tryExactLocale = function(locale) {
- for (var l = 0; l < supportedLocales.length; l++) {
- if (locale == supportedLocales[l]) {
- loadLocale[locale] = true;
- return true;
- }
- }
- return false;
- }
- var tryLocale = function(locale) {
- if (tryExactLocale(locale)) {
- return locale;
- }
-
- var dash = locale.indexOf("-");
- if (dash > 0 && tryExactLocale(locale.substr(0, dash))) {
- return locale.substr(0, dash);
- }
-
- return null;
- }
-
- for (var l = 0; l < desiredLocales.length; l++) {
- tryLocale(desiredLocales[l]);
- }
-
- var defaultClientLocale = defaultServerLocale;
- var defaultClientLocales = ("language" in navigator ? navigator.language : navigator.browserLanguage).split(";");
- for (var l = 0; l < defaultClientLocales.length; l++) {
- var locale = tryLocale(defaultClientLocales[l]);
- if (locale != null) {
- defaultClientLocale = locale;
- break;
- }
- }
-
- for (var l = 0; l < supportedLocales.length; l++) {
- var locale = supportedLocales[l];
- if (loadLocale[locale]) {
- // XXX adim includeJavascriptFiles(Timeline.urlPrefix + "scripts/l10n/" + locale + "/", localizedJavascriptFiles);
- // XXX adim includeCssFiles(Timeline.urlPrefix + "styles/l10n/" + locale + "/", localizedCssFiles);
- }
- }
-
- if (forceLocale == null) {
- Timeline.serverLocale = defaultServerLocale;
- Timeline.clientLocale = defaultClientLocale;
- } else {
- Timeline.serverLocale = forceLocale;
- Timeline.clientLocale = forceLocale;
- }
- } catch (e) {
- alert(e);
- }
- };
-
- /*
- * Load SimileAjax if it's not already loaded
- */
- if (typeof SimileAjax == "undefined") {
- window.SimileAjax_onLoad = loadMe;
-
- var url = useLocalResources ?
- "http://127.0.0.1:9999/ajax/api/simile-ajax-api.js?bundle=false" :
- "http://api.simile-widgets.org/ajax/" + simile_ajax_ver + "/simile-ajax-api.js";
- if (typeof Timeline_ajax_url == "string") {
- url = Timeline_ajax_url;
- }
- var createScriptElement = function() {
- var script = document.createElement("script");
- script.type = "text/javascript";
- script.language = "JavaScript";
- script.src = url;
- document.getElementsByTagName("head")[0].appendChild(script);
- }
- if (document.body == null) {
- try {
- document.write("<script src='" + url + "' type='text/javascript'></script>");
- } catch (e) {
- createScriptElement();
- }
- } else {
- createScriptElement();
- }
- } else {
- loadMe();
- }
-})();
-/*
- *
- * Coding standards:
- *
- * We aim towards Douglas Crockford's Javascript conventions.
- * See: http://javascript.crockford.com/code.html
- * See also: http://www.crockford.com/javascript/javascript.html
- *
- * That said, this JS code was written before some recent JS
- * support libraries became widely used or available.
- * In particular, the _ character is used to indicate a class function or
- * variable that should be considered private to the class.
- *
- * The code mostly uses accessor methods for getting/setting the private
- * class variables.
- *
- * Over time, we'd like to formalize the convention by using support libraries
- * which enforce privacy in objects.
- *
- * We also want to use jslint: http://www.jslint.com/
- *
- *
- *
- */
-
-
-
-/*
- * Timeline VERSION
- *
- */
-// Note: version is also stored in the build.xml file
-Timeline.version = 'pre 2.4.0'; // use format 'pre 1.2.3' for trunk versions
-Timeline.ajax_lib_version = SimileAjax.version;
-Timeline.display_version = Timeline.version + ' (with Ajax lib ' + Timeline.ajax_lib_version + ')';
- // cf method Timeline.writeVersion
-
-/*
- * Timeline
- *
- */
-Timeline.strings = {}; // localization string tables
-Timeline.HORIZONTAL = 0;
-Timeline.VERTICAL = 1;
-Timeline._defaultTheme = null;
-
-Timeline.getDefaultLocale = function() {
- return Timeline.clientLocale;
-};
-
-Timeline.create = function(elmt, bandInfos, orientation, unit) {
- if (Timeline.timelines == null) {
- Timeline.timelines = [];
- // Timeline.timelines array can have null members--Timelines that
- // once existed on the page, but were later disposed of.
- }
-
- var timelineID = Timeline.timelines.length;
- Timeline.timelines[timelineID] = null; // placeholder until we have the object
- var new_tl = new Timeline._Impl(elmt, bandInfos, orientation, unit,
- timelineID);
- Timeline.timelines[timelineID] = new_tl;
- return new_tl;
-};
-
-Timeline.createBandInfo = function(params) {
- var theme = ("theme" in params) ? params.theme : Timeline.getDefaultTheme();
-
- var eventSource = ("eventSource" in params) ? params.eventSource : null;
-
- var etherParams = {
- interval: SimileAjax.DateTime.gregorianUnitLengths[params.intervalUnit],
- pixelsPerInterval: params.intervalPixels,
- theme: theme
- };
- if ('startsOn' in params || 'endsOn' in params) {
- if ('startsOn' in params) {
- etherParams.startsOn = params.startsOn;
- }
- if ('endsOn' in params) {
- etherParams.endsOn = params.endsOn;
- }
- } else {
- etherParams.centersOn = ("date" in params) ? params.date : new Date();
- }
- var ether = new Timeline.LinearEther(etherParams);
-
- var etherPainter = new Timeline.GregorianEtherPainter({
- unit: params.intervalUnit,
- multiple: ("multiple" in params) ? params.multiple : 1,
- theme: theme,
- align: ("align" in params) ? params.align : undefined
- });
-
- var eventPainterParams = {
- showText: ("showEventText" in params) ? params.showEventText : true,
- theme: theme
- };
- // pass in custom parameters for the event painter
- if ("eventPainterParams" in params) {
- for (var prop in params.eventPainterParams) {
- eventPainterParams[prop] = params.eventPainterParams[prop];
- }
- }
-
- if ("trackHeight" in params) {
- eventPainterParams.trackHeight = params.trackHeight;
- }
- if ("trackGap" in params) {
- eventPainterParams.trackGap = params.trackGap;
- }
-
- var layout = ("overview" in params && params.overview) ? "overview" : ("layout" in params ? params.layout : "original");
- var eventPainter;
- if ("eventPainter" in params) {
- eventPainter = new params.eventPainter(eventPainterParams);
- } else {
- switch (layout) {
- case "overview" :
- eventPainter = new Timeline.OverviewEventPainter(eventPainterParams);
- break;
- case "detailed" :
- eventPainter = new Timeline.DetailedEventPainter(eventPainterParams);
- break;
- default:
- eventPainter = new Timeline.OriginalEventPainter(eventPainterParams);
- }
- }
-
- return {
- width: params.width,
- eventSource: eventSource,
- timeZone: ("timeZone" in params) ? params.timeZone : 0,
- ether: ether,
- etherPainter: etherPainter,
- eventPainter: eventPainter,
- theme: theme,
- zoomIndex: ("zoomIndex" in params) ? params.zoomIndex : 0,
- zoomSteps: ("zoomSteps" in params) ? params.zoomSteps : null
- };
-};
-
-Timeline.createHotZoneBandInfo = function(params) {
- var theme = ("theme" in params) ? params.theme : Timeline.getDefaultTheme();
-
- var eventSource = ("eventSource" in params) ? params.eventSource : null;
-
- var ether = new Timeline.HotZoneEther({
- centersOn: ("date" in params) ? params.date : new Date(),
- interval: SimileAjax.DateTime.gregorianUnitLengths[params.intervalUnit],
- pixelsPerInterval: params.intervalPixels,
- zones: params.zones,
- theme: theme
- });
-
- var etherPainter = new Timeline.HotZoneGregorianEtherPainter({
- unit: params.intervalUnit,
- zones: params.zones,
- theme: theme,
- align: ("align" in params) ? params.align : undefined
- });
-
- var eventPainterParams = {
- showText: ("showEventText" in params) ? params.showEventText : true,
- theme: theme
- };
- // pass in custom parameters for the event painter
- if ("eventPainterParams" in params) {
- for (var prop in params.eventPainterParams) {
- eventPainterParams[prop] = params.eventPainterParams[prop];
- }
- }
- if ("trackHeight" in params) {
- eventPainterParams.trackHeight = params.trackHeight;
- }
- if ("trackGap" in params) {
- eventPainterParams.trackGap = params.trackGap;
- }
-
- var layout = ("overview" in params && params.overview) ? "overview" : ("layout" in params ? params.layout : "original");
- var eventPainter;
- if ("eventPainter" in params) {
- eventPainter = new params.eventPainter(eventPainterParams);
- } else {
- switch (layout) {
- case "overview" :
- eventPainter = new Timeline.OverviewEventPainter(eventPainterParams);
- break;
- case "detailed" :
- eventPainter = new Timeline.DetailedEventPainter(eventPainterParams);
- break;
- default:
- eventPainter = new Timeline.OriginalEventPainter(eventPainterParams);
- }
- }
- return {
- width: params.width,
- eventSource: eventSource,
- timeZone: ("timeZone" in params) ? params.timeZone : 0,
- ether: ether,
- etherPainter: etherPainter,
- eventPainter: eventPainter,
- theme: theme,
- zoomIndex: ("zoomIndex" in params) ? params.zoomIndex : 0,
- zoomSteps: ("zoomSteps" in params) ? params.zoomSteps : null
- };
-};
-
-Timeline.getDefaultTheme = function() {
- if (Timeline._defaultTheme == null) {
- Timeline._defaultTheme = Timeline.ClassicTheme.create(Timeline.getDefaultLocale());
- }
- return Timeline._defaultTheme;
-};
-
-Timeline.setDefaultTheme = function(theme) {
- Timeline._defaultTheme = theme;
-};
-
-Timeline.loadXML = function(url, f) {
- var fError = function(statusText, status, xmlhttp) {
- alert("Failed to load data xml from " + url + "\n" + statusText);
- };
- var fDone = function(xmlhttp) {
- var xml = xmlhttp.responseXML;
- if (!xml.documentElement && xmlhttp.responseStream) {
- xml.load(xmlhttp.responseStream);
- }
- f(xml, url);
- };
- SimileAjax.XmlHttp.get(url, fError, fDone);
-};
-
-
-Timeline.loadJSON = function(url, f) {
- var fError = function(statusText, status, xmlhttp) {
- alert("Failed to load json data from " + url + "\n" + statusText);
- };
- var fDone = function(xmlhttp) {
- f(eval('(' + xmlhttp.responseText + ')'), url);
- };
- SimileAjax.XmlHttp.get(url, fError, fDone);
-};
-
-Timeline.getTimelineFromID = function(timelineID) {
- return Timeline.timelines[timelineID];
-};
-
-// Write the current Timeline version as the contents of element with id el_id
-Timeline.writeVersion = function(el_id) {
- document.getElementById(el_id).innerHTML = this.display_version;
-};
-
-
-
-/*
- * Timeline Implementation object
- *
- */
-Timeline._Impl = function(elmt, bandInfos, orientation, unit, timelineID) {
- SimileAjax.WindowManager.initialize();
-
- this._containerDiv = elmt;
-
- this._bandInfos = bandInfos;
- this._orientation = orientation == null ? Timeline.HORIZONTAL : orientation;
- this._unit = (unit != null) ? unit : SimileAjax.NativeDateUnit;
- this._starting = true; // is the Timeline being created? Used by autoWidth
- // functions
- this._autoResizing = false;
-
- // autoWidth is a "public" property of the Timeline object
- this.autoWidth = bandInfos && bandInfos[0] && bandInfos[0].theme &&
- bandInfos[0].theme.autoWidth;
- this.autoWidthAnimationTime = bandInfos && bandInfos[0] && bandInfos[0].theme &&
- bandInfos[0].theme.autoWidthAnimationTime;
- this.timelineID = timelineID; // also public attribute
- this.timeline_start = bandInfos && bandInfos[0] && bandInfos[0].theme &&
- bandInfos[0].theme.timeline_start;
- this.timeline_stop = bandInfos && bandInfos[0] && bandInfos[0].theme &&
- bandInfos[0].theme.timeline_stop;
- this.timeline_at_start = false; // already at start or stop? Then won't
- this.timeline_at_stop = false; // try to move further in the wrong direction
-
- this._initialize();
-};
-
-//
-// Public functions used by client sw
-//
-Timeline._Impl.prototype.dispose = function() {
- for (var i = 0; i < this._bands.length; i++) {
- this._bands[i].dispose();
- }
- this._bands = null;
- this._bandInfos = null;
- this._containerDiv.innerHTML = "";
- // remove from array of Timelines
- Timeline.timelines[this.timelineID] = null;
-};
-
-Timeline._Impl.prototype.getBandCount = function() {
- return this._bands.length;
-};
-
-Timeline._Impl.prototype.getBand = function(index) {
- return this._bands[index];
-};
-
-Timeline._Impl.prototype.finishedEventLoading = function() {
- // Called by client after events have been loaded into Timeline
- // Only used if the client has set autoWidth
- // Sets width to Timeline's requested amount and will shrink down the div if
- // need be.
- this._autoWidthCheck(true);
- this._starting = false;
-};
-
-Timeline._Impl.prototype.layout = function() {
- // called by client when browser is resized
- this._autoWidthCheck(true);
- this._distributeWidths();
-};
-
-Timeline._Impl.prototype.paint = function() {
- for (var i = 0; i < this._bands.length; i++) {
- this._bands[i].paint();
- }
-};
-
-Timeline._Impl.prototype.getDocument = function() {
- return this._containerDiv.ownerDocument;
-};
-
-Timeline._Impl.prototype.addDiv = function(div) {
- this._containerDiv.appendChild(div);
-};
-
-Timeline._Impl.prototype.removeDiv = function(div) {
- this._containerDiv.removeChild(div);
-};
-
-Timeline._Impl.prototype.isHorizontal = function() {
- return this._orientation == Timeline.HORIZONTAL;
-};
-
-Timeline._Impl.prototype.isVertical = function() {
- return this._orientation == Timeline.VERTICAL;
-};
-
-Timeline._Impl.prototype.getPixelLength = function() {
- return this._orientation == Timeline.HORIZONTAL ?
- this._containerDiv.offsetWidth : this._containerDiv.offsetHeight;
-};
-
-Timeline._Impl.prototype.getPixelWidth = function() {
- return this._orientation == Timeline.VERTICAL ?
- this._containerDiv.offsetWidth : this._containerDiv.offsetHeight;
-};
-
-Timeline._Impl.prototype.getUnit = function() {
- return this._unit;
-};
-
-Timeline._Impl.prototype.getWidthStyle = function() {
- // which element.style attribute should be changed to affect Timeline's "width"
- return this._orientation == Timeline.HORIZONTAL ? 'height' : 'width';
-};
-
-Timeline._Impl.prototype.loadXML = function(url, f) {
- var tl = this;
-
-
- var fError = function(statusText, status, xmlhttp) {
- alert("Failed to load data xml from " + url + "\n" + statusText);
- tl.hideLoadingMessage();
- };
- var fDone = function(xmlhttp) {
- try {
- var xml = xmlhttp.responseXML;
- if (!xml.documentElement && xmlhttp.responseStream) {
- xml.load(xmlhttp.responseStream);
- }
- f(xml, url);
- } finally {
- tl.hideLoadingMessage();
- }
- };
-
- this.showLoadingMessage();
- window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
-};
-
-Timeline._Impl.prototype.loadJSON = function(url, f) {
- var tl = this;
-
- var fError = function(statusText, status, xmlhttp) {
- alert("Failed to load json data from " + url + "\n" + statusText);
- tl.hideLoadingMessage();
- };
- var fDone = function(xmlhttp) {
- try {
- f(eval('(' + xmlhttp.responseText + ')'), url);
- } finally {
- tl.hideLoadingMessage();
- }
- };
-
- this.showLoadingMessage();
- window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
-};
-
-
-//
-// Private functions used by Timeline object functions
-//
-
-Timeline._Impl.prototype._autoWidthScrollListener = function(band) {
- band.getTimeline()._autoWidthCheck(false);
-};
-
-// called to re-calculate auto width and adjust the overall Timeline div if needed
-Timeline._Impl.prototype._autoWidthCheck = function(okToShrink) {
- var timeline = this; // this Timeline
- var immediateChange = timeline._starting;
- var newWidth = 0;
-
- function changeTimelineWidth() {
- var widthStyle = timeline.getWidthStyle();
- if (immediateChange) {
- timeline._containerDiv.style[widthStyle] = newWidth + 'px';
- } else {
- // animate change
- timeline._autoResizing = true;
- var animateParam ={};
- animateParam[widthStyle] = newWidth + 'px';
-
- SimileAjax.jQuery(timeline._containerDiv).animate(
- animateParam, timeline.autoWidthAnimationTime,
- 'linear', function(){timeline._autoResizing = false;});
- }
- }
-
- function checkTimelineWidth() {
- var targetWidth = 0; // the new desired width
- var currentWidth = timeline.getPixelWidth();
-
- if (timeline._autoResizing) {
- return; // early return
- }
-
- // compute targetWidth
- for (var i = 0; i < timeline._bands.length; i++) {
- timeline._bands[i].checkAutoWidth();
- targetWidth += timeline._bandInfos[i].width;
- }
-
- if (targetWidth > currentWidth || okToShrink) {
- // yes, let's change the size
- newWidth = targetWidth;
- changeTimelineWidth();
- timeline._distributeWidths();
- }
- }
-
- // function's mainline
- if (!timeline.autoWidth) {
- return; // early return
- }
-
- checkTimelineWidth();
-};
-
-Timeline._Impl.prototype._initialize = function() {
- var containerDiv = this._containerDiv;
- var doc = containerDiv.ownerDocument;
-
- containerDiv.className =
- containerDiv.className.split(" ").concat("timeline-container").join(" ");
-
- /*
- * Set css-class on container div that will define orientation
- */
- var orientation = (this.isHorizontal()) ? 'horizontal' : 'vertical'
- containerDiv.className +=' timeline-'+orientation;
-
-
- while (containerDiv.firstChild) {
- containerDiv.removeChild(containerDiv.firstChild);
- }
-
- /*
- * inserting copyright and link to simile
- */
- var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeline.urlPrefix + (this.isHorizontal() ? "images/copyright-vertical.png" : "images/copyright.png"));
- elmtCopyright.className = "timeline-copyright";
- elmtCopyright.title = "Timeline copyright SIMILE - www.code.google.com/p/simile-widgets/";
- SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://www.simile-widgets.org/"; });
- containerDiv.appendChild(elmtCopyright);
-
- /*
- * creating bands
- */
- this._bands = [];
- for (var i = 0; i < this._bandInfos.length; i++) {
- var bandInfo = this._bandInfos[i];
- var bandClass = bandInfo.bandClass || Timeline._Band;
- var band = new bandClass(this, this._bandInfos[i], i);
- this._bands.push(band);
- }
- this._distributeWidths();
-
- /*
- * sync'ing bands
- */
- for (var i = 0; i < this._bandInfos.length; i++) {
- var bandInfo = this._bandInfos[i];
- if ("syncWith" in bandInfo) {
- this._bands[i].setSyncWithBand(
- this._bands[bandInfo.syncWith],
- ("highlight" in bandInfo) ? bandInfo.highlight : false
- );
- }
- }
-
-
- if (this.autoWidth) {
- for (var i = 0; i < this._bands.length; i++) {
- this._bands[i].addOnScrollListener(this._autoWidthScrollListener);
- }
- }
-
-
- /*
- * creating loading UI
- */
- var message = SimileAjax.Graphics.createMessageBubble(doc);
- message.containerDiv.className = "timeline-message-container";
- containerDiv.appendChild(message.containerDiv);
-
- message.contentDiv.className = "timeline-message";
- message.contentDiv.innerHTML = "<img src='" + Timeline.urlPrefix + "images/progress-running.gif' /> Loading...";
-
- this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; };
- this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; };
-};
-
-Timeline._Impl.prototype._distributeWidths = function() {
- var length = this.getPixelLength();
- var width = this.getPixelWidth();
- var cumulativeWidth = 0;
-
- for (var i = 0; i < this._bands.length; i++) {
- var band = this._bands[i];
- var bandInfos = this._bandInfos[i];
- var widthString = bandInfos.width;
- var bandWidth;
-
- if (typeof widthString == 'string') {
- var x = widthString.indexOf("%");
- if (x > 0) {
- var percent = parseInt(widthString.substr(0, x));
- bandWidth = Math.round(percent * width / 100);
- } else {
- bandWidth = parseInt(widthString);
- }
- } else {
- // was given an integer
- bandWidth = widthString;
- }
-
- band.setBandShiftAndWidth(cumulativeWidth, bandWidth);
- band.setViewLength(length);
-
- cumulativeWidth += bandWidth;
- }
-};
-
-Timeline._Impl.prototype.shiftOK = function(index, shift) {
- // Returns true if the proposed shift is ok
- //
- // Positive shift means going back in time
- var going_back = shift > 0,
- going_forward = shift < 0;
-
- // Is there an edge?
- if ((going_back && this.timeline_start == null) ||
- (going_forward && this.timeline_stop == null) ||
- (shift == 0)) {
- return (true); // early return
- }
-
- // If any of the bands has noted that it is changing the others,
- // then this shift is a secondary shift in reaction to the real shift,
- // which already happened. In such cases, ignore it. (The issue is
- // that a positive original shift can cause a negative secondary shift,
- // as the bands adjust.)
- var secondary_shift = false;
- for (var i = 0; i < this._bands.length && !secondary_shift; i++) {
- secondary_shift = this._bands[i].busy();
- }
- if (secondary_shift) {
- return(true); // early return
- }
-
- // If we are already at an edge, then don't even think about going any further
- if ((going_back && this.timeline_at_start) ||
- (going_forward && this.timeline_at_stop)) {
- return (false); // early return
- }
-
- // Need to check all the bands
- var ok = false; // return value
- // If any of the bands will be or are showing an ok date, then let the shift proceed.
- for (var i = 0; i < this._bands.length && !ok; i++) {
- var band = this._bands[i];
- if (going_back) {
- ok = (i == index ? band.getMinVisibleDateAfterDelta(shift) : band.getMinVisibleDate())
- >= this.timeline_start;
- } else {
- ok = (i == index ? band.getMaxVisibleDateAfterDelta(shift) : band.getMaxVisibleDate())
- <= this.timeline_stop;
- }
- }
-
- // process results
- if (going_back) {
- this.timeline_at_start = !ok;
- this.timeline_at_stop = false;
- } else {
- this.timeline_at_stop = !ok;
- this.timeline_at_start = false;
- }
- // This is where you could have an effect once per hitting an
- // edge of the Timeline. Eg jitter the Timeline
- //if (!ok) {
- //alert(going_back ? "At beginning" : "At end");
- //}
- return (ok);
-};
-
-Timeline._Impl.prototype.zoom = function (zoomIn, x, y, target) {
- var matcher = new RegExp("^timeline-band-([0-9]+)$");
- var bandIndex = null;
-
- var result = matcher.exec(target.id);
- if (result) {
- bandIndex = parseInt(result[1]);
- }
-
- if (bandIndex != null) {
- this._bands[bandIndex].zoom(zoomIn, x, y, target);
- }
-
- this.paint();
-};
-
-/*
- *
- * Coding standards:
- *
- * We aim towards Douglas Crockford's Javascript conventions.
- * See: http://javascript.crockford.com/code.html
- * See also: http://www.crockford.com/javascript/javascript.html
- *
- * That said, this JS code was written before some recent JS
- * support libraries became widely used or available.
- * In particular, the _ character is used to indicate a class function or
- * variable that should be considered private to the class.
- *
- * The code mostly uses accessor methods for getting/setting the private
- * class variables.
- *
- * Over time, we'd like to formalize the convention by using support libraries
- * which enforce privacy in objects.
- *
- * We also want to use jslint: http://www.jslint.com/
- *
- *
- *
- */
-
-
-
-/*
- * Band
- *
- */
-Timeline._Band = function(timeline, bandInfo, index) {
- // hack for easier subclassing
- if (timeline !== undefined) {
- this.initialize(timeline, bandInfo, index);
- }
-};
-
-Timeline._Band.prototype.initialize = function(timeline, bandInfo, index) {
- // Set up the band's object
-
- // Munge params: If autoWidth is on for the Timeline, then ensure that
- // bandInfo.width is an integer
- if (timeline.autoWidth && typeof bandInfo.width == 'string') {
- bandInfo.width = bandInfo.width.indexOf("%") > -1 ? 0 : parseInt(bandInfo.width);
- }
-
- this._timeline = timeline;
- this._bandInfo = bandInfo;
-
- this._index = index;
-
- this._locale = ("locale" in bandInfo) ? bandInfo.locale : Timeline.getDefaultLocale();
- this._timeZone = ("timeZone" in bandInfo) ? bandInfo.timeZone : 0;
- this._labeller = ("labeller" in bandInfo) ? bandInfo.labeller :
- (("createLabeller" in timeline.getUnit()) ?
- timeline.getUnit().createLabeller(this._locale, this._timeZone) :
- new Timeline.GregorianDateLabeller(this._locale, this._timeZone));
- this._theme = bandInfo.theme;
- this._zoomIndex = ("zoomIndex" in bandInfo) ? bandInfo.zoomIndex : 0;
- this._zoomSteps = ("zoomSteps" in bandInfo) ? bandInfo.zoomSteps : null;
-
- this._dragging = false;
- this._changing = false;
- this._originalScrollSpeed = 5; // pixels
- this._scrollSpeed = this._originalScrollSpeed;
- this._viewOrthogonalOffset= 0; // vertical offset if the timeline is horizontal, and vice versa
- this._onScrollListeners = [];
-
- var b = this;
- this._syncWithBand = null;
- this._syncWithBandHandler = function(band) {
- b._onHighlightBandScroll();
- };
- this._selectorListener = function(band) {
- b._onHighlightBandScroll();
- };
-
- /*
- * Install a textbox to capture keyboard events
- */
- var inputDiv = this._timeline.getDocument().createElement("div");
- inputDiv.className = "timeline-band-input";
- this._timeline.addDiv(inputDiv);
-
- this._keyboardInput = document.createElement("input");
- this._keyboardInput.type = "text";
- inputDiv.appendChild(this._keyboardInput);
- SimileAjax.DOM.registerEventWithObject(this._keyboardInput, "keydown", this, "_onKeyDown");
- SimileAjax.DOM.registerEventWithObject(this._keyboardInput, "keyup", this, "_onKeyUp");
-
- /*
- * The band's outer most div that slides with respect to the timeline's div
- */
- this._div = this._timeline.getDocument().createElement("div");
- this._div.id = "timeline-band-" + index;
- this._div.className = "timeline-band timeline-band-" + index;
- this._timeline.addDiv(this._div);
-
- SimileAjax.DOM.registerEventWithObject(this._div, "mousedown", this, "_onMouseDown");
- SimileAjax.DOM.registerEventWithObject(this._div, "mousemove", this, "_onMouseMove");
- SimileAjax.DOM.registerEventWithObject(this._div, "mouseup", this, "_onMouseUp");
- SimileAjax.DOM.registerEventWithObject(this._div, "mouseout", this, "_onMouseOut");
- SimileAjax.DOM.registerEventWithObject(this._div, "dblclick", this, "_onDblClick");
-
- var mouseWheel = this._theme!= null ? this._theme.mouseWheel : 'scroll'; // theme is not always defined
- if (mouseWheel === 'zoom' || mouseWheel === 'scroll' || this._zoomSteps) {
- // capture mouse scroll
- if (SimileAjax.Platform.browser.isFirefox) {
- SimileAjax.DOM.registerEventWithObject(this._div, "DOMMouseScroll", this, "_onMouseScroll");
- } else {
- SimileAjax.DOM.registerEventWithObject(this._div, "mousewheel", this, "_onMouseScroll");
- }
- }
-
- /*
- * The inner div that contains layers
- */
- this._innerDiv = this._timeline.getDocument().createElement("div");
- this._innerDiv.className = "timeline-band-inner";
- this._div.appendChild(this._innerDiv);
-
- /*
- * Initialize parts of the band
- */
- this._ether = bandInfo.ether;
- bandInfo.ether.initialize(this, timeline);
-
- this._etherPainter = bandInfo.etherPainter;
- bandInfo.etherPainter.initialize(this, timeline);
-
- this._eventSource = bandInfo.eventSource;
- if (this._eventSource) {
- this._eventListener = {
- onAddMany: function() { b._onAddMany(); },
- onClear: function() { b._onClear(); }
- }
- this._eventSource.addListener(this._eventListener);
- }
-
- this._eventPainter = bandInfo.eventPainter;
- this._eventTracksNeeded = 0; // set by painter via updateEventTrackInfo
- this._eventTrackIncrement = 0;
- bandInfo.eventPainter.initialize(this, timeline);
-
- this._decorators = ("decorators" in bandInfo) ? bandInfo.decorators : [];
- for (var i = 0; i < this._decorators.length; i++) {
- this._decorators[i].initialize(this, timeline);
- }
-};
-
-Timeline._Band.SCROLL_MULTIPLES = 5;
-
-Timeline._Band.prototype.dispose = function() {
- this.closeBubble();
-
- if (this._eventSource) {
- this._eventSource.removeListener(this._eventListener);
- this._eventListener = null;
- this._eventSource = null;
- }
-
- this._timeline = null;
- this._bandInfo = null;
-
- this._labeller = null;
- this._ether = null;
- this._etherPainter = null;
- this._eventPainter = null;
- this._decorators = null;
-
- this._onScrollListeners = null;
- this._syncWithBandHandler = null;
- this._selectorListener = null;
-
- this._div = null;
- this._innerDiv = null;
- this._keyboardInput = null;
-};
-
-Timeline._Band.prototype.addOnScrollListener = function(listener) {
- this._onScrollListeners.push(listener);
-};
-
-Timeline._Band.prototype.removeOnScrollListener = function(listener) {
- for (var i = 0; i < this._onScrollListeners.length; i++) {
- if (this._onScrollListeners[i] == listener) {
- this._onScrollListeners.splice(i, 1);
- break;
- }
- }
-};
-
-Timeline._Band.prototype.setSyncWithBand = function(band, highlight) {
- if (this._syncWithBand) {
- this._syncWithBand.removeOnScrollListener(this._syncWithBandHandler);
- }
-
- this._syncWithBand = band;
- this._syncWithBand.addOnScrollListener(this._syncWithBandHandler);
- this._highlight = highlight;
- this._positionHighlight();
-};
-
-Timeline._Band.prototype.getLocale = function() {
- return this._locale;
-};
-
-Timeline._Band.prototype.getTimeZone = function() {
- return this._timeZone;
-};
-
-Timeline._Band.prototype.getLabeller = function() {
- return this._labeller;
-};
-
-Timeline._Band.prototype.getIndex = function() {
- return this._index;
-};
-
-Timeline._Band.prototype.getEther = function() {
- return this._ether;
-};
-
-Timeline._Band.prototype.getEtherPainter = function() {
- return this._etherPainter;
-};
-
-Timeline._Band.prototype.getEventSource = function() {
- return this._eventSource;
-};
-
-Timeline._Band.prototype.getEventPainter = function() {
- return this._eventPainter;
-};
-
-Timeline._Band.prototype.getTimeline = function() {
- return this._timeline;
-};
-
-// Autowidth support
-Timeline._Band.prototype.updateEventTrackInfo = function(tracks, increment) {
- this._eventTrackIncrement = increment; // doesn't vary for a specific band
-
- if (tracks > this._eventTracksNeeded) {
- this._eventTracksNeeded = tracks;
- }
-};
-
-// Autowidth support
-Timeline._Band.prototype.checkAutoWidth = function() {
- // if a new (larger) width is needed by the band
- // then: a) updates the band's bandInfo.width
- //
- // desiredWidth for the band is
- // (number of tracks + margin) * track increment
- if (! this._timeline.autoWidth) {
- return; // early return
- }
-
- var overviewBand = this._eventPainter.getType() == 'overview';
- var margin = overviewBand ?
- this._theme.event.overviewTrack.autoWidthMargin :
- this._theme.event.track.autoWidthMargin;
- var desiredWidth = Math.ceil((this._eventTracksNeeded + margin) *
- this._eventTrackIncrement);
- // add offset amount (additional margin)
- desiredWidth += overviewBand ? this._theme.event.overviewTrack.offset :
- this._theme.event.track.offset;
- var bandInfo = this._bandInfo;
-
- if (desiredWidth != bandInfo.width) {
- bandInfo.width = desiredWidth;
- }
-};
-
-Timeline._Band.prototype.layout = function() {
- this.paint();
-};
-
-Timeline._Band.prototype.paint = function() {
- this._etherPainter.paint();
- this._paintDecorators();
- this._paintEvents();
-};
-
-Timeline._Band.prototype.softLayout = function() {
- this.softPaint();
-};
-
-Timeline._Band.prototype.softPaint = function() {
- this._etherPainter.softPaint();
- this._softPaintDecorators();
- this._softPaintEvents();
-};
-
-Timeline._Band.prototype.setBandShiftAndWidth = function(shift, width) {
- var inputDiv = this._keyboardInput.parentNode;
- var middle = shift + Math.floor(width / 2);
- if (this._timeline.isHorizontal()) {
- this._div.style.top = shift + "px";
- this._div.style.height = width + "px";
-
- inputDiv.style.top = middle + "px";
- inputDiv.style.left = "-1em";
- } else {
- this._div.style.left = shift + "px";
- this._div.style.width = width + "px";
-
- inputDiv.style.left = middle + "px";
- inputDiv.style.top = "-1em";
- }
-};
-
-Timeline._Band.prototype.getViewWidth = function() {
- if (this._timeline.isHorizontal()) {
- return this._div.offsetHeight;
- } else {
- return this._div.offsetWidth;
- }
-};
-
-Timeline._Band.prototype.setViewLength = function(length) {
- this._viewLength = length;
- this._recenterDiv();
- this._onChanging();
-};
-
-Timeline._Band.prototype.getViewLength = function() {
- return this._viewLength;
-};
-
-Timeline._Band.prototype.getTotalViewLength = function() {
- return Timeline._Band.SCROLL_MULTIPLES * this._viewLength;
-};
-
-Timeline._Band.prototype.getViewOffset = function() {
- return this._viewOffset;
-};
-
-Timeline._Band.prototype.getMinDate = function() {
- return this._ether.pixelOffsetToDate(this._viewOffset);
-};
-
-Timeline._Band.prototype.getMaxDate = function() {
- return this._ether.pixelOffsetToDate(this._viewOffset + Timeline._Band.SCROLL_MULTIPLES * this._viewLength);
-};
-
-Timeline._Band.prototype.getMinVisibleDate = function() {
- return this._ether.pixelOffsetToDate(0);
-};
-
-Timeline._Band.prototype.getMinVisibleDateAfterDelta = function(delta) {
- return this._ether.pixelOffsetToDate(delta);
-};
-
-Timeline._Band.prototype.getMaxVisibleDate = function() {
- // Max date currently visible on band
- return this._ether.pixelOffsetToDate(this._viewLength);
-};
-
-Timeline._Band.prototype.getMaxVisibleDateAfterDelta = function(delta) {
- // Max date visible on band after delta px view change is applied
- return this._ether.pixelOffsetToDate(this._viewLength + delta);
-};
-
-Timeline._Band.prototype.getCenterVisibleDate = function() {
- return this._ether.pixelOffsetToDate(this._viewLength / 2);
-};
-
-Timeline._Band.prototype.setMinVisibleDate = function(date) {
- if (!this._changing) {
- this._moveEther(Math.round(-this._ether.dateToPixelOffset(date)));
- }
-};
-
-Timeline._Band.prototype.setMaxVisibleDate = function(date) {
- if (!this._changing) {
- this._moveEther(Math.round(this._viewLength - this._ether.dateToPixelOffset(date)));
- }
-};
-
-Timeline._Band.prototype.setCenterVisibleDate = function(date) {
- if (!this._changing) {
- this._moveEther(Math.round(this._viewLength / 2 - this._ether.dateToPixelOffset(date)));
- }
-};
-
-Timeline._Band.prototype.dateToPixelOffset = function(date) {
- return this._ether.dateToPixelOffset(date) - this._viewOffset;
-};
-
-Timeline._Band.prototype.pixelOffsetToDate = function(pixels) {
- return this._ether.pixelOffsetToDate(pixels + this._viewOffset);
-};
-
-Timeline._Band.prototype.getViewOrthogonalOffset = function() {
- return this._viewOrthogonalOffset;
-};
-
-Timeline._Band.prototype.setViewOrthogonalOffset = function(offset) {
- this._viewOrthogonalOffset = Math.max(0, offset);
-};
-
-Timeline._Band.prototype.createLayerDiv = function(zIndex, className) {
- var div = this._timeline.getDocument().createElement("div");
- div.className = "timeline-band-layer" + (typeof className == "string" ? (" " + className) : "");
- div.style.zIndex = zIndex;
- this._innerDiv.appendChild(div);
-
- var innerDiv = this._timeline.getDocument().createElement("div");
- innerDiv.className = "timeline-band-layer-inner";
- if (SimileAjax.Platform.browser.isIE) {
- innerDiv.style.cursor = "move";
- } else {
- innerDiv.style.cursor = "-moz-grab";
- }
- div.appendChild(innerDiv);
-
- return innerDiv;
-};
-
-Timeline._Band.prototype.removeLayerDiv = function(div) {
- this._innerDiv.removeChild(div.parentNode);
-};
-
-Timeline._Band.prototype.scrollToCenter = function(date, f) {
- var pixelOffset = this._ether.dateToPixelOffset(date);
- if (pixelOffset < -this._viewLength / 2) {
- this.setCenterVisibleDate(this.pixelOffsetToDate(pixelOffset + this._viewLength));
- } else if (pixelOffset > 3 * this._viewLength / 2) {
- this.setCenterVisibleDate(this.pixelOffsetToDate(pixelOffset - this._viewLength));
- }
- this._autoScroll(Math.round(this._viewLength / 2 - this._ether.dateToPixelOffset(date)), f);
-};
-
-Timeline._Band.prototype.showBubbleForEvent = function(eventID) {
- var evt = this.getEventSource().getEvent(eventID);
- if (evt) {
- var self = this;
- this.scrollToCenter(evt.getStart(), function() {
- self._eventPainter.showBubble(evt);
- });
- }
-};
-
-Timeline._Band.prototype.zoom = function(zoomIn, x, y, target) {
- if (!this._zoomSteps) {
- // zoom disabled
- return;
- }
-
- // shift the x value by our offset
- x += this._viewOffset;
-
- var zoomDate = this._ether.pixelOffsetToDate(x);
- var netIntervalChange = this._ether.zoom(zoomIn);
- this._etherPainter.zoom(netIntervalChange);
-
- // shift our zoom date to the far left
- this._moveEther(Math.round(-this._ether.dateToPixelOffset(zoomDate)));
- // then shift it back to where the mouse was
- this._moveEther(x);
-};
-
-Timeline._Band.prototype._onMouseDown = function(innerFrame, evt, target) {
- this.closeBubble();
-
- this._dragging = true;
- this._dragX = evt.clientX;
- this._dragY = evt.clientY;
-};
-
-Timeline._Band.prototype._onMouseMove = function(innerFrame, evt, target) {
- if (this._dragging) {
- var diffX = evt.clientX - this._dragX;
- var diffY = evt.clientY - this._dragY;
-
- this._dragX = evt.clientX;
- this._dragY = evt.clientY;
-
- if (this._timeline.isHorizontal()) {
- this._moveEther(diffX, diffY);
- } else {
- this._moveEther(diffY, diffX);
- }
- this._positionHighlight();
- }
-};
-
-Timeline._Band.prototype._onMouseUp = function(innerFrame, evt, target) {
- this._dragging = false;
- this._keyboardInput.focus();
-};
-
-Timeline._Band.prototype._onMouseOut = function(innerFrame, evt, target) {
- var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
- coords.x += this._viewOffset;
- if (coords.x < 0 || coords.x > innerFrame.offsetWidth ||
- coords.y < 0 || coords.y > innerFrame.offsetHeight) {
- this._dragging = false;
- }
-};
-
-Timeline._Band.prototype._onMouseScroll = function(innerFrame, evt, target) {
- var now = new Date();
- now = now.getTime();
-
- if (!this._lastScrollTime || ((now - this._lastScrollTime) > 50)) {
- // limit 1 scroll per 200ms due to FF3 sending multiple events back to back
- this._lastScrollTime = now;
-
- var delta = 0;
- if (evt.wheelDelta) {
- delta = evt.wheelDelta/120;
- } else if (evt.detail) {
- delta = -evt.detail/3;
- }
-
- // either scroll or zoom
- var mouseWheel = this._theme.mouseWheel;
-
- if (this._zoomSteps || mouseWheel === 'zoom') {
- var loc = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
- if (delta != 0) {
- var zoomIn;
- if (delta > 0)
- zoomIn = true;
- if (delta < 0)
- zoomIn = false;
- // call zoom on the timeline so we could zoom multiple bands if desired
- this._timeline.zoom(zoomIn, loc.x, loc.y, innerFrame);
- }
- }
- else if (mouseWheel === 'scroll') {
- var move_amt = 50 * (delta < 0 ? -1 : 1);
- this._moveEther(move_amt);
- }
- }
-
- // prevent bubble
- if (evt.stopPropagation) {
- evt.stopPropagation();
- }
- evt.cancelBubble = true;
-
- // prevent the default action
- if (evt.preventDefault) {
- evt.preventDefault();
- }
- evt.returnValue = false;
-};
-
-Timeline._Band.prototype._onDblClick = function(innerFrame, evt, target) {
- var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
- var distance = coords.x - (this._viewLength / 2 - this._viewOffset);
-
- this._autoScroll(-distance);
-};
-
-Timeline._Band.prototype._onKeyDown = function(keyboardInput, evt, target) {
- if (!this._dragging) {
- switch (evt.keyCode) {
- case 27: // ESC
- break;
- case 37: // left arrow
- case 38: // up arrow
- this._scrollSpeed = Math.min(50, Math.abs(this._scrollSpeed * 1.05));
- this._moveEther(this._scrollSpeed);
- break;
- case 39: // right arrow
- case 40: // down arrow
- this._scrollSpeed = -Math.min(50, Math.abs(this._scrollSpeed * 1.05));
- this._moveEther(this._scrollSpeed);
- break;
- default:
- return true;
- }
- this.closeBubble();
-
- SimileAjax.DOM.cancelEvent(evt);
- return false;
- }
- return true;
-};
-
-Timeline._Band.prototype._onKeyUp = function(keyboardInput, evt, target) {
- if (!this._dragging) {
- this._scrollSpeed = this._originalScrollSpeed;
-
- switch (evt.keyCode) {
- case 35: // end
- this.setCenterVisibleDate(this._eventSource.getLatestDate());
- break;
- case 36: // home
- this.setCenterVisibleDate(this._eventSource.getEarliestDate());
- break;
- case 33: // page up
- this._autoScroll(this._timeline.getPixelLength());
- break;
- case 34: // page down
- this._autoScroll(-this._timeline.getPixelLength());
- break;
- default:
- return true;
- }
-
- this.closeBubble();
-
- SimileAjax.DOM.cancelEvent(evt);
- return false;
- }
- return true;
-};
-
-Timeline._Band.prototype._autoScroll = function(distance, f) {
- var b = this;
- var a = SimileAjax.Graphics.createAnimation(
- function(abs, diff) {
- b._moveEther(diff);
- },
- 0,
- distance,
- 1000,
- f
- );
- a.run();
-};
-
-Timeline._Band.prototype._moveEther = function(shift, orthogonalShift) {
- if (orthogonalShift === undefined) {
- orthogonalShift = 0;
- }
-
- this.closeBubble();
-
- // A positive shift means back in time
- // Check that we're not moving beyond Timeline's limits
- if (!this._timeline.shiftOK(this._index, shift)) {
- return; // early return
- }
-
- this._viewOffset += shift;
- this._viewOrthogonalOffset = Math.min(0, this._viewOrthogonalOffset + orthogonalShift);
-
- this._ether.shiftPixels(-shift);
- if (this._timeline.isHorizontal()) {
- this._div.style.left = this._viewOffset + "px";
- } else {
- this._div.style.top = this._viewOffset + "px";
- }
-
- if (this._viewOffset > -this._viewLength * 0.5 ||
- this._viewOffset < -this._viewLength * (Timeline._Band.SCROLL_MULTIPLES - 1.5)) {
-
- this._recenterDiv();
- } else {
- this.softLayout();
- }
-
- this._onChanging();
-}
-
-Timeline._Band.prototype._onChanging = function() {
- this._changing = true;
-
- this._fireOnScroll();
- this._setSyncWithBandDate();
-
- this._changing = false;
-};
-
-Timeline._Band.prototype.busy = function() {
- // Is this band busy changing other bands?
- return(this._changing);
-};
-
-Timeline._Band.prototype._fireOnScroll = function() {
- for (var i = 0; i < this._onScrollListeners.length; i++) {
- this._onScrollListeners[i](this);
- }
-};
-
-Timeline._Band.prototype._setSyncWithBandDate = function() {
- if (this._syncWithBand) {
- var centerDate = this._ether.pixelOffsetToDate(this.getViewLength() / 2);
- this._syncWithBand.setCenterVisibleDate(centerDate);
- }
-};
-
-Timeline._Band.prototype._onHighlightBandScroll = function() {
- if (this._syncWithBand) {
- var centerDate = this._syncWithBand.getCenterVisibleDate();
- var centerPixelOffset = this._ether.dateToPixelOffset(centerDate);
-
- this._moveEther(Math.round(this._viewLength / 2 - centerPixelOffset));
-
- if (this._highlight) {
- this._etherPainter.setHighlight(
- this._syncWithBand.getMinVisibleDate(),
- this._syncWithBand.getMaxVisibleDate());
- }
- }
-};
-
-Timeline._Band.prototype._onAddMany = function() {
- this._paintEvents();
-};
-
-Timeline._Band.prototype._onClear = function() {
- this._paintEvents();
-};
-
-Timeline._Band.prototype._positionHighlight = function() {
- if (this._syncWithBand) {
- var startDate = this._syncWithBand.getMinVisibleDate();
- var endDate = this._syncWithBand.getMaxVisibleDate();
-
- if (this._highlight) {
- this._etherPainter.setHighlight(startDate, endDate);
- }
- }
-};
-
-Timeline._Band.prototype._recenterDiv = function() {
- this._viewOffset = -this._viewLength * (Timeline._Band.SCROLL_MULTIPLES - 1) / 2;
- if (this._timeline.isHorizontal()) {
- this._div.style.left = this._viewOffset + "px";
- this._div.style.width = (Timeline._Band.SCROLL_MULTIPLES * this._viewLength) + "px";
- } else {
- this._div.style.top = this._viewOffset + "px";
- this._div.style.height = (Timeline._Band.SCROLL_MULTIPLES * this._viewLength) + "px";
- }
- this.layout();
-};
-
-Timeline._Band.prototype._paintEvents = function() {
- this._eventPainter.paint();
-};
-
-Timeline._Band.prototype._softPaintEvents = function() {
- this._eventPainter.softPaint();
-};
-
-Timeline._Band.prototype._paintDecorators = function() {
- for (var i = 0; i < this._decorators.length; i++) {
- this._decorators[i].paint();
- }
-};
-
-Timeline._Band.prototype._softPaintDecorators = function() {
- for (var i = 0; i < this._decorators.length; i++) {
- this._decorators[i].softPaint();
- }
-};
-
-Timeline._Band.prototype.closeBubble = function() {
- SimileAjax.WindowManager.cancelPopups();
-};
-/*
- * Classic Theme
- *
- */
-
-
-
-Timeline.ClassicTheme = new Object();
-
-Timeline.ClassicTheme.implementations = [];
-
-Timeline.ClassicTheme.create = function(locale) {
- if (locale == null) {
- locale = Timeline.getDefaultLocale();
- }
-
- var f = Timeline.ClassicTheme.implementations[locale];
- if (f == null) {
- f = Timeline.ClassicTheme._Impl;
- }
- return new f();
-};
-
-Timeline.ClassicTheme._Impl = function() {
- this.firstDayOfWeek = 0; // Sunday
-
- // Note: Many styles previously set here are now set using CSS
- // The comments indicate settings controlled by CSS, not
- // lines to be un-commented.
- //
- //
- // Attributes autoWidth, autoWidthAnimationTime, timeline_start
- // and timeline_stop must be set on the first band's theme.
- // The other attributes can be set differently for each
- // band by using different themes for the bands.
- this.autoWidth = false; // Should the Timeline automatically grow itself, as
- // needed when too many events for the available width
- // are painted on the visible part of the Timeline?
- this.autoWidthAnimationTime = 500; // mSec
- this.timeline_start = null; // Setting a date, eg new Date(Date.UTC(2010,0,17,20,00,00,0)) will prevent the
- // Timeline from being moved to anytime before the date.
- this.timeline_stop = null; // Use for setting a maximum date. The Timeline will not be able
- // to be moved to anytime after this date.
- this.ether = {
- backgroundColors: [
- // "#EEE",
- // "#DDD",
- // "#CCC",
- // "#AAA"
- ],
- // highlightColor: "white",
- highlightOpacity: 50,
- interval: {
- line: {
- show: true,
- opacity: 25
- // color: "#aaa",
- },
- weekend: {
- opacity: 30
- // color: "#FFFFE0",
- },
- marker: {
- hAlign: "Bottom",
- vAlign: "Right"
- /*
- hBottomStyler: function(elmt) {
- elmt.className = "timeline-ether-marker-bottom";
- },
- hBottomEmphasizedStyler: function(elmt) {
- elmt.className = "timeline-ether-marker-bottom-emphasized";
- },
- hTopStyler: function(elmt) {
- elmt.className = "timeline-ether-marker-top";
- },
- hTopEmphasizedStyler: function(elmt) {
- elmt.className = "timeline-ether-marker-top-emphasized";
- },
- */
-
-
- /*
- vRightStyler: function(elmt) {
- elmt.className = "timeline-ether-marker-right";
- },
- vRightEmphasizedStyler: function(elmt) {
- elmt.className = "timeline-ether-marker-right-emphasized";
- },
- vLeftStyler: function(elmt) {
- elmt.className = "timeline-ether-marker-left";
- },
- vLeftEmphasizedStyler:function(elmt) {
- elmt.className = "timeline-ether-marker-left-emphasized";
- }
- */
- }
- }
- };
-
- this.event = {
- track: {
- height: 10, // px. You will need to change the track
- // height if you change the tape height.
- gap: 2, // px. Gap between tracks
- offset: 2, // px. top margin above tapes
- autoWidthMargin: 1.5
- /* autoWidthMargin is only used if autoWidth (see above) is true.
- The autoWidthMargin setting is used to set how close the bottom of the
- lowest track is to the edge of the band's div. The units are total track
- width (tape + label + gap). A min of 0.5 is suggested. Use this setting to
- move the bottom track's tapes above the axis markers, if needed for your
- Timeline.
- */
- },
- overviewTrack: {
- offset: 20, // px -- top margin above tapes
- tickHeight: 6, // px
- height: 2, // px
- gap: 1, // px
- autoWidthMargin: 5 // This attribute is only used if autoWidth (see above) is true.
- },
- tape: {
- height: 4 // px. For thicker tapes, remember to change track height too.
- },
- instant: {
- icon: Timeline.urlPrefix + "images/dull-blue-circle.png",
- // default icon. Icon can also be specified per event
- iconWidth: 10,
- iconHeight: 10,
- impreciseOpacity: 20, // opacity of the tape when durationEvent is false
- impreciseIconMargin: 3 // A tape and an icon are painted for imprecise instant
- // events. This attribute is the margin between the
- // bottom of the tape and the top of the icon in that
- // case.
- // color: "#58A0DC",
- // impreciseColor: "#58A0DC",
- },
- duration: {
- impreciseOpacity: 20 // tape opacity for imprecise part of duration events
- // color: "#58A0DC",
- // impreciseColor: "#58A0DC",
- },
- label: {
- backgroundOpacity: 50,// only used in detailed painter
- offsetFromLine: 3 // px left margin amount from icon's right edge
- // backgroundColor: "white",
- // lineColor: "#58A0DC",
- },
- highlightColors: [ // Use with getEventPainter().setHighlightMatcher
- // See webapp/examples/examples.js
- "#FFFF00",
- "#FFC000",
- "#FF0000",
- "#0000FF"
- ],
- highlightLabelBackground: false, // When highlighting an event, also change the event's label background?
- bubble: {
- width: 250, // px
- maxHeight: 0, // px Maximum height of bubbles. 0 means no max height.
- // scrollbar will be added for taller bubbles
- titleStyler: function(elmt) {
- elmt.className = "timeline-event-bubble-title";
- },
- bodyStyler: function(elmt) {
- elmt.className = "timeline-event-bubble-body";
- },
- imageStyler: function(elmt) {
- elmt.className = "timeline-event-bubble-image";
- },
- wikiStyler: function(elmt) {
- elmt.className = "timeline-event-bubble-wiki";
- },
- timeStyler: function(elmt) {
- elmt.className = "timeline-event-bubble-time";
- }
- }
- };
-
- this.mouseWheel = 'scroll'; // 'default', 'zoom', 'scroll'
-};/*
- * An "ether" is a object that maps date/time to pixel coordinates.
- *
- */
-
-/*
- * Linear Ether
- *
- */
-
-Timeline.LinearEther = function(params) {
- this._params = params;
- this._interval = params.interval;
- this._pixelsPerInterval = params.pixelsPerInterval;
-};
-
-Timeline.LinearEther.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
- this._unit = timeline.getUnit();
-
- if ("startsOn" in this._params) {
- this._start = this._unit.parseFromObject(this._params.startsOn);
- } else if ("endsOn" in this._params) {
- this._start = this._unit.parseFromObject(this._params.endsOn);
- this.shiftPixels(-this._timeline.getPixelLength());
- } else if ("centersOn" in this._params) {
- this._start = this._unit.parseFromObject(this._params.centersOn);
- this.shiftPixels(-this._timeline.getPixelLength() / 2);
- } else {
- this._start = this._unit.makeDefaultValue();
- this.shiftPixels(-this._timeline.getPixelLength() / 2);
- }
-};
-
-Timeline.LinearEther.prototype.setDate = function(date) {
- this._start = this._unit.cloneValue(date);
-};
-
-Timeline.LinearEther.prototype.shiftPixels = function(pixels) {
- var numeric = this._interval * pixels / this._pixelsPerInterval;
- this._start = this._unit.change(this._start, numeric);
-};
-
-Timeline.LinearEther.prototype.dateToPixelOffset = function(date) {
- var numeric = this._unit.compare(date, this._start);
- return this._pixelsPerInterval * numeric / this._interval;
-};
-
-Timeline.LinearEther.prototype.pixelOffsetToDate = function(pixels) {
- var numeric = pixels * this._interval / this._pixelsPerInterval;
- return this._unit.change(this._start, numeric);
-};
-
-Timeline.LinearEther.prototype.zoom = function(zoomIn) {
- var netIntervalChange = 0;
- var currentZoomIndex = this._band._zoomIndex;
- var newZoomIndex = currentZoomIndex;
-
- if (zoomIn && (currentZoomIndex > 0)) {
- newZoomIndex = currentZoomIndex - 1;
- }
-
- if (!zoomIn && (currentZoomIndex < (this._band._zoomSteps.length - 1))) {
- newZoomIndex = currentZoomIndex + 1;
- }
-
- this._band._zoomIndex = newZoomIndex;
- this._interval =
- SimileAjax.DateTime.gregorianUnitLengths[this._band._zoomSteps[newZoomIndex].unit];
- this._pixelsPerInterval = this._band._zoomSteps[newZoomIndex].pixelsPerInterval;
- netIntervalChange = this._band._zoomSteps[newZoomIndex].unit -
- this._band._zoomSteps[currentZoomIndex].unit;
-
- return netIntervalChange;
-};
-
-
-/*
- * Hot Zone Ether
- *
- */
-
-Timeline.HotZoneEther = function(params) {
- this._params = params;
- this._interval = params.interval;
- this._pixelsPerInterval = params.pixelsPerInterval;
- this._theme = params.theme;
-};
-
-Timeline.HotZoneEther.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
- this._unit = timeline.getUnit();
-
- this._zones = [{
- startTime: Number.NEGATIVE_INFINITY,
- endTime: Number.POSITIVE_INFINITY,
- magnify: 1
- }];
- var params = this._params;
- for (var i = 0; i < params.zones.length; i++) {
- var zone = params.zones[i];
- var zoneStart = this._unit.parseFromObject(zone.start);
- var zoneEnd = this._unit.parseFromObject(zone.end);
-
- for (var j = 0; j < this._zones.length && this._unit.compare(zoneEnd, zoneStart) > 0; j++) {
- var zone2 = this._zones[j];
-
- if (this._unit.compare(zoneStart, zone2.endTime) < 0) {
- if (this._unit.compare(zoneStart, zone2.startTime) > 0) {
- this._zones.splice(j, 0, {
- startTime: zone2.startTime,
- endTime: zoneStart,
- magnify: zone2.magnify
- });
- j++;
-
- zone2.startTime = zoneStart;
- }
-
- if (this._unit.compare(zoneEnd, zone2.endTime) < 0) {
- this._zones.splice(j, 0, {
- startTime: zoneStart,
- endTime: zoneEnd,
- magnify: zone.magnify * zone2.magnify
- });
- j++;
-
- zone2.startTime = zoneEnd;
- zoneStart = zoneEnd;
- } else {
- zone2.magnify *= zone.magnify;
- zoneStart = zone2.endTime;
- }
- } // else, try the next existing zone
- }
- }
-
- if ("startsOn" in this._params) {
- this._start = this._unit.parseFromObject(this._params.startsOn);
- } else if ("endsOn" in this._params) {
- this._start = this._unit.parseFromObject(this._params.endsOn);
- this.shiftPixels(-this._timeline.getPixelLength());
- } else if ("centersOn" in this._params) {
- this._start = this._unit.parseFromObject(this._params.centersOn);
- this.shiftPixels(-this._timeline.getPixelLength() / 2);
- } else {
- this._start = this._unit.makeDefaultValue();
- this.shiftPixels(-this._timeline.getPixelLength() / 2);
- }
-};
-
-Timeline.HotZoneEther.prototype.setDate = function(date) {
- this._start = this._unit.cloneValue(date);
-};
-
-Timeline.HotZoneEther.prototype.shiftPixels = function(pixels) {
- this._start = this.pixelOffsetToDate(pixels);
-};
-
-Timeline.HotZoneEther.prototype.dateToPixelOffset = function(date) {
- return this._dateDiffToPixelOffset(this._start, date);
-};
-
-Timeline.HotZoneEther.prototype.pixelOffsetToDate = function(pixels) {
- return this._pixelOffsetToDate(pixels, this._start);
-};
-
-Timeline.HotZoneEther.prototype.zoom = function(zoomIn) {
- var netIntervalChange = 0;
- var currentZoomIndex = this._band._zoomIndex;
- var newZoomIndex = currentZoomIndex;
-
- if (zoomIn && (currentZoomIndex > 0)) {
- newZoomIndex = currentZoomIndex - 1;
- }
-
- if (!zoomIn && (currentZoomIndex < (this._band._zoomSteps.length - 1))) {
- newZoomIndex = currentZoomIndex + 1;
- }
-
- this._band._zoomIndex = newZoomIndex;
- this._interval =
- SimileAjax.DateTime.gregorianUnitLengths[this._band._zoomSteps[newZoomIndex].unit];
- this._pixelsPerInterval = this._band._zoomSteps[newZoomIndex].pixelsPerInterval;
- netIntervalChange = this._band._zoomSteps[newZoomIndex].unit -
- this._band._zoomSteps[currentZoomIndex].unit;
-
- return netIntervalChange;
-};
-
-Timeline.HotZoneEther.prototype._dateDiffToPixelOffset = function(fromDate, toDate) {
- var scale = this._getScale();
- var fromTime = fromDate;
- var toTime = toDate;
-
- var pixels = 0;
- if (this._unit.compare(fromTime, toTime) < 0) {
- var z = 0;
- while (z < this._zones.length) {
- if (this._unit.compare(fromTime, this._zones[z].endTime) < 0) {
- break;
- }
- z++;
- }
-
- while (this._unit.compare(fromTime, toTime) < 0) {
- var zone = this._zones[z];
- var toTime2 = this._unit.earlier(toTime, zone.endTime);
-
- pixels += (this._unit.compare(toTime2, fromTime) / (scale / zone.magnify));
-
- fromTime = toTime2;
- z++;
- }
- } else {
- var z = this._zones.length - 1;
- while (z >= 0) {
- if (this._unit.compare(fromTime, this._zones[z].startTime) > 0) {
- break;
- }
- z--;
- }
-
- while (this._unit.compare(fromTime, toTime) > 0) {
- var zone = this._zones[z];
- var toTime2 = this._unit.later(toTime, zone.startTime);
-
- pixels += (this._unit.compare(toTime2, fromTime) / (scale / zone.magnify));
-
- fromTime = toTime2;
- z--;
- }
- }
- return pixels;
-};
-
-Timeline.HotZoneEther.prototype._pixelOffsetToDate = function(pixels, fromDate) {
- var scale = this._getScale();
- var time = fromDate;
- if (pixels > 0) {
- var z = 0;
- while (z < this._zones.length) {
- if (this._unit.compare(time, this._zones[z].endTime) < 0) {
- break;
- }
- z++;
- }
-
- while (pixels > 0) {
- var zone = this._zones[z];
- var scale2 = scale / zone.magnify;
-
- if (zone.endTime == Number.POSITIVE_INFINITY) {
- time = this._unit.change(time, pixels * scale2);
- pixels = 0;
- } else {
- var pixels2 = this._unit.compare(zone.endTime, time) / scale2;
- if (pixels2 > pixels) {
- time = this._unit.change(time, pixels * scale2);
- pixels = 0;
- } else {
- time = zone.endTime;
- pixels -= pixels2;
- }
- }
- z++;
- }
- } else {
- var z = this._zones.length - 1;
- while (z >= 0) {
- if (this._unit.compare(time, this._zones[z].startTime) > 0) {
- break;
- }
- z--;
- }
-
- pixels = -pixels;
- while (pixels > 0) {
- var zone = this._zones[z];
- var scale2 = scale / zone.magnify;
-
- if (zone.startTime == Number.NEGATIVE_INFINITY) {
- time = this._unit.change(time, -pixels * scale2);
- pixels = 0;
- } else {
- var pixels2 = this._unit.compare(time, zone.startTime) / scale2;
- if (pixels2 > pixels) {
- time = this._unit.change(time, -pixels * scale2);
- pixels = 0;
- } else {
- time = zone.startTime;
- pixels -= pixels2;
- }
- }
- z--;
- }
- }
- return time;
-};
-
-Timeline.HotZoneEther.prototype._getScale = function() {
- return this._interval / this._pixelsPerInterval;
-};
-/*
- * Gregorian Ether Painter
- *
- */
-
-Timeline.GregorianEtherPainter = function(params) {
- this._params = params;
- this._theme = params.theme;
- this._unit = params.unit;
- this._multiple = ("multiple" in params) ? params.multiple : 1;
-};
-
-Timeline.GregorianEtherPainter.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._backgroundLayer = band.createLayerDiv(0);
- this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
- this._backgroundLayer.className = 'timeline-ether-bg';
- // this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
-
-
- this._markerLayer = null;
- this._lineLayer = null;
-
- var align = ("align" in this._params && this._params.align != undefined) ? this._params.align :
- this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
- var showLine = ("showLine" in this._params) ? this._params.showLine :
- this._theme.ether.interval.line.show;
-
- this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
- this._timeline, this._band, this._theme, align, showLine);
-
- this._highlight = new Timeline.EtherHighlight(
- this._timeline, this._band, this._theme, this._backgroundLayer);
-}
-
-Timeline.GregorianEtherPainter.prototype.setHighlight = function(startDate, endDate) {
- this._highlight.position(startDate, endDate);
-}
-
-Timeline.GregorianEtherPainter.prototype.paint = function() {
- if (this._markerLayer) {
- this._band.removeLayerDiv(this._markerLayer);
- }
- this._markerLayer = this._band.createLayerDiv(100);
- this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
- this._markerLayer.style.display = "none";
-
- if (this._lineLayer) {
- this._band.removeLayerDiv(this._lineLayer);
- }
- this._lineLayer = this._band.createLayerDiv(1);
- this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
- this._lineLayer.style.display = "none";
-
- var minDate = this._band.getMinDate();
- var maxDate = this._band.getMaxDate();
-
- var timeZone = this._band.getTimeZone();
- var labeller = this._band.getLabeller();
-
- SimileAjax.DateTime.roundDownToInterval(minDate, this._unit, timeZone, this._multiple, this._theme.firstDayOfWeek);
-
- var p = this;
- var incrementDate = function(date) {
- for (var i = 0; i < p._multiple; i++) {
- SimileAjax.DateTime.incrementByInterval(date, p._unit);
- }
- };
-
- while (minDate.getTime() < maxDate.getTime()) {
- this._intervalMarkerLayout.createIntervalMarker(
- minDate, labeller, this._unit, this._markerLayer, this._lineLayer);
-
- incrementDate(minDate);
- }
- this._markerLayer.style.display = "block";
- this._lineLayer.style.display = "block";
-};
-
-Timeline.GregorianEtherPainter.prototype.softPaint = function() {
-};
-
-Timeline.GregorianEtherPainter.prototype.zoom = function(netIntervalChange) {
- if (netIntervalChange != 0) {
- this._unit += netIntervalChange;
- }
-};
-
-
-/*
- * Hot Zone Gregorian Ether Painter
- *
- */
-
-Timeline.HotZoneGregorianEtherPainter = function(params) {
- this._params = params;
- this._theme = params.theme;
-
- this._zones = [{
- startTime: Number.NEGATIVE_INFINITY,
- endTime: Number.POSITIVE_INFINITY,
- unit: params.unit,
- multiple: 1
- }];
- for (var i = 0; i < params.zones.length; i++) {
- var zone = params.zones[i];
- var zoneStart = SimileAjax.DateTime.parseGregorianDateTime(zone.start).getTime();
- var zoneEnd = SimileAjax.DateTime.parseGregorianDateTime(zone.end).getTime();
-
- for (var j = 0; j < this._zones.length && zoneEnd > zoneStart; j++) {
- var zone2 = this._zones[j];
-
- if (zoneStart < zone2.endTime) {
- if (zoneStart > zone2.startTime) {
- this._zones.splice(j, 0, {
- startTime: zone2.startTime,
- endTime: zoneStart,
- unit: zone2.unit,
- multiple: zone2.multiple
- });
- j++;
-
- zone2.startTime = zoneStart;
- }
-
- if (zoneEnd < zone2.endTime) {
- this._zones.splice(j, 0, {
- startTime: zoneStart,
- endTime: zoneEnd,
- unit: zone.unit,
- multiple: (zone.multiple) ? zone.multiple : 1
- });
- j++;
-
- zone2.startTime = zoneEnd;
- zoneStart = zoneEnd;
- } else {
- zone2.multiple = zone.multiple;
- zone2.unit = zone.unit;
- zoneStart = zone2.endTime;
- }
- } // else, try the next existing zone
- }
- }
-};
-
-Timeline.HotZoneGregorianEtherPainter.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._backgroundLayer = band.createLayerDiv(0);
- this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
- this._backgroundLayer.className ='timeline-ether-bg';
- //this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
-
- this._markerLayer = null;
- this._lineLayer = null;
-
- var align = ("align" in this._params && this._params.align != undefined) ? this._params.align :
- this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
- var showLine = ("showLine" in this._params) ? this._params.showLine :
- this._theme.ether.interval.line.show;
-
- this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
- this._timeline, this._band, this._theme, align, showLine);
-
- this._highlight = new Timeline.EtherHighlight(
- this._timeline, this._band, this._theme, this._backgroundLayer);
-}
-
-Timeline.HotZoneGregorianEtherPainter.prototype.setHighlight = function(startDate, endDate) {
- this._highlight.position(startDate, endDate);
-}
-
-Timeline.HotZoneGregorianEtherPainter.prototype.paint = function() {
- if (this._markerLayer) {
- this._band.removeLayerDiv(this._markerLayer);
- }
- this._markerLayer = this._band.createLayerDiv(100);
- this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
- this._markerLayer.style.display = "none";
-
- if (this._lineLayer) {
- this._band.removeLayerDiv(this._lineLayer);
- }
- this._lineLayer = this._band.createLayerDiv(1);
- this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
- this._lineLayer.style.display = "none";
-
- var minDate = this._band.getMinDate();
- var maxDate = this._band.getMaxDate();
-
- var timeZone = this._band.getTimeZone();
- var labeller = this._band.getLabeller();
-
- var p = this;
- var incrementDate = function(date, zone) {
- for (var i = 0; i < zone.multiple; i++) {
- SimileAjax.DateTime.incrementByInterval(date, zone.unit);
- }
- };
-
- var zStart = 0;
- while (zStart < this._zones.length) {
- if (minDate.getTime() < this._zones[zStart].endTime) {
- break;
- }
- zStart++;
- }
- var zEnd = this._zones.length - 1;
- while (zEnd >= 0) {
- if (maxDate.getTime() > this._zones[zEnd].startTime) {
- break;
- }
- zEnd--;
- }
-
- for (var z = zStart; z <= zEnd; z++) {
- var zone = this._zones[z];
-
- var minDate2 = new Date(Math.max(minDate.getTime(), zone.startTime));
- var maxDate2 = new Date(Math.min(maxDate.getTime(), zone.endTime));
-
- SimileAjax.DateTime.roundDownToInterval(minDate2, zone.unit, timeZone, zone.multiple, this._theme.firstDayOfWeek);
- SimileAjax.DateTime.roundUpToInterval(maxDate2, zone.unit, timeZone, zone.multiple, this._theme.firstDayOfWeek);
-
- while (minDate2.getTime() < maxDate2.getTime()) {
- this._intervalMarkerLayout.createIntervalMarker(
- minDate2, labeller, zone.unit, this._markerLayer, this._lineLayer);
-
- incrementDate(minDate2, zone);
- }
- }
- this._markerLayer.style.display = "block";
- this._lineLayer.style.display = "block";
-};
-
-Timeline.HotZoneGregorianEtherPainter.prototype.softPaint = function() {
-};
-
-Timeline.HotZoneGregorianEtherPainter.prototype.zoom = function(netIntervalChange) {
- if (netIntervalChange != 0) {
- for (var i = 0; i < this._zones.length; ++i) {
- if (this._zones[i]) {
- this._zones[i].unit += netIntervalChange;
- }
- }
- }
-};
-
-/*
- * Year Count Ether Painter
- *
- */
-
-Timeline.YearCountEtherPainter = function(params) {
- this._params = params;
- this._theme = params.theme;
- this._startDate = SimileAjax.DateTime.parseGregorianDateTime(params.startDate);
- this._multiple = ("multiple" in params) ? params.multiple : 1;
-};
-
-Timeline.YearCountEtherPainter.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._backgroundLayer = band.createLayerDiv(0);
- this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
- this._backgroundLayer.className = 'timeline-ether-bg';
- // this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
-
- this._markerLayer = null;
- this._lineLayer = null;
-
- var align = ("align" in this._params) ? this._params.align :
- this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
- var showLine = ("showLine" in this._params) ? this._params.showLine :
- this._theme.ether.interval.line.show;
-
- this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
- this._timeline, this._band, this._theme, align, showLine);
-
- this._highlight = new Timeline.EtherHighlight(
- this._timeline, this._band, this._theme, this._backgroundLayer);
-};
-
-Timeline.YearCountEtherPainter.prototype.setHighlight = function(startDate, endDate) {
- this._highlight.position(startDate, endDate);
-};
-
-Timeline.YearCountEtherPainter.prototype.paint = function() {
- if (this._markerLayer) {
- this._band.removeLayerDiv(this._markerLayer);
- }
- this._markerLayer = this._band.createLayerDiv(100);
- this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
- this._markerLayer.style.display = "none";
-
- if (this._lineLayer) {
- this._band.removeLayerDiv(this._lineLayer);
- }
- this._lineLayer = this._band.createLayerDiv(1);
- this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
- this._lineLayer.style.display = "none";
-
- var minDate = new Date(this._startDate.getTime());
- var maxDate = this._band.getMaxDate();
- var yearDiff = this._band.getMinDate().getUTCFullYear() - this._startDate.getUTCFullYear();
- minDate.setUTCFullYear(this._band.getMinDate().getUTCFullYear() - yearDiff % this._multiple);
-
- var p = this;
- var incrementDate = function(date) {
- for (var i = 0; i < p._multiple; i++) {
- SimileAjax.DateTime.incrementByInterval(date, SimileAjax.DateTime.YEAR);
- }
- };
- var labeller = {
- labelInterval: function(date, intervalUnit) {
- var diff = date.getUTCFullYear() - p._startDate.getUTCFullYear();
- return {
- text: diff,
- emphasized: diff == 0
- };
- }
- };
-
- while (minDate.getTime() < maxDate.getTime()) {
- this._intervalMarkerLayout.createIntervalMarker(
- minDate, labeller, SimileAjax.DateTime.YEAR, this._markerLayer, this._lineLayer);
-
- incrementDate(minDate);
- }
- this._markerLayer.style.display = "block";
- this._lineLayer.style.display = "block";
-};
-
-Timeline.YearCountEtherPainter.prototype.softPaint = function() {
-};
-
-/*
- * Quarterly Ether Painter
- *
- */
-
-Timeline.QuarterlyEtherPainter = function(params) {
- this._params = params;
- this._theme = params.theme;
- this._startDate = SimileAjax.DateTime.parseGregorianDateTime(params.startDate);
-};
-
-Timeline.QuarterlyEtherPainter.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._backgroundLayer = band.createLayerDiv(0);
- this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
- this._backgroundLayer.className = 'timeline-ether-bg';
- // this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
-
- this._markerLayer = null;
- this._lineLayer = null;
-
- var align = ("align" in this._params) ? this._params.align :
- this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
- var showLine = ("showLine" in this._params) ? this._params.showLine :
- this._theme.ether.interval.line.show;
-
- this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
- this._timeline, this._band, this._theme, align, showLine);
-
- this._highlight = new Timeline.EtherHighlight(
- this._timeline, this._band, this._theme, this._backgroundLayer);
-};
-
-Timeline.QuarterlyEtherPainter.prototype.setHighlight = function(startDate, endDate) {
- this._highlight.position(startDate, endDate);
-};
-
-Timeline.QuarterlyEtherPainter.prototype.paint = function() {
- if (this._markerLayer) {
- this._band.removeLayerDiv(this._markerLayer);
- }
- this._markerLayer = this._band.createLayerDiv(100);
- this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
- this._markerLayer.style.display = "none";
-
- if (this._lineLayer) {
- this._band.removeLayerDiv(this._lineLayer);
- }
- this._lineLayer = this._band.createLayerDiv(1);
- this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
- this._lineLayer.style.display = "none";
-
- var minDate = new Date(0);
- var maxDate = this._band.getMaxDate();
-
- minDate.setUTCFullYear(Math.max(this._startDate.getUTCFullYear(), this._band.getMinDate().getUTCFullYear()));
- minDate.setUTCMonth(this._startDate.getUTCMonth());
-
- var p = this;
- var incrementDate = function(date) {
- date.setUTCMonth(date.getUTCMonth() + 3);
- };
- var labeller = {
- labelInterval: function(date, intervalUnit) {
- var quarters = (4 + (date.getUTCMonth() - p._startDate.getUTCMonth()) / 3) % 4;
- if (quarters != 0) {
- return { text: "Q" + (quarters + 1), emphasized: false };
- } else {
- return { text: "Y" + (date.getUTCFullYear() - p._startDate.getUTCFullYear() + 1), emphasized: true };
- }
- }
- };
-
- while (minDate.getTime() < maxDate.getTime()) {
- this._intervalMarkerLayout.createIntervalMarker(
- minDate, labeller, SimileAjax.DateTime.YEAR, this._markerLayer, this._lineLayer);
-
- incrementDate(minDate);
- }
- this._markerLayer.style.display = "block";
- this._lineLayer.style.display = "block";
-};
-
-Timeline.QuarterlyEtherPainter.prototype.softPaint = function() {
-};
-
-/*
- * Ether Interval Marker Layout
- *
- */
-
-Timeline.EtherIntervalMarkerLayout = function(timeline, band, theme, align, showLine) {
- var horizontal = timeline.isHorizontal();
- if (horizontal) {
- if (align == "Top") {
- this.positionDiv = function(div, offset) {
- div.style.left = offset + "px";
- div.style.top = "0px";
- };
- } else {
- this.positionDiv = function(div, offset) {
- div.style.left = offset + "px";
- div.style.bottom = "0px";
- };
- }
- } else {
- if (align == "Left") {
- this.positionDiv = function(div, offset) {
- div.style.top = offset + "px";
- div.style.left = "0px";
- };
- } else {
- this.positionDiv = function(div, offset) {
- div.style.top = offset + "px";
- div.style.right = "0px";
- };
- }
- }
-
- var markerTheme = theme.ether.interval.marker;
- var lineTheme = theme.ether.interval.line;
- var weekendTheme = theme.ether.interval.weekend;
-
- var stylePrefix = (horizontal ? "h" : "v") + align;
- var labelStyler = markerTheme[stylePrefix + "Styler"];
- var emphasizedLabelStyler = markerTheme[stylePrefix + "EmphasizedStyler"];
- var day = SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.DAY];
-
- this.createIntervalMarker = function(date, labeller, unit, markerDiv, lineDiv) {
- var offset = Math.round(band.dateToPixelOffset(date));
-
- if (showLine && unit != SimileAjax.DateTime.WEEK) {
- var divLine = timeline.getDocument().createElement("div");
- divLine.className = "timeline-ether-lines";
-
- if (lineTheme.opacity < 100) {
- SimileAjax.Graphics.setOpacity(divLine, lineTheme.opacity);
- }
-
- if (horizontal) {
- //divLine.className += " timeline-ether-lines-vertical";
- divLine.style.left = offset + "px";
- } else {
- //divLine.className += " timeline-ether-lines-horizontal";
- divLine.style.top = offset + "px";
- }
- lineDiv.appendChild(divLine);
- }
- if (unit == SimileAjax.DateTime.WEEK) {
- var firstDayOfWeek = theme.firstDayOfWeek;
-
- var saturday = new Date(date.getTime() + (6 - firstDayOfWeek - 7) * day);
- var monday = new Date(saturday.getTime() + 2 * day);
-
- var saturdayPixel = Math.round(band.dateToPixelOffset(saturday));
- var mondayPixel = Math.round(band.dateToPixelOffset(monday));
- var length = Math.max(1, mondayPixel - saturdayPixel);
-
- var divWeekend = timeline.getDocument().createElement("div");
- divWeekend.className = 'timeline-ether-weekends'
-
- if (weekendTheme.opacity < 100) {
- SimileAjax.Graphics.setOpacity(divWeekend, weekendTheme.opacity);
- }
-
- if (horizontal) {
- divWeekend.style.left = saturdayPixel + "px";
- divWeekend.style.width = length + "px";
- } else {
- divWeekend.style.top = saturdayPixel + "px";
- divWeekend.style.height = length + "px";
- }
- lineDiv.appendChild(divWeekend);
- }
-
- var label = labeller.labelInterval(date, unit);
-
- var div = timeline.getDocument().createElement("div");
- div.innerHTML = label.text;
-
-
-
- div.className = 'timeline-date-label'
- if(label.emphasized) div.className += ' timeline-date-label-em'
-
- this.positionDiv(div, offset);
- markerDiv.appendChild(div);
-
- return div;
- };
-};
-
-/*
- * Ether Highlight Layout
- *
- */
-
-Timeline.EtherHighlight = function(timeline, band, theme, backgroundLayer) {
- var horizontal = timeline.isHorizontal();
-
- this._highlightDiv = null;
- this._createHighlightDiv = function() {
- if (this._highlightDiv == null) {
- this._highlightDiv = timeline.getDocument().createElement("div");
- this._highlightDiv.setAttribute("name", "ether-highlight"); // for debugging
- this._highlightDiv.className = 'timeline-ether-highlight'
-
- var opacity = theme.ether.highlightOpacity;
- if (opacity < 100) {
- SimileAjax.Graphics.setOpacity(this._highlightDiv, opacity);
- }
-
- backgroundLayer.appendChild(this._highlightDiv);
- }
- }
-
- this.position = function(startDate, endDate) {
- this._createHighlightDiv();
-
- var startPixel = Math.round(band.dateToPixelOffset(startDate));
- var endPixel = Math.round(band.dateToPixelOffset(endDate));
- var length = Math.max(endPixel - startPixel, 3);
- if (horizontal) {
- this._highlightDiv.style.left = startPixel + "px";
- this._highlightDiv.style.width = length + "px";
- this._highlightDiv.style.height = (band.getViewWidth() - 4) + "px";
- } else {
- this._highlightDiv.style.top = startPixel + "px";
- this._highlightDiv.style.height = length + "px";
- this._highlightDiv.style.width = (band.getViewWidth() - 4) + "px";
- }
- }
-};
-/*
- * Event Utils
- *
- */
-Timeline.EventUtils = {};
-
-Timeline.EventUtils.getNewEventID = function() {
- // global across page
- if (this._lastEventID == null) {
- this._lastEventID = 0;
- }
-
- this._lastEventID += 1;
- return "e" + this._lastEventID;
-};
-
-Timeline.EventUtils.decodeEventElID = function(elementID) {
- /*
- *
- * Use this function to decode an event element's id on a band (label div,
- * tape div or icon img).
- *
- * Returns {band: <bandObj>, evt: <eventObj>}
- *
- * To enable a single event listener to monitor everything
- * on a Timeline, a set format is used for the id's of the
- * elements on the Timeline--
- *
- * element id format for labels, icons, tapes:
- * labels: label-tl-<timelineID>-<band_index>-<evt.id>
- * icons: icon-tl-<timelineID>-<band_index>-<evt.id>
- * tapes: tape1-tl-<timelineID>-<band_index>-<evt.id>
- * tape2-tl-<timelineID>-<band_index>-<evt.id>
- * // some events have more than one tape
- * highlight: highlight1-tl-<timelineID>-<band_index>-<evt.id>
- * highlight2-tl-<timelineID>-<band_index>-<evt.id>
- * // some events have more than one highlight div (future)
- * Note: use split('-') to get array of the format's parts
- *
- * You can then retrieve the timeline object and event object
- * by using Timeline.getTimeline, Timeline.getBand, or
- * Timeline.getEvent and passing in the element's id
- *
- *
- */
-
- var parts = elementID.split('-');
- if (parts[1] != 'tl') {
- alert("Internal Timeline problem 101, please consult support");
- return {band: null, evt: null}; // early return
- }
-
- var timeline = Timeline.getTimelineFromID(parts[2]);
- var band = timeline.getBand(parts[3]);
- var evt = band.getEventSource.getEvent(parts[4]);
-
- return {band: band, evt: evt};
-};
-
-Timeline.EventUtils.encodeEventElID = function(timeline, band, elType, evt) {
- // elType should be one of {label | icon | tapeN | highlightN}
- return elType + "-tl-" + timeline.timelineID +
- "-" + band.getIndex() + "-" + evt.getID();
-};/*
- * Gregorian Date Labeller
- *
- */
-
-Timeline.GregorianDateLabeller = function(locale, timeZone) {
- this._locale = locale;
- this._timeZone = timeZone;
-};
-
-Timeline.GregorianDateLabeller.monthNames = [];
-Timeline.GregorianDateLabeller.dayNames = [];
-Timeline.GregorianDateLabeller.labelIntervalFunctions = [];
-
-Timeline.GregorianDateLabeller.getMonthName = function(month, locale) {
- return Timeline.GregorianDateLabeller.monthNames[locale][month];
-};
-
-Timeline.GregorianDateLabeller.prototype.labelInterval = function(date, intervalUnit) {
- var f = Timeline.GregorianDateLabeller.labelIntervalFunctions[this._locale];
- if (f == null) {
- f = Timeline.GregorianDateLabeller.prototype.defaultLabelInterval;
- }
- return f.call(this, date, intervalUnit);
-};
-
-Timeline.GregorianDateLabeller.prototype.labelPrecise = function(date) {
- return SimileAjax.DateTime.removeTimeZoneOffset(
- date,
- this._timeZone //+ (new Date().getTimezoneOffset() / 60)
- ).toUTCString();
-};
-
-Timeline.GregorianDateLabeller.prototype.defaultLabelInterval = function(date, intervalUnit) {
- var text;
- var emphasized = false;
-
- date = SimileAjax.DateTime.removeTimeZoneOffset(date, this._timeZone);
-
- switch(intervalUnit) {
- case SimileAjax.DateTime.MILLISECOND:
- text = date.getUTCMilliseconds();
- break;
- case SimileAjax.DateTime.SECOND:
- text = date.getUTCSeconds();
- break;
- case SimileAjax.DateTime.MINUTE:
- var m = date.getUTCMinutes();
- if (m == 0) {
- text = date.getUTCHours() + ":00";
- emphasized = true;
- } else {
- text = m;
- }
- break;
- case SimileAjax.DateTime.HOUR:
- text = date.getUTCHours() + "hr";
- break;
- case SimileAjax.DateTime.DAY:
- text = Timeline.GregorianDateLabeller.getMonthName(date.getUTCMonth(), this._locale) + " " + date.getUTCDate();
- break;
- case SimileAjax.DateTime.WEEK:
- text = Timeline.GregorianDateLabeller.getMonthName(date.getUTCMonth(), this._locale) + " " + date.getUTCDate();
- break;
- case SimileAjax.DateTime.MONTH:
- var m = date.getUTCMonth();
- if (m != 0) {
- text = Timeline.GregorianDateLabeller.getMonthName(m, this._locale);
- break;
- } // else, fall through
- case SimileAjax.DateTime.YEAR:
- case SimileAjax.DateTime.DECADE:
- case SimileAjax.DateTime.CENTURY:
- case SimileAjax.DateTime.MILLENNIUM:
- var y = date.getUTCFullYear();
- if (y > 0) {
- text = date.getUTCFullYear();
- } else {
- text = (1 - y) + "BC";
- }
- emphasized =
- (intervalUnit == SimileAjax.DateTime.MONTH) ||
- (intervalUnit == SimileAjax.DateTime.DECADE && y % 100 == 0) ||
- (intervalUnit == SimileAjax.DateTime.CENTURY && y % 1000 == 0);
- break;
- default:
- text = date.toUTCString();
- }
- return { text: text, emphasized: emphasized };
-}
-
-/*
- * Default Event Source
- *
- */
-
-
-Timeline.DefaultEventSource = function(eventIndex) {
- this._events = (eventIndex instanceof Object) ? eventIndex : new SimileAjax.EventIndex();
- this._listeners = [];
-};
-
-Timeline.DefaultEventSource.prototype.addListener = function(listener) {
- this._listeners.push(listener);
-};
-
-Timeline.DefaultEventSource.prototype.removeListener = function(listener) {
- for (var i = 0; i < this._listeners.length; i++) {
- if (this._listeners[i] == listener) {
- this._listeners.splice(i, 1);
- break;
- }
- }
-};
-
-Timeline.DefaultEventSource.prototype.loadXML = function(xml, url) {
- var base = this._getBaseURL(url);
-
- var wikiURL = xml.documentElement.getAttribute("wiki-url");
- var wikiSection = xml.documentElement.getAttribute("wiki-section");
-
- var dateTimeFormat = xml.documentElement.getAttribute("date-time-format");
- var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
-
- var node = xml.documentElement.firstChild;
- var added = false;
- while (node != null) {
- if (node.nodeType == 1) {
- var description = "";
- if (node.firstChild != null && node.firstChild.nodeType == 3) {
- description = node.firstChild.nodeValue;
- }
- // instant event: default is true. Or use values from isDuration or durationEvent
- var instant = (node.getAttribute("isDuration") === null &&
- node.getAttribute("durationEvent") === null) ||
- node.getAttribute("isDuration") == "false" ||
- node.getAttribute("durationEvent") == "false";
-
- var evt = new Timeline.DefaultEventSource.Event( {
- id: node.getAttribute("id"),
- start: parseDateTimeFunction(node.getAttribute("start")),
- end: parseDateTimeFunction(node.getAttribute("end")),
- latestStart: parseDateTimeFunction(node.getAttribute("latestStart")),
- earliestEnd: parseDateTimeFunction(node.getAttribute("earliestEnd")),
- instant: instant,
- text: node.getAttribute("title"),
- description: description,
- image: this._resolveRelativeURL(node.getAttribute("image"), base),
- link: this._resolveRelativeURL(node.getAttribute("link") , base),
- icon: this._resolveRelativeURL(node.getAttribute("icon") , base),
- color: node.getAttribute("color"),
- textColor: node.getAttribute("textColor"),
- hoverText: node.getAttribute("hoverText"),
- classname: node.getAttribute("classname"),
- tapeImage: node.getAttribute("tapeImage"),
- tapeRepeat: node.getAttribute("tapeRepeat"),
- caption: node.getAttribute("caption"),
- eventID: node.getAttribute("eventID"),
- trackNum: node.getAttribute("trackNum")
- });
-
- evt._node = node;
- evt.getProperty = function(name) {
- return this._node.getAttribute(name);
- };
- evt.setWikiInfo(wikiURL, wikiSection);
-
- this._events.add(evt);
-
- added = true;
- }
- node = node.nextSibling;
- }
-
- if (added) {
- this._fire("onAddMany", []);
- }
-};
-
-
-Timeline.DefaultEventSource.prototype.loadJSON = function(data, url) {
- var base = this._getBaseURL(url);
- var added = false;
- if (data && data.events){
- var wikiURL = ("wikiURL" in data) ? data.wikiURL : null;
- var wikiSection = ("wikiSection" in data) ? data.wikiSection : null;
-
- var dateTimeFormat = ("dateTimeFormat" in data) ? data.dateTimeFormat : null;
- var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
-
- for (var i=0; i < data.events.length; i++){
- var event = data.events[i];
- // Fixing issue 33:
- // instant event: default (for JSON only) is false. Or use values from isDuration or durationEvent
- // isDuration was negated (see issue 33, so keep that interpretation
- var instant = event.isDuration || (event.durationEvent != null && !event.durationEvent);
-
- var evt = new Timeline.DefaultEventSource.Event({
- id: ("id" in event) ? event.id : undefined,
- start: parseDateTimeFunction(event.start),
- end: parseDateTimeFunction(event.end),
- latestStart: parseDateTimeFunction(event.latestStart),
- earliestEnd: parseDateTimeFunction(event.earliestEnd),
- instant: instant,
- text: event.title,
- description: event.description,
- image: this._resolveRelativeURL(event.image, base),
- link: this._resolveRelativeURL(event.link , base),
- icon: this._resolveRelativeURL(event.icon , base),
- color: event.color,
- textColor: event.textColor,
- hoverText: event.hoverText,
- classname: event.classname,
- tapeImage: event.tapeImage,
- tapeRepeat: event.tapeRepeat,
- caption: event.caption,
- eventID: event.eventID,
- trackNum: event.trackNum
- });
- evt._obj = event;
- evt.getProperty = function(name) {
- return this._obj[name];
- };
- evt.setWikiInfo(wikiURL, wikiSection);
-
- this._events.add(evt);
- added = true;
- }
- }
-
- if (added) {
- this._fire("onAddMany", []);
- }
-};
-
-/*
- * Contributed by Morten Frederiksen, http://www.wasab.dk/morten/
- */
-Timeline.DefaultEventSource.prototype.loadSPARQL = function(xml, url) {
- var base = this._getBaseURL(url);
-
- var dateTimeFormat = 'iso8601';
- var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
-
- if (xml == null) {
- return;
- }
-
- /*
- * Find <results> tag
- */
- var node = xml.documentElement.firstChild;
- while (node != null && (node.nodeType != 1 || node.nodeName != 'results')) {
- node = node.nextSibling;
- }
-
- var wikiURL = null;
- var wikiSection = null;
- if (node != null) {
- wikiURL = node.getAttribute("wiki-url");
- wikiSection = node.getAttribute("wiki-section");
-
- node = node.firstChild;
- }
-
- var added = false;
- while (node != null) {
- if (node.nodeType == 1) {
- var bindings = { };
- var binding = node.firstChild;
- while (binding != null) {
- if (binding.nodeType == 1 &&
- binding.firstChild != null &&
- binding.firstChild.nodeType == 1 &&
- binding.firstChild.firstChild != null &&
- binding.firstChild.firstChild.nodeType == 3) {
- bindings[binding.getAttribute('name')] = binding.firstChild.firstChild.nodeValue;
- }
- binding = binding.nextSibling;
- }
-
- if (bindings["start"] == null && bindings["date"] != null) {
- bindings["start"] = bindings["date"];
- }
-
- // instant event: default is true. Or use values from isDuration or durationEvent
- var instant = (bindings["isDuration"] === null &&
- bindings["durationEvent"] === null) ||
- bindings["isDuration"] == "false" ||
- bindings["durationEvent"] == "false";
-
- var evt = new Timeline.DefaultEventSource.Event({
- id: bindings["id"],
- start: parseDateTimeFunction(bindings["start"]),
- end: parseDateTimeFunction(bindings["end"]),
- latestStart: parseDateTimeFunction(bindings["latestStart"]),
- earliestEnd: parseDateTimeFunction(bindings["earliestEnd"]),
- instant: instant, // instant
- text: bindings["title"], // text
- description: bindings["description"],
- image: this._resolveRelativeURL(bindings["image"], base),
- link: this._resolveRelativeURL(bindings["link"] , base),
- icon: this._resolveRelativeURL(bindings["icon"] , base),
- color: bindings["color"],
- textColor: bindings["textColor"],
- hoverText: bindings["hoverText"],
- caption: bindings["caption"],
- classname: bindings["classname"],
- tapeImage: bindings["tapeImage"],
- tapeRepeat: bindings["tapeRepeat"],
- eventID: bindings["eventID"],
- trackNum: bindings["trackNum"]
- });
- evt._bindings = bindings;
- evt.getProperty = function(name) {
- return this._bindings[name];
- };
- evt.setWikiInfo(wikiURL, wikiSection);
-
- this._events.add(evt);
- added = true;
- }
- node = node.nextSibling;
- }
-
- if (added) {
- this._fire("onAddMany", []);
- }
-};
-
-Timeline.DefaultEventSource.prototype.add = function(evt) {
- this._events.add(evt);
- this._fire("onAddOne", [evt]);
-};
-
-Timeline.DefaultEventSource.prototype.addMany = function(events) {
- for (var i = 0; i < events.length; i++) {
- this._events.add(events[i]);
- }
- this._fire("onAddMany", []);
-};
-
-Timeline.DefaultEventSource.prototype.clear = function() {
- this._events.removeAll();
- this._fire("onClear", []);
-};
-
-Timeline.DefaultEventSource.prototype.getEvent = function(id) {
- return this._events.getEvent(id);
-};
-
-Timeline.DefaultEventSource.prototype.getEventIterator = function(startDate, endDate) {
- return this._events.getIterator(startDate, endDate);
-};
-
-Timeline.DefaultEventSource.prototype.getEventReverseIterator = function(startDate, endDate) {
- return this._events.getReverseIterator(startDate, endDate);
-};
-
-Timeline.DefaultEventSource.prototype.getAllEventIterator = function() {
- return this._events.getAllIterator();
-};
-
-Timeline.DefaultEventSource.prototype.getCount = function() {
- return this._events.getCount();
-};
-
-Timeline.DefaultEventSource.prototype.getEarliestDate = function() {
- return this._events.getEarliestDate();
-};
-
-Timeline.DefaultEventSource.prototype.getLatestDate = function() {
- return this._events.getLatestDate();
-};
-
-Timeline.DefaultEventSource.prototype._fire = function(handlerName, args) {
- for (var i = 0; i < this._listeners.length; i++) {
- var listener = this._listeners[i];
- if (handlerName in listener) {
- try {
- listener[handlerName].apply(listener, args);
- } catch (e) {
- SimileAjax.Debug.exception(e);
- }
- }
- }
-};
-
-Timeline.DefaultEventSource.prototype._getBaseURL = function(url) {
- if (url.indexOf("://") < 0) {
- var url2 = this._getBaseURL(document.location.href);
- if (url.substr(0,1) == "/") {
- url = url2.substr(0, url2.indexOf("/", url2.indexOf("://") + 3)) + url;
- } else {
- url = url2 + url;
- }
- }
-
- var i = url.lastIndexOf("/");
- if (i < 0) {
- return "";
- } else {
- return url.substr(0, i+1);
- }
-};
-
-Timeline.DefaultEventSource.prototype._resolveRelativeURL = function(url, base) {
- if (url == null || url == "") {
- return url;
- } else if (url.indexOf("://") > 0) {
- return url;
- } else if (url.substr(0,1) == "/") {
- return base.substr(0, base.indexOf("/", base.indexOf("://") + 3)) + url;
- } else {
- return base + url;
- }
-};
-
-
-Timeline.DefaultEventSource.Event = function(args) {
- //
- // Attention developers!
- // If you add a new event attribute, please be sure to add it to
- // all three load functions: loadXML, loadSPARCL, loadJSON.
- // Thanks!
- //
- // args is a hash/object. It supports the following keys. Most are optional
- // id -- an internal id. Really shouldn't be used by events.
- // Timeline library clients should use eventID
- // eventID -- For use by library client when writing custom painters or
- // custom fillInfoBubble
- // start
- // end
- // latestStart
- // earliestEnd
- // instant -- boolean. Controls precise/non-precise logic & duration/instant issues
- // text -- event source attribute 'title' -- used as the label on Timelines and in bubbles.
- // description -- used in bubbles
- // image -- used in bubbles
- // link -- used in bubbles
- // icon -- on the Timeline
- // color -- Timeline label and tape color
- // textColor -- Timeline label color, overrides color attribute
- // hoverText -- deprecated, here for backwards compatibility.
- // Superceeded by caption
- // caption -- tooltip-like caption on the Timeline. Uses HTML title attribute
- // classname -- used to set classname in Timeline. Enables better CSS selector rules
- // tapeImage -- background image of the duration event's tape div on the Timeline
- // tapeRepeat -- repeat attribute for tapeImage. {repeat | repeat-x | repeat-y }
-
- function cleanArg(arg) {
- // clean up an arg
- return (args[arg] != null && args[arg] != "") ? args[arg] : null;
- }
-
- var id = args.id ? args.id.trim() : "";
- this._id = id.length > 0 ? id : Timeline.EventUtils.getNewEventID();
-
- this._instant = args.instant || (args.end == null);
-
- this._start = args.start;
- this._end = (args.end != null) ? args.end : args.start;
-
- this._latestStart = (args.latestStart != null) ?
- args.latestStart : (args.instant ? this._end : this._start);
- this._earliestEnd = (args.earliestEnd != null) ? args.earliestEnd : this._end;
-
- // check sanity of dates since incorrect dates will later cause calculation errors
- // when painting
- var err=[];
- if (this._start > this._latestStart) {
- this._latestStart = this._start;
- err.push("start is > latestStart");}
- if (this._start > this._earliestEnd) {
- this._earliestEnd = this._latestStart;
- err.push("start is > earliestEnd");}
- if (this._start > this._end) {
- this._end = this._earliestEnd;
- err.push("start is > end");}
- if (this._latestStart > this._earliestEnd) {
- this._earliestEnd = this._latestStart;
- err.push("latestStart is > earliestEnd");}
- if (this._latestStart > this._end) {
- this._end = this._earliestEnd;
- err.push("latestStart is > end");}
- if (this._earliestEnd > this._end) {
- this._end = this._earliestEnd;
- err.push("earliestEnd is > end");}
-
- this._eventID = cleanArg('eventID');
- this._text = (args.text != null) ? SimileAjax.HTML.deEntify(args.text) : ""; // Change blank titles to ""
- if (err.length > 0) {
- this._text += " PROBLEM: " + err.join(", ");
- }
-
- this._description = SimileAjax.HTML.deEntify(args.description);
- this._image = cleanArg('image');
- this._link = cleanArg('link');
- this._title = cleanArg('hoverText');
- this._title = cleanArg('caption');
-
- this._icon = cleanArg('icon');
- this._color = cleanArg('color');
- this._textColor = cleanArg('textColor');
- this._classname = cleanArg('classname');
- this._tapeImage = cleanArg('tapeImage');
- this._tapeRepeat = cleanArg('tapeRepeat');
- this._trackNum = cleanArg('trackNum');
- if (this._trackNum != null) {
- this._trackNum = parseInt(this._trackNum);
- }
-
- this._wikiURL = null;
- this._wikiSection = null;
-};
-
-Timeline.DefaultEventSource.Event.prototype = {
- getID: function() { return this._id; },
-
- isInstant: function() { return this._instant; },
- isImprecise: function() { return this._start != this._latestStart || this._end != this._earliestEnd; },
-
- getStart: function() { return this._start; },
- getEnd: function() { return this._end; },
- getLatestStart: function() { return this._latestStart; },
- getEarliestEnd: function() { return this._earliestEnd; },
-
- getEventID: function() { return this._eventID; },
- getText: function() { return this._text; }, // title
- getDescription: function() { return this._description; },
- getImage: function() { return this._image; },
- getLink: function() { return this._link; },
-
- getIcon: function() { return this._icon; },
- getColor: function() { return this._color; },
- getTextColor: function() { return this._textColor; },
- getClassName: function() { return this._classname; },
- getTapeImage: function() { return this._tapeImage; },
- getTapeRepeat: function() { return this._tapeRepeat; },
- getTrackNum: function() { return this._trackNum; },
-
- getProperty: function(name) { return null; },
-
- getWikiURL: function() { return this._wikiURL; },
- getWikiSection: function() { return this._wikiSection; },
- setWikiInfo: function(wikiURL, wikiSection) {
- this._wikiURL = wikiURL;
- this._wikiSection = wikiSection;
- },
-
- fillDescription: function(elmt) {
- elmt.innerHTML = this._description;
- },
- fillWikiInfo: function(elmt) {
- // Many bubbles will not support a wiki link.
- //
- // Strategy: assume no wiki link. If we do have
- // enough parameters for one, then create it.
- elmt.style.display = "none"; // default
-
- if (this._wikiURL == null || this._wikiSection == null) {
- return; // EARLY RETURN
- }
-
- // create the wikiID from the property or from the event text (the title)
- var wikiID = this.getProperty("wikiID");
- if (wikiID == null || wikiID.length == 0) {
- wikiID = this.getText(); // use the title as the backup wiki id
- }
-
- if (wikiID == null || wikiID.length == 0) {
- return; // No wikiID. Thus EARLY RETURN
- }
-
- // ready to go...
- elmt.style.display = "inline";
- wikiID = wikiID.replace(/\s/g, "_");
- var url = this._wikiURL + this._wikiSection.replace(/\s/g, "_") + "/" + wikiID;
- var a = document.createElement("a");
- a.href = url;
- a.target = "new";
- a.innerHTML = Timeline.strings[Timeline.clientLocale].wikiLinkLabel;
-
- elmt.appendChild(document.createTextNode("["));
- elmt.appendChild(a);
- elmt.appendChild(document.createTextNode("]"));
- },
-
- fillTime: function(elmt, labeller) {
- if (this._instant) {
- if (this.isImprecise()) {
- elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
- elmt.appendChild(elmt.ownerDocument.createElement("br"));
- elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
- } else {
- elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
- }
- } else {
- if (this.isImprecise()) {
- elmt.appendChild(elmt.ownerDocument.createTextNode(
- labeller.labelPrecise(this._start) + " ~ " + labeller.labelPrecise(this._latestStart)));
- elmt.appendChild(elmt.ownerDocument.createElement("br"));
- elmt.appendChild(elmt.ownerDocument.createTextNode(
- labeller.labelPrecise(this._earliestEnd) + " ~ " + labeller.labelPrecise(this._end)));
- } else {
- elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
- elmt.appendChild(elmt.ownerDocument.createElement("br"));
- elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
- }
- }
- },
-
- fillInfoBubble: function(elmt, theme, labeller) {
- var doc = elmt.ownerDocument;
-
- var title = this.getText();
- var link = this.getLink();
- var image = this.getImage();
-
- if (image != null) {
- var img = doc.createElement("img");
- img.src = image;
-
- theme.event.bubble.imageStyler(img);
- elmt.appendChild(img);
- }
-
- var divTitle = doc.createElement("div");
- var textTitle = doc.createTextNode(title);
- if (link != null) {
- var a = doc.createElement("a");
- a.href = link;
- a.appendChild(textTitle);
- divTitle.appendChild(a);
- } else {
- divTitle.appendChild(textTitle);
- }
- theme.event.bubble.titleStyler(divTitle);
- elmt.appendChild(divTitle);
-
- var divBody = doc.createElement("div");
- this.fillDescription(divBody);
- theme.event.bubble.bodyStyler(divBody);
- elmt.appendChild(divBody);
-
- var divTime = doc.createElement("div");
- this.fillTime(divTime, labeller);
- theme.event.bubble.timeStyler(divTime);
- elmt.appendChild(divTime);
-
- var divWiki = doc.createElement("div");
- this.fillWikiInfo(divWiki);
- theme.event.bubble.wikiStyler(divWiki);
- elmt.appendChild(divWiki);
- }
-};
-
-
-/*
- * Original Event Painter
- *
- */
-
-/*
- *
- * To enable a single event listener to monitor everything
- * on a Timeline, we need a way to map from an event's icon,
- * label or tape element to the associated timeline, band and
- * specific event.
- *
- * Thus a set format is used for the id's of the
- * events' elements on the Timeline--
- *
- * element id format for labels, icons, tapes:
- * labels: label-tl-<timelineID>-<band_index>-<evt.id>
- * icons: icon-tl-<timelineID>-<band_index>-<evt.id>
- * tapes: tape1-tl-<timelineID>-<band_index>-<evt.id>
- * tape2-tl-<timelineID>-<band_index>-<evt.id>
- * // some events have more than one tape
- * highlight: highlight1-tl-<timelineID>-<band_index>-<evt.id>
- * highlight2-tl-<timelineID>-<band_index>-<evt.id>
- * // some events have more than one highlight div (future)
- * You can then retrieve the band/timeline objects and event object
- * by using Timeline.EventUtils.decodeEventElID
- *
- *
- */
-
-/*
- * eventPaintListener functions receive calls about painting.
- * function(band, op, evt, els)
- * context: 'this' will be an OriginalEventPainter object.
- * It has properties and methods for obtaining
- * the relevant band, timeline, etc
- * band = the band being painted
- * op = 'paintStarting' // the painter is about to remove
- * all previously painted events, if any. It will
- * then start painting all of the visible events that
- * pass the filter.
- * evt = null, els = null
- * op = 'paintEnded' // the painter has finished painting
- * all of the visible events that passed the filter
- * evt = null, els = null
- * op = 'paintedEvent' // the painter just finished painting an event
- * evt = event just painted
- * els = array of painted elements' divs. Depending on the event,
- * the array could be just a tape or icon (if no label).
- * Or could include label, multiple tape divs (imprecise event),
- * highlight divs. The array is not ordered. The meaning of
- * each el is available by decoding the el's id
- * Note that there may be no paintedEvent calls if no events were visible
- * or passed the filter.
- */
-
-Timeline.OriginalEventPainter = function(params) {
- this._params = params;
- this._onSelectListeners = [];
- this._eventPaintListeners = [];
-
- this._filterMatcher = null;
- this._highlightMatcher = null;
- this._frc = null;
-
- this._eventIdToElmt = {};
-};
-
-Timeline.OriginalEventPainter.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._backLayer = null;
- this._eventLayer = null;
- this._lineLayer = null;
- this._highlightLayer = null;
-
- this._eventIdToElmt = null;
-};
-
-Timeline.OriginalEventPainter.prototype.getType = function() {
- return 'original';
-};
-
-Timeline.OriginalEventPainter.prototype.addOnSelectListener = function(listener) {
- this._onSelectListeners.push(listener);
-};
-
-Timeline.OriginalEventPainter.prototype.removeOnSelectListener = function(listener) {
- for (var i = 0; i < this._onSelectListeners.length; i++) {
- if (this._onSelectListeners[i] == listener) {
- this._onSelectListeners.splice(i, 1);
- break;
- }
- }
-};
-
-Timeline.OriginalEventPainter.prototype.addEventPaintListener = function(listener) {
- this._eventPaintListeners.push(listener);
-};
-
-Timeline.OriginalEventPainter.prototype.removeEventPaintListener = function(listener) {
- for (var i = 0; i < this._eventPaintListeners.length; i++) {
- if (this._eventPaintListeners[i] == listener) {
- this._eventPaintListeners.splice(i, 1);
- break;
- }
- }
-};
-
-Timeline.OriginalEventPainter.prototype.getFilterMatcher = function() {
- return this._filterMatcher;
-};
-
-Timeline.OriginalEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
- this._filterMatcher = filterMatcher;
-};
-
-Timeline.OriginalEventPainter.prototype.getHighlightMatcher = function() {
- return this._highlightMatcher;
-};
-
-Timeline.OriginalEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
- this._highlightMatcher = highlightMatcher;
-};
-
-Timeline.OriginalEventPainter.prototype.paint = function() {
- // Paints the events for a given section of the band--what is
- // visible on screen and some extra.
- var eventSource = this._band.getEventSource();
- if (eventSource == null) {
- return;
- }
-
- this._eventIdToElmt = {};
- this._fireEventPaintListeners('paintStarting', null, null);
- this._prepareForPainting();
-
- var metrics = this._computeMetrics();
- var minDate = this._band.getMinDate();
- var maxDate = this._band.getMaxDate();
-
- var filterMatcher = (this._filterMatcher != null) ?
- this._filterMatcher :
- function(evt) { return true; };
- var highlightMatcher = (this._highlightMatcher != null) ?
- this._highlightMatcher :
- function(evt) { return -1; };
-
- var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
- while (iterator.hasNext()) {
- var evt = iterator.next();
- if (filterMatcher(evt)) {
- this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
- }
- }
-
- this._highlightLayer.style.display = "block";
- this._lineLayer.style.display = "block";
- this._eventLayer.style.display = "block";
- // update the band object for max number of tracks in this section of the ether
- this._band.updateEventTrackInfo(this._tracks.length, metrics.trackIncrement);
- this._fireEventPaintListeners('paintEnded', null, null);
-
- this._setOrthogonalOffset(metrics);
-};
-
-Timeline.OriginalEventPainter.prototype.softPaint = function() {
- this._setOrthogonalOffset(this._computeMetrics());
-};
-
-Timeline.OriginalEventPainter.prototype._setOrthogonalOffset = function(metrics) {
- var actualViewWidth = 2 * metrics.trackOffset + this._tracks.length * metrics.trackIncrement;
- var minOrthogonalOffset = Math.min(0, this._band.getViewWidth() - actualViewWidth);
- var orthogonalOffset = Math.max(minOrthogonalOffset, this._band.getViewOrthogonalOffset());
-
- this._highlightLayer.style.top =
- this._lineLayer.style.top =
- this._eventLayer.style.top =
- orthogonalOffset + "px";
-};
-
-Timeline.OriginalEventPainter.prototype._computeMetrics = function() {
- var eventTheme = this._params.theme.event;
- var trackHeight = Math.max(eventTheme.track.height, eventTheme.tape.height +
- this._frc.getLineHeight());
- var metrics = {
- trackOffset: eventTheme.track.offset,
- trackHeight: trackHeight,
- trackGap: eventTheme.track.gap,
- trackIncrement: trackHeight + eventTheme.track.gap,
- icon: eventTheme.instant.icon,
- iconWidth: eventTheme.instant.iconWidth,
- iconHeight: eventTheme.instant.iconHeight,
- labelWidth: eventTheme.label.width,
- maxLabelChar: eventTheme.label.maxLabelChar,
- impreciseIconMargin: eventTheme.instant.impreciseIconMargin
- };
-
- return metrics;
-};
-
-Timeline.OriginalEventPainter.prototype._prepareForPainting = function() {
- // Remove everything previously painted: highlight, line and event layers.
- // Prepare blank layers for painting.
- var band = this._band;
-
- if (this._backLayer == null) {
- this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
- this._backLayer.style.visibility = "hidden";
-
- var eventLabelPrototype = document.createElement("span");
- eventLabelPrototype.className = "timeline-event-label";
- this._backLayer.appendChild(eventLabelPrototype);
- this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
- }
- this._frc.update();
- this._tracks = [];
-
- if (this._highlightLayer != null) {
- band.removeLayerDiv(this._highlightLayer);
- }
- this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
- this._highlightLayer.style.display = "none";
-
- if (this._lineLayer != null) {
- band.removeLayerDiv(this._lineLayer);
- }
- this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
- this._lineLayer.style.display = "none";
-
- if (this._eventLayer != null) {
- band.removeLayerDiv(this._eventLayer);
- }
- this._eventLayer = band.createLayerDiv(115, "timeline-band-events");
- this._eventLayer.style.display = "none";
-};
-
-Timeline.OriginalEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isInstant()) {
- this.paintInstantEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintDurationEvent(evt, metrics, theme, highlightIndex);
- }
-};
-
-Timeline.OriginalEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isImprecise()) {
- this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
- }
-}
-
-Timeline.OriginalEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isImprecise()) {
- this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
- }
-}
-
-Timeline.OriginalEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
- var doc = this._timeline.getDocument();
- var text = evt.getText();
-
- var startDate = evt.getStart();
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
- var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
- var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
-
- var labelDivClassName = this._getLabelDivClassName(evt);
- var labelSize = this._frc.computeSize(text, labelDivClassName);
- var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
- var labelRight = labelLeft + labelSize.width;
-
- var rightEdge = labelRight;
- var track = this._findFreeTrack(evt, rightEdge);
-
- var labelTop = Math.round(
- metrics.trackOffset + track * metrics.trackIncrement +
- metrics.trackHeight / 2 - labelSize.height / 2);
-
- var iconElmtData = this._paintEventIcon(evt, track, iconLeftEdge, metrics, theme, 0);
- var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
- labelSize.height, theme, labelDivClassName, highlightIndex);
- var els = [iconElmtData.elmt, labelElmtData.elmt];
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- var hDiv = this._createHighlightDiv(highlightIndex, iconElmtData, theme, evt);
- if (hDiv != null) {els.push(hDiv);}
- this._fireEventPaintListeners('paintedEvent', evt, els);
-
-
- this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
- this._tracks[track] = iconLeftEdge;
-};
-
-Timeline.OriginalEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
- var doc = this._timeline.getDocument();
- var text = evt.getText();
-
- var startDate = evt.getStart();
- var endDate = evt.getEnd();
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
- var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-
- var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
- var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
-
- var labelDivClassName = this._getLabelDivClassName(evt);
- var labelSize = this._frc.computeSize(text, labelDivClassName);
- var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
- var labelRight = labelLeft + labelSize.width;
-
- var rightEdge = Math.max(labelRight, endPixel);
- var track = this._findFreeTrack(evt, rightEdge);
- var tapeHeight = theme.event.tape.height;
- var labelTop = Math.round(
- metrics.trackOffset + track * metrics.trackIncrement + tapeHeight);
-
- var iconElmtData = this._paintEventIcon(evt, track, iconLeftEdge, metrics, theme, tapeHeight);
- var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
- labelSize.height, theme, labelDivClassName, highlightIndex);
-
- var color = evt.getColor();
- color = color != null ? color : theme.event.instant.impreciseColor;
-
- var tapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel,
- color, theme.event.instant.impreciseOpacity, metrics, theme, 0);
- var els = [iconElmtData.elmt, labelElmtData.elmt, tapeElmtData.elmt];
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- var hDiv = this._createHighlightDiv(highlightIndex, iconElmtData, theme, evt);
- if (hDiv != null) {els.push(hDiv);}
- this._fireEventPaintListeners('paintedEvent', evt, els);
-
- this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
- this._tracks[track] = iconLeftEdge;
-};
-
-Timeline.OriginalEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
- var doc = this._timeline.getDocument();
- var text = evt.getText();
-
- var startDate = evt.getStart();
- var endDate = evt.getEnd();
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
- var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-
- var labelDivClassName = this._getLabelDivClassName(evt);
- var labelSize = this._frc.computeSize(text, labelDivClassName);
- var labelLeft = startPixel;
- var labelRight = labelLeft + labelSize.width;
-
- var rightEdge = Math.max(labelRight, endPixel);
- var track = this._findFreeTrack(evt, rightEdge);
- var labelTop = Math.round(
- metrics.trackOffset + track * metrics.trackIncrement + theme.event.tape.height);
-
- var color = evt.getColor();
- color = color != null ? color : theme.event.duration.color;
-
- var tapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel, color, 100, metrics, theme, 0);
- var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
- labelSize.height, theme, labelDivClassName, highlightIndex);
- var els = [tapeElmtData.elmt, labelElmtData.elmt];
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- var hDiv = this._createHighlightDiv(highlightIndex, tapeElmtData, theme, evt);
- if (hDiv != null) {els.push(hDiv);}
- this._fireEventPaintListeners('paintedEvent', evt, els);
-
- this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
- this._tracks[track] = startPixel;
-};
-
-Timeline.OriginalEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
- var doc = this._timeline.getDocument();
- var text = evt.getText();
-
- var startDate = evt.getStart();
- var latestStartDate = evt.getLatestStart();
- var endDate = evt.getEnd();
- var earliestEndDate = evt.getEarliestEnd();
-
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
- var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
- var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
- var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
-
- var labelDivClassName = this._getLabelDivClassName(evt);
- var labelSize = this._frc.computeSize(text, labelDivClassName);
- var labelLeft = latestStartPixel;
- var labelRight = labelLeft + labelSize.width;
-
- var rightEdge = Math.max(labelRight, endPixel);
- var track = this._findFreeTrack(evt, rightEdge);
- var labelTop = Math.round(
- metrics.trackOffset + track * metrics.trackIncrement + theme.event.tape.height);
-
- var color = evt.getColor();
- color = color != null ? color : theme.event.duration.color;
-
- // Imprecise events can have two event tapes
- // The imprecise dates tape, uses opacity to be dimmer than precise dates
- var impreciseTapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel,
- theme.event.duration.impreciseColor,
- theme.event.duration.impreciseOpacity, metrics, theme, 0);
- // The precise dates tape, regular (100%) opacity
- var tapeElmtData = this._paintEventTape(evt, track, latestStartPixel,
- earliestEndPixel, color, 100, metrics, theme, 1);
-
- var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop,
- labelSize.width, labelSize.height, theme, labelDivClassName, highlightIndex);
- var els = [impreciseTapeElmtData.elmt, tapeElmtData.elmt, labelElmtData.elmt];
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- var hDiv = this._createHighlightDiv(highlightIndex, tapeElmtData, theme, evt);
- if (hDiv != null) {els.push(hDiv);}
- this._fireEventPaintListeners('paintedEvent', evt, els);
-
- this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
- this._tracks[track] = startPixel;
-};
-
-Timeline.OriginalEventPainter.prototype._encodeEventElID = function(elType, evt) {
- return Timeline.EventUtils.encodeEventElID(this._timeline, this._band, elType, evt);
-};
-
-Timeline.OriginalEventPainter.prototype._findFreeTrack = function(event, rightEdge) {
- var trackAttribute = event.getTrackNum();
- if (trackAttribute != null) {
- return trackAttribute; // early return since event includes track number
- }
-
- // normal case: find an open track
- for (var i = 0; i < this._tracks.length; i++) {
- var t = this._tracks[i];
- if (t > rightEdge) {
- break;
- }
- }
- return i;
-};
-
-Timeline.OriginalEventPainter.prototype._paintEventIcon = function(evt, iconTrack, left, metrics, theme, tapeHeight) {
- // If no tape, then paint the icon in the middle of the track.
- // If there is a tape, paint the icon below the tape + impreciseIconMargin
- var icon = evt.getIcon();
- icon = icon != null ? icon : metrics.icon;
-
- var top; // top of the icon
- if (tapeHeight > 0) {
- top = metrics.trackOffset + iconTrack * metrics.trackIncrement +
- tapeHeight + metrics.impreciseIconMargin;
- } else {
- var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement +
- metrics.trackHeight / 2;
- top = Math.round(middle - metrics.iconHeight / 2);
- }
- var img = SimileAjax.Graphics.createTranslucentImage(icon);
- var iconDiv = this._timeline.getDocument().createElement("div");
- iconDiv.className = this._getElClassName('timeline-event-icon', evt, 'icon');
- iconDiv.id = this._encodeEventElID('icon', evt);
- iconDiv.style.left = left + "px";
- iconDiv.style.top = top + "px";
- iconDiv.appendChild(img);
-
- if(evt._title != null)
- iconDiv.title = evt._title;
-
- this._eventLayer.appendChild(iconDiv);
-
- return {
- left: left,
- top: top,
- width: metrics.iconWidth,
- height: metrics.iconHeight,
- elmt: iconDiv
- };
-};
-
-Timeline.OriginalEventPainter.prototype._paintEventLabel = function(evt, text, left, top, width,
- height, theme, labelDivClassName, highlightIndex) {
- var doc = this._timeline.getDocument();
-
- var labelDiv = doc.createElement("div");
- labelDiv.className = labelDivClassName;
- labelDiv.id = this._encodeEventElID('label', evt);
- labelDiv.style.left = left + "px";
- labelDiv.style.width = width + "px";
- labelDiv.style.top = top + "px";
- labelDiv.innerHTML = text;
-
- if(evt._title != null)
- labelDiv.title = evt._title;
-
- var color = evt.getTextColor();
- if (color == null) {
- color = evt.getColor();
- }
- if (color != null) {
- labelDiv.style.color = color;
- }
- if (theme.event.highlightLabelBackground && highlightIndex >= 0) {
- labelDiv.style.background = this._getHighlightColor(highlightIndex, theme);
- }
-
- this._eventLayer.appendChild(labelDiv);
-
- return {
- left: left,
- top: top,
- width: width,
- height: height,
- elmt: labelDiv
- };
-};
-
-Timeline.OriginalEventPainter.prototype._paintEventTape = function(
- evt, iconTrack, startPixel, endPixel, color, opacity, metrics, theme, tape_index) {
-
- var tapeWidth = endPixel - startPixel;
- var tapeHeight = theme.event.tape.height;
- var top = metrics.trackOffset + iconTrack * metrics.trackIncrement;
-
- var tapeDiv = this._timeline.getDocument().createElement("div");
- tapeDiv.className = this._getElClassName('timeline-event-tape', evt, 'tape');
- tapeDiv.id = this._encodeEventElID('tape' + tape_index, evt);
- tapeDiv.style.left = startPixel + "px";
- tapeDiv.style.width = tapeWidth + "px";
- tapeDiv.style.height = tapeHeight + "px";
- tapeDiv.style.top = top + "px";
-
- if(evt._title != null)
- tapeDiv.title = evt._title;
-
- if(color != null) {
- tapeDiv.style.backgroundColor = color;
- }
-
- var backgroundImage = evt.getTapeImage();
- var backgroundRepeat = evt.getTapeRepeat();
- backgroundRepeat = backgroundRepeat != null ? backgroundRepeat : 'repeat';
- if(backgroundImage != null) {
- tapeDiv.style.backgroundImage = "url(" + backgroundImage + ")";
- tapeDiv.style.backgroundRepeat = backgroundRepeat;
- }
-
- SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
-
- this._eventLayer.appendChild(tapeDiv);
-
- return {
- left: startPixel,
- top: top,
- width: tapeWidth,
- height: tapeHeight,
- elmt: tapeDiv
- };
-}
-
-Timeline.OriginalEventPainter.prototype._getLabelDivClassName = function(evt) {
- return this._getElClassName('timeline-event-label', evt, 'label');
-};
-
-Timeline.OriginalEventPainter.prototype._getElClassName = function(elClassName, evt, prefix) {
- // Prefix and '_' is added to the event's classname. Set to null for no prefix
- var evt_classname = evt.getClassName(),
- pieces = [];
-
- if (evt_classname) {
- if (prefix) {pieces.push(prefix + '-' + evt_classname + ' ');}
- pieces.push(evt_classname + ' ');
- }
- pieces.push(elClassName);
- return(pieces.join(''));
-};
-
-Timeline.OriginalEventPainter.prototype._getHighlightColor = function(highlightIndex, theme) {
- var highlightColors = theme.event.highlightColors;
- return highlightColors[Math.min(highlightIndex, highlightColors.length - 1)];
-};
-
-Timeline.OriginalEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme, evt) {
- var div = null;
- if (highlightIndex >= 0) {
- var doc = this._timeline.getDocument();
- var color = this._getHighlightColor(highlightIndex, theme);
-
- div = doc.createElement("div");
- div.className = this._getElClassName('timeline-event-highlight', evt, 'highlight');
- div.id = this._encodeEventElID('highlight0', evt); // in future will have other
- // highlight divs for tapes + icons
- div.style.position = "absolute";
- div.style.overflow = "hidden";
- div.style.left = (dimensions.left - 2) + "px";
- div.style.width = (dimensions.width + 4) + "px";
- div.style.top = (dimensions.top - 2) + "px";
- div.style.height = (dimensions.height + 4) + "px";
- div.style.background = color;
-
- this._highlightLayer.appendChild(div);
- }
- return div;
-};
-
-Timeline.OriginalEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
- var c = SimileAjax.DOM.getPageCoordinates(icon);
- this._showBubble(
- c.left + Math.ceil(icon.offsetWidth / 2),
- c.top + Math.ceil(icon.offsetHeight / 2),
- evt
- );
- this._fireOnSelect(evt.getID());
-
- domEvt.cancelBubble = true;
- SimileAjax.DOM.cancelEvent(domEvt);
- return false;
-};
-
-Timeline.OriginalEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
- if ("pageX" in domEvt) {
- var x = domEvt.pageX;
- var y = domEvt.pageY;
- } else {
- var c = SimileAjax.DOM.getPageCoordinates(target);
- var x = domEvt.offsetX + c.left;
- var y = domEvt.offsetY + c.top;
- }
- this._showBubble(x, y, evt);
- this._fireOnSelect(evt.getID());
-
- domEvt.cancelBubble = true;
- SimileAjax.DOM.cancelEvent(domEvt);
- return false;
-};
-
-Timeline.OriginalEventPainter.prototype.showBubble = function(evt) {
- var elmt = this._eventIdToElmt[evt.getID()];
- if (elmt) {
- var c = SimileAjax.DOM.getPageCoordinates(elmt);
- this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, evt);
- }
-};
-
-Timeline.OriginalEventPainter.prototype._showBubble = function(x, y, evt) {
- var div = document.createElement("div");
- var themeBubble = this._params.theme.event.bubble;
- evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
-
- SimileAjax.WindowManager.cancelPopups();
- SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y,
- themeBubble.width, null, themeBubble.maxHeight);
-};
-
-Timeline.OriginalEventPainter.prototype._fireOnSelect = function(eventID) {
- for (var i = 0; i < this._onSelectListeners.length; i++) {
- this._onSelectListeners[i](eventID);
- }
-};
-
-Timeline.OriginalEventPainter.prototype._fireEventPaintListeners = function(op, evt, els) {
- for (var i = 0; i < this._eventPaintListeners.length; i++) {
- this._eventPaintListeners[i](this._band, op, evt, els);
- }
-};
-/*
- * Detailed Event Painter
- *
- */
-
-// Note: a number of features from original-painter
-// are not yet implemented in detailed painter.
-// Eg classname, id attributes for icons, labels, tapes
-
-Timeline.DetailedEventPainter = function(params) {
- this._params = params;
- this._onSelectListeners = [];
-
- this._filterMatcher = null;
- this._highlightMatcher = null;
- this._frc = null;
-
- this._eventIdToElmt = {};
-};
-
-Timeline.DetailedEventPainter.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._backLayer = null;
- this._eventLayer = null;
- this._lineLayer = null;
- this._highlightLayer = null;
-
- this._eventIdToElmt = null;
-};
-
-Timeline.DetailedEventPainter.prototype.getType = function() {
- return 'detailed';
-};
-
-Timeline.DetailedEventPainter.prototype.addOnSelectListener = function(listener) {
- this._onSelectListeners.push(listener);
-};
-
-Timeline.DetailedEventPainter.prototype.removeOnSelectListener = function(listener) {
- for (var i = 0; i < this._onSelectListeners.length; i++) {
- if (this._onSelectListeners[i] == listener) {
- this._onSelectListeners.splice(i, 1);
- break;
- }
- }
-};
-
-Timeline.DetailedEventPainter.prototype.getFilterMatcher = function() {
- return this._filterMatcher;
-};
-
-Timeline.DetailedEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
- this._filterMatcher = filterMatcher;
-};
-
-Timeline.DetailedEventPainter.prototype.getHighlightMatcher = function() {
- return this._highlightMatcher;
-};
-
-Timeline.DetailedEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
- this._highlightMatcher = highlightMatcher;
-};
-
-Timeline.DetailedEventPainter.prototype.paint = function() {
- var eventSource = this._band.getEventSource();
- if (eventSource == null) {
- return;
- }
-
- this._eventIdToElmt = {};
- this._prepareForPainting();
-
- var eventTheme = this._params.theme.event;
- var trackHeight = Math.max(eventTheme.track.height, this._frc.getLineHeight());
- var metrics = {
- trackOffset: Math.round(this._band.getViewWidth() / 2 - trackHeight / 2),
- trackHeight: trackHeight,
- trackGap: eventTheme.track.gap,
- trackIncrement: trackHeight + eventTheme.track.gap,
- icon: eventTheme.instant.icon,
- iconWidth: eventTheme.instant.iconWidth,
- iconHeight: eventTheme.instant.iconHeight,
- labelWidth: eventTheme.label.width
- }
-
- var minDate = this._band.getMinDate();
- var maxDate = this._band.getMaxDate();
-
- var filterMatcher = (this._filterMatcher != null) ?
- this._filterMatcher :
- function(evt) { return true; };
- var highlightMatcher = (this._highlightMatcher != null) ?
- this._highlightMatcher :
- function(evt) { return -1; };
-
- var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
- while (iterator.hasNext()) {
- var evt = iterator.next();
- if (filterMatcher(evt)) {
- this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
- }
- }
-
- this._highlightLayer.style.display = "block";
- this._lineLayer.style.display = "block";
- this._eventLayer.style.display = "block";
- // update the band object for max number of tracks in this section of the ether
- this._band.updateEventTrackInfo(this._lowerTracks.length + this._upperTracks.length,
- metrics.trackIncrement);
-};
-
-Timeline.DetailedEventPainter.prototype.softPaint = function() {
-};
-
-Timeline.DetailedEventPainter.prototype._prepareForPainting = function() {
- var band = this._band;
-
- if (this._backLayer == null) {
- this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
- this._backLayer.style.visibility = "hidden";
-
- var eventLabelPrototype = document.createElement("span");
- eventLabelPrototype.className = "timeline-event-label";
- this._backLayer.appendChild(eventLabelPrototype);
- this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
- }
- this._frc.update();
- this._lowerTracks = [];
- this._upperTracks = [];
-
- if (this._highlightLayer != null) {
- band.removeLayerDiv(this._highlightLayer);
- }
- this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
- this._highlightLayer.style.display = "none";
-
- if (this._lineLayer != null) {
- band.removeLayerDiv(this._lineLayer);
- }
- this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
- this._lineLayer.style.display = "none";
-
- if (this._eventLayer != null) {
- band.removeLayerDiv(this._eventLayer);
- }
- this._eventLayer = band.createLayerDiv(110, "timeline-band-events");
- this._eventLayer.style.display = "none";
-};
-
-Timeline.DetailedEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isInstant()) {
- this.paintInstantEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintDurationEvent(evt, metrics, theme, highlightIndex);
- }
-};
-
-Timeline.DetailedEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isImprecise()) {
- this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
- }
-}
-
-Timeline.DetailedEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isImprecise()) {
- this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
- }
-}
-
-Timeline.DetailedEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
- var doc = this._timeline.getDocument();
- var text = evt.getText();
-
- var startDate = evt.getStart();
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
- var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
- var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
-
- var labelSize = this._frc.computeSize(text);
- var iconTrack = this._findFreeTrackForSolid(iconRightEdge, startPixel);
- var iconElmtData = this._paintEventIcon(evt, iconTrack, iconLeftEdge, metrics, theme);
-
- var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
- var labelTrack = iconTrack;
-
- var iconTrackData = this._getTrackData(iconTrack);
- if (Math.min(iconTrackData.solid, iconTrackData.text) >= labelLeft + labelSize.width) { // label on the same track, to the right of icon
- iconTrackData.solid = iconLeftEdge;
- iconTrackData.text = labelLeft;
- } else { // label on a different track, below icon
- iconTrackData.solid = iconLeftEdge;
-
- labelLeft = startPixel + theme.event.label.offsetFromLine;
- labelTrack = this._findFreeTrackForText(iconTrack, labelLeft + labelSize.width, function(t) { t.line = startPixel - 2; });
- this._getTrackData(labelTrack).text = iconLeftEdge;
-
- this._paintEventLine(evt, startPixel, iconTrack, labelTrack, metrics, theme);
- }
-
- var labelTop = Math.round(
- metrics.trackOffset + labelTrack * metrics.trackIncrement +
- metrics.trackHeight / 2 - labelSize.height / 2);
-
- var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- this._createHighlightDiv(highlightIndex, iconElmtData, theme);
-
- this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
-};
-
-Timeline.DetailedEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
- var doc = this._timeline.getDocument();
- var text = evt.getText();
-
- var startDate = evt.getStart();
- var endDate = evt.getEnd();
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
- var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-
- var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
- var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
-
- var labelSize = this._frc.computeSize(text);
- var iconTrack = this._findFreeTrackForSolid(endPixel, startPixel);
-
- var tapeElmtData = this._paintEventTape(evt, iconTrack, startPixel, endPixel,
- theme.event.instant.impreciseColor, theme.event.instant.impreciseOpacity, metrics, theme);
- var iconElmtData = this._paintEventIcon(evt, iconTrack, iconLeftEdge, metrics, theme);
-
- var iconTrackData = this._getTrackData(iconTrack);
- iconTrackData.solid = iconLeftEdge;
-
- var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
- var labelRight = labelLeft + labelSize.width;
- var labelTrack;
- if (labelRight < endPixel) {
- labelTrack = iconTrack;
- } else {
- labelLeft = startPixel + theme.event.label.offsetFromLine;
- labelRight = labelLeft + labelSize.width;
-
- labelTrack = this._findFreeTrackForText(iconTrack, labelRight, function(t) { t.line = startPixel - 2; });
- this._getTrackData(labelTrack).text = iconLeftEdge;
-
- this._paintEventLine(evt, startPixel, iconTrack, labelTrack, metrics, theme);
- }
- var labelTop = Math.round(
- metrics.trackOffset + labelTrack * metrics.trackIncrement +
- metrics.trackHeight / 2 - labelSize.height / 2);
-
- var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- this._createHighlightDiv(highlightIndex, iconElmtData, theme);
-
- this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
-};
-
-Timeline.DetailedEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
- var doc = this._timeline.getDocument();
- var text = evt.getText();
-
- var startDate = evt.getStart();
- var endDate = evt.getEnd();
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
- var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
-
- var labelSize = this._frc.computeSize(text);
- var tapeTrack = this._findFreeTrackForSolid(endPixel);
- var color = evt.getColor();
- color = color != null ? color : theme.event.duration.color;
-
- var tapeElmtData = this._paintEventTape(evt, tapeTrack, startPixel, endPixel, color, 100, metrics, theme);
-
- var tapeTrackData = this._getTrackData(tapeTrack);
- tapeTrackData.solid = startPixel;
-
- var labelLeft = startPixel + theme.event.label.offsetFromLine;
- var labelTrack = this._findFreeTrackForText(tapeTrack, labelLeft + labelSize.width, function(t) { t.line = startPixel - 2; });
- this._getTrackData(labelTrack).text = startPixel - 2;
-
- this._paintEventLine(evt, startPixel, tapeTrack, labelTrack, metrics, theme);
-
- var labelTop = Math.round(
- metrics.trackOffset + labelTrack * metrics.trackIncrement +
- metrics.trackHeight / 2 - labelSize.height / 2);
-
- var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
-
- this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
-};
-
-Timeline.DetailedEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
- var doc = this._timeline.getDocument();
- var text = evt.getText();
-
- var startDate = evt.getStart();
- var latestStartDate = evt.getLatestStart();
- var endDate = evt.getEnd();
- var earliestEndDate = evt.getEarliestEnd();
-
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
- var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
- var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
- var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
-
- var labelSize = this._frc.computeSize(text);
- var tapeTrack = this._findFreeTrackForSolid(endPixel);
- var color = evt.getColor();
- color = color != null ? color : theme.event.duration.color;
-
- var impreciseTapeElmtData = this._paintEventTape(evt, tapeTrack, startPixel, endPixel,
- theme.event.duration.impreciseColor, theme.event.duration.impreciseOpacity, metrics, theme);
- var tapeElmtData = this._paintEventTape(evt, tapeTrack, latestStartPixel, earliestEndPixel, color, 100, metrics, theme);
-
- var tapeTrackData = this._getTrackData(tapeTrack);
- tapeTrackData.solid = startPixel;
-
- var labelLeft = latestStartPixel + theme.event.label.offsetFromLine;
- var labelTrack = this._findFreeTrackForText(tapeTrack, labelLeft + labelSize.width, function(t) { t.line = latestStartPixel - 2; });
- this._getTrackData(labelTrack).text = latestStartPixel - 2;
-
- this._paintEventLine(evt, latestStartPixel, tapeTrack, labelTrack, metrics, theme);
-
- var labelTop = Math.round(
- metrics.trackOffset + labelTrack * metrics.trackIncrement +
- metrics.trackHeight / 2 - labelSize.height / 2);
-
- var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
-
- this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
-};
-
-Timeline.DetailedEventPainter.prototype._findFreeTrackForSolid = function(solidEdge, softEdge) {
- for (var i = 0; true; i++) {
- if (i < this._lowerTracks.length) {
- var t = this._lowerTracks[i];
- if (Math.min(t.solid, t.text) > solidEdge && (!(softEdge) || t.line > softEdge)) {
- return i;
- }
- } else {
- this._lowerTracks.push({
- solid: Number.POSITIVE_INFINITY,
- text: Number.POSITIVE_INFINITY,
- line: Number.POSITIVE_INFINITY
- });
-
- return i;
- }
-
- if (i < this._upperTracks.length) {
- var t = this._upperTracks[i];
- if (Math.min(t.solid, t.text) > solidEdge && (!(softEdge) || t.line > softEdge)) {
- return -1 - i;
- }
- } else {
- this._upperTracks.push({
- solid: Number.POSITIVE_INFINITY,
- text: Number.POSITIVE_INFINITY,
- line: Number.POSITIVE_INFINITY
- });
-
- return -1 - i;
- }
- }
-};
-
-Timeline.DetailedEventPainter.prototype._findFreeTrackForText = function(fromTrack, edge, occupiedTrackVisitor) {
- var extendUp;
- var index;
- var firstIndex;
- var result;
-
- if (fromTrack < 0) {
- extendUp = true;
- firstIndex = -fromTrack;
-
- index = this._findFreeUpperTrackForText(firstIndex, edge);
- result = -1 - index;
- } else if (fromTrack > 0) {
- extendUp = false;
- firstIndex = fromTrack + 1;
-
- index = this._findFreeLowerTrackForText(firstIndex, edge);
- result = index;
- } else {
- var upIndex = this._findFreeUpperTrackForText(0, edge);
- var downIndex = this._findFreeLowerTrackForText(1, edge);
-
- if (downIndex - 1 <= upIndex) {
- extendUp = false;
- firstIndex = 1;
- index = downIndex;
- result = index;
- } else {
- extendUp = true;
- firstIndex = 0;
- index = upIndex;
- result = -1 - index;
- }
- }
-
- if (extendUp) {
- if (index == this._upperTracks.length) {
- this._upperTracks.push({
- solid: Number.POSITIVE_INFINITY,
- text: Number.POSITIVE_INFINITY,
- line: Number.POSITIVE_INFINITY
- });
- }
- for (var i = firstIndex; i < index; i++) {
- occupiedTrackVisitor(this._upperTracks[i]);
- }
- } else {
- if (index == this._lowerTracks.length) {
- this._lowerTracks.push({
- solid: Number.POSITIVE_INFINITY,
- text: Number.POSITIVE_INFINITY,
- line: Number.POSITIVE_INFINITY
- });
- }
- for (var i = firstIndex; i < index; i++) {
- occupiedTrackVisitor(this._lowerTracks[i]);
- }
- }
- return result;
-};
-
-Timeline.DetailedEventPainter.prototype._findFreeLowerTrackForText = function(index, edge) {
- for (; index < this._lowerTracks.length; index++) {
- var t = this._lowerTracks[index];
- if (Math.min(t.solid, t.text) >= edge) {
- break;
- }
- }
- return index;
-};
-
-Timeline.DetailedEventPainter.prototype._findFreeUpperTrackForText = function(index, edge) {
- for (; index < this._upperTracks.length; index++) {
- var t = this._upperTracks[index];
- if (Math.min(t.solid, t.text) >= edge) {
- break;
- }
- }
- return index;
-};
-
-Timeline.DetailedEventPainter.prototype._getTrackData = function(index) {
- return (index < 0) ? this._upperTracks[-index - 1] : this._lowerTracks[index];
-};
-
-Timeline.DetailedEventPainter.prototype._paintEventLine = function(evt, left, startTrack, endTrack, metrics, theme) {
- var top = Math.round(metrics.trackOffset + startTrack * metrics.trackIncrement + metrics.trackHeight / 2);
- var height = Math.round(Math.abs(endTrack - startTrack) * metrics.trackIncrement);
-
- var lineStyle = "1px solid " + theme.event.label.lineColor;
- var lineDiv = this._timeline.getDocument().createElement("div");
- lineDiv.style.position = "absolute";
- lineDiv.style.left = left + "px";
- lineDiv.style.width = theme.event.label.offsetFromLine + "px";
- lineDiv.style.height = height + "px";
- if (startTrack > endTrack) {
- lineDiv.style.top = (top - height) + "px";
- lineDiv.style.borderTop = lineStyle;
- } else {
- lineDiv.style.top = top + "px";
- lineDiv.style.borderBottom = lineStyle;
- }
- lineDiv.style.borderLeft = lineStyle;
- this._lineLayer.appendChild(lineDiv);
-};
-
-Timeline.DetailedEventPainter.prototype._paintEventIcon = function(evt, iconTrack, left, metrics, theme) {
- var icon = evt.getIcon();
- icon = icon != null ? icon : metrics.icon;
-
- var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement + metrics.trackHeight / 2;
- var top = Math.round(middle - metrics.iconHeight / 2);
-
- var img = SimileAjax.Graphics.createTranslucentImage(icon);
- var iconDiv = this._timeline.getDocument().createElement("div");
- iconDiv.style.position = "absolute";
- iconDiv.style.left = left + "px";
- iconDiv.style.top = top + "px";
- iconDiv.appendChild(img);
- iconDiv.style.cursor = "pointer";
-
- if(evt._title != null)
- iconDiv.title = evt._title
-
- this._eventLayer.appendChild(iconDiv);
-
- return {
- left: left,
- top: top,
- width: metrics.iconWidth,
- height: metrics.iconHeight,
- elmt: iconDiv
- };
-};
-
-Timeline.DetailedEventPainter.prototype._paintEventLabel = function(evt, text, left, top, width, height, theme) {
- var doc = this._timeline.getDocument();
-
- var labelBackgroundDiv = doc.createElement("div");
- labelBackgroundDiv.style.position = "absolute";
- labelBackgroundDiv.style.left = left + "px";
- labelBackgroundDiv.style.width = width + "px";
- labelBackgroundDiv.style.top = top + "px";
- labelBackgroundDiv.style.height = height + "px";
- labelBackgroundDiv.style.backgroundColor = theme.event.label.backgroundColor;
- SimileAjax.Graphics.setOpacity(labelBackgroundDiv, theme.event.label.backgroundOpacity);
- this._eventLayer.appendChild(labelBackgroundDiv);
-
- var labelDiv = doc.createElement("div");
- labelDiv.style.position = "absolute";
- labelDiv.style.left = left + "px";
- labelDiv.style.width = width + "px";
- labelDiv.style.top = top + "px";
- labelDiv.innerHTML = text;
- labelDiv.style.cursor = "pointer";
-
- if(evt._title != null)
- labelDiv.title = evt._title;
-
- var color = evt.getTextColor();
- if (color == null) {
- color = evt.getColor();
- }
- if (color != null) {
- labelDiv.style.color = color;
- }
-
- this._eventLayer.appendChild(labelDiv);
-
- return {
- left: left,
- top: top,
- width: width,
- height: height,
- elmt: labelDiv
- };
-};
-
-Timeline.DetailedEventPainter.prototype._paintEventTape = function(
- evt, iconTrack, startPixel, endPixel, color, opacity, metrics, theme) {
-
- var tapeWidth = endPixel - startPixel;
- var tapeHeight = theme.event.tape.height;
- var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement + metrics.trackHeight / 2;
- var top = Math.round(middle - tapeHeight / 2);
-
- var tapeDiv = this._timeline.getDocument().createElement("div");
- tapeDiv.style.position = "absolute";
- tapeDiv.style.left = startPixel + "px";
- tapeDiv.style.width = tapeWidth + "px";
- tapeDiv.style.top = top + "px";
- tapeDiv.style.height = tapeHeight + "px";
- tapeDiv.style.backgroundColor = color;
- tapeDiv.style.overflow = "hidden";
- tapeDiv.style.cursor = "pointer";
-
- if(evt._title != null)
- tapeDiv.title = evt._title;
-
- SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
-
- this._eventLayer.appendChild(tapeDiv);
-
- return {
- left: startPixel,
- top: top,
- width: tapeWidth,
- height: tapeHeight,
- elmt: tapeDiv
- };
-}
-
-Timeline.DetailedEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
- if (highlightIndex >= 0) {
- var doc = this._timeline.getDocument();
- var eventTheme = theme.event;
-
- var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
-
- var div = doc.createElement("div");
- div.style.position = "absolute";
- div.style.overflow = "hidden";
- div.style.left = (dimensions.left - 2) + "px";
- div.style.width = (dimensions.width + 4) + "px";
- div.style.top = (dimensions.top - 2) + "px";
- div.style.height = (dimensions.height + 4) + "px";
- div.style.background = color;
-
- this._highlightLayer.appendChild(div);
- }
-};
-
-Timeline.DetailedEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
- var c = SimileAjax.DOM.getPageCoordinates(icon);
- this._showBubble(
- c.left + Math.ceil(icon.offsetWidth / 2),
- c.top + Math.ceil(icon.offsetHeight / 2),
- evt
- );
- this._fireOnSelect(evt.getID());
-
- domEvt.cancelBubble = true;
- SimileAjax.DOM.cancelEvent(domEvt);
- return false;
-};
-
-Timeline.DetailedEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
- if ("pageX" in domEvt) {
- var x = domEvt.pageX;
- var y = domEvt.pageY;
- } else {
- var c = SimileAjax.DOM.getPageCoordinates(target);
- var x = domEvt.offsetX + c.left;
- var y = domEvt.offsetY + c.top;
- }
- this._showBubble(x, y, evt);
- this._fireOnSelect(evt.getID());
-
- domEvt.cancelBubble = true;
- SimileAjax.DOM.cancelEvent(domEvt);
- return false;
-};
-
-Timeline.DetailedEventPainter.prototype.showBubble = function(evt) {
- var elmt = this._eventIdToElmt[evt.getID()];
- if (elmt) {
- var c = SimileAjax.DOM.getPageCoordinates(elmt);
- this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, evt);
- }
-};
-
-Timeline.DetailedEventPainter.prototype._showBubble = function(x, y, evt) {
- var div = document.createElement("div");
- var themeBubble = this._params.theme.event.bubble;
- evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
-
- SimileAjax.WindowManager.cancelPopups();
- SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y,
- themeBubble.width, null, themeBubble.maxHeight);
-};
-
-Timeline.DetailedEventPainter.prototype._fireOnSelect = function(eventID) {
- for (var i = 0; i < this._onSelectListeners.length; i++) {
- this._onSelectListeners[i](eventID);
- }
-};
-/*
- * Overview Event Painter
- *
- */
-
-Timeline.OverviewEventPainter = function(params) {
- this._params = params;
- this._onSelectListeners = [];
-
- this._filterMatcher = null;
- this._highlightMatcher = null;
-};
-
-Timeline.OverviewEventPainter.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._eventLayer = null;
- this._highlightLayer = null;
-};
-
-Timeline.OverviewEventPainter.prototype.getType = function() {
- return 'overview';
-};
-
-Timeline.OverviewEventPainter.prototype.addOnSelectListener = function(listener) {
- this._onSelectListeners.push(listener);
-};
-
-Timeline.OverviewEventPainter.prototype.removeOnSelectListener = function(listener) {
- for (var i = 0; i < this._onSelectListeners.length; i++) {
- if (this._onSelectListeners[i] == listener) {
- this._onSelectListeners.splice(i, 1);
- break;
- }
- }
-};
-
-Timeline.OverviewEventPainter.prototype.getFilterMatcher = function() {
- return this._filterMatcher;
-};
-
-Timeline.OverviewEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
- this._filterMatcher = filterMatcher;
-};
-
-Timeline.OverviewEventPainter.prototype.getHighlightMatcher = function() {
- return this._highlightMatcher;
-};
-
-Timeline.OverviewEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
- this._highlightMatcher = highlightMatcher;
-};
-
-Timeline.OverviewEventPainter.prototype.paint = function() {
- var eventSource = this._band.getEventSource();
- if (eventSource == null) {
- return;
- }
-
- this._prepareForPainting();
-
- var eventTheme = this._params.theme.event;
- var metrics = {
- trackOffset: eventTheme.overviewTrack.offset,
- trackHeight: eventTheme.overviewTrack.height,
- trackGap: eventTheme.overviewTrack.gap,
- trackIncrement: eventTheme.overviewTrack.height + eventTheme.overviewTrack.gap
- }
-
- var minDate = this._band.getMinDate();
- var maxDate = this._band.getMaxDate();
-
- var filterMatcher = (this._filterMatcher != null) ?
- this._filterMatcher :
- function(evt) { return true; };
- var highlightMatcher = (this._highlightMatcher != null) ?
- this._highlightMatcher :
- function(evt) { return -1; };
-
- var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
- while (iterator.hasNext()) {
- var evt = iterator.next();
- if (filterMatcher(evt)) {
- this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
- }
- }
-
- this._highlightLayer.style.display = "block";
- this._eventLayer.style.display = "block";
- // update the band object for max number of tracks in this section of the ether
- this._band.updateEventTrackInfo(this._tracks.length, metrics.trackIncrement);
-};
-
-Timeline.OverviewEventPainter.prototype.softPaint = function() {
-};
-
-Timeline.OverviewEventPainter.prototype._prepareForPainting = function() {
- var band = this._band;
-
- this._tracks = [];
-
- if (this._highlightLayer != null) {
- band.removeLayerDiv(this._highlightLayer);
- }
- this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
- this._highlightLayer.style.display = "none";
-
- if (this._eventLayer != null) {
- band.removeLayerDiv(this._eventLayer);
- }
- this._eventLayer = band.createLayerDiv(110, "timeline-band-events");
- this._eventLayer.style.display = "none";
-};
-
-Timeline.OverviewEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isInstant()) {
- this.paintInstantEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintDurationEvent(evt, metrics, theme, highlightIndex);
- }
-};
-
-Timeline.OverviewEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
- var startDate = evt.getStart();
- var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
-
- var color = evt.getColor(),
- klassName = evt.getClassName();
- if (klassName) {
- color = null;
- } else {
- color = color != null ? color : theme.event.duration.color;
- }
-
- var tickElmtData = this._paintEventTick(evt, startPixel, color, 100, metrics, theme);
-
- this._createHighlightDiv(highlightIndex, tickElmtData, theme);
-};
-
-Timeline.OverviewEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
- var latestStartDate = evt.getLatestStart();
- var earliestEndDate = evt.getEarliestEnd();
-
- var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
- var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
-
- var tapeTrack = 0;
- for (; tapeTrack < this._tracks.length; tapeTrack++) {
- if (earliestEndPixel < this._tracks[tapeTrack]) {
- break;
- }
- }
- this._tracks[tapeTrack] = earliestEndPixel;
-
- var color = evt.getColor(),
- klassName = evt.getClassName();
- if (klassName) {
- color = null;
- } else {
- color = color != null ? color : theme.event.duration.color;
- }
-
- var tapeElmtData = this._paintEventTape(evt, tapeTrack, latestStartPixel, earliestEndPixel,
- color, 100, metrics, theme, klassName);
-
- this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
-};
-
-Timeline.OverviewEventPainter.prototype._paintEventTape = function(
- evt, track, left, right, color, opacity, metrics, theme, klassName) {
-
- var top = metrics.trackOffset + track * metrics.trackIncrement;
- var width = right - left;
- var height = metrics.trackHeight;
-
- var tapeDiv = this._timeline.getDocument().createElement("div");
- tapeDiv.className = 'timeline-small-event-tape'
- if (klassName) {tapeDiv.className += ' small-' + klassName;}
- tapeDiv.style.left = left + "px";
- tapeDiv.style.width = width + "px";
- tapeDiv.style.top = top + "px";
- tapeDiv.style.height = height + "px";
-
- if (color) {
- tapeDiv.style.backgroundColor = color; // set color here if defined by event. Else use css
- }
- // tapeDiv.style.overflow = "hidden"; // now set in css
- // tapeDiv.style.position = "absolute";
- if(opacity<100) SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
-
- this._eventLayer.appendChild(tapeDiv);
-
- return {
- left: left,
- top: top,
- width: width,
- height: height,
- elmt: tapeDiv
- };
-}
-
-Timeline.OverviewEventPainter.prototype._paintEventTick = function(
- evt, left, color, opacity, metrics, theme) {
-
- var height = theme.event.overviewTrack.tickHeight;
- var top = metrics.trackOffset - height;
- var width = 1;
-
- var tickDiv = this._timeline.getDocument().createElement("div");
- tickDiv.className = 'timeline-small-event-icon'
- tickDiv.style.left = left + "px";
- tickDiv.style.top = top + "px";
- // tickDiv.style.width = width + "px";
- // tickDiv.style.position = "absolute";
- // tickDiv.style.height = height + "px";
- // tickDiv.style.backgroundColor = color;
- // tickDiv.style.overflow = "hidden";
-
- var klassName = evt.getClassName()
- if (klassName) {tickDiv.className +=' small-' + klassName};
-
- if(opacity<100) {SimileAjax.Graphics.setOpacity(tickDiv, opacity)};
-
- this._eventLayer.appendChild(tickDiv);
-
- return {
- left: left,
- top: top,
- width: width,
- height: height,
- elmt: tickDiv
- };
-}
-
-Timeline.OverviewEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
- if (highlightIndex >= 0) {
- var doc = this._timeline.getDocument();
- var eventTheme = theme.event;
-
- var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
-
- var div = doc.createElement("div");
- div.style.position = "absolute";
- div.style.overflow = "hidden";
- div.style.left = (dimensions.left - 1) + "px";
- div.style.width = (dimensions.width + 2) + "px";
- div.style.top = (dimensions.top - 1) + "px";
- div.style.height = (dimensions.height + 2) + "px";
- div.style.background = color;
-
- this._highlightLayer.appendChild(div);
- }
-};
-
-Timeline.OverviewEventPainter.prototype.showBubble = function(evt) {
- // not implemented
-};
-/*
- * Compact Event Painter
- *
- */
-
-Timeline.CompactEventPainter = function(params) {
- this._params = params;
- this._onSelectListeners = [];
-
- this._filterMatcher = null;
- this._highlightMatcher = null;
- this._frc = null;
-
- this._eventIdToElmt = {};
-};
-
-Timeline.CompactEventPainter.prototype.getType = function() {
- return 'compact';
-};
-
-Timeline.CompactEventPainter.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._backLayer = null;
- this._eventLayer = null;
- this._lineLayer = null;
- this._highlightLayer = null;
-
- this._eventIdToElmt = null;
-};
-
-Timeline.CompactEventPainter.prototype.addOnSelectListener = function(listener) {
- this._onSelectListeners.push(listener);
-};
-
-Timeline.CompactEventPainter.prototype.removeOnSelectListener = function(listener) {
- for (var i = 0; i < this._onSelectListeners.length; i++) {
- if (this._onSelectListeners[i] == listener) {
- this._onSelectListeners.splice(i, 1);
- break;
- }
- }
-};
-
-Timeline.CompactEventPainter.prototype.getFilterMatcher = function() {
- return this._filterMatcher;
-};
-
-Timeline.CompactEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
- this._filterMatcher = filterMatcher;
-};
-
-Timeline.CompactEventPainter.prototype.getHighlightMatcher = function() {
- return this._highlightMatcher;
-};
-
-Timeline.CompactEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
- this._highlightMatcher = highlightMatcher;
-};
-
-Timeline.CompactEventPainter.prototype.paint = function() {
- var eventSource = this._band.getEventSource();
- if (eventSource == null) {
- return;
- }
-
- this._eventIdToElmt = {};
- this._prepareForPainting();
-
- var metrics = this._computeMetrics();
- var minDate = this._band.getMinDate();
- var maxDate = this._band.getMaxDate();
-
- var filterMatcher = (this._filterMatcher != null) ?
- this._filterMatcher :
- function(evt) { return true; };
-
- var highlightMatcher = (this._highlightMatcher != null) ?
- this._highlightMatcher :
- function(evt) { return -1; };
-
- var iterator = eventSource.getEventIterator(minDate, maxDate);
-
- var stackConcurrentPreciseInstantEvents = "stackConcurrentPreciseInstantEvents" in this._params && typeof this._params.stackConcurrentPreciseInstantEvents == "object";
- var collapseConcurrentPreciseInstantEvents = "collapseConcurrentPreciseInstantEvents" in this._params && this._params.collapseConcurrentPreciseInstantEvents;
- if (collapseConcurrentPreciseInstantEvents || stackConcurrentPreciseInstantEvents) {
- var bufferedEvents = [];
- var previousInstantEvent = null;
-
- while (iterator.hasNext()) {
- var evt = iterator.next();
- if (filterMatcher(evt)) {
- if (!evt.isInstant() || evt.isImprecise()) {
- this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
- } else if (previousInstantEvent != null &&
- previousInstantEvent.getStart().getTime() == evt.getStart().getTime()) {
- bufferedEvents[bufferedEvents.length - 1].push(evt);
- } else {
- bufferedEvents.push([ evt ]);
- previousInstantEvent = evt;
- }
- }
- }
-
- for (var i = 0; i < bufferedEvents.length; i++) {
- var compositeEvents = bufferedEvents[i];
- if (compositeEvents.length == 1) {
- this.paintEvent(compositeEvents[0], metrics, this._params.theme, highlightMatcher(evt));
- } else {
- var match = -1;
- for (var j = 0; match < 0 && j < compositeEvents.length; j++) {
- match = highlightMatcher(compositeEvents[j]);
- }
-
- if (stackConcurrentPreciseInstantEvents) {
- this.paintStackedPreciseInstantEvents(compositeEvents, metrics, this._params.theme, match);
- } else {
- this.paintCompositePreciseInstantEvents(compositeEvents, metrics, this._params.theme, match);
- }
- }
- }
- } else {
- while (iterator.hasNext()) {
- var evt = iterator.next();
- if (filterMatcher(evt)) {
- this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
- }
- }
- }
-
- this._highlightLayer.style.display = "block";
- this._lineLayer.style.display = "block";
- this._eventLayer.style.display = "block";
-
- this._setOrthogonalOffset(metrics);
-};
-
-Timeline.CompactEventPainter.prototype.softPaint = function() {
- this._setOrthogonalOffset(this._computeMetrics());
-};
-
-Timeline.CompactEventPainter.prototype._setOrthogonalOffset = function(metrics) {
- var actualViewWidth = 2 * metrics.trackOffset + this._tracks.length * metrics.trackHeight;
- var minOrthogonalOffset = Math.min(0, this._band.getViewWidth() - actualViewWidth);
- var orthogonalOffset = Math.max(minOrthogonalOffset, this._band.getViewOrthogonalOffset());
-
- this._highlightLayer.style.top =
- this._lineLayer.style.top =
- this._eventLayer.style.top =
- orthogonalOffset + "px";
-};
-
-Timeline.CompactEventPainter.prototype._computeMetrics = function() {
- var theme = this._params.theme;
- var eventTheme = theme.event;
-
- var metrics = {
- trackOffset: "trackOffset" in this._params ? this._params.trackOffset : 10,
- trackHeight: "trackHeight" in this._params ? this._params.trackHeight : 10,
-
- tapeHeight: theme.event.tape.height,
- tapeBottomMargin: "tapeBottomMargin" in this._params ? this._params.tapeBottomMargin : 2,
-
- labelBottomMargin: "labelBottomMargin" in this._params ? this._params.labelBottomMargin : 5,
- labelRightMargin: "labelRightMargin" in this._params ? this._params.labelRightMargin : 5,
-
- defaultIcon: eventTheme.instant.icon,
- defaultIconWidth: eventTheme.instant.iconWidth,
- defaultIconHeight: eventTheme.instant.iconHeight,
-
- customIconWidth: "iconWidth" in this._params ? this._params.iconWidth : eventTheme.instant.iconWidth,
- customIconHeight: "iconHeight" in this._params ? this._params.iconHeight : eventTheme.instant.iconHeight,
-
- iconLabelGap: "iconLabelGap" in this._params ? this._params.iconLabelGap : 2,
- iconBottomMargin: "iconBottomMargin" in this._params ? this._params.iconBottomMargin : 2
- };
- if ("compositeIcon" in this._params) {
- metrics.compositeIcon = this._params.compositeIcon;
- metrics.compositeIconWidth = this._params.compositeIconWidth || metrics.customIconWidth;
- metrics.compositeIconHeight = this._params.compositeIconHeight || metrics.customIconHeight;
- } else {
- metrics.compositeIcon = metrics.defaultIcon;
- metrics.compositeIconWidth = metrics.defaultIconWidth;
- metrics.compositeIconHeight = metrics.defaultIconHeight;
- }
- metrics.defaultStackIcon = "icon" in this._params.stackConcurrentPreciseInstantEvents ?
- this._params.stackConcurrentPreciseInstantEvents.icon : metrics.defaultIcon;
- metrics.defaultStackIconWidth = "iconWidth" in this._params.stackConcurrentPreciseInstantEvents ?
- this._params.stackConcurrentPreciseInstantEvents.iconWidth : metrics.defaultIconWidth;
- metrics.defaultStackIconHeight = "iconHeight" in this._params.stackConcurrentPreciseInstantEvents ?
- this._params.stackConcurrentPreciseInstantEvents.iconHeight : metrics.defaultIconHeight;
-
- return metrics;
-};
-
-Timeline.CompactEventPainter.prototype._prepareForPainting = function() {
- var band = this._band;
-
- if (this._backLayer == null) {
- this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
- this._backLayer.style.visibility = "hidden";
-
- var eventLabelPrototype = document.createElement("span");
- eventLabelPrototype.className = "timeline-event-label";
- this._backLayer.appendChild(eventLabelPrototype);
- this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
- }
- this._frc.update();
- this._tracks = [];
-
- if (this._highlightLayer != null) {
- band.removeLayerDiv(this._highlightLayer);
- }
- this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
- this._highlightLayer.style.display = "none";
-
- if (this._lineLayer != null) {
- band.removeLayerDiv(this._lineLayer);
- }
- this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
- this._lineLayer.style.display = "none";
-
- if (this._eventLayer != null) {
- band.removeLayerDiv(this._eventLayer);
- }
- this._eventLayer = band.createLayerDiv(115, "timeline-band-events");
- this._eventLayer.style.display = "none";
-};
-
-Timeline.CompactEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isInstant()) {
- this.paintInstantEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintDurationEvent(evt, metrics, theme, highlightIndex);
- }
-};
-
-Timeline.CompactEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isImprecise()) {
- this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
- }
-}
-
-Timeline.CompactEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
- if (evt.isImprecise()) {
- this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
- } else {
- this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
- }
-}
-
-Timeline.CompactEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
- var commonData = {
- tooltip: evt.getProperty("tooltip") || evt.getText()
- };
-
- var iconData = {
- url: evt.getIcon()
- };
- if (iconData.url == null) {
- iconData.url = metrics.defaultIcon;
- iconData.width = metrics.defaultIconWidth;
- iconData.height = metrics.defaultIconHeight;
- iconData.className = "timeline-event-icon-default";
- } else {
- iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
- iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
- }
-
- var labelData = {
- text: evt.getText(),
- color: evt.getTextColor() || evt.getColor(),
- className: evt.getClassName()
- };
-
- var result = this.paintTapeIconLabel(
- evt.getStart(),
- commonData,
- null, // no tape data
- iconData,
- labelData,
- metrics,
- theme,
- highlightIndex
- );
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
- };
- SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
-
- this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
-};
-
-Timeline.CompactEventPainter.prototype.paintCompositePreciseInstantEvents = function(events, metrics, theme, highlightIndex) {
- var evt = events[0];
-
- var tooltips = [];
- for (var i = 0; i < events.length; i++) {
- tooltips.push(events[i].getProperty("tooltip") || events[i].getText());
- }
- var commonData = {
- tooltip: tooltips.join("; ")
- };
-
- var iconData = {
- url: metrics.compositeIcon,
- width: metrics.compositeIconWidth,
- height: metrics.compositeIconHeight,
- className: "timeline-event-icon-composite"
- };
-
- var labelData = {
- text: String.substitute(this._params.compositeEventLabelTemplate, [ events.length ])
- };
-
- var result = this.paintTapeIconLabel(
- evt.getStart(),
- commonData,
- null, // no tape data
- iconData,
- labelData,
- metrics,
- theme,
- highlightIndex
- );
-
- var self = this;
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickMultiplePreciseInstantEvent(result.iconElmtData.elmt, domEvt, events);
- };
-
- SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
-
- for (var i = 0; i < events.length; i++) {
- this._eventIdToElmt[events[i].getID()] = result.iconElmtData.elmt;
- }
-};
-
-Timeline.CompactEventPainter.prototype.paintStackedPreciseInstantEvents = function(events, metrics, theme, highlightIndex) {
- var limit = "limit" in this._params.stackConcurrentPreciseInstantEvents ?
- this._params.stackConcurrentPreciseInstantEvents.limit : 10;
- var moreMessageTemplate = "moreMessageTemplate" in this._params.stackConcurrentPreciseInstantEvents ?
- this._params.stackConcurrentPreciseInstantEvents.moreMessageTemplate : "%0 More Events";
- var showMoreMessage = limit <= events.length - 2; // We want at least 2 more events above the limit.
- // Otherwise we'd need the singular case of "1 More Event"
-
- var band = this._band;
- var getPixelOffset = function(date) {
- return Math.round(band.dateToPixelOffset(date));
- };
- var getIconData = function(evt) {
- var iconData = {
- url: evt.getIcon()
- };
- if (iconData.url == null) {
- iconData.url = metrics.defaultStackIcon;
- iconData.width = metrics.defaultStackIconWidth;
- iconData.height = metrics.defaultStackIconHeight;
- iconData.className = "timeline-event-icon-stack timeline-event-icon-default";
- } else {
- iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
- iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
- iconData.className = "timeline-event-icon-stack";
- }
- return iconData;
- };
-
- var firstIconData = getIconData(events[0]);
- var horizontalIncrement = 5;
- var leftIconEdge = 0;
- var totalLabelWidth = 0;
- var totalLabelHeight = 0;
- var totalIconHeight = 0;
-
- var records = [];
- for (var i = 0; i < events.length && (!showMoreMessage || i < limit); i++) {
- var evt = events[i];
- var text = evt.getText();
- var iconData = getIconData(evt);
- var labelSize = this._frc.computeSize(text);
- var record = {
- text: text,
- iconData: iconData,
- labelSize: labelSize,
- iconLeft: firstIconData.width + i * horizontalIncrement - iconData.width
- };
- record.labelLeft = firstIconData.width + i * horizontalIncrement + metrics.iconLabelGap;
- record.top = totalLabelHeight;
- records.push(record);
-
- leftIconEdge = Math.min(leftIconEdge, record.iconLeft);
- totalLabelHeight += labelSize.height;
- totalLabelWidth = Math.max(totalLabelWidth, record.labelLeft + labelSize.width);
- totalIconHeight = Math.max(totalIconHeight, record.top + iconData.height);
- }
- if (showMoreMessage) {
- var moreMessage = String.substitute(moreMessageTemplate, [ events.length - limit ]);
-
- var moreMessageLabelSize = this._frc.computeSize(moreMessage);
- var moreMessageLabelLeft = firstIconData.width + (limit - 1) * horizontalIncrement + metrics.iconLabelGap;
- var moreMessageLabelTop = totalLabelHeight;
-
- totalLabelHeight += moreMessageLabelSize.height;
- totalLabelWidth = Math.max(totalLabelWidth, moreMessageLabelLeft + moreMessageLabelSize.width);
- }
- totalLabelWidth += metrics.labelRightMargin;
- totalLabelHeight += metrics.labelBottomMargin;
- totalIconHeight += metrics.iconBottomMargin;
-
- var anchorPixel = getPixelOffset(events[0].getStart());
- var newTracks = [];
-
- var trackCount = Math.ceil(Math.max(totalIconHeight, totalLabelHeight) / metrics.trackHeight);
- var rightIconEdge = firstIconData.width + (events.length - 1) * horizontalIncrement;
- for (var i = 0; i < trackCount; i++) {
- newTracks.push({ start: leftIconEdge, end: rightIconEdge });
- }
- var labelTrackCount = Math.ceil(totalLabelHeight / metrics.trackHeight);
- for (var i = 0; i < labelTrackCount; i++) {
- var track = newTracks[i];
- track.end = Math.max(track.end, totalLabelWidth);
- }
-
- var firstTrack = this._fitTracks(anchorPixel, newTracks);
- var verticalPixelOffset = firstTrack * metrics.trackHeight + metrics.trackOffset;
-
- var iconStackDiv = this._timeline.getDocument().createElement("div");
- iconStackDiv.className = 'timeline-event-icon-stack';
- iconStackDiv.style.position = "absolute";
- iconStackDiv.style.overflow = "visible";
- iconStackDiv.style.left = anchorPixel + "px";
- iconStackDiv.style.top = verticalPixelOffset + "px";
- iconStackDiv.style.width = rightIconEdge + "px";
- iconStackDiv.style.height = totalIconHeight + "px";
- iconStackDiv.innerHTML = "<div style='position: relative'></div>";
- this._eventLayer.appendChild(iconStackDiv);
-
- var self = this;
- var onMouseOver = function(domEvt) {
- try {
- var n = parseInt(this.getAttribute("index"));
- var childNodes = iconStackDiv.firstChild.childNodes;
- for (var i = 0; i < childNodes.length; i++) {
- var child = childNodes[i];
- if (i == n) {
- child.style.zIndex = childNodes.length;
- } else {
- child.style.zIndex = childNodes.length - i;
- }
- }
- } catch (e) {
- }
- };
- var paintEvent = function(index) {
- var record = records[index];
- var evt = events[index];
- var tooltip = evt.getProperty("tooltip") || evt.getText();
-
- var labelElmtData = self._paintEventLabel(
- { tooltip: tooltip },
- { text: record.text },
- anchorPixel + record.labelLeft,
- verticalPixelOffset + record.top,
- record.labelSize.width,
- record.labelSize.height,
- theme
- );
- labelElmtData.elmt.setAttribute("index", index);
- labelElmtData.elmt.onmouseover = onMouseOver;
-
- var img = SimileAjax.Graphics.createTranslucentImage(record.iconData.url);
- var iconDiv = self._timeline.getDocument().createElement("div");
- iconDiv.className = 'timeline-event-icon' + ("className" in record.iconData ? (" " + record.iconData.className) : "");
- iconDiv.style.left = record.iconLeft + "px";
- iconDiv.style.top = record.top + "px";
- iconDiv.style.zIndex = (records.length - index);
- iconDiv.appendChild(img);
- iconDiv.setAttribute("index", index);
- iconDiv.onmouseover = onMouseOver;
-
- iconStackDiv.firstChild.appendChild(iconDiv);
-
- var clickHandler = function(elmt, domEvt, target) {
- return self._onClickInstantEvent(labelElmtData.elmt, domEvt, evt);
- };
-
- SimileAjax.DOM.registerEvent(iconDiv, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
-
- self._eventIdToElmt[evt.getID()] = iconDiv;
- };
- for (var i = 0; i < records.length; i++) {
- paintEvent(i);
- }
-
- if (showMoreMessage) {
- var moreEvents = events.slice(limit);
- var moreMessageLabelElmtData = this._paintEventLabel(
- { tooltip: moreMessage },
- { text: moreMessage },
- anchorPixel + moreMessageLabelLeft,
- verticalPixelOffset + moreMessageLabelTop,
- moreMessageLabelSize.width,
- moreMessageLabelSize.height,
- theme
- );
-
- var moreMessageClickHandler = function(elmt, domEvt, target) {
- return self._onClickMultiplePreciseInstantEvent(moreMessageLabelElmtData.elmt, domEvt, moreEvents);
- };
- SimileAjax.DOM.registerEvent(moreMessageLabelElmtData.elmt, "mousedown", moreMessageClickHandler);
-
- for (var i = 0; i < moreEvents.length; i++) {
- this._eventIdToElmt[moreEvents[i].getID()] = moreMessageLabelElmtData.elmt;
- }
- }
- //this._createHighlightDiv(highlightIndex, iconElmtData, theme);
-};
-
-Timeline.CompactEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
- var commonData = {
- tooltip: evt.getProperty("tooltip") || evt.getText()
- };
-
- var tapeData = {
- start: evt.getStart(),
- end: evt.getEnd(),
- latestStart: evt.getLatestStart(),
- earliestEnd: evt.getEarliestEnd(),
- isInstant: true
- };
-
- var iconData = {
- url: evt.getIcon()
- };
- if (iconData.url == null) {
- iconData = null;
- } else {
- iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
- iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
- }
-
- var labelData = {
- text: evt.getText(),
- color: evt.getTextColor() || evt.getColor(),
- className: evt.getClassName()
- };
-
- var result = this.paintTapeIconLabel(
- evt.getStart(),
- commonData,
- tapeData, // no tape data
- iconData,
- labelData,
- metrics,
- theme,
- highlightIndex
- );
-
- var self = this;
- var clickHandler = iconData != null ?
- function(elmt, domEvt, target) {
- return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
- } :
- function(elmt, domEvt, target) {
- return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
- };
-
- SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(result.impreciseTapeElmtData.elmt, "mousedown", clickHandler);
-
- if (iconData != null) {
- SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
- this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
- } else {
- this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
- }
-};
-
-Timeline.CompactEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
- var commonData = {
- tooltip: evt.getProperty("tooltip") || evt.getText()
- };
-
- var tapeData = {
- start: evt.getStart(),
- end: evt.getEnd(),
- isInstant: false
- };
-
- var iconData = {
- url: evt.getIcon()
- };
- if (iconData.url == null) {
- iconData = null;
- } else {
- iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
- iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
- }
-
- var labelData = {
- text: evt.getText(),
- color: evt.getTextColor() || evt.getColor(),
- className: evt.getClassName()
- };
-
- var result = this.paintTapeIconLabel(
- evt.getLatestStart(),
- commonData,
- tapeData, // no tape data
- iconData,
- labelData,
- metrics,
- theme,
- highlightIndex
- );
-
- var self = this;
- var clickHandler = iconData != null ?
- function(elmt, domEvt, target) {
- return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
- } :
- function(elmt, domEvt, target) {
- return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
- };
-
- SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(result.tapeElmtData.elmt, "mousedown", clickHandler);
-
- if (iconData != null) {
- SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
- this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
- } else {
- this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
- }
-};
-
-Timeline.CompactEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
- var commonData = {
- tooltip: evt.getProperty("tooltip") || evt.getText()
- };
-
- var tapeData = {
- start: evt.getStart(),
- end: evt.getEnd(),
- latestStart: evt.getLatestStart(),
- earliestEnd: evt.getEarliestEnd(),
- isInstant: false
- };
-
- var iconData = {
- url: evt.getIcon()
- };
- if (iconData.url == null) {
- iconData = null;
- } else {
- iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
- iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
- }
-
- var labelData = {
- text: evt.getText(),
- color: evt.getTextColor() || evt.getColor(),
- className: evt.getClassName()
- };
-
- var result = this.paintTapeIconLabel(
- evt.getLatestStart(),
- commonData,
- tapeData, // no tape data
- iconData,
- labelData,
- metrics,
- theme,
- highlightIndex
- );
-
- var self = this;
- var clickHandler = iconData != null ?
- function(elmt, domEvt, target) {
- return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
- } :
- function(elmt, domEvt, target) {
- return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
- };
-
- SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
- SimileAjax.DOM.registerEvent(result.tapeElmtData.elmt, "mousedown", clickHandler);
-
- if (iconData != null) {
- SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
- this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
- } else {
- this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
- }
-};
-
-Timeline.CompactEventPainter.prototype.paintTapeIconLabel = function(
- anchorDate,
- commonData,
- tapeData,
- iconData,
- labelData,
- metrics,
- theme,
- highlightIndex
-) {
- var band = this._band;
- var getPixelOffset = function(date) {
- return Math.round(band.dateToPixelOffset(date));
- };
-
- var anchorPixel = getPixelOffset(anchorDate);
- var newTracks = [];
-
- var tapeHeightOccupied = 0; // how many pixels (vertically) the tape occupies, including bottom margin
- var tapeTrackCount = 0; // how many tracks the tape takes up, usually just 1
- var tapeLastTrackExtraSpace = 0; // on the last track that the tape occupies, how many pixels are left (for icon and label to occupy as well)
- if (tapeData != null) {
- tapeHeightOccupied = metrics.tapeHeight + metrics.tapeBottomMargin;
- tapeTrackCount = Math.ceil(metrics.tapeHeight / metrics.trackHeight);
-
- var tapeEndPixelOffset = getPixelOffset(tapeData.end) - anchorPixel;
- var tapeStartPixelOffset = getPixelOffset(tapeData.start) - anchorPixel;
-
- for (var t = 0; t < tapeTrackCount; t++) {
- newTracks.push({ start: tapeStartPixelOffset, end: tapeEndPixelOffset });
- }
-
- tapeLastTrackExtraSpace = metrics.trackHeight - (tapeHeightOccupied % metrics.tapeHeight);
- }
-
- var iconStartPixelOffset = 0; // where the icon starts compared to the anchor pixel;
- // this can be negative if the icon is center-aligned around the anchor
- var iconHorizontalSpaceOccupied = 0; // how many pixels the icon take up from the anchor pixel,
- // including the gap between the icon and the label
- if (iconData != null) {
- if ("iconAlign" in iconData && iconData.iconAlign == "center") {
- iconStartPixelOffset = -Math.floor(iconData.width / 2);
- }
- iconHorizontalSpaceOccupied = iconStartPixelOffset + iconData.width + metrics.iconLabelGap;
-
- if (tapeTrackCount > 0) {
- newTracks[tapeTrackCount - 1].end = Math.max(newTracks[tapeTrackCount - 1].end, iconHorizontalSpaceOccupied);
- }
-
- var iconHeight = iconData.height + metrics.iconBottomMargin + tapeLastTrackExtraSpace;
- while (iconHeight > 0) {
- newTracks.push({ start: iconStartPixelOffset, end: iconHorizontalSpaceOccupied });
- iconHeight -= metrics.trackHeight;
- }
- }
-
- var text = labelData.text;
- var labelSize = this._frc.computeSize(text);
- var labelHeight = labelSize.height + metrics.labelBottomMargin + tapeLastTrackExtraSpace;
- var labelEndPixelOffset = iconHorizontalSpaceOccupied + labelSize.width + metrics.labelRightMargin;
- if (tapeTrackCount > 0) {
- newTracks[tapeTrackCount - 1].end = Math.max(newTracks[tapeTrackCount - 1].end, labelEndPixelOffset);
- }
- for (var i = 0; labelHeight > 0; i++) {
- if (tapeTrackCount + i < newTracks.length) {
- var track = newTracks[tapeTrackCount + i];
- track.end = labelEndPixelOffset;
- } else {
- newTracks.push({ start: 0, end: labelEndPixelOffset });
- }
- labelHeight -= metrics.trackHeight;
- }
-
- /*
- * Try to fit the new track on top of the existing tracks, then
- * render the various elements.
- */
- var firstTrack = this._fitTracks(anchorPixel, newTracks);
- var verticalPixelOffset = firstTrack * metrics.trackHeight + metrics.trackOffset;
- var result = {};
-
- result.labelElmtData = this._paintEventLabel(
- commonData,
- labelData,
- anchorPixel + iconHorizontalSpaceOccupied,
- verticalPixelOffset + tapeHeightOccupied,
- labelSize.width,
- labelSize.height,
- theme
- );
-
- if (tapeData != null) {
- if ("latestStart" in tapeData || "earliestEnd" in tapeData) {
- result.impreciseTapeElmtData = this._paintEventTape(
- commonData,
- tapeData,
- metrics.tapeHeight,
- verticalPixelOffset,
- getPixelOffset(tapeData.start),
- getPixelOffset(tapeData.end),
- theme.event.duration.impreciseColor,
- theme.event.duration.impreciseOpacity,
- metrics,
- theme
- );
- }
- if (!tapeData.isInstant && "start" in tapeData && "end" in tapeData) {
- result.tapeElmtData = this._paintEventTape(
- commonData,
- tapeData,
- metrics.tapeHeight,
- verticalPixelOffset,
- anchorPixel,
- getPixelOffset("earliestEnd" in tapeData ? tapeData.earliestEnd : tapeData.end),
- tapeData.color,
- 100,
- metrics,
- theme
- );
- }
- }
-
- if (iconData != null) {
- result.iconElmtData = this._paintEventIcon(
- commonData,
- iconData,
- verticalPixelOffset + tapeHeightOccupied,
- anchorPixel + iconStartPixelOffset,
- metrics,
- theme
- );
- }
- //this._createHighlightDiv(highlightIndex, iconElmtData, theme);
-
- return result;
-};
-
-Timeline.CompactEventPainter.prototype._fitTracks = function(anchorPixel, newTracks) {
- var firstTrack;
- for (firstTrack = 0; firstTrack < this._tracks.length; firstTrack++) {
- var fit = true;
- for (var j = 0; j < newTracks.length && (firstTrack + j) < this._tracks.length; j++) {
- var existingTrack = this._tracks[firstTrack + j];
- var newTrack = newTracks[j];
- if (anchorPixel + newTrack.start < existingTrack) {
- fit = false;
- break;
- }
- }
-
- if (fit) {
- break;
- }
- }
- for (var i = 0; i < newTracks.length; i++) {
- this._tracks[firstTrack + i] = anchorPixel + newTracks[i].end;
- }
-
- return firstTrack;
-};
-
-
-Timeline.CompactEventPainter.prototype._paintEventIcon = function(commonData, iconData, top, left, metrics, theme) {
- var img = SimileAjax.Graphics.createTranslucentImage(iconData.url);
- var iconDiv = this._timeline.getDocument().createElement("div");
- iconDiv.className = 'timeline-event-icon' + ("className" in iconData ? (" " + iconData.className) : "");
- iconDiv.style.left = left + "px";
- iconDiv.style.top = top + "px";
- iconDiv.appendChild(img);
-
- if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
- iconDiv.title = commonData.tooltip;
- }
-
- this._eventLayer.appendChild(iconDiv);
-
- return {
- left: left,
- top: top,
- width: metrics.iconWidth,
- height: metrics.iconHeight,
- elmt: iconDiv
- };
-};
-
-Timeline.CompactEventPainter.prototype._paintEventLabel = function(commonData, labelData, left, top, width, height, theme) {
- var doc = this._timeline.getDocument();
-
- var labelDiv = doc.createElement("div");
- labelDiv.className = 'timeline-event-label';
-
- labelDiv.style.left = left + "px";
- labelDiv.style.width = (width + 1) + "px";
- labelDiv.style.top = top + "px";
- labelDiv.innerHTML = labelData.text;
-
- if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
- labelDiv.title = commonData.tooltip;
- }
- if ("color" in labelData && typeof labelData.color == "string") {
- labelDiv.style.color = labelData.color;
- }
- if ("className" in labelData && typeof labelData.className == "string") {
- labelDiv.className += ' ' + labelData.className;
- }
-
- this._eventLayer.appendChild(labelDiv);
-
- return {
- left: left,
- top: top,
- width: width,
- height: height,
- elmt: labelDiv
- };
-};
-
-Timeline.CompactEventPainter.prototype._paintEventTape = function(
- commonData, tapeData, height, top, startPixel, endPixel, color, opacity, metrics, theme) {
-
- var width = endPixel - startPixel;
-
- var tapeDiv = this._timeline.getDocument().createElement("div");
- tapeDiv.className = "timeline-event-tape"
-
- tapeDiv.style.left = startPixel + "px";
- tapeDiv.style.top = top + "px";
- tapeDiv.style.width = width + "px";
- tapeDiv.style.height = height + "px";
-
- if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
- tapeDiv.title = commonData.tooltip;
- }
- if (color != null && typeof tapeData.color == "string") {
- tapeDiv.style.backgroundColor = color;
- }
-
- if ("backgroundImage" in tapeData && typeof tapeData.backgroundImage == "string") {
- tapeDiv.style.backgroundImage = "url(" + backgroundImage + ")";
- tapeDiv.style.backgroundRepeat =
- ("backgroundRepeat" in tapeData && typeof tapeData.backgroundRepeat == "string")
- ? tapeData.backgroundRepeat : 'repeat';
- }
-
- SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
-
- if ("className" in tapeData && typeof tapeData.className == "string") {
- tapeDiv.className += ' ' + tapeData.className;
- }
-
- this._eventLayer.appendChild(tapeDiv);
-
- return {
- left: startPixel,
- top: top,
- width: width,
- height: height,
- elmt: tapeDiv
- };
-}
-
-Timeline.CompactEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
- if (highlightIndex >= 0) {
- var doc = this._timeline.getDocument();
- var eventTheme = theme.event;
-
- var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
-
- var div = doc.createElement("div");
- div.style.position = "absolute";
- div.style.overflow = "hidden";
- div.style.left = (dimensions.left - 2) + "px";
- div.style.width = (dimensions.width + 4) + "px";
- div.style.top = (dimensions.top - 2) + "px";
- div.style.height = (dimensions.height + 4) + "px";
-// div.style.background = color;
-
- this._highlightLayer.appendChild(div);
- }
-};
-
-Timeline.CompactEventPainter.prototype._onClickMultiplePreciseInstantEvent = function(icon, domEvt, events) {
- var c = SimileAjax.DOM.getPageCoordinates(icon);
- this._showBubble(
- c.left + Math.ceil(icon.offsetWidth / 2),
- c.top + Math.ceil(icon.offsetHeight / 2),
- events
- );
-
- var ids = [];
- for (var i = 0; i < events.length; i++) {
- ids.push(events[i].getID());
- }
- this._fireOnSelect(ids);
-
- domEvt.cancelBubble = true;
- SimileAjax.DOM.cancelEvent(domEvt);
-
- return false;
-};
-
-Timeline.CompactEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
- var c = SimileAjax.DOM.getPageCoordinates(icon);
- this._showBubble(
- c.left + Math.ceil(icon.offsetWidth / 2),
- c.top + Math.ceil(icon.offsetHeight / 2),
- [evt]
- );
- this._fireOnSelect([evt.getID()]);
-
- domEvt.cancelBubble = true;
- SimileAjax.DOM.cancelEvent(domEvt);
- return false;
-};
-
-Timeline.CompactEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
- if ("pageX" in domEvt) {
- var x = domEvt.pageX;
- var y = domEvt.pageY;
- } else {
- var c = SimileAjax.DOM.getPageCoordinates(target);
- var x = domEvt.offsetX + c.left;
- var y = domEvt.offsetY + c.top;
- }
- this._showBubble(x, y, [evt]);
- this._fireOnSelect([evt.getID()]);
-
- domEvt.cancelBubble = true;
- SimileAjax.DOM.cancelEvent(domEvt);
- return false;
-};
-
-Timeline.CompactEventPainter.prototype.showBubble = function(evt) {
- var elmt = this._eventIdToElmt[evt.getID()];
- if (elmt) {
- var c = SimileAjax.DOM.getPageCoordinates(elmt);
- this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, [evt]);
- }
-};
-
-Timeline.CompactEventPainter.prototype._showBubble = function(x, y, evts) {
- var div = document.createElement("div");
-
- evts = ("fillInfoBubble" in evts) ? [evts] : evts;
- for (var i = 0; i < evts.length; i++) {
- var div2 = document.createElement("div");
- div.appendChild(div2);
-
- evts[i].fillInfoBubble(div2, this._params.theme, this._band.getLabeller());
- }
-
- SimileAjax.WindowManager.cancelPopups();
- SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, this._params.theme.event.bubble.width);
-};
-
-Timeline.CompactEventPainter.prototype._fireOnSelect = function(eventIDs) {
- for (var i = 0; i < this._onSelectListeners.length; i++) {
- this._onSelectListeners[i](eventIDs);
- }
-};
-/*
- * Span Highlight Decorator
- *
- */
-
-Timeline.SpanHighlightDecorator = function(params) {
- // When evaluating params, test against null. Not "p in params". Testing against
- // null enables caller to explicitly request the default. Testing against "in" means
- // that the param has to be ommitted to get the default.
- this._unit = params.unit != null ? params.unit : SimileAjax.NativeDateUnit;
- this._startDate = (typeof params.startDate == "string") ?
- this._unit.parseFromObject(params.startDate) : params.startDate;
- this._endDate = (typeof params.endDate == "string") ?
- this._unit.parseFromObject(params.endDate) : params.endDate;
- this._startLabel = params.startLabel != null ? params.startLabel : ""; // not null!
- this._endLabel = params.endLabel != null ? params.endLabel : ""; // not null!
- this._color = params.color;
- this._cssClass = params.cssClass != null ? params.cssClass : null;
- this._opacity = params.opacity != null ? params.opacity : 100;
- // Default z is 10, behind everything but background grid.
- // If inFront, then place just behind events, in front of everything else
- this._zIndex = (params.inFront != null && params.inFront) ? 113 : 10;
-};
-
-Timeline.SpanHighlightDecorator.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
-
- this._layerDiv = null;
-};
-
-Timeline.SpanHighlightDecorator.prototype.paint = function() {
- if (this._layerDiv != null) {
- this._band.removeLayerDiv(this._layerDiv);
- }
- this._layerDiv = this._band.createLayerDiv(this._zIndex);
- this._layerDiv.setAttribute("name", "span-highlight-decorator"); // for debugging
- this._layerDiv.style.display = "none";
-
- var minDate = this._band.getMinDate();
- var maxDate = this._band.getMaxDate();
-
- if (this._unit.compare(this._startDate, maxDate) < 0 &&
- this._unit.compare(this._endDate, minDate) > 0) {
-
- minDate = this._unit.later(minDate, this._startDate);
- maxDate = this._unit.earlier(maxDate, this._endDate);
-
- var minPixel = this._band.dateToPixelOffset(minDate);
- var maxPixel = this._band.dateToPixelOffset(maxDate);
-
- var doc = this._timeline.getDocument();
-
- var createTable = function() {
- var table = doc.createElement("table");
- table.insertRow(0).insertCell(0);
- return table;
- };
-
- var div = doc.createElement("div");
- div.className='timeline-highlight-decorator'
- if(this._cssClass) {
- div.className += ' ' + this._cssClass;
- }
- if(this._color != null) {
- div.style.backgroundColor = this._color;
- }
- if (this._opacity < 100) {
- SimileAjax.Graphics.setOpacity(div, this._opacity);
- }
- this._layerDiv.appendChild(div);
-
- var tableStartLabel = createTable();
- tableStartLabel.className = 'timeline-highlight-label timeline-highlight-label-start'
- var tdStart = tableStartLabel.rows[0].cells[0]
- tdStart.innerHTML = this._startLabel;
- if (this._cssClass) {
- tdStart.className = 'label_' + this._cssClass;
- }
- this._layerDiv.appendChild(tableStartLabel);
-
- var tableEndLabel = createTable();
- tableEndLabel.className = 'timeline-highlight-label timeline-highlight-label-end'
- var tdEnd = tableEndLabel.rows[0].cells[0]
- tdEnd.innerHTML = this._endLabel;
- if (this._cssClass) {
- tdEnd.className = 'label_' + this._cssClass;
- }
- this._layerDiv.appendChild(tableEndLabel);
-
- if (this._timeline.isHorizontal()){
- div.style.left = minPixel + "px";
- div.style.width = (maxPixel - minPixel) + "px";
-
- tableStartLabel.style.right = (this._band.getTotalViewLength() - minPixel) + "px";
- tableStartLabel.style.width = (this._startLabel.length) + "em";
-
- tableEndLabel.style.left = maxPixel + "px";
- tableEndLabel.style.width = (this._endLabel.length) + "em";
-
- } else {
- div.style.top = minPixel + "px";
- div.style.height = (maxPixel - minPixel) + "px";
-
- tableStartLabel.style.bottom = minPixel + "px";
- tableStartLabel.style.height = "1.5px";
-
- tableEndLabel.style.top = maxPixel + "px";
- tableEndLabel.style.height = "1.5px";
- }
- }
- this._layerDiv.style.display = "block";
-};
-
-Timeline.SpanHighlightDecorator.prototype.softPaint = function() {
-};
-
-/*
- * Point Highlight Decorator
- *
- */
-
-Timeline.PointHighlightDecorator = function(params) {
- this._unit = params.unit != null ? params.unit : SimileAjax.NativeDateUnit;
- this._date = (typeof params.date == "string") ?
- this._unit.parseFromObject(params.date) : params.date;
- this._width = params.width != null ? params.width : 10;
- // Since the width is used to calculate placements (see minPixel, below), we
- // specify width here, not in css.
- this._color = params.color;
- this._cssClass = params.cssClass != null ? params.cssClass : '';
- this._opacity = params.opacity != null ? params.opacity : 100;
-};
-
-Timeline.PointHighlightDecorator.prototype.initialize = function(band, timeline) {
- this._band = band;
- this._timeline = timeline;
- this._layerDiv = null;
-};
-
-Timeline.PointHighlightDecorator.prototype.paint = function() {
- if (this._layerDiv != null) {
- this._band.removeLayerDiv(this._layerDiv);
- }
- this._layerDiv = this._band.createLayerDiv(10);
- this._layerDiv.setAttribute("name", "span-highlight-decorator"); // for debugging
- this._layerDiv.style.display = "none";
-
- var minDate = this._band.getMinDate();
- var maxDate = this._band.getMaxDate();
-
- if (this._unit.compare(this._date, maxDate) < 0 &&
- this._unit.compare(this._date, minDate) > 0) {
-
- var pixel = this._band.dateToPixelOffset(this._date);
- var minPixel = pixel - Math.round(this._width / 2);
-
- var doc = this._timeline.getDocument();
-
- var div = doc.createElement("div");
- div.className='timeline-highlight-point-decorator';
- div.className += ' ' + this._cssClass;
-
- if(this._color != null) {
- div.style.backgroundColor = this._color;
- }
- if (this._opacity < 100) {
- SimileAjax.Graphics.setOpacity(div, this._opacity);
- }
- this._layerDiv.appendChild(div);
-
- if (this._timeline.isHorizontal()) {
- div.style.left = minPixel + "px";
- div.style.width = this._width;
- } else {
- div.style.top = minPixel + "px";
- div.style.height = this._width;
- }
- }
- this._layerDiv.style.display = "block";
-};
-
-Timeline.PointHighlightDecorator.prototype.softPaint = function() {
-};
-/*
- * Default Unit
- *
- */
-
-Timeline.NativeDateUnit = new Object();
-
-Timeline.NativeDateUnit.createLabeller = function(locale, timeZone) {
- return new Timeline.GregorianDateLabeller(locale, timeZone);
-};
-
-Timeline.NativeDateUnit.makeDefaultValue = function() {
- return new Date();
-};
-
-Timeline.NativeDateUnit.cloneValue = function(v) {
- return new Date(v.getTime());
-};
-
-Timeline.NativeDateUnit.getParser = function(format) {
- if (typeof format == "string") {
- format = format.toLowerCase();
- }
- return (format == "iso8601" || format == "iso 8601") ?
- Timeline.DateTime.parseIso8601DateTime :
- Timeline.DateTime.parseGregorianDateTime;
-};
-
-Timeline.NativeDateUnit.parseFromObject = function(o) {
- return Timeline.DateTime.parseGregorianDateTime(o);
-};
-
-Timeline.NativeDateUnit.toNumber = function(v) {
- return v.getTime();
-};
-
-Timeline.NativeDateUnit.fromNumber = function(n) {
- return new Date(n);
-};
-
-Timeline.NativeDateUnit.compare = function(v1, v2) {
- var n1, n2;
- if (typeof v1 == "object") {
- n1 = v1.getTime();
- } else {
- n1 = Number(v1);
- }
- if (typeof v2 == "object") {
- n2 = v2.getTime();
- } else {
- n2 = Number(v2);
- }
-
- return n1 - n2;
-};
-
-Timeline.NativeDateUnit.earlier = function(v1, v2) {
- return Timeline.NativeDateUnit.compare(v1, v2) < 0 ? v1 : v2;
-};
-
-Timeline.NativeDateUnit.later = function(v1, v2) {
- return Timeline.NativeDateUnit.compare(v1, v2) > 0 ? v1 : v2;
-};
-
-Timeline.NativeDateUnit.change = function(v, n) {
- return new Date(v.getTime() + n);
-};
-
-/*
- * Common localization strings
- *
- */
-
-Timeline.strings["fr"] = {
- wikiLinkLabel: "Discute"
-};
-
-/*
- * Localization of labellers.js
- *
- */
-
-Timeline.GregorianDateLabeller.monthNames["fr"] = [
- "jan", "fev", "mar", "avr", "mai", "jui", "jui", "aou", "sep", "oct", "nov", "dec"
-];
-/*
- * Common localization strings
- *
- */
-
-Timeline.strings["en"] = {
- wikiLinkLabel: "Discuss"
-};
-
-/*
- * Localization of labellers.js
- *
- */
-
-Timeline.GregorianDateLabeller.monthNames["en"] = [
- "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
-];
-
-Timeline.GregorianDateLabeller.dayNames["en"] = [
- "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
-];
--- a/web/data/cubicweb.timeline-ext.js Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-/**
- * :organization: Logilab
- * :copyright: 2008-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
- * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
- *
- */
-
-/**
- * provide our own custom date parser since the default
- * one only understands iso8601 and gregorian dates
- */
-SimileAjax.NativeDateUnit.getParser = Timeline.NativeDateUnit.getParser = function(format) {
- if (typeof format == "string") {
- if (format.indexOf('%') != - 1) {
- return function(datestring) {
- if (datestring) {
- return strptime(datestring, format);
- }
- return null;
- };
- }
- format = format.toLowerCase();
- }
- if (format == "iso8601" || format == "iso 8601") {
- return Timeline.DateTime.parseIso8601DateTime;
- }
- return Timeline.DateTime.parseGregorianDateTime;
-};
-
-/*** CUBICWEB EVENT PAINTER *****************************************************/
-Timeline.CubicWebEventPainter = function(params) {
- // Timeline.OriginalEventPainter.apply(this, arguments);
- this._params = params;
- this._onSelectListeners = [];
-
- this._filterMatcher = null;
- this._highlightMatcher = null;
- this._frc = null;
-
- this._eventIdToElmt = {};
-};
-
-Timeline.CubicWebEventPainter.prototype = new Timeline.OriginalEventPainter();
-
-Timeline.CubicWebEventPainter.prototype._paintEventLabel = function(
-evt, text, left, top, width, height, theme) {
- var doc = this._timeline.getDocument();
-
- var labelDiv = doc.createElement("div");
- labelDiv.className = 'timeline-event-label';
-
- labelDiv.style.left = left + "px";
- labelDiv.style.width = width + "px";
- labelDiv.style.top = top + "px";
-
- if (evt._obj.onclick) {
- labelDiv.appendChild(A({
- 'href': evt._obj.onclick
- },
- text));
- } else if (evt._obj.image) {
- labelDiv.appendChild(IMG({
- src: evt._obj.image,
- width: '30px',
- height: '30px'
- }));
- } else {
- labelDiv.innerHTML = text;
- }
-
- if (evt._title != null) labelDiv.title = evt._title;
-
- var color = evt.getTextColor();
- if (color == null) {
- color = evt.getColor();
- }
- if (color != null) {
- labelDiv.style.color = color;
- }
- var classname = evt.getClassName();
- if (classname) labelDiv.className += ' ' + classname;
-
- this._eventLayer.appendChild(labelDiv);
-
- return {
- left: left,
- top: top,
- width: width,
- height: height,
- elmt: labelDiv
- };
-};
-
-Timeline.CubicWebEventPainter.prototype._showBubble = function(x, y, evt) {
- var div = DIV({
- id: 'xxx'
- });
- var width = this._params.theme.event.bubble.width;
- if (!evt._obj.bubbleUrl) {
- evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
- }
- SimileAjax.WindowManager.cancelPopups();
- SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, width);
- if (evt._obj.bubbleUrl) {
- jQuery('#xxx').loadxhtml(evt._obj.bubbleUrl, null, 'post', 'replace');
- }
-};
-
--- a/web/data/cubicweb.widgets.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/data/cubicweb.widgets.js Tue Jul 19 16:13:12 2016 +0200
@@ -347,62 +347,6 @@
});
/**
- * .. class:: Widgets.SuggestForm
- *
- * suggestform displays a suggest field and associated validate / cancel buttons
- * constructor's argumemts are the same that BaseSuggestField widget
- */
-Widgets.SuggestForm = defclass("SuggestForm", null, {
-
- __init__: function(inputid, initfunc, varargs, validatefunc, options) {
- this.validatefunc = validatefunc || $.noop;
- this.sgfield = new Widgets.BaseSuggestField(inputid, initfunc, varargs, options);
- this.oklabel = options.oklabel || 'ok';
- this.cancellabel = options.cancellabel || 'cancel';
- bindMethods(this);
- connect(this.sgfield, 'validate', this, this.entryValidated);
- },
-
- show: function(parentnode) {
- var sgnode = this.sgfield.builddom();
- var buttons = DIV({
- 'class': "sgformbuttons"
- },
- [A({
- 'href': "javascript: $.noop();",
- 'onclick': this.onValidateClicked
- },
- this.oklabel), ' / ', A({
- 'href': "javascript: $.noop();",
- 'onclick': this.destroy
- },
- escapeHTML(this.cancellabel))]);
- var formnode = DIV({
- 'class': "sgform"
- },
- [sgnode, buttons]);
- appendChildNodes(parentnode, formnode);
- this.sgfield.textinput.focus();
- this.formnode = formnode;
- return formnode;
- },
-
- destroy: function() {
- signal(this, 'destroy');
- this.sgfield.destroy();
- removeElement(this.formnode);
- },
-
- onValidateClicked: function() {
- this.validatefunc(this, this.sgfield.taglist());
- },
- /* just an indirection to pass the form instead of the sgfield as first parameter */
- entryValidated: function(sgfield, taglist) {
- this.validatefunc(this, taglist);
- }
-});
-
-/**
* .. function:: toggleTree(event)
*
* called when the use clicks on a tree node
@@ -428,41 +372,6 @@
}
}
-/**
- * .. class:: Widgets.TimelineWidget
- *
- * widget based on SIMILE's timeline widget
- * http://code.google.com/p/simile-widgets/
- *
- * Beware not to mess with SIMILE's Timeline JS namepsace !
- */
-
-Widgets.TimelineWidget = defclass("TimelineWidget", null, {
- __init__: function(wdgnode) {
- var tldiv = DIV({
- id: "tl",
- style: 'height: 200px; border: 1px solid #ccc;'
- });
- wdgnode.appendChild(tldiv);
- var tlunit = wdgnode.getAttribute('cubicweb:tlunit') || 'YEAR';
- var eventSource = new Timeline.DefaultEventSource();
- var bandData = {
- eventPainter: Timeline.CubicWebEventPainter,
- eventSource: eventSource,
- width: "100%",
- intervalUnit: Timeline.DateTime[tlunit.toUpperCase()],
- intervalPixels: 100
- };
- var bandInfos = [Timeline.createBandInfo(bandData)];
- this.tl = Timeline.create(tldiv, bandInfos);
- var loadurl = wdgnode.getAttribute('cubicweb:loadurl');
- Timeline.loadJSON(loadurl, function(json, url) {
- eventSource.loadJSON(json, url);
- });
-
- }
-});
-
Widgets.TemplateTextField = defclass("TemplateTextField", null, {
__init__: function(wdgnode) {
--- a/web/data/timeline-bundle.css Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,401 +0,0 @@
-div.simileAjax-bubble-container {
- margin: 0px;
- padding: 0px;
- border: none;
- position: absolute;
- z-index: 1000;
-}
-
-div.simileAjax-bubble-innerContainer {
- margin: 0px;
- padding: 0px;
- border: none;
- position: relative;
- width: 100%;
- height: 100%;
- overflow: visible;
-}
-
-div.simileAjax-bubble-contentContainer {
- margin: 0px;
- padding: 0px;
- border: none;
- position: absolute;
- left: 0px;
- top: 0px;
- width: 100%;
- height: 100%;
- overflow: auto;
- background: white;
-}
-
-div.simileAjax-bubble-border-left {
- position: absolute;
- left: -50px;
- top: 0px;
- width: 50px;
- height: 100%;
-}
-div.simileAjax-bubble-border-left-pngTranslucent {
- background: url(../images/bubble-left.png) top right repeat-y;
-}
-
-div.simileAjax-bubble-border-right {
- position: absolute;
- right: -50px;
- top: 0px;
- width: 50px;
- height: 100%;
-}
-.simileAjax-bubble-border-right-pngTranslucent {
- background: url(../images/bubble-right.png) top left repeat-y;
-}
-
-div.simileAjax-bubble-border-top {
- position: absolute;
- top: -50px;
- left: 0px;
- width: 100%;
- height: 50px;
-}
-.simileAjax-bubble-border-top-pngTranslucent {
- background: url(../images/bubble-top.png) bottom left repeat-x;
-}
-
-div.simileAjax-bubble-border-bottom {
- position: absolute;
- bottom: -50px;
- left: 0px;
- width: 100%;
- height: 50px;
-}
-.simileAjax-bubble-border-bottom-pngTranslucent {
- background: url(../images/bubble-bottom.png) top left repeat-x;
-}
-
-div.simileAjax-bubble-border-top-left {
- position: absolute;
- top: -50px;
- left: -50px;
- width: 50px;
- height: 50px;
-}
-.simileAjax-bubble-border-top-left-pngTranslucent {
- background: url(../images/bubble-top-left.png) bottom right no-repeat;
-}
-
-div.simileAjax-bubble-border-top-right {
- position: absolute;
- top: -50px;
- right: -50px;
- width: 50px;
- height: 50px;
-}
-.simileAjax-bubble-border-top-right-pngTranslucent {
- background: url(../images/bubble-top-right.png) bottom left no-repeat;
-}
-
-div.simileAjax-bubble-border-bottom-left {
- position: absolute;
- bottom: -50px;
- left: -50px;
- width: 50px;
- height: 50px;
-}
-.simileAjax-bubble-border-bottom-left-pngTranslucent {
- background: url(../images/bubble-bottom-left.png) top right no-repeat;
-}
-
-div.simileAjax-bubble-border-bottom-right {
- position: absolute;
- bottom: -50px;
- right: -50px;
- width: 50px;
- height: 50px;
-}
-.simileAjax-bubble-border-bottom-right-pngTranslucent {
- background: url(../images/bubble-bottom-right.png) top left no-repeat;
-}
-
-div.simileAjax-bubble-arrow-point-left {
- position: absolute;
- left: -100px;
- width: 100px;
- height: 49px;
-}
-.simileAjax-bubble-arrow-point-left-pngTranslucent {
- background: url(../images/bubble-arrow-point-left.png) center right no-repeat;
-}
-
-div.simileAjax-bubble-arrow-point-right {
- position: absolute;
- right: -100px;
- width: 100px;
- height: 49px;
-}
-.simileAjax-bubble-arrow-point-right-pngTranslucent {
- background: url(../images/bubble-arrow-point-right.png) center left no-repeat;
-}
-
-div.simileAjax-bubble-arrow-point-up {
- position: absolute;
- top: -100px;
- width: 49px;
- height: 100px;
-}
-.simileAjax-bubble-arrow-point-up-pngTranslucent {
- background: url(../images/bubble-arrow-point-up.png) bottom center no-repeat;
-}
-
-div.simileAjax-bubble-arrow-point-down {
- position: absolute;
- bottom: -100px;
- width: 49px;
- height: 100px;
-}
-.simileAjax-bubble-arrow-point-down-pngTranslucent {
- background: url(../images/bubble-arrow-point-down.png) bottom center no-repeat;
-}
-
-
-div.simileAjax-bubble-close {
- position: absolute;
- right: -10px;
- top: -12px;
- width: 16px;
- height: 16px;
- cursor: pointer;
-}
-.simileAjax-bubble-close-pngTranslucent {
- background: url(../images/close-button.png) no-repeat;
-}
-.timeline-container {
- position: relative;
- overflow: hidden;
-}
-
-.timeline-copyright {
- position: absolute;
- bottom: 0px;
- left: 0px;
- z-index: 1000;
- cursor: pointer;
-}
-
-.timeline-message-container {
- position: absolute;
- top: 30%;
- left: 35%;
- right: 35%;
- z-index: 1000;
- display: none;
-}
-.timeline-message {
- font-size: 120%;
- font-weight: bold;
- text-align: center;
-}
-.timeline-message img {
- vertical-align: middle;
-}
-
-.timeline-band {
- position: absolute;
- background: #eee;
- z-index: 10;
-}
-
-.timeline-band-inner {
- position: relative;
- width: 100%;
- height: 100%;
-}
-
-.timeline-band-input {
- position: absolute;
- width: 1em;
- height: 1em;
- overflow: hidden;
- z-index: 0;
-}
-.timeline-band-input input{
- width: 0;
-}
-
-.timeline-band-layer {
- position: absolute;
- width: 100%;
- height: 100%;
-}
-
-.timeline-band-layer-inner {
- position: relative;
- width: 100%;
- height: 100%;
-}
-
-
-
-/*------------------- Horizontal / Vertical lines ----------------*/
-
-/* style for ethers */
-.timeline-ether-lines{border-color:#666; border-style:dotted; position:absolute;}
-.timeline-horizontal .timeline-ether-lines{border-width:0 0 0 1px; height:100%; top: 0; width: 1px;}
-.timeline-vertical .timeline-ether-lines{border-width:1px 0 0; height:1px; left: 0; width: 100%;}
-
-
-
-/*---------------- Weekends ---------------------------*/
-.timeline-ether-weekends{
- position:absolute;
- background-color:#FFFFE0;
-}
-
-.timeline-vertical .timeline-ether-weekends{left:0;width:100%;}
-.timeline-horizontal .timeline-ether-weekends{top:0; height:100%;}
-
-
-/*-------------------------- HIGHLIGHT DECORATORS -------------------*/
-/* Used for decorators, not used for Timeline Highlight */
-.timeline-highlight-decorator,
-.timeline-highlight-point-decorator{
- position:absolute;
- overflow:hidden;
-}
-
-/* Width of horizontal decorators and Height of vertical decorators is
- set in the decorator function params */
-.timeline-horizontal .timeline-highlight-point-decorator,
-.timeline-horizontal .timeline-highlight-decorator{
- top:0;
- height:100%;
-}
-
-.timeline-vertical .timeline-highlight-point-decorator,
-.timeline-vertical .timeline-highlight-decorator{
- width:100%;
- left:0;
-}
-
-.timeline-highlight-decorator{background-color:#FFC080;}
-.timeline-highlight-point-decorator{background-color:#ff5;}
-
-
-/*---------------------------- LABELS -------------------------*/
-.timeline-highlight-label {
- position:absolute; overflow:hidden; font-size:200%;
- font-weight:bold; color:#999; }
-
-
-/*---------------- VERTICAL LABEL -------------------*/
-.timeline-horizontal .timeline-highlight-label {top:0; height:100%;}
-.timeline-horizontal .timeline-highlight-label td {vertical-align:middle;}
-.timeline-horizontal .timeline-highlight-label-start {text-align:right;}
-.timeline-horizontal .timeline-highlight-label-end {text-align:left;}
-
-
-/*---------------- HORIZONTAL LABEL -------------------*/
-.timeline-vertical .timeline-highlight-label {left:0;width:100%;}
-.timeline-vertical .timeline-highlight-label td {vertical-align:top;}
-.timeline-vertical .timeline-highlight-label-start {text-align:center;}
-.timeline-vertical .timeline-highlight-label-end {text-align:center;}
-
-
-/*-------------------------------- DATE LABELS --------------------------------*/
-.timeline-date-label {
- position: absolute;
- border: solid #aaa;
- color: #aaa;
- width: 5em;
- height: 1.5em;}
-.timeline-date-label-em {color: #000;}
-
-/* horizontal */
-.timeline-horizontal .timeline-date-label{padding-left:2px;}
-.timeline-horizontal .timeline-date-label{border-width:0 0 0 1px;}
-.timeline-horizontal .timeline-date-label-em{height:2em}
-
-/* vertical */
-.timeline-vertical .timeline-date-label{padding-top:2px;}
-.timeline-vertical .timeline-date-label{border-width:1px 0 0;}
-.timeline-vertical .timeline-date-label-em{width:7em}
-
-
-/*------------------------------- Ether.highlight -------------------------*/
-.timeline-ether-highlight{position:absolute; background-color:#fff;}
-.timeline-horizontal .timeline-ether-highlight{top:2px;}
-.timeline-vertical .timeline-ether-highlight{left:2px;}
-
-
-/*------------------------------ EVENTS ------------------------------------*/
-.timeline-event-icon, .timeline-event-label,.timeline-event-tape{
- position:absolute;
- cursor:pointer;
-}
-
-.timeline-event-tape,
-.timeline-small-event-tape,
-.timeline-small-event-icon{
- background-color:#58A0DC;
- overflow:hidden;
-}
-
-.timeline-small-event-tape,
-.timeline-small-event-icon{
- position:absolute;
-}
-
-.timeline-small-event-icon{width:1px; height:6px;}
-
-
-/*--------------------------------- TIMELINE-------------------------*/
-.timeline-ether-bg{width:100%; height:100%;}
-.timeline-band-0 .timeline-ether-bg{background-color:#eee}
-.timeline-band-1 .timeline-ether-bg{background-color:#ddd}
-.timeline-band-2 .timeline-ether-bg{background-color:#ccc}
-.timeline-band-3 .timeline-ether-bg{background-color:#aaa}
-.timeline-duration-event {
- position: absolute;
- overflow: hidden;
- border: 1px solid blue;
-}
-
-.timeline-instant-event2 {
- position: absolute;
- overflow: hidden;
- border-left: 1px solid blue;
- padding-left: 2px;
-}
-
-.timeline-instant-event {
- position: absolute;
- overflow: hidden;
-}
-
-.timeline-event-bubble-title {
- font-weight: bold;
- border-bottom: 1px solid #888;
- margin-bottom: 0.5em;
-}
-
-.timeline-event-bubble-body {
-}
-
-.timeline-event-bubble-wiki {
- margin: 0.5em;
- text-align: right;
- color: #A0A040;
-}
-.timeline-event-bubble-wiki a {
- color: #A0A040;
-}
-
-.timeline-event-bubble-time {
- color: #aaa;
-}
-
-.timeline-event-bubble-image {
- float: right;
- padding-left: 5px;
- padding-bottom: 5px;
-}
\ No newline at end of file
Binary file web/data/timeline/blue-circle.png has changed
Binary file web/data/timeline/bubble-arrows.png has changed
Binary file web/data/timeline/bubble-body-and-arrows.png has changed
Binary file web/data/timeline/bubble-body.png has changed
Binary file web/data/timeline/bubble-bottom-arrow.png has changed
Binary file web/data/timeline/bubble-bottom-left.png has changed
Binary file web/data/timeline/bubble-bottom-right.png has changed
Binary file web/data/timeline/bubble-bottom.png has changed
Binary file web/data/timeline/bubble-left-arrow.png has changed
Binary file web/data/timeline/bubble-left.png has changed
Binary file web/data/timeline/bubble-right-arrow.png has changed
Binary file web/data/timeline/bubble-right.png has changed
Binary file web/data/timeline/bubble-top-arrow.png has changed
Binary file web/data/timeline/bubble-top-left.png has changed
Binary file web/data/timeline/bubble-top-right.png has changed
Binary file web/data/timeline/bubble-top.png has changed
Binary file web/data/timeline/close-button.png has changed
Binary file web/data/timeline/copyright-vertical.png has changed
Binary file web/data/timeline/copyright.png has changed
Binary file web/data/timeline/dark-blue-circle.png has changed
Binary file web/data/timeline/dark-green-circle.png has changed
Binary file web/data/timeline/dark-red-circle.png has changed
Binary file web/data/timeline/dull-blue-circle.png has changed
Binary file web/data/timeline/dull-green-circle.png has changed
Binary file web/data/timeline/dull-red-circle.png has changed
Binary file web/data/timeline/gray-circle.png has changed
Binary file web/data/timeline/green-circle.png has changed
Binary file web/data/timeline/message-bottom-left.png has changed
Binary file web/data/timeline/message-bottom-right.png has changed
Binary file web/data/timeline/message-left.png has changed
Binary file web/data/timeline/message-right.png has changed
Binary file web/data/timeline/message-top-left.png has changed
Binary file web/data/timeline/message-top-right.png has changed
Binary file web/data/timeline/message.png has changed
Binary file web/data/timeline/progress-running.gif has changed
Binary file web/data/timeline/red-circle.png has changed
Binary file web/data/timeline/sundial.png has changed
Binary file web/data/timeline/top-bubble.png has changed
--- a/web/facet.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/facet.py Tue Jul 19 16:13:12 2016 +0200
@@ -1210,7 +1210,7 @@
The image below display the rendering of the slider:
- .. image:: ../images/facet_range.png
+ .. image:: ../../images/facet_range.png
.. _jquery: http://www.jqueryui.com/
"""
@@ -1309,7 +1309,7 @@
The image below display the rendering of the slider for a date range:
- .. image:: ../images/facet_date_range.png
+ .. image:: ../../images/facet_date_range.png
"""
target_attr_type = 'Date' # only date types are supported
@@ -1440,7 +1440,7 @@
Here is an example of the rendering of thos facet to filter book with image
and the corresponding code:
- .. image:: ../images/facet_has_image.png
+ .. image:: ../../images/facet_has_image.png
.. sourcecode:: python
@@ -1465,15 +1465,17 @@
def add_rql_restrictions(self):
"""add restriction for this facet into the rql syntax tree"""
- self.select.set_distinct(True) # XXX
value = self._cw.form.get(self.__regid__)
if not value: # no value sent for this facet
return
+ exists = nodes.Exists()
+ self.select.add_restriction(exists)
var = self.select.make_variable()
if self.role == 'subject':
- self.select.add_relation(self.filtered_variable, self.rtype, var)
+ subj, obj = self.filtered_variable, var
else:
- self.select.add_relation(var, self.rtype, self.filtered_variable)
+ subj, obj = var, self.filtered_variable
+ exists.add_relation(subj, self.rtype, obj)
class BitFieldFacet(AttributeFacet):
--- a/web/formwidgets.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/formwidgets.py Tue Jul 19 16:13:12 2016 +0200
@@ -712,7 +712,7 @@
class JQueryTimePicker(JQueryDatePicker):
- """Use jquery.timePicker to define a time picker. Will return the time as an
+ """Use jquery.timePicker to define a time picker. Will return the time as a
unicode string.
"""
needs_js = ('jquery.timePicker.js',)
--- a/web/htmlwidgets.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/htmlwidgets.py Tue Jul 19 16:13:12 2016 +0200
@@ -141,7 +141,7 @@
class RawBoxItem(HTMLWidget): # XXX deprecated
- """a simpe box item displaying raw data"""
+ """a simple box item displaying raw data"""
def __init__(self, label, liclass=None):
self.label = label
--- a/web/httpcache.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/httpcache.py Tue Jul 19 16:13:12 2016 +0200
@@ -22,10 +22,6 @@
from time import mktime
from datetime import datetime
-# time delta usable to convert localized time to GMT time
-# XXX this become erroneous after a DST transition!!!
-GMTOFFSET = - (datetime.now() - datetime.utcnow())
-
class NoHTTPCacheManager(object):
"""default cache manager: set no-cache cache control policy"""
def __init__(self, view):
--- a/web/propertysheet.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/propertysheet.py Tue Jul 19 16:13:12 2016 +0200
@@ -63,15 +63,15 @@
if not isinstance(self[name], type):
msg = "Configuration error: %s.%s should be a %s" % (fpath, name, type)
raise Exception(msg)
- self._propfile_mtime[fpath] = os.stat(fpath)[-2]
+ self._propfile_mtime[fpath] = os.stat(fpath).st_mtime
self._ordered_propfiles.append(fpath)
def need_reload(self):
for rid, (adirectory, rdirectory, mtime) in self._cache.items():
- if os.stat(osp.join(rdirectory, rid))[-2] > mtime:
+ if os.stat(osp.join(rdirectory, rid)).st_mtime > mtime:
del self._cache[rid]
for fpath, mtime in self._propfile_mtime.iteritems():
- if os.stat(fpath)[-2] > mtime:
+ if os.stat(fpath).st_mtime > mtime:
return True
return False
@@ -109,7 +109,7 @@
stream.write(content)
stream.close()
adirectory = self._cache_directory
- self._cache[rid] = (adirectory, rdirectory, os.stat(sourcefile)[-2])
+ self._cache[rid] = (adirectory, rdirectory, os.stat(sourcefile).st_mtime)
return adirectory
def compile(self, content):
--- a/web/request.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/request.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -38,14 +38,14 @@
from logilab.common.deprecation import deprecated
from logilab.mtconverter import xml_escape
+from cubicweb import AuthenticationError
from cubicweb.req import RequestSessionBase
-from cubicweb.dbapi import DBAPIRequest
from cubicweb.uilib import remove_html_tags, js
from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
from cubicweb.view import TRANSITIONAL_DOCTYPE_NOEXT
from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
RequestError, StatusResponse)
-from cubicweb.web.httpcache import GMTOFFSET, get_validators
+from cubicweb.web.httpcache import get_validators
from cubicweb.web.http_headers import Headers, Cookie, parseDateTime
_MARKER = object()
@@ -155,9 +155,7 @@
#: shared among various components used to publish the request (views,
#: controller, application...)
self.data = {}
- #: search state: 'normal' or 'linksearch' (eg searching for an object
- #: to create a relation with another)
- self.search_state = ('normal',)
+ self._search_state = None
#: page id, set by htmlheader template
self.pageid = None
self._set_pageid()
@@ -354,21 +352,36 @@
self.session.data.pop(self._msgid, u'')
del self._msgid
+ def _load_search_state(self, searchstate):
+ if searchstate is None or searchstate == 'normal':
+ self._search_state = ('normal',)
+ else:
+ self._search_state = ('linksearch', searchstate.split(':'))
+ assert len(self._search_state[-1]) == 4, 'invalid searchstate'
+
+ @property
+ def search_state(self):
+ """search state: 'normal' or 'linksearch' (i.e. searching for an object
+ to create a relation with another)"""
+ if self._search_state is None:
+ searchstate = self.session.data.get('search_state', 'normal')
+ self._load_search_state(searchstate)
+ return self._search_state
+
+ @search_state.setter
+ def search_state(self, searchstate):
+ self._search_state = searchstate
+
def update_search_state(self):
- """update the current search state"""
+ """update the current search state if needed"""
searchstate = self.form.get('__mode')
- if not searchstate:
- searchstate = self.session.data.get('search_state', 'normal')
- self.set_search_state(searchstate)
+ if searchstate:
+ self.set_search_state(searchstate)
def set_search_state(self, searchstate):
"""set a new search state"""
- if searchstate is None or searchstate == 'normal':
- self.search_state = (searchstate or 'normal',)
- else:
- self.search_state = ('linksearch', searchstate.split(':'))
- assert len(self.search_state[-1]) == 4
self.session.data['search_state'] = searchstate
+ self._load_search_state(searchstate)
def match_search_state(self, rset):
"""when searching an entity to create a relation, return True if entities in
@@ -385,7 +398,7 @@
def update_breadcrumbs(self):
"""stores the last visisted page in session data"""
- searchstate = self.session.data.get('search_state')
+ searchstate = self.search_state[0]
if searchstate == 'normal':
breadcrumbs = self.session.data.get('breadcrumbs')
if breadcrumbs is None:
@@ -403,67 +416,6 @@
return breadcrumbs.pop()
return self.base_url()
- @deprecated('[3.19] use a traditional ajaxfunc / controller')
- def user_rql_callback(self, rqlargs, *args, **kwargs):
- """register a user callback to execute some rql query, and return a URL
- to call that callback which can be inserted in an HTML view.
-
- `rqlargs` should be a tuple containing argument to give to the execute function.
-
- The first argument following rqlargs must be the message to be
- displayed after the callback is called.
-
- For other allowed arguments, see :meth:`user_callback` method
- """
- def rqlexec(req, rql, args=None, key=None):
- req.execute(rql, args, key)
- return self.user_callback(rqlexec, rqlargs, *args, **kwargs)
-
- @deprecated('[3.19] use a traditional ajaxfunc / controller')
- def user_callback(self, cb, cbargs, *args, **kwargs):
- """register the given user callback and return a URL which can
- be inserted in an HTML view. When the URL is accessed, the
- callback function will be called (as 'cb(req, \*cbargs)', and a
- message will be displayed in the web interface. The third
- positional argument must be 'msg', containing the message.
-
- You can specify the underlying js function to call using a 'jsfunc'
- named args, to one of :func:`userCallback`,
- ':func:`userCallbackThenUpdateUI`, ':func:`userCallbackThenReloadPage`
- (the default). Take care arguments may vary according to the used
- function.
- """
- self.add_js('cubicweb.ajax.js')
- jsfunc = kwargs.pop('jsfunc', 'userCallbackThenReloadPage')
- assert not kwargs, 'dunno what to do with remaining kwargs: %s' % kwargs
- cbname = self.register_onetime_callback(cb, *cbargs)
- return "javascript: %s" % getattr(js, jsfunc)(cbname, *args)
-
- @deprecated('[3.19] use a traditional ajaxfunc / controller')
- def register_onetime_callback(self, func, *args):
- cbname = build_cb_uid(func.__name__)
- def _cb(req):
- try:
- return func(req, *args)
- finally:
- self.unregister_callback(self.pageid, cbname)
- self.set_page_data(cbname, _cb)
- return cbname
-
- @deprecated('[3.19] use a traditional ajaxfunc / controller')
- def unregister_callback(self, pageid, cbname):
- assert pageid is not None
- assert cbname.startswith('cb_')
- self.info('unregistering callback %s for pageid %s', cbname, pageid)
- self.del_page_data(cbname)
-
- @deprecated('[3.19] use a traditional ajaxfunc / controller')
- def clear_user_callbacks(self):
- if self.session is not None: # XXX
- for key in list(self.session.data):
- if key.startswith('cb_'):
- del self.session.data[key]
-
# web edition helpers #####################################################
@cached # so it's writed only once
@@ -584,8 +536,8 @@
# we don't want to handle times before the EPOCH (cause bug on
# windows). Also use > and not >= else expires == 0 and Cookie think
# that means no expire...
- assert expires + GMTOFFSET > date(1970, 1, 1)
- expires = timegm((expires + GMTOFFSET).timetuple())
+ assert expires > date(1970, 1, 1)
+ expires = timegm(expires.timetuple())
else:
expires = None
# make sure cookie is set on the correct path
@@ -859,8 +811,7 @@
"""
mtime = self.get_header('If-modified-since', raw=False)
if mtime:
- # :/ twisted is returned a localized time stamp
- return datetime.fromtimestamp(mtime) + GMTOFFSET
+ return datetime.utcfromtimestamp(mtime)
return None
### outcoming headers
@@ -1005,29 +956,36 @@
self.set_default_language(vreg)
-class DBAPICubicWebRequestBase(_CubicWebRequestBase, DBAPIRequest):
-
- def set_session(self, session):
- """method called by the session handler when the user is authenticated
- or an anonymous connection is open
- """
- super(CubicWebRequestBase, self).set_session(session)
- # set request language
- self.set_user_language(session.user)
-
-
def _cnx_func(name):
def proxy(req, *args, **kwargs):
return getattr(req.cnx, name)(*args, **kwargs)
return proxy
+class _NeedAuthAccessMock(object):
+
+ def __getattribute__(self, attr):
+ raise AuthenticationError()
+
+ def __nonzero__(self):
+ return False
+
+class _MockAnonymousSession(object):
+ sessionid = 'thisisnotarealsession'
+
+ @property
+ def data(self):
+ return {}
+
+ @property
+ def anonymous_session(self):
+ return True
class ConnectionCubicWebRequestBase(_CubicWebRequestBase):
+ cnx = None
+ session = None
def __init__(self, vreg, https=False, form=None, headers={}):
""""""
- self.cnx = None
- self.session = None
self.vreg = vreg
try:
# no vreg or config which doesn't handle translations
@@ -1036,8 +994,7 @@
self.translations = {}
super(ConnectionCubicWebRequestBase, self).__init__(vreg, https=https,
form=form, headers=headers)
- from cubicweb.dbapi import DBAPISession, _NeedAuthAccessMock
- self.session = DBAPISession(None)
+ self.session = _MockAnonymousSession()
self.cnx = self.user = _NeedAuthAccessMock()
@property
@@ -1045,8 +1002,10 @@
return self.cnx.transaction_data
def set_cnx(self, cnx):
+ if 'ecache' in cnx.transaction_data:
+ del cnx.transaction_data['ecache']
self.cnx = cnx
- self.session = cnx._session
+ self.session = cnx.session
self._set_user(cnx.user)
self.set_user_language(cnx.user)
@@ -1056,7 +1015,6 @@
return rset
def set_default_language(self, vreg):
- # XXX copy from dbapi
try:
lang = vreg.property_value('ui.language')
except Exception: # property may not be registered
--- a/web/test/data/bootstrap_cubes Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/data/bootstrap_cubes Tue Jul 19 16:13:12 2016 +0200
@@ -1,1 +1,1 @@
-file, blog, tag, folder
+file, blog, tag
--- a/web/test/data/schema.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/data/schema.py Tue Jul 19 16:13:12 2016 +0200
@@ -120,9 +120,9 @@
object = 'Directory'
composite = 'object'
-# used by windmill for `test_edit_relation`
-from cubes.folder.schema import Folder
-
+class Folder(EntityType):
+ name = String(required=True)
+ filed_under = SubjectRelation('Folder', description=_('parent folder'))
class TreeNode(EntityType):
name = String(required=True)
--- a/web/test/data/views.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/data/views.py Tue Jul 19 16:13:12 2016 +0200
@@ -16,7 +16,9 @@
# 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 cubicweb.predicates import has_related_entities
from cubicweb.web.views.ajaxcontroller import ajaxfunc
+from cubicweb.web.views.ibreadcrumbs import IBreadCrumbsAdapter
def _recursive_replace_stream_by_content(tree):
""" Search for streams (i.e. object that have a 'read' method) in a tree
@@ -46,3 +48,10 @@
except Exception, ex:
import traceback as tb
tb.print_exc(ex)
+
+
+class FolderIBreadCrumbsAdapter(IBreadCrumbsAdapter):
+ __select__ = IBreadCrumbsAdapter.__select__ & has_related_entities('filed_under')
+
+ def parent_entity(self):
+ return self.entity.filed_under[0]
--- a/web/test/jstests/ajax_url1.html Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/jstests/ajax_url1.html Tue Jul 19 16:13:12 2016 +0200
@@ -1,6 +1,6 @@
<div id="ajaxroot">
<div class="ajaxHtmlHead">
- <script src="http://foo.js" type="text/javascript"> </script>
+ <cubicweb:script src="http://foo.js" type="text/javascript"> </cubicweb:script>
</div>
<h1>Hello</h1>
</div>
--- a/web/test/jstests/ajaxresult.json Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/jstests/ajaxresult.json Tue Jul 19 16:13:12 2016 +0200
@@ -1,1 +1,1 @@
-['foo', 'bar']
+["foo", "bar"]
--- a/web/test/jstests/test_ajax.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/jstests/test_ajax.js Tue Jul 19 16:13:12 2016 +0200
@@ -1,17 +1,17 @@
$(document).ready(function() {
- module("ajax", {
+ QUnit.module("ajax", {
setup: function() {
this.scriptsLength = $('head script[src]').length-1;
this.cssLength = $('head link[rel=stylesheet]').length-1;
// re-initialize cw loaded cache so that each tests run in a
// clean environment, have a lookt at _loadAjaxHtmlHead implementation
// in cubicweb.ajax.js for more information.
- cw.loaded_src = [];
- cw.loaded_href = [];
+ cw.loaded_scripts = [];
+ cw.loaded_links = [];
},
teardown: function() {
- $('head script[src]:gt(' + this.scriptsLength + ')').remove();
+ $('head script[src]:lt(' + ($('head script[src]').length - 1 - this.scriptsLength) + ')').remove();
$('head link[rel=stylesheet]:gt(' + this.cssLength + ')').remove();
}
});
@@ -22,66 +22,66 @@
});
}
- test('test simple h1 inclusion (ajax_url0.html)', function() {
- expect(3);
- equals(jQuery('#main').children().length, 0);
- stop();
- jQuery('#main').loadxhtml('/../ajax_url0.html', {
- callback: function() {
+ QUnit.test('test simple h1 inclusion (ajax_url0.html)', function (assert) {
+ assert.expect(3);
+ assert.equal($('#qunit-fixture').children().length, 0);
+ var done = assert.async();
+ $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
+ .addCallback(function() {
try {
- equals(jQuery('#main').children().length, 1);
- equals(jQuery('#main h1').html(), 'Hello');
+ assert.equal($('#qunit-fixture').children().length, 1);
+ assert.equal($('#qunit-fixture h1').html(), 'Hello');
} finally {
- start();
+ done();
};
}
- });
+ );
});
- test('test simple html head inclusion (ajax_url1.html)', function() {
- expect(6);
+ QUnit.test('test simple html head inclusion (ajax_url1.html)', function (assert) {
+ assert.expect(6);
var scriptsIncluded = jsSources();
- equals(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
- stop();
- jQuery('#main').loadxhtml('/../ajax_url1.html', {
- callback: function() {
+ assert.equal(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+ var done = assert.async();
+ $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url1.html')
+ .addCallback(function() {
try {
var origLength = scriptsIncluded.length;
scriptsIncluded = jsSources();
- // check that foo.js has been *appended* to <head>
- equals(scriptsIncluded.length, origLength + 1);
- equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+ // check that foo.js has been prepended to <head>
+ assert.equal(scriptsIncluded.length, origLength + 1);
+ assert.equal(scriptsIncluded.indexOf('http://foo.js'), 0);
// check that <div class="ajaxHtmlHead"> has been removed
- equals(jQuery('#main').children().length, 1);
- equals(jQuery('div.ajaxHtmlHead').length, 0);
- equals(jQuery('#main h1').html(), 'Hello');
+ assert.equal($('#qunit-fixture').children().length, 1);
+ assert.equal($('div.ajaxHtmlHead').length, 0);
+ assert.equal($('#qunit-fixture h1').html(), 'Hello');
} finally {
- start();
+ done();
};
}
- });
+ );
});
- test('test addCallback', function() {
- expect(3);
- equals(jQuery('#main').children().length, 0);
- stop();
- var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+ QUnit.test('test addCallback', function (assert) {
+ assert.expect(3);
+ assert.equal($('#qunit-fixture').children().length, 0);
+ var done = assert.async();
+ var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
d.addCallback(function() {
try {
- equals(jQuery('#main').children().length, 1);
- equals(jQuery('#main h1').html(), 'Hello');
+ assert.equal($('#qunit-fixture').children().length, 1);
+ assert.equal($('#qunit-fixture h1').html(), 'Hello');
} finally {
- start();
+ done();
};
});
});
- test('test callback after synchronous request', function() {
- expect(1);
+ QUnit.test('test callback after synchronous request', function (assert) {
+ assert.expect(1);
var deferred = new Deferred();
var result = jQuery.ajax({
- url: './ajax_url0.html',
+ url: BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html',
async: false,
beforeSend: function(xhr) {
deferred._req = xhr;
@@ -90,44 +90,44 @@
deferred.success(data);
}
});
- stop();
+ var done = assert.async();
deferred.addCallback(function() {
try {
// add an assertion to ensure the callback is executed
- ok(true, "callback is executed");
+ assert.ok(true, "callback is executed");
} finally {
- start();
+ done();
};
});
});
- test('test addCallback with parameters', function() {
- expect(3);
- equals(jQuery('#main').children().length, 0);
- stop();
- var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+ QUnit.test('test addCallback with parameters', function (assert) {
+ assert.expect(3);
+ assert.equal($('#qunit-fixture').children().length, 0);
+ var done = assert.async();
+ var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
d.addCallback(function(data, req, arg1, arg2) {
try {
- equals(arg1, 'Hello');
- equals(arg2, 'world');
+ assert.equal(arg1, 'Hello');
+ assert.equal(arg2, 'world');
} finally {
- start();
+ done();
};
},
'Hello', 'world');
});
- test('test callback after synchronous request with parameters', function() {
- expect(2);
+ QUnit.test('test callback after synchronous request with parameters', function (assert) {
+ assert.expect(3);
var deferred = new Deferred();
deferred.addCallback(function(data, req, arg1, arg2) {
// add an assertion to ensure the callback is executed
try {
- ok(true, "callback is executed");
- equals(arg1, 'Hello');
- equals(arg2, 'world');
+ assert.ok(true, "callback is executed");
+ assert.equal(arg1, 'Hello');
+ assert.equal(arg2, 'world');
} finally {
- start();
+ done();
};
},
'Hello', 'world');
@@ -136,12 +136,12 @@
try {
throw this._error;
} finally {
- start();
+ done();
};
});
- stop();
+ var done = assert.async();
var result = jQuery.ajax({
- url: '/../ajax_url0.html',
+ url: BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html',
async: false,
beforeSend: function(xhr) {
deferred._req = xhr;
@@ -152,138 +152,123 @@
});
});
- test('test addErrback', function() {
- expect(1);
- stop();
- var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+ QUnit.test('test addErrback', function (assert) {
+ assert.expect(1);
+ var done = assert.async();
+ var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/nonexistent.html');
d.addCallback(function() {
- // throw an exception to start errback chain
- try {
- throw new Error();
- } finally {
- start();
- };
+ // should not be executed
+ assert.ok(false, "callback is executed");
});
d.addErrback(function() {
try {
- ok(true, "errback is executed");
+ assert.ok(true, "errback is executed");
} finally {
- start();
+ done();
};
});
});
- test('test callback / errback execution order', function() {
- expect(4);
+ QUnit.test('test callback execution order', function (assert) {
+ assert.expect(3);
var counter = 0;
- stop();
- var d = jQuery('#main').loadxhtml('/../ajax_url0.html', {
- callback: function() {
- try {
- equals(++counter, 1); // should be executed first
- } finally {
- start();
- };
- }
+ var done = assert.async();
+ var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
+ d.addCallback(function() {
+ assert.equal(++counter, 1); // should be executed first
});
d.addCallback(function() {
- equals(++counter, 2); // should be executed and break callback chain
- throw new Error();
+ assert.equal(++counter, 2);
});
d.addCallback(function() {
- // should not be executed since second callback raised an error
- ok(false, "callback is executed");
- });
- d.addErrback(function() {
- // should be executed after the second callback
- equals(++counter, 3);
- });
- d.addErrback(function() {
- // should be executed after the first errback
- equals(++counter, 4);
+ try {
+ assert.equal(++counter, 3);
+ } finally {
+ done();
+ }
});
});
- test('test already included resources are ignored (ajax_url1.html)', function() {
- expect(10);
+ QUnit.test('test already included resources are ignored (ajax_url1.html)', function (assert) {
+ assert.expect(10);
var scriptsIncluded = jsSources();
// NOTE:
- equals(jQuery.inArray('http://foo.js', scriptsIncluded), -1);
- equals(jQuery('head link').length, 1);
+ assert.equal(jQuery.inArray('http://foo.js', scriptsIncluded), -1);
+ assert.equal($('head link').length, 1);
/* use endswith because in pytest context we have an absolute path */
- ok(jQuery('head link').attr('href').endswith('/qunit.css'));
- stop();
- jQuery('#main').loadxhtml('/../ajax_url1.html', {
- callback: function() {
+ assert.ok($('head link').attr('href').endswith('/qunit.css'), 'qunit.css is loaded');
+ var done = assert.async();
+ $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url1.html')
+ .addCallback(function() {
var origLength = scriptsIncluded.length;
scriptsIncluded = jsSources();
try {
// check that foo.js has been inserted in <head>
- equals(scriptsIncluded.length, origLength + 1);
- equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+ assert.equal(scriptsIncluded.length, origLength + 1);
+ assert.equal(scriptsIncluded.indexOf('http://foo.js'), 0);
// check that <div class="ajaxHtmlHead"> has been removed
- equals(jQuery('#main').children().length, 1);
- equals(jQuery('div.ajaxHtmlHead').length, 0);
- equals(jQuery('#main h1').html(), 'Hello');
+ assert.equal($('#qunit-fixture').children().length, 1);
+ assert.equal($('div.ajaxHtmlHead').length, 0);
+ assert.equal($('#qunit-fixture h1').html(), 'Hello');
// qunit.css is not added twice
- equals(jQuery('head link').length, 1);
+ assert.equal($('head link').length, 1);
/* use endswith because in pytest context we have an absolute path */
- ok(jQuery('head link').attr('href').endswith('/qunit.css'));
+ assert.ok($('head link').attr('href').endswith('/qunit.css'), 'qunit.css is loaded');
} finally {
- start();
+ done();
}
}
- });
+ );
});
- test('test synchronous request loadRemote', function() {
- var res = loadRemote('/../ajaxresult.json', {},
+ QUnit.test('test synchronous request loadRemote', function (assert) {
+ var res = loadRemote(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajaxresult.json', {},
'GET', true);
- same(res, ['foo', 'bar']);
+ assert.deepEqual(res, ['foo', 'bar']);
});
- test('test event on CubicWeb', function() {
- expect(1);
- stop();
+ QUnit.test('test event on CubicWeb', function (assert) {
+ assert.expect(1);
+ var done = assert.async();
var events = null;
- jQuery(CubicWeb).bind('server-response', function() {
+ $(CubicWeb).bind('server-response', function() {
// check that server-response event on CubicWeb is triggered
events = 'CubicWeb';
});
- jQuery('#main').loadxhtml('/../ajax_url0.html', {
- callback: function() {
+ $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
+ .addCallback(function() {
try {
- equals(events, 'CubicWeb');
+ assert.equal(events, 'CubicWeb');
} finally {
- start();
+ done();
};
}
- });
+ );
});
- test('test event on node', function() {
- expect(3);
- stop();
+ QUnit.test('test event on node', function (assert) {
+ assert.expect(3);
+ var done = assert.async();
var nodes = [];
- jQuery('#main').bind('server-response', function() {
+ $('#qunit-fixture').bind('server-response', function() {
nodes.push('node');
});
- jQuery(CubicWeb).bind('server-response', function() {
+ $(CubicWeb).bind('server-response', function() {
nodes.push('CubicWeb');
});
- jQuery('#main').loadxhtml('/../ajax_url0.html', {
- callback: function() {
+ $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
+ .addCallback(function() {
try {
- equals(nodes.length, 2);
+ assert.equal(nodes.length, 2);
// check that server-response event on CubicWeb is triggered
// only once and event server-response on node is triggered
- equals(nodes[0], 'CubicWeb');
- equals(nodes[1], 'node');
+ assert.equal(nodes[0], 'CubicWeb');
+ assert.equal(nodes[1], 'node');
} finally {
- start();
+ done();
};
}
- });
+ );
});
});
--- a/web/test/jstests/test_htmlhelpers.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/jstests/test_htmlhelpers.js Tue Jul 19 16:13:12 2016 +0200
@@ -1,35 +1,35 @@
$(document).ready(function() {
- module("module2", {
+ QUnit.module("module2", {
setup: function() {
- $('#main').append('<select id="theselect" multiple="multiple" size="2">' +
+ $('#qunit-fixture').append('<select id="theselect" multiple="multiple" size="2">' +
'</select>');
}
});
- test("test first selected", function() {
+ QUnit.test("test first selected", function (assert) {
$('#theselect').append('<option value="foo">foo</option>' +
'<option selected="selected" value="bar">bar</option>' +
'<option value="baz">baz</option>' +
'<option selected="selecetd"value="spam">spam</option>');
var selected = firstSelected(document.getElementById("theselect"));
- equals(selected.value, 'bar');
+ assert.equal(selected.value, 'bar');
});
- test("test first selected 2", function() {
+ QUnit.test("test first selected 2", function (assert) {
$('#theselect').append('<option value="foo">foo</option>' +
'<option value="bar">bar</option>' +
'<option value="baz">baz</option>' +
'<option value="spam">spam</option>');
var selected = firstSelected(document.getElementById("theselect"));
- equals(selected, null);
+ assert.equal(selected, null);
});
- module("visibilty");
- test('toggleVisibility', function() {
- $('#main').append('<div id="foo"></div>');
+ QUnit.module("visibilty");
+ QUnit.test('toggleVisibility', function (assert) {
+ $('#qunit-fixture').append('<div id="foo"></div>');
toggleVisibility('foo');
- ok($('#foo').hasClass('hidden'), 'check hidden class is set');
+ assert.ok($('#foo').hasClass('hidden'), 'check hidden class is set');
});
});
--- a/web/test/jstests/test_utils.js Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/jstests/test_utils.js Tue Jul 19 16:13:12 2016 +0200
@@ -1,66 +1,66 @@
$(document).ready(function() {
- module("datetime");
+ QUnit.module("datetime");
- test("test full datetime", function() {
- equals(cw.utils.toISOTimestamp(new Date(1986, 3, 18, 10, 30, 0, 0)),
+ QUnit.test("test full datetime", function (assert) {
+ assert.equal(cw.utils.toISOTimestamp(new Date(1986, 3, 18, 10, 30, 0, 0)),
'1986-04-18 10:30:00');
});
- test("test only date", function() {
- equals(cw.utils.toISOTimestamp(new Date(1986, 3, 18)), '1986-04-18 00:00:00');
+ QUnit.test("test only date", function (assert) {
+ assert.equal(cw.utils.toISOTimestamp(new Date(1986, 3, 18)), '1986-04-18 00:00:00');
});
- test("test null", function() {
- equals(cw.utils.toISOTimestamp(null), null);
+ QUnit.test("test null", function (assert) {
+ assert.equal(cw.utils.toISOTimestamp(null), null);
});
- module("parsing");
- test("test basic number parsing", function() {
+ QUnit.module("parsing");
+ QUnit.test("test basic number parsing", function (assert) {
var d = strptime('2008/08/08', '%Y/%m/%d');
- same(datetuple(d), [2008, 8, 8, 0, 0]);
+ assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
d = strptime('2008/8/8', '%Y/%m/%d');
- same(datetuple(d), [2008, 8, 8, 0, 0]);
+ assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
d = strptime('8/8/8', '%Y/%m/%d');
- same(datetuple(d), [8, 8, 8, 0, 0]);
+ assert.deepEqual(datetuple(d), [8, 8, 8, 0, 0]);
d = strptime('0/8/8', '%Y/%m/%d');
- same(datetuple(d), [0, 8, 8, 0, 0]);
+ assert.deepEqual(datetuple(d), [0, 8, 8, 0, 0]);
d = strptime('-10/8/8', '%Y/%m/%d');
- same(datetuple(d), [-10, 8, 8, 0, 0]);
+ assert.deepEqual(datetuple(d), [-10, 8, 8, 0, 0]);
d = strptime('-35000', '%Y');
- same(datetuple(d), [-35000, 1, 1, 0, 0]);
+ assert.deepEqual(datetuple(d), [-35000, 1, 1, 0, 0]);
});
- test("test custom format parsing", function() {
+ QUnit.test("test custom format parsing", function (assert) {
var d = strptime('2008-08-08', '%Y-%m-%d');
- same(datetuple(d), [2008, 8, 8, 0, 0]);
+ assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
d = strptime('2008 - ! 08: 08', '%Y - ! %m: %d');
- same(datetuple(d), [2008, 8, 8, 0, 0]);
+ assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
d = strptime('2008-08-08 12:14', '%Y-%m-%d %H:%M');
- same(datetuple(d), [2008, 8, 8, 12, 14]);
+ assert.deepEqual(datetuple(d), [2008, 8, 8, 12, 14]);
d = strptime('2008-08-08 1:14', '%Y-%m-%d %H:%M');
- same(datetuple(d), [2008, 8, 8, 1, 14]);
+ assert.deepEqual(datetuple(d), [2008, 8, 8, 1, 14]);
d = strptime('2008-08-08 01:14', '%Y-%m-%d %H:%M');
- same(datetuple(d), [2008, 8, 8, 1, 14]);
+ assert.deepEqual(datetuple(d), [2008, 8, 8, 1, 14]);
});
- module("sliceList");
- test("test slicelist", function() {
+ QUnit.module("sliceList");
+ QUnit.test("test slicelist", function (assert) {
var list = ['a', 'b', 'c', 'd', 'e', 'f'];
- same(cw.utils.sliceList(list, 2), ['c', 'd', 'e', 'f']);
- same(cw.utils.sliceList(list, 2, -2), ['c', 'd']);
- same(cw.utils.sliceList(list, -3), ['d', 'e', 'f']);
- same(cw.utils.sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
- same(cw.utils.sliceList(list), list);
+ assert.deepEqual(cw.utils.sliceList(list, 2), ['c', 'd', 'e', 'f']);
+ assert.deepEqual(cw.utils.sliceList(list, 2, -2), ['c', 'd']);
+ assert.deepEqual(cw.utils.sliceList(list, -3), ['d', 'e', 'f']);
+ assert.deepEqual(cw.utils.sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
+ assert.deepEqual(cw.utils.sliceList(list), list);
});
- module("formContents", {
+ QUnit.module("formContents", {
setup: function() {
- $('#main').append('<form id="test-form"></form>');
+ $('#qunit-fixture').append('<form id="test-form"></form>');
}
});
// XXX test fckeditor
- test("test formContents", function() {
+ QUnit.test("test formContents", function (assert) {
$('#test-form').append('<input name="input-text" ' +
'type="text" value="toto" />');
$('#test-form').append('<textarea rows="10" cols="30" '+
@@ -83,7 +83,7 @@
'value="one" />');
$('#test-form').append('<input name="unchecked-choice" type="radio" ' +
'value="two"/>');
- same(cw.utils.formContents($('#test-form')[0]), [
+ assert.deepEqual(cw.utils.formContents($('#test-form')[0]), [
['input-text', 'mytextarea', 'choice', 'check', 'theselect'],
['toto', 'Hello World!', 'no', 'no', 'foo']
]);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,6 @@
+requests
+webtest
+Twisted
+cubicweb-blog
+cubicweb-file
+cubicweb-tag
--- a/web/test/unittest_application.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_application.py Tue Jul 19 16:13:12 2016 +0200
@@ -29,7 +29,6 @@
from cubicweb.web import LogOut, Redirect, INTERNAL_FIELD_VALUE
from cubicweb.web.views.basecontrollers import ViewController
from cubicweb.web.application import anonymized_request
-from cubicweb.dbapi import DBAPISession, _NeedAuthAccessMock
from cubicweb import repoapi
class FakeMapping:
@@ -620,15 +619,11 @@
req.set_request_header('Cookie', cookie[sessioncookie].OutputString(),
raw=True)
clear_cache(req, 'get_authorization')
- # reset session as if it was a new incoming request
- req.session = DBAPISession(None)
- req.user = req.cnx = _NeedAuthAccessMock
-
def _test_auth_anon(self, req):
asession = self.app.get_session(req)
# important otherwise _reset_cookie will not use the right session
- req.set_cnx(repoapi.ClientConnection(asession))
+ req.set_cnx(repoapi.Connection(asession))
self.assertEqual(len(self.open_sessions), 1)
self.assertEqual(asession.login, 'anon')
self.assertTrue(asession.anonymous_session)
@@ -638,7 +633,7 @@
self.assertEqual(1, len(self.open_sessions))
session = self.app.get_session(req)
# important otherwise _reset_cookie will not use the right session
- req.set_cnx(repoapi.ClientConnection(session))
+ req.set_cnx(repoapi.Connection(session))
self.assertEqual(req.message, 'authentication failure')
self.assertEqual(req.session.anonymous_session, True)
self.assertEqual(1, len(self.open_sessions))
--- a/web/test/unittest_facet.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_facet.py Tue Jul 19 16:13:12 2016 +0200
@@ -129,8 +129,6 @@
self.assertEqual(f.select.as_string(),
'DISTINCT Any WHERE X is CWUser')
-
-
def test_relationattribute(self):
with self.admin_access.web_request() as req:
f, (guests, managers) = self._in_group_facet(req, cls=facet.RelationAttributeFacet)
@@ -150,6 +148,20 @@
self.assertEqual(f.select.as_string(),
"DISTINCT Any WHERE X is CWUser, X in_group E, E name 'guests'")
+ def test_hasrelation(self):
+ with self.admin_access.web_request() as req:
+ rset, rqlst, filtered_variable = self.prepare_rqlst(req)
+ f = facet.HasRelationFacet(req, rset=rset,
+ select=rqlst.children[0],
+ filtered_variable=filtered_variable)
+ f.__regid__ = 'has_group'
+ f.rtype = 'in_group'
+ f.role = 'subject'
+ f._cw.form[f.__regid__] = 'feed me'
+ f.add_rql_restrictions()
+ self.assertEqual(f.select.as_string(),
+ 'DISTINCT Any WHERE X is CWUser, EXISTS(X in_group A)')
+
def test_daterange(self):
with self.admin_access.web_request() as req:
rset, rqlst, filtered_variable = self.prepare_rqlst(req)
--- a/web/test/unittest_form.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_form.py Tue Jul 19 16:13:12 2016 +0200
@@ -21,7 +21,7 @@
from xml.etree.ElementTree import fromstring
from lxml import html
-from logilab.common.testlib import unittest_main, mock_object
+from logilab.common.testlib import unittest_main
from cubicweb import Binary, ValidationError
from cubicweb.devtools.testlib import CubicWebTC
@@ -39,7 +39,7 @@
def test_form_field_format(self):
with self.admin_access.web_request() as req:
form = FieldsForm(req, None)
- self.assertEqual(StringField().format(form), 'text/html')
+ self.assertEqual(StringField().format(form), 'text/plain')
req.cnx.execute('INSERT CWProperty X: X pkey "ui.default-text-format", X value "text/rest", X for_user U WHERE U login "admin"')
req.cnx.commit()
self.assertEqual(StringField().format(form), 'text/rest')
--- a/web/test/unittest_formfields.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_formfields.py Tue Jul 19 16:13:12 2016 +0200
@@ -150,7 +150,7 @@
self.assertEqual(description_format_field.internationalizable, True)
self.assertEqual(description_format_field.sort, True)
# unlike below, initial is bound to form.form_field_format
- self.assertEqual(description_format_field.value(form), 'text/html')
+ self.assertEqual(description_format_field.value(form), 'text/plain')
req.cnx.execute('INSERT CWProperty X: X pkey "ui.default-text-format", X value "text/rest", X for_user U WHERE U login "admin"')
req.cnx.commit()
self.assertEqual(description_format_field.value(form), 'text/rest')
--- a/web/test/unittest_propertysheet.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_propertysheet.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,23 +1,37 @@
import os
from os.path import join, dirname
from shutil import rmtree
-
-from logilab.common.testlib import TestCase, unittest_main
+import errno
+import tempfile
+from unittest import TestCase, main
from cubicweb.web.propertysheet import PropertySheet, lazystr
+
DATADIR = join(dirname(__file__), 'data')
-CACHEDIR = join(DATADIR, 'uicache')
+
class PropertySheetTC(TestCase):
+ def setUp(self):
+ uicache = join(DATADIR, 'uicache')
+ try:
+ os.makedirs(uicache)
+ except OSError as err:
+ if err.errno != errno.EEXIST:
+ raise
+ self.cachedir = tempfile.mkdtemp(dir=uicache)
+
def tearDown(self):
- rmtree(CACHEDIR)
+ rmtree(self.cachedir)
+
+ def data(self, filename):
+ return join(DATADIR, filename)
def test(self):
- ps = PropertySheet(CACHEDIR, datadir_url='http://cwtest.com')
- ps.load(join(DATADIR, 'sheet1.py'))
- ps.load(join(DATADIR, 'sheet2.py'))
+ ps = PropertySheet(self.cachedir, datadir_url='http://cwtest.com')
+ ps.load(self.data('sheet1.py'))
+ ps.load(self.data('sheet2.py'))
# defined by sheet1
self.assertEqual(ps['logo'], 'http://cwtest.com/logo.png')
# defined by sheet1, overriden by sheet2
@@ -34,10 +48,10 @@
self.assertEqual(ps.compile('a {bgcolor: %(bgcolor)s; size: 1%;}'),
'a {bgcolor: #FFFFFF; size: 1%;}')
self.assertEqual(ps.process_resource(DATADIR, 'pouet.css'),
- CACHEDIR)
+ self.cachedir)
self.assertIn('pouet.css', ps._cache)
self.assertFalse(ps.need_reload())
- os.utime(join(DATADIR, 'sheet1.py'), None)
+ os.utime(self.data('sheet1.py'), None)
self.assertIn('pouet.css', ps._cache)
self.assertTrue(ps.need_reload())
self.assertIn('pouet.css', ps._cache)
@@ -45,9 +59,10 @@
self.assertNotIn('pouet.css', ps._cache)
self.assertFalse(ps.need_reload())
ps.process_resource(DATADIR, 'pouet.css') # put in cache
- os.utime(join(DATADIR, 'pouet.css'), None)
+ os.utime(self.data('pouet.css'), None)
self.assertFalse(ps.need_reload())
self.assertNotIn('pouet.css', ps._cache)
+
if __name__ == '__main__':
- unittest_main()
+ main()
--- a/web/test/unittest_reledit.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_reledit.py Tue Jul 19 16:13:12 2016 +0200
@@ -53,7 +53,7 @@
def test_default_forms(self):
self.skipTest('Need to check if this test should still run post reledit/doreledit merge')
- doreledit = {'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('invisible')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="title-subject-%(eid)s-form" onsubmit="return freezeFormButtons('title-subject-%(eid)s-form');" class="releditForm" cubicweb:target="eformframe">
+ doreledit = {'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('invisible')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="title-subject-%(eid)s-form" onsubmit="return freezeFormButtons('title-subject-%(eid)s-form');" class="releditForm" target="eformframe">
<fieldset>
<input name="__form_id" type="hidden" value="base" />
<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&vid=blop#title-subject-%(eid)s-form" />
@@ -82,9 +82,10 @@
<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('title-subject-%(eid)s')" tabindex="3" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
</tr></table>
</fieldset>
+<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
</form><div id="title-subject-%(eid)s" class="editableField invisible"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', %(eid)s, 'title', 'subject', 'title-subject-%(eid)s', false, '');" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
- 'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('invisible')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue"><not specified></div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="long_desc-subject-%(eid)s-form" onsubmit="return freezeFormButtons('long_desc-subject-%(eid)s-form');" class="releditForm" cubicweb:target="eformframe">
+ 'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('invisible')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue"><not specified></div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="long_desc-subject-%(eid)s-form" onsubmit="return freezeFormButtons('long_desc-subject-%(eid)s-form');" class="releditForm" target="eformframe">
<fieldset>
<input name="__form_id" type="invisible" value="edition" />
<input name="__errorurl" type="invisible" value="http://testing.fr/cubicweb/view?rql=Blop&vid=blop#long_desc-subject-%(eid)s-form" />
@@ -126,9 +127,10 @@
<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('long_desc-subject-%(eid)s')" tabindex="8" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
</tr></table>
</fieldset>
+<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
</form><div id="long_desc-subject-%(eid)s" class="editableField invisible"><div id="long_desc-subject-%(eid)s-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm('edition', %(eid)s, 'long_desc', 'subject', 'long_desc-subject-%(eid)s', false, 'autolimited');" title="click to add a value"><img title="click to add a value" src="http://testing.fr/cubicweb/data/plus.png" alt="click to add a value"/></div></div></div>""",
- 'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('invisible')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue"><not specified></div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="manager-subject-%(eid)s-form" onsubmit="return freezeFormButtons('manager-subject-%(eid)s-form');" class="releditForm" cubicweb:target="eformframe">
+ 'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('invisible')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('invisible')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue"><not specified></div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="manager-subject-%(eid)s-form" onsubmit="return freezeFormButtons('manager-subject-%(eid)s-form');" class="releditForm" target="eformframe">
<fieldset>
<input name="__form_id" type="hidden" value="base" />
<input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&vid=blop#manager-subject-%(eid)s-form" />
@@ -162,6 +164,7 @@
<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('manager-subject-%(eid)s')" tabindex="11" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
</tr></table>
</fieldset>
+<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
</form><div id="manager-subject-%(eid)s" class="editableField invisible"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', %(eid)s, 'manager', 'subject', 'manager-subject-%(eid)s', false, 'autolimited');" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
'composite_card11_2ttypes': """<not specified>""",
'concerns': """<not specified>"""
--- a/web/test/unittest_urlpublisher.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_urlpublisher.py Tue Jul 19 16:13:12 2016 +0200
@@ -66,8 +66,8 @@
ctrl, rset = self.process(req, 'CWEType')
self.assertEqual(ctrl, 'view')
self.assertEqual(rset.description[0][0], 'CWEType')
- self.assertEqual("Any X,AA,AB ORDERBY AA WHERE X is_instance_of CWEType, "
- "X name AA, X modification_date AB",
+ self.assertEqual("Any X,AA,AB ORDERBY AB WHERE X is_instance_of CWEType, "
+ "X modification_date AA, X name AB",
rset.printable_rql())
def test_rest_path_by_attr(self):
@@ -77,8 +77,8 @@
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'CWUser')
self.assertEqual('Any X,AA,AB,AC,AD WHERE X is_instance_of CWUser, '
- 'X login AA, X firstname AB, X surname AC, '
- 'X modification_date AD, X login "admin"',
+ 'X firstname AA, X login AB, X modification_date AC, '
+ 'X surname AD, X login "admin"',
rset.printable_rql())
def test_rest_path_unique_attr(self):
@@ -88,8 +88,8 @@
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'CWUser')
self.assertEqual('Any X,AA,AB,AC,AD WHERE X is_instance_of CWUser, '
- 'X login AA, X firstname AB, X surname AC, '
- 'X modification_date AD, X login "admin"',
+ 'X firstname AA, X login AB, X modification_date AC, '
+ 'X surname AD, X login "admin"',
rset.printable_rql())
def test_rest_path_eid(self):
@@ -99,8 +99,8 @@
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'CWUser')
self.assertEqual('Any X,AA,AB,AC,AD WHERE X is_instance_of CWUser, '
- 'X login AA, X firstname AB, X surname AC, '
- 'X modification_date AD, X eid %s' % rset[0][0],
+ 'X firstname AA, X login AB, X modification_date AC, '
+ 'X surname AD, X eid %s' % rset[0][0],
rset.printable_rql())
def test_rest_path_non_ascii_paths(self):
@@ -110,8 +110,8 @@
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'CWUser')
self.assertEqual(u'Any X,AA,AB,AC,AD WHERE X is_instance_of CWUser, '
- u'X login AA, X firstname AB, X surname AC, '
- u'X modification_date AD, X login "\xffsa\xffe"',
+ u'X firstname AA, X login AB, X modification_date AC, '
+ u'X surname AD, X login "\xffsa\xffe"',
rset.printable_rql())
def test_rest_path_quoted_paths(self):
@@ -121,7 +121,7 @@
self.assertEqual(len(rset), 1)
self.assertEqual(rset.description[0][0], 'BlogEntry')
self.assertEqual(u'Any X,AA,AB,AC WHERE X is_instance_of BlogEntry, '
- 'X creation_date AA, X title AB, X modification_date AC, '
+ 'X creation_date AA, X modification_date AB, X title AC, '
'X title "hell\'o"',
rset.printable_rql())
--- a/web/test/unittest_views_basecontrollers.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_views_basecontrollers.py Tue Jul 19 16:13:12 2016 +0200
@@ -156,9 +156,9 @@
def test_user_can_change_its_password(self):
with self.admin_access.repo_cnx() as cnx:
- self.create_user(cnx, 'user')
+ self.create_user(cnx, u'user')
cnx.commit()
- with self.new_access('user').web_request() as req:
+ with self.new_access(u'user').web_request() as req:
eid = unicode(req.user.eid)
req.form = {
'eid': eid, '__maineid' : eid,
@@ -287,7 +287,7 @@
def test_edit_multiple_linked(self):
with self.admin_access.web_request() as req:
- peid = unicode(self.create_user(req, 'adim').eid)
+ peid = unicode(self.create_user(req, u'adim').eid)
req.form = {'eid': [peid, 'Y'], '__maineid': peid,
'__type:'+peid: u'CWUser',
@@ -548,7 +548,7 @@
self.assertIn('_cwmsgid', params)
eid = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
req.execute('SET X use_email E WHERE E eid %(e)s, X eid %(x)s',
- {'x': self.session.user.eid, 'e': eid})
+ {'x': req.user.eid, 'e': eid})
req.cnx.commit()
req.form = {'eid': unicode(eid), '__type:%s'%eid: 'EmailAddress',
'__action_delete': ''}
@@ -692,7 +692,7 @@
def test_nonregr_rollback_on_validation_error(self):
with self.admin_access.web_request() as req:
- p = self.create_user(req, "doe")
+ p = self.create_user(req, u"doe")
# do not try to skip 'primary_email' for this test
old_skips = p.__class__.skip_copy_for
p.__class__.skip_copy_for = ()
@@ -754,10 +754,10 @@
class ReportBugControllerTC(CubicWebTC):
def test_usable_by_guest(self):
- with self.new_access('anon').web_request() as req:
+ with self.new_access(u'anon').web_request() as req:
self.assertRaises(NoSelectableObject,
self.vreg['controllers'].select, 'reportbug', req)
- with self.new_access('anon').web_request(description='hop') as req:
+ with self.new_access(u'anon').web_request(description='hop') as req:
self.vreg['controllers'].select('reportbug', req)
@@ -836,9 +836,9 @@
deletes = get_pending_deletes(req)
self.assertEqual(deletes, [])
inserts = get_pending_inserts(req)
- self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
+ self.assertCountEqual(inserts, ['12:tags:13', '12:tags:14'])
inserts = get_pending_inserts(req, 12)
- self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
+ self.assertCountEqual(inserts, ['12:tags:13', '12:tags:14'])
inserts = get_pending_inserts(req, 13)
self.assertEqual(inserts, ['12:tags:13'])
inserts = get_pending_inserts(req, 14)
@@ -855,9 +855,9 @@
inserts = get_pending_inserts(req)
self.assertEqual(inserts, [])
deletes = get_pending_deletes(req)
- self.assertEqual(deletes, ['12:tags:13', '12:tags:14'])
+ self.assertCountEqual(deletes, ['12:tags:13', '12:tags:14'])
deletes = get_pending_deletes(req, 12)
- self.assertEqual(deletes, ['12:tags:13', '12:tags:14'])
+ self.assertCountEqual(deletes, ['12:tags:13', '12:tags:14'])
deletes = get_pending_deletes(req, 13)
self.assertEqual(deletes, ['12:tags:13'])
deletes = get_pending_deletes(req, 14)
@@ -880,7 +880,7 @@
with self.remote_calling('add_pending_inserts',
[('12', 'tags', '13'), ('12', 'tags', '14')]) as (_, req):
inserts = get_pending_inserts(req)
- self.assertEqual(inserts, ['12:tags:13', '12:tags:14'])
+ self.assertCountEqual(inserts, ['12:tags:13', '12:tags:14'])
req.remove_pending_operations()
@@ -1016,8 +1016,8 @@
def setup_database(self):
with self.admin_access.repo_cnx() as cnx:
- self.toto = self.create_user(cnx, 'toto',
- password='toto',
+ self.toto = self.create_user(cnx, u'toto',
+ password=u'toto',
groups=('users',),
commit=False)
self.txuuid_toto = cnx.commit()
--- a/web/test/unittest_views_editforms.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_views_editforms.py Tue Jul 19 16:13:12 2016 +0200
@@ -43,7 +43,7 @@
with self.admin_access.web_request() as req:
AFFK.tag_subject_of(('CWUser', 'login', '*'),
{'widget': AutoCompletionWidget(autocomplete_initfunc='get_logins')})
- form = self.vreg['forms'].select('edition', req, entity=self.user(req))
+ form = self.vreg['forms'].select('edition', req, entity=req.user)
field = form.field_by_name('login', 'subject')
self.assertIsInstance(field.widget, AutoCompletionWidget)
AFFK.del_rtag('CWUser', 'login', '*', 'subject')
@@ -54,18 +54,18 @@
e = self.vreg['etypes'].etype_class('CWUser')(req)
# see custom configuration in views.cwuser
self.assertEqual(rbc(e, 'main', 'attributes'),
- [('login', 'subject'),
- ('upassword', 'subject'),
- ('firstname', 'subject'),
- ('surname', 'subject'),
- ('in_group', 'subject'),
- ])
- self.assertListEqual(rbc(e, 'muledit', 'attributes'),
+ [('login', 'subject'),
+ ('upassword', 'subject'),
+ ('firstname', 'subject'),
+ ('surname', 'subject'),
+ ('in_group', 'subject'),
+ ])
+ self.assertEqual(rbc(e, 'muledit', 'attributes'),
[('login', 'subject'),
('upassword', 'subject'),
('in_group', 'subject'),
])
- self.assertListEqual(rbc(e, 'main', 'metadata'),
+ self.assertCountEqual(rbc(e, 'main', 'metadata'),
[('last_login_time', 'subject'),
('cw_source', 'subject'),
('creation_date', 'subject'),
@@ -77,7 +77,7 @@
# XXX skip 'tags' relation here and in the hidden category because
# of some test interdependancy when pytest is launched on whole cw
# (appears here while expected in hidden
- self.assertListEqual([x for x in rbc(e, 'main', 'relations')
+ self.assertCountEqual([x for x in rbc(e, 'main', 'relations')
if x != ('tags', 'object')],
[('connait', 'subject'),
('custom_workflow', 'subject'),
@@ -125,14 +125,14 @@
self.assertListEqual(rbc(e, 'muledit', 'attributes'),
[('nom', 'subject'),
])
- self.assertListEqual(rbc(e, 'main', 'metadata'),
+ self.assertCountEqual(rbc(e, 'main', 'metadata'),
[('cw_source', 'subject'),
('creation_date', 'subject'),
('modification_date', 'subject'),
('created_by', 'subject'),
('owned_by', 'subject'),
])
- self.assertListEqual(rbc(e, 'main', 'relations'),
+ self.assertCountEqual(rbc(e, 'main', 'relations'),
[('travaille', 'subject'),
('manager', 'object'),
('connait', 'object'),
@@ -158,15 +158,15 @@
def test_attribute_add_permissions(self):
# https://www.cubicweb.org/ticket/4342844
with self.admin_access.repo_cnx() as cnx:
- self.create_user(cnx, 'toto')
+ self.create_user(cnx, u'toto')
cnx.commit()
- with self.new_access('toto').web_request() as req:
+ with self.new_access(u'toto').web_request() as req:
e = self.vreg['etypes'].etype_class('Personne')(req)
cform = self.vreg['forms'].select('edition', req, entity=e)
self.assertIn('sexe',
[rschema.type
for rschema, _ in cform.editable_attributes()])
- with self.new_access('toto').repo_cnx() as cnx:
+ with self.new_access(u'toto').repo_cnx() as cnx:
person_eid = cnx.create_entity('Personne', nom=u'Robert').eid
cnx.commit()
person = req.entity_from_eid(person_eid)
--- a/web/test/unittest_views_xmlrss.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_views_xmlrss.py Tue Jul 19 16:13:12 2016 +0200
@@ -11,13 +11,13 @@
req.user.view('xml'),
'''\
<CWUser eid="6" cwuri="http://testing.fr/cubicweb/6" cwsource="system">
+ <creation_date>%(cdate)s</creation_date>
+ <firstname/>
+ <last_login_time/>
<login>admin</login>
- <upassword/>
- <firstname/>
+ <modification_date>%(mdate)s</modification_date>
<surname/>
- <last_login_time/>
- <creation_date>%(cdate)s</creation_date>
- <modification_date>%(mdate)s</modification_date>
+ <upassword/>
<tags role="object">
</tags>
<in_group role="subject">
--- a/web/test/unittest_viewselector.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_viewselector.py Tue Jul 19 16:13:12 2016 +0200
@@ -33,7 +33,6 @@
cwproperties, cwsources, xmlrss, rdf, csvexport, json,
undohistory)
-from cubes.folder import views as folderviews
USERACTIONS = [actions.UserPreferencesAction,
actions.UserInfoAction,
@@ -49,7 +48,7 @@
debug.SiteInfoAction]
if hasattr(rdf, 'RDFView'): # not available if rdflib not installed
- RDFVIEWS = [('rdf', rdf.RDFView)]
+ RDFVIEWS = [('rdf', rdf.RDFView), ('n3rdf', rdf.RDFN3View)]
else:
RDFVIEWS = []
@@ -101,7 +100,6 @@
('schema', schema.SchemaView),
('siteinfo', debug.SiteInfoView),
('systempropertiesform', cwproperties.SystemCWPropertiesForm),
- ('tree', folderviews.FolderTreeView),
('undohistory', undohistory.UndoHistoryView)])
def test_possible_views_noresult(self):
@@ -117,50 +115,51 @@
def test_possible_views_one_egroup(self):
with self.admin_access.web_request() as req:
rset = req.execute('CWGroup X WHERE X name "managers"')
- self.assertListEqual(self.pviews(req, rset),
- [('csvexport', csvexport.CSVRsetView),
- ('ecsvexport', csvexport.CSVEntityView),
- ('ejsonexport', json.JsonEntityView),
- ('filetree', treeview.FileTreeView),
- ('jsonexport', json.JsonRsetView),
- ('list', baseviews.ListView),
- ('oneline', baseviews.OneLineView),
- ('owlabox', owl.OWLABOXView),
- ('primary', cwuser.CWGroupPrimaryView)] + \
- RDFVIEWS + \
- [('rsetxml', xmlrss.XMLRsetView),
- ('rss', xmlrss.RSSView),
- ('sameetypelist', baseviews.SameETypeListView),
- ('security', management.SecurityManagementView),
- ('table', tableview.RsetTableView),
- ('text', baseviews.TextView),
- ('treeview', treeview.TreeView),
- ('xbel', xbel.XbelView),
- ('xml', xmlrss.XMLView)])
+ self.assertCountEqual(self.pviews(req, rset),
+ RDFVIEWS +
+ [('csvexport', csvexport.CSVRsetView),
+ ('ecsvexport', csvexport.CSVEntityView),
+ ('ejsonexport', json.JsonEntityView),
+ ('filetree', treeview.FileTreeView),
+ ('jsonexport', json.JsonRsetView),
+ ('list', baseviews.ListView),
+ ('oneline', baseviews.OneLineView),
+ ('owlabox', owl.OWLABOXView),
+ ('primary', cwuser.CWGroupPrimaryView),
+ ('rsetxml', xmlrss.XMLRsetView),
+ ('rss', xmlrss.RSSView),
+ ('sameetypelist', baseviews.SameETypeListView),
+ ('security', management.SecurityManagementView),
+ ('table', tableview.RsetTableView),
+ ('text', baseviews.TextView),
+ ('treeview', treeview.TreeView),
+ ('xbel', xbel.XbelView),
+ ('xml', xmlrss.XMLView)])
def test_possible_views_multiple_egroups(self):
with self.admin_access.web_request() as req:
rset = req.execute('CWGroup X')
- self.assertListEqual(self.pviews(req, rset),
- [('csvexport', csvexport.CSVRsetView),
- ('ecsvexport', csvexport.CSVEntityView),
- ('ejsonexport', json.JsonEntityView),
- ('filetree', treeview.FileTreeView),
- ('jsonexport', json.JsonRsetView),
- ('list', baseviews.ListView),
- ('oneline', baseviews.OneLineView),
- ('owlabox', owl.OWLABOXView),
- ('primary', cwuser.CWGroupPrimaryView)] + RDFVIEWS + [
- ('rsetxml', xmlrss.XMLRsetView),
- ('rss', xmlrss.RSSView),
- ('sameetypelist', baseviews.SameETypeListView),
- ('security', management.SecurityManagementView),
- ('table', tableview.RsetTableView),
- ('text', baseviews.TextView),
- ('treeview', treeview.TreeView),
- ('xbel', xbel.XbelView),
- ('xml', xmlrss.XMLView),
- ])
+ self.assertCountEqual(self.pviews(req, rset),
+ RDFVIEWS +
+ [('csvexport', csvexport.CSVRsetView),
+ ('ecsvexport', csvexport.CSVEntityView),
+ ('ejsonexport', json.JsonEntityView),
+ ('filetree', treeview.FileTreeView),
+ ('jsonexport', json.JsonRsetView),
+ ('list', baseviews.ListView),
+ ('oneline', baseviews.OneLineView),
+ ('owlabox', owl.OWLABOXView),
+ ('primary', cwuser.CWGroupPrimaryView),
+ ('rsetxml', xmlrss.XMLRsetView),
+ ('rss', xmlrss.RSSView),
+ ('sameetypelist', baseviews.SameETypeListView),
+ ('security', management.SecurityManagementView),
+ ('table', tableview.RsetTableView),
+ ('text', baseviews.TextView),
+ ('treeview', treeview.TreeView),
+ ('xbel', xbel.XbelView),
+ ('xml', xmlrss.XMLView),
+ ])
def test_propertiesform_admin(self):
assert self.vreg['views']['propertiesform']
@@ -172,7 +171,7 @@
self.assertTrue(self.vreg['views'].select('propertiesform', req, rset=rset2))
def test_propertiesform_anon(self):
- with self.new_access('anon').web_request() as req:
+ with self.new_access(u'anon').web_request() as req:
rset1 = req.execute('CWUser X WHERE X login "admin"')
self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'propertiesform', req, rset=None)
self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'propertiesform', req, rset=rset1)
@@ -181,9 +180,9 @@
def test_propertiesform_jdoe(self):
with self.admin_access.repo_cnx() as cnx:
- self.create_user(cnx, 'jdoe')
+ self.create_user(cnx, u'jdoe')
cnx.commit()
- with self.new_access('jdoe').web_request() as req:
+ with self.new_access(u'jdoe').web_request() as req:
rset1 = req.execute('CWUser X WHERE X login "admin"')
rset2 = req.execute('CWUser X WHERE X login "jdoe"')
self.assertTrue(self.vreg['views'].select('propertiesform', req, rset=None))
@@ -193,24 +192,25 @@
def test_possible_views_multiple_different_types(self):
with self.admin_access.web_request() as req:
rset = req.execute('Any X')
- self.assertListEqual(self.pviews(req, rset),
- [('csvexport', csvexport.CSVRsetView),
- ('ecsvexport', csvexport.CSVEntityView),
- ('ejsonexport', json.JsonEntityView),
- ('filetree', treeview.FileTreeView),
- ('jsonexport', json.JsonRsetView),
- ('list', baseviews.ListView),
- ('oneline', baseviews.OneLineView),
- ('owlabox', owl.OWLABOXView),
- ('primary', primary.PrimaryView),] + RDFVIEWS + [
- ('rsetxml', xmlrss.XMLRsetView),
- ('rss', xmlrss.RSSView),
- ('security', management.SecurityManagementView),
- ('table', tableview.RsetTableView),
- ('text', baseviews.TextView),
- ('treeview', treeview.TreeView),
- ('xbel', xbel.XbelView),
- ('xml', xmlrss.XMLView),
+ self.assertCountEqual(self.pviews(req, rset),
+ RDFVIEWS +
+ [('csvexport', csvexport.CSVRsetView),
+ ('ecsvexport', csvexport.CSVEntityView),
+ ('ejsonexport', json.JsonEntityView),
+ ('filetree', treeview.FileTreeView),
+ ('jsonexport', json.JsonRsetView),
+ ('list', baseviews.ListView),
+ ('oneline', baseviews.OneLineView),
+ ('owlabox', owl.OWLABOXView),
+ ('primary', primary.PrimaryView),
+ ('rsetxml', xmlrss.XMLRsetView),
+ ('rss', xmlrss.RSSView),
+ ('security', management.SecurityManagementView),
+ ('table', tableview.RsetTableView),
+ ('text', baseviews.TextView),
+ ('treeview', treeview.TreeView),
+ ('xbel', xbel.XbelView),
+ ('xml', xmlrss.XMLView),
])
def test_possible_views_any_rset(self):
@@ -226,28 +226,29 @@
def test_possible_views_multiple_eusers(self):
with self.admin_access.web_request() as req:
rset = req.execute('CWUser X')
- self.assertListEqual(self.pviews(req, rset),
- [('csvexport', csvexport.CSVRsetView),
- ('ecsvexport', csvexport.CSVEntityView),
- ('ejsonexport', json.JsonEntityView),
- ('filetree', treeview.FileTreeView),
- ('foaf', cwuser.FoafView),
- ('jsonexport', json.JsonRsetView),
- ('list', baseviews.ListView),
- ('oneline', baseviews.OneLineView),
- ('owlabox', owl.OWLABOXView),
- ('primary', primary.PrimaryView)] + RDFVIEWS + [
- ('rsetxml', xmlrss.XMLRsetView),
- ('rss', xmlrss.RSSView),
- ('sameetypelist', baseviews.SameETypeListView),
- ('security', management.SecurityManagementView),
- ('table', tableview.RsetTableView),
- ('text', baseviews.TextView),
- ('treeview', treeview.TreeView),
- ('vcard', vcard.VCardCWUserView),
- ('xbel', xbel.XbelView),
- ('xml', xmlrss.XMLView),
- ])
+ self.assertCountEqual(self.pviews(req, rset),
+ RDFVIEWS +
+ [('csvexport', csvexport.CSVRsetView),
+ ('ecsvexport', csvexport.CSVEntityView),
+ ('ejsonexport', json.JsonEntityView),
+ ('filetree', treeview.FileTreeView),
+ ('foaf', cwuser.FoafView),
+ ('jsonexport', json.JsonRsetView),
+ ('list', baseviews.ListView),
+ ('oneline', baseviews.OneLineView),
+ ('owlabox', owl.OWLABOXView),
+ ('primary', primary.PrimaryView),
+ ('rsetxml', xmlrss.XMLRsetView),
+ ('rss', xmlrss.RSSView),
+ ('sameetypelist', baseviews.SameETypeListView),
+ ('security', management.SecurityManagementView),
+ ('table', tableview.RsetTableView),
+ ('text', baseviews.TextView),
+ ('treeview', treeview.TreeView),
+ ('vcard', vcard.VCardCWUserView),
+ ('xbel', xbel.XbelView),
+ ('xml', xmlrss.XMLView),
+ ])
def test_possible_actions_none_rset(self):
with self.admin_access.web_request() as req:
--- a/web/test/unittest_web.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/test/unittest_web.py Tue Jul 19 16:13:12 2016 +0200
@@ -126,17 +126,24 @@
self.assertIn('HttpOnly', webreq.getheader('set-cookie'))
-class LogQueriesTC(CubicWebServerTC):
+class MiscOptionsTC(CubicWebServerTC):
@classmethod
def init_config(cls, config):
- super(LogQueriesTC, cls).init_config(config)
+ super(MiscOptionsTC, cls).init_config(config)
cls.logfile = tempfile.NamedTemporaryFile()
config.global_set_option('query-log-file', cls.logfile.name)
+ config.global_set_option('datadir-url', '//static.testing.fr/')
+ # call load_configuration again to let the config reset its datadir_url
+ config.load_configuration()
def test_log_queries(self):
self.web_request()
self.assertTrue(self.logfile.read())
+ def test_datadir_url(self):
+ webreq = self.web_request()
+ self.assertNotIn('/data/', webreq.read())
+
@classmethod
def tearDownClass(cls):
cls.logfile.close()
--- a/web/views/ajaxcontroller.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/ajaxcontroller.py Tue Jul 19 16:13:12 2016 +0200
@@ -427,26 +427,6 @@
"""returns the URL of the external resource named `resource`"""
return self._cw.uiprops[resource]
-@ajaxfunc(output_type='json', check_pageid=True)
-def user_callback(self, cbname):
- """execute the previously registered user callback `cbname`.
-
- If matching callback is not found, return None
- """
- page_data = self._cw.session.data.get(self._cw.pageid, {})
- try:
- cb = page_data[cbname]
- except KeyError:
- self.warning('unable to find user callback %s', cbname)
- return None
- return cb(self._cw)
-
-
-@ajaxfunc
-def unregister_user_callback(self, cbname):
- """unregister user callback `cbname`"""
- self._cw.unregister_callback(self._cw.pageid, cbname)
-
@ajaxfunc
def unload_page_data(self):
"""remove user's session data associated to current pageid"""
--- a/web/views/authentication.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/authentication.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -26,9 +26,8 @@
from cubicweb import AuthenticationError, BadConnectionId
from cubicweb.view import Component
-from cubicweb.dbapi import _repo_connect, ConnectionProperties
from cubicweb.web import InvalidSession
-from cubicweb.web.application import AbstractAuthenticationManager
+
class NoAuthInfo(Exception): pass
@@ -102,6 +101,36 @@
'("ie" instead of "ei")')
+class AbstractAuthenticationManager(Component):
+ """authenticate user associated to a request and check session validity"""
+ __abstract__ = True
+ __regid__ = 'authmanager'
+
+ def __init__(self, repo):
+ self.vreg = repo.vreg
+
+ 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).
+
+ raise :exc:`InvalidSession` if session is corrupted for a reason or
+ another and should be closed
+ """
+ raise NotImplementedError()
+
+ 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.
+
+ raise :exc:`cubicweb.AuthenticationError` if authentication failed
+ (no authentication info found or wrong user/password)
+ """
+ raise NotImplementedError()
+
+
class RepositoryAuthenticationManager(AbstractAuthenticationManager):
"""authenticate user associated to a request and check session validity"""
--- a/web/views/autoform.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/autoform.py Tue Jul 19 16:13:12 2016 +0200
@@ -746,15 +746,6 @@
# action on the form tag
_default_form_action_path = 'validateform'
- @deprecated('[3.18] you should override form_action()')
- def set_action(self, action):
- self._action = action
-
- @deprecated('[3.18] use form_action()')
- def get_action(self):
- return self._action
-
-
@iclassmethod
def field_by_name(cls_or_self, name, role=None, eschema=None):
"""return field with the given name and role. If field is not explicitly
--- a/web/views/basecomponents.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/basecomponents.py Tue Jul 19 16:13:12 2016 +0200
@@ -55,7 +55,7 @@
else:
rset = self.cw_rset
# display multilines query as one line
- rql = rset is not None and rset.printable_rql(encoded=False) or req.form.get('rql', '')
+ rql = rset is not None and rset.printable_rql() or req.form.get('rql', '')
rql = rql.replace(u"\n", u" ")
rql_suggestion_comp = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw)
if rql_suggestion_comp is not None:
--- a/web/views/basecontrollers.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/basecontrollers.py Tue Jul 19 16:13:12 2016 +0200
@@ -92,7 +92,7 @@
def publish(self, rset=None):
"""log in the instance"""
path = self._cw.form.get('postlogin_path', '')
- # redirect expect a URL, not a path. Also path may contains a query
+ # Redirect expects a URL, not a path. Also path may contain a query
# string, hence should not be given to _cw.build_url()
raise Redirect(self._cw.base_url() + path)
--- a/web/views/basetemplates.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/basetemplates.py Tue Jul 19 16:13:12 2016 +0200
@@ -490,7 +490,7 @@
class LogFormView(View):
- # XXX an awfull lot of hardcoded assumptions there
+ # XXX an awful lot of hardcoded assumptions there
# makes it unobvious to reuse/specialize
__regid__ = 'logform'
__select__ = match_kwargs('id', 'klass')
@@ -516,10 +516,6 @@
if config['auth-mode'] != 'http':
self.login_form(id) # Cookie authentication
w(u'</div>')
- if self._cw.https and config.anonymous_user()[0] and config['https-deny-anonymous']:
- path = xml_escape(config['base-url'] + self._cw.relative_path())
- w(u'<div class="loginMessage"><a href="%s">%s</a></div>\n'
- % (path, self._cw._('No account? Try public access at %s') % path))
w(u'</div>\n')
def login_form(self, id):
--- a/web/views/cwsources.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/cwsources.py Tue Jul 19 16:13:12 2016 +0200
@@ -231,6 +231,7 @@
__regid__ = 'cwsource'
title = _('data sources')
category = 'manage'
+ order = 100
class CWSourcesManagementView(StartupView):
--- a/web/views/debug.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/debug.py Tue Jul 19 16:13:12 2016 +0200
@@ -24,7 +24,6 @@
from logilab.mtconverter import xml_escape
-from cubicweb import BadConnectionId
from cubicweb.predicates import none_rset, match_user_groups
from cubicweb.view import StartupView
from cubicweb.web.views import actions, tabs
@@ -98,6 +97,13 @@
w(u'<h3>%s</h3>' % _('resources usage'))
w(u'<table>')
stats = self._cw.call_service('repo_stats')
+ stats['looping_tasks'] = ', '.join('%s (%s seconds)' % (n, i) for n, i in stats['looping_tasks'])
+ stats['threads'] = ', '.join(sorted(stats['threads']))
+ for k in stats:
+ if k in ('extid_cache_size', 'type_source_cache_size'):
+ continue
+ if k.endswith('_cache_size'):
+ stats[k] = '%s / %s' % (stats[k]['size'], stats[k]['maxsize'])
for element in sorted(stats):
w(u'<tr><th align="left">%s</th><td>%s %s</td></tr>'
% (element, xml_escape(unicode(stats[element])),
@@ -179,31 +185,13 @@
cache_max_age = 0
def call(self, **kwargs):
- from cubicweb._gcdebug import gc_info
- from rql.stmts import Union
- from cubicweb.appobject import AppObject
- from cubicweb.rset import ResultSet
- from cubicweb.dbapi import Connection, Cursor
- from cubicweb.web.request import CubicWebRequestBase
- lookupclasses = (AppObject,
- Union, ResultSet,
- Connection, Cursor,
- CubicWebRequestBase)
- try:
- from cubicweb.server.session import Session, InternalSession
- lookupclasses += (InternalSession, Session)
- except ImportError:
- pass # no server part installed
+ stats = self._cw.call_service('repo_gc_stats')
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'))
- values = sorted(counters.iteritems(), key=lambda x: x[1], reverse=True)
- self.wview('pyvaltable', pyvalue=values)
+ self.wview('pyvaltable', pyvalue=stats['lookupclasses'])
self.w(u'<h3>%s</h3>' % self._cw._('Most referenced classes'))
- values = sorted(ocounters.iteritems(), key=lambda x: x[1], reverse=True)
- self.wview('pyvaltable', pyvalue=values[:self._cw.form.get('nb', 20)])
- if garbage:
+ self.wview('pyvaltable', pyvalue=stats['referenced'])
+ if stats['unreachable']:
self.w(u'<h3>%s</h3>' % self._cw._('Unreachable objects'))
- values = sorted(xml_escape(repr(o)) for o in garbage)
+ values = [xml_escape(val) for val in stats['unreachable']]
self.wview('pyvallist', pyvalue=values)
--- a/web/views/formrenderers.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/formrenderers.py Tue Jul 19 16:13:12 2016 +0200
@@ -196,7 +196,7 @@
if form.cssclass:
attrs.setdefault('class', form.cssclass)
if form.cwtarget:
- attrs.setdefault('cubicweb:target', form.cwtarget)
+ attrs.setdefault('target', form.cwtarget)
if not form.autocomplete:
attrs.setdefault('autocomplete', 'off')
return '<form %s>' % uilib.sgml_attributes(attrs)
@@ -206,7 +206,13 @@
for form renderers overriding open_form to use something else or more than
and <form>
"""
- return u'</form>'
+ out = u'</form>'
+ if form.cwtarget:
+ attrs = {'name': form.cwtarget, 'id': form.cwtarget,
+ 'width': '0px', 'height': '0px',
+ 'src': 'javascript: void(0);'}
+ out = (u'<iframe %s></iframe>\n' % uilib.sgml_attributes(attrs)) + out
+ return out
def render_fields(self, w, form, values):
fields = self._render_hidden_fields(w, form)
--- a/web/views/forms.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/forms.py Tue Jul 19 16:13:12 2016 +0200
@@ -95,7 +95,7 @@
value for the "style" attribute of the <form> tag
:attr:`cwtarget`
- value for the "cubicweb:target" attribute of the <form> tag
+ value for the "target" attribute of the <form> tag
:attr:`redirect_path`
relative to redirect to after submitting the form
@@ -241,10 +241,7 @@
_default_form_action_path = 'edit'
def form_action(self):
- try:
- action = self.get_action() # avoid spurious warning w/ autoform bw compat property
- except AttributeError:
- action = self.action
+ action = self.action
if action is None:
return self._cw.build_url(self._default_form_action_path)
return action
--- a/web/views/management.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/management.py Tue Jul 19 16:13:12 2016 +0200
@@ -187,6 +187,13 @@
def call(self):
stats = self._cw.call_service('repo_stats')
+ stats['looping_tasks'] = ', '.join('%s (%s seconds)' % (n, i) for n, i in stats['looping_tasks'])
+ stats['threads'] = ', '.join(sorted(stats['threads']))
+ for k in stats:
+ if k in ('extid_cache_size', 'type_source_cache_size'):
+ continue
+ if k.endswith('_cache_size'):
+ stats[k] = '%s / %s' % (stats[k]['size'], stats[k]['maxsize'])
results = []
for element in stats:
results.append(u'%s %s' % (element, stats[element]))
--- a/web/views/rdf.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/rdf.py Tue Jul 19 16:13:12 2016 +0200
@@ -47,6 +47,8 @@
__regid__ = 'rdf'
title = _('rdf export')
templatable = False
+ binary = True
+ format = 'xml'
content_type = 'text/xml' # +rdf
def call(self):
@@ -57,7 +59,7 @@
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'))
+ self.w(graph.serialize(format=self.format))
def entity_call(self, entity):
self.call()
@@ -100,3 +102,8 @@
else:
add( (URIRef(related.cwuri), CW[rtype], cwuri) )
+
+ class RDFN3View(RDFView):
+ __regid__ = 'n3rdf'
+ format = 'n3'
+ content_type = 'text/n3'
--- a/web/views/sessions.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/sessions.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,20 +15,84 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""web session component: by dfault the session is actually the db connection
-object :/
-"""
-
+"""web session: by default the session is actually the db connection """
__docformat__ = "restructuredtext en"
from time import time
-from cubicweb import (RepositoryError, Unauthorized, AuthenticationError,
- BadConnectionId)
-from cubicweb.web import InvalidSession, Redirect
-from cubicweb.web.application import AbstractSessionManager
-from cubicweb.dbapi import ProgrammingError, DBAPISession
-from cubicweb import repoapi
+from cubicweb import RepositoryError, Unauthorized, BadConnectionId
+from cubicweb.web import InvalidSession, component
+
+
+class AbstractSessionManager(component.Component):
+ """manage session data associated to a session identifier"""
+ __abstract__ = True
+ __regid__ = 'sessionmanager'
+
+ def __init__(self, repo):
+ vreg = repo.vreg
+ self.session_time = vreg.config['http-session-time'] or None
+ self.authmanager = vreg['components'].select('authmanager', repo=repo)
+ interval = (self.session_time or 0) / 2.
+ if vreg.config.anonymous_user()[0] is not None:
+ self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60
+ assert self.cleanup_anon_session_time > 0
+ if self.session_time is not None:
+ self.cleanup_anon_session_time = min(self.session_time,
+ self.cleanup_anon_session_time)
+ interval = self.cleanup_anon_session_time / 2.
+ # we don't want to check session more than once every 5 minutes
+ self.clean_sessions_interval = max(5 * 60, interval)
+
+ def clean_sessions(self):
+ """cleanup sessions which has not been unused since a given amount of
+ time. Return the number of sessions which have been closed.
+ """
+ self.debug('cleaning http sessions')
+ session_time = self.session_time
+ closed, total = 0, 0
+ for session in self.current_sessions():
+ total += 1
+ try:
+ last_usage_time = session.cnx.check()
+ except AttributeError:
+ last_usage_time = session.mtime
+ except BadConnectionId:
+ self.close_session(session)
+ closed += 1
+ continue
+
+ no_use_time = (time() - last_usage_time)
+ if session.anonymous_session:
+ if no_use_time >= self.cleanup_anon_session_time:
+ self.close_session(session)
+ closed += 1
+ elif session_time is not None and no_use_time >= session_time:
+ self.close_session(session)
+ closed += 1
+ return closed, total - closed
+
+ def current_sessions(self):
+ """return currently open sessions"""
+ raise NotImplementedError()
+
+ def get_session(self, req, sessionid):
+ """return existing session for the given session identifier"""
+ raise NotImplementedError()
+
+ def open_session(self, req):
+ """open and return a new session for the given request.
+
+ raise :exc:`cubicweb.AuthenticationError` if authentication failed
+ (no authentication info found or wrong user/password)
+ """
+ raise NotImplementedError()
+
+ def close_session(self, session):
+ """close session on logout or on invalid session detected (expired out,
+ corrupted...)
+ """
+ raise NotImplementedError()
class InMemoryRepositorySessionManager(AbstractSessionManager):
@@ -97,8 +161,7 @@
# XXX should properly detect missing permission / non writeable source
# and avoid "except (RepositoryError, Unauthorized)" below
try:
- cnx = repoapi.ClientConnection(session)
- with cnx:
+ with session.new_cnx() as cnx:
cnx.execute('SET X last_login_time NOW WHERE X eid %(x)s',
{'x' : session.user.eid})
cnx.commit()
--- a/web/views/staticcontrollers.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/staticcontrollers.py Tue Jul 19 16:13:12 2016 +0200
@@ -188,7 +188,6 @@
def __init__(self, *args, **kwargs):
super(DataController, self).__init__(*args, **kwargs)
config = self._cw.vreg.config
- md5_version = config.instance_md5_version()
self.base_datapath = config.data_relpath()
self.data_modconcat_basepath = '%s??' % self.base_datapath
self.concat_files_registry = ConcatFilesHandler(config)
--- a/web/views/tableview.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/tableview.py Tue Jul 19 16:13:12 2016 +0200
@@ -992,8 +992,10 @@
@cachedproperty
def initial_load(self):
- """We detect a bit heuristically if we are built for the first time of
- from subsequent calls by the form filter or by the pagination hooks
+ """We detect a bit heuristically if we are built for the first time or
+ from subsequent calls by the form filter or by the pagination
+ hooks.
+
"""
form = self._cw.form
return 'fromformfilter' not in form and '__start' not in form
--- a/web/views/timeline.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/timeline.py Tue Jul 19 16:13:12 2016 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,124 +15,20 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""basic support for SIMILE's timeline widgets
-cf. http://code.google.com/p/simile-widgets/
-"""
-
-__docformat__ = "restructuredtext en"
-
-from logilab.mtconverter import xml_escape
-from logilab.common.date import ustrftime
-
-from cubicweb.predicates import adaptable
-from cubicweb.view import EntityView, StartupView
-from cubicweb.utils import json_dumps
-
-_ = unicode
-
-class TimelineJsonView(EntityView):
- """generates a json file to feed Timeline.loadJSON()
- NOTE: work in progress (image_url, bubbleUrl and so on
- should be properties of entity classes or subviews)
- """
- __regid__ = 'timeline-json'
- __select__ = adaptable('ICalendarable')
-
- binary = True
- templatable = False
- content_type = 'application/json'
-
- date_fmt = '%Y/%m/%d'
-
- def call(self):
- events = []
- for entity in self.cw_rset.entities():
- event = self.build_event(entity)
- if event is not None:
- events.append(event)
- timeline_data = {'dateTimeFormat': self.date_fmt,
- 'events': events}
- self.w(json_dumps(timeline_data))
-
- # FIXME: those properties should be defined by the entity class
- def onclick_url(self, entity):
- return entity.absolute_url()
-
- def onclick(self, entity):
- url = self.onclick_url(entity)
- if url:
- return u"javascript: document.location.href='%s'" % url
- return None
+try:
+ from cubes.timeline.views import (
+ TimelineJsonView,
+ TimelineViewMixIn,
+ TimelineView,
+ StaticTimelineView)
- def build_event(self, entity):
- """converts `entity` into a JSON object
- {'start': '1891',
- 'end': '1915',
- 'title': 'Portrait of Horace Brodsky',
- 'description': 'by Henri Gaudier-Brzeska, French Sculptor, 1891-1915',
- 'image': 'http://imagecache2.allposters.com/images/BRGPOD/102770_b.jpg',
- 'link': 'http://www.allposters.com/-sp/Portrait-of-Horace-Brodsky-Posters_i1584413_.htm'
- }
- """
- icalendarable = entity.cw_adapt_to('ICalendarable')
- start = icalendarable.start
- stop = icalendarable.stop
- start = start or stop
- if start is None and stop is None:
- return None
- event_data = {'start': ustrftime(start, self.date_fmt),
- 'title': xml_escape(entity.dc_title()),
- 'description': entity.dc_description(format='text/html'),
- 'link': entity.absolute_url(),
- }
- onclick = self.onclick(entity)
- if onclick:
- event_data['onclick'] = onclick
- if stop:
- event_data['end'] = ustrftime(stop, self.date_fmt)
- return event_data
-
-
-class TimelineViewMixIn(object):
- widget_class = 'TimelineWidget'
- jsfiles = ('cubicweb.timeline-bundle.js', 'cubicweb.widgets.js',
- 'cubicweb.timeline-ext.js', 'cubicweb.ajax.js')
+except ImportError:
+ pass
+else:
+ from logilab.common.deprecation import class_moved
- def render_url(self, loadurl, tlunit=None):
- tlunit = tlunit or self._cw.form.get('tlunit')
- self._cw.add_js(self.jsfiles)
- self._cw.add_css('timeline-bundle.css')
- if tlunit:
- additional = u' cubicweb:tlunit="%s"' % tlunit
- else:
- additional = u''
- self.w(u'<div class="widget" cubicweb:wdgtype="%s" '
- u'cubicweb:loadtype="auto" cubicweb:loadurl="%s" %s >' %
- (self.widget_class, xml_escape(loadurl),
- additional))
- self.w(u'</div>')
-
-
-class TimelineView(TimelineViewMixIn, EntityView):
- """builds a cubicweb timeline widget node"""
- __regid__ = 'timeline'
- title = _('timeline')
- __select__ = adaptable('ICalendarable')
- paginable = False
- def call(self, tlunit=None):
- self._cw.html_headers.define_var('Timeline_urlPrefix', self._cw.datadir_url)
- rql = self.cw_rset.printable_rql()
- loadurl = self._cw.build_url(rql=rql, vid='timeline-json')
- self.render_url(loadurl, tlunit)
-
-
-class StaticTimelineView(TimelineViewMixIn, StartupView):
- """similar to `TimelineView` but loads data from a static
- JSON file instead of one after a RQL query.
- """
- __regid__ = 'static-timeline'
-
- def call(self, loadurl, tlunit=None, wdgclass=None):
- self.widget_class = wdgclass or self.widget_class
- self.render_url(loadurl, tlunit)
+ TimelineJsonView = class_moved(TimelineJsonView, 'TimelineJsonView')
+ TimelineViewMixIn = class_moved(TimelineViewMixIn, 'TimelineViewMixIn')
+ TimelineView = class_moved(TimelineView, 'TimelineView')
+ StaticTimelineView = class_moved(StaticTimelineView, 'StaticTimelineView')
--- a/web/views/urlpublishing.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/urlpublishing.py Tue Jul 19 16:13:12 2016 +0200
@@ -97,7 +97,7 @@
self.evaluators = sorted(evaluators, key=lambda x: x.priority)
def process(self, req, path):
- """Given a URL (essentialy caracterized by a path on the
+ """Given a URL (essentially characterized by a path on the
server, but additional information may be found in the request
object), return a publishing method identifier
(e.g. controller) and an optional result set.
--- a/web/views/xmlrss.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/views/xmlrss.py Tue Jul 19 16:13:12 2016 +0200
@@ -79,7 +79,7 @@
self.w(u'<%s eid="%s" cwuri="%s" cwsource="%s">\n'
% (entity.cw_etype, entity.eid, xml_escape(entity.cwuri),
xml_escape(source)))
- for rschema, attrschema in entity.e_schema.attribute_definitions():
+ for rschema, attrschema in sorted(entity.e_schema.attribute_definitions()):
attr = rschema.type
if attr in ('eid', 'cwuri'):
continue
--- a/web/webconfig.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/webconfig.py Tue Jul 19 16:13:12 2016 +0200
@@ -49,7 +49,7 @@
}),
# user web ui configuration
('fckeditor',
- {'type' : 'yn', 'default': True,
+ {'type' : 'yn', 'default': False,
'help': _('should html fields being edited using fckeditor (a HTML '
'WYSIWYG editor). You should also select text/html as default '
'text format to actually get fckeditor.'),
@@ -124,16 +124,13 @@
'where the cubicweb web server is listening on port 8080.',
'group': 'main', 'level': 3,
}),
- ('https-deny-anonymous',
- {'type': 'yn',
- 'default': False,
- 'help': 'Prevent anonymous user to browse through https version of '
- 'the site (https-url). Login form will then be displayed '
- 'until logged',
+ ('datadir-url',
+ {'type': 'string', 'default': None,
+ 'help': ('base url for static data, if different from "${base-url}/data/". '
+ 'If served from a different domain, that domain should allow '
+ 'cross-origin requests.'),
'group': 'web',
- 'level': 2
- }
- ),
+ }),
('auth-mode',
{'type' : 'choice',
'choices' : ('cookie', 'http'),
@@ -378,9 +375,9 @@
if exists(fpath):
yield join(fpath)
- def load_configuration(self):
+ def load_configuration(self, **kw):
"""load instance's configuration files"""
- super(WebConfiguration, self).load_configuration()
+ super(WebConfiguration, self).load_configuration(**kw)
# load external resources definition
self._init_base_url()
self._build_ui_properties()
@@ -392,6 +389,14 @@
baseurl += '/'
if not (self.repairing or self.creating):
self.global_set_option('base-url', baseurl)
+ self.datadir_url = self['datadir-url']
+ if self.datadir_url:
+ if self.datadir_url[-1] != '/':
+ self.datadir_url += '/'
+ if self.mode != 'test':
+ self.datadir_url += '%s/' % self.instance_md5_version()
+ self.https_datadir_url = self.datadir_url
+ return
httpsurl = self['https-url']
data_relpath = self.data_relpath()
if httpsurl:
@@ -477,8 +482,3 @@
def static_file_del(self, rpath):
if self.static_file_exists(rpath):
os.remove(join(self.static_directory, rpath))
-
- @deprecated('[3.9] use _cw.uiprops.get(rid)')
- def has_resource(self, rid):
- """return true if an external resource is defined"""
- return bool(self.uiprops.get(rid))
--- a/web/webctl.py Tue Jul 19 15:59:02 2016 +0200
+++ b/web/webctl.py Tue Jul 19 16:13:12 2016 +0200
@@ -46,9 +46,6 @@
if not automatic:
print '\n' + underline_title('Generic web configuration')
config = self.config
- if config['repository-uri'].startswith('pyro://') or config.pyro_enabled():
- print '\n' + underline_title('Pyro configuration')
- config.input_config('pyro', inputlevel)
config.input_config('web', inputlevel)
if ASK.confirm('Allow anonymous access ?', False):
config.global_set_option('anonymous-user', 'anon')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/wsgi/test/requirements.txt Tue Jul 19 16:13:12 2016 +0200
@@ -0,0 +1,1 @@
+webtest
--- a/zmqclient.py Tue Jul 19 15:59:02 2016 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# 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/>.
-"""Source to query another RQL repository using pyro"""
-
-__docformat__ = "restructuredtext en"
-_ = unicode
-
-from functools import partial
-import zmq
-
-from cubicweb.server.cwzmq import cwproto_to_zmqaddr
-
-# XXX hack to overpass old zmq limitation that force to have
-# only one context per python process
-try:
- from cubicweb.server.cwzmq import ctx
-except ImportError:
- ctx = zmq.Context()
-
-class ZMQRepositoryClient(object):
- """
- This class delegates the overall repository stuff to a remote source.
-
- So calling a method of this repository will result on calling the
- corresponding method of the remote source repository.
-
- Any raised exception on the remote source is propagated locally.
-
- ZMQ is used as the transport layer and cPickle is used to serialize data.
- """
-
- def __init__(self, zmq_address):
- """A zmq address provided here will be like
- `zmqpickle-tcp://127.0.0.1:42000`. W
-
- We chop the prefix to get a real zmq address.
- """
- self.socket = ctx.socket(zmq.REQ)
- self.socket.connect(cwproto_to_zmqaddr(zmq_address))
-
- def __zmqcall__(self, name, *args, **kwargs):
- self.socket.send_pyobj([name, args, kwargs])
- result = self.socket.recv_pyobj()
- if isinstance(result, BaseException):
- raise result
- return result
-
- def __getattr__(self, name):
- return partial(self.__zmqcall__, name)