--- a/__init__.py Sun Nov 30 21:24:36 2014 +0100
+++ b/__init__.py Mon Dec 01 11:13:10 2014 +0100
@@ -42,12 +42,10 @@
from logilab.common.deprecation import deprecated
from logilab.common.logging_ext import set_log_methods
-from yams.constraints import BASE_CONVERTERS
+from yams.constraints import BASE_CONVERTERS, BASE_CHECKERS
-if os.environ.get('APYCOT_ROOT'):
- logging.basicConfig(level=logging.CRITICAL)
-else:
- logging.basicConfig()
+# pre python 2.7.2 safety
+logging.basicConfig()
from cubicweb.__pkginfo__ import version as __version__
@@ -142,6 +140,10 @@
return cPickle.loads(zlib.decompress(self.getvalue()))
+def check_password(eschema, value):
+ return isinstance(value, (str, Binary))
+BASE_CHECKERS['Password'] = check_password
+
def str_or_binary(value):
if isinstance(value, Binary):
return value
--- a/__pkginfo__.py Sun Nov 30 21:24:36 2014 +0100
+++ b/__pkginfo__.py Mon Dec 01 11:13:10 2014 +0100
@@ -42,7 +42,7 @@
'logilab-common': '>= 0.62.0',
'logilab-mtconverter': '>= 0.8.0',
'rql': '>= 0.31.2',
- 'yams': '>= 0.39.1, < 0.39.99', # CW 3.19 is not compatible with yams 0.40
+ 'yams': '>= 0.40.0',
#gettext # for xgettext, msgcat, etc...
# web dependencies
'lxml': '',
@@ -51,6 +51,7 @@
# server dependencies
'logilab-database': '>= 1.12.1',
'passlib': '',
+ 'Markdown': ''
}
__recommends__ = {
--- a/cubicweb.spec Sun Nov 30 21:24:36 2014 +0100
+++ b/cubicweb.spec Mon Dec 01 11:13:10 2014 +0100
@@ -23,11 +23,12 @@
Requires: %{python}-logilab-common >= 0.62.0
Requires: %{python}-logilab-mtconverter >= 0.8.0
Requires: %{python}-rql >= 0.31.2
-Requires: %{python}-yams >= 0.39.1
+Requires: %{python}-yams >= 0.40.0
Requires: %{python}-logilab-database >= 1.12.1
Requires: %{python}-passlib
Requires: %{python}-lxml
Requires: %{python}-twisted-web
+Requires: %{python}-markdown
# the schema view uses `dot'; at least on el5, png output requires graphviz-gd
Requires: graphviz-gd
Requires: gettext
--- a/cwconfig.py Sun Nov 30 21:24:36 2014 +0100
+++ b/cwconfig.py Mon Dec 01 11:13:10 2014 +0100
@@ -278,7 +278,7 @@
}),
('default-text-format',
{'type' : 'choice',
- 'choices': ('text/plain', 'text/rest', 'text/html'),
+ 'choices': ('text/plain', 'text/rest', 'text/html', 'text/markdown'),
'default': 'text/html', # use fckeditor in the web ui
'help': _('default text format for rich text fields.'),
'group': 'ui',
@@ -827,13 +827,6 @@
else:
_INSTANCES_DIR = join(_INSTALL_PREFIX, 'etc', 'cubicweb.d')
- if os.environ.get('APYCOT_ROOT'):
- _cubes_init = join(CubicWebNoAppConfiguration.CUBES_DIR, '__init__.py')
- if not exists(_cubes_init):
- file(join(_cubes_init), 'w').close()
- if not exists(_INSTANCES_DIR):
- os.makedirs(_INSTANCES_DIR)
-
# set to true during repair (shell, migration) to allow some things which
# wouldn't be possible otherwise
repairing = False
--- a/cwctl.py Sun Nov 30 21:24:36 2014 +0100
+++ b/cwctl.py Mon Dec 01 11:13:10 2014 +0100
@@ -836,6 +836,8 @@
config = cwcfg.config_for(appid)
# should not raise error if db versions don't match fs versions
config.repairing = True
+ # no need to load all appobjects and schema
+ config.quick_start = True
if hasattr(config, 'set_sources_mode'):
config.set_sources_mode(('migration',))
repo = config.migration_handler().repo_connect()
--- a/dataimport.py Sun Nov 30 21:24:36 2014 +0100
+++ b/dataimport.py Mon Dec 01 11:13:10 2014 +0100
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# 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.
@@ -49,12 +49,7 @@
GENERATORS.append( (gen_users, CHK) )
# create controller
- if 'cnx' in globals():
- ctl = CWImportController(RQLObjectStore(cnx))
- else:
- print 'debug mode (not connected)'
- print 'run through cubicweb-ctl shell to access an instance'
- ctl = CWImportController(ObjectStore())
+ ctl = CWImportController(RQLObjectStore(cnx))
ctl.askerror = 1
ctl.generators = GENERATORS
ctl.data['utilisateurs'] = lazytable(ucsvreader(open('users.csv')))
@@ -76,7 +71,7 @@
import inspect
from collections import defaultdict
from copy import copy
-from datetime import date, datetime
+from datetime import date, datetime, time
from time import asctime
from StringIO import StringIO
@@ -425,16 +420,87 @@
cnx.commit()
cu.close()
-def _create_copyfrom_buffer(data, columns, encoding='utf-8', replace_sep=None):
+
+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...
+ 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 isinstance(data[0], (tuple, list)):
- columns = range(len(data[0]))
+ 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.
@@ -444,43 +510,19 @@
try:
value = row[col]
except KeyError:
- warnings.warn(u"Column %s is not accessible in row %s"
+ 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
+ # 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
- if value is None:
- value = 'NULL'
- elif isinstance(value, (long, int, float)):
- value = str(value)
- elif isinstance(value, (str, unicode)):
- # 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 instead of returning None
- # (and thus avoid empty buffer)
- if replace_sep:
- value = value.replace(_char, replace_sep)
- else:
- return
- value = value.replace('\\', r'\\')
- if value is None:
- return
- if isinstance(value, unicode):
- value = value.encode(encoding)
- elif isinstance(value, (date, datetime)):
- value = '%04d-%02d-%02d' % (value.year,
- value.month,
- value.day)
- if isinstance(value, datetime):
- value += ' %02d:%02d:%02d' % (value.hour,
- value.minutes,
- value.second)
+ for types, converter in _COPYFROM_BUFFER_CONVERTERS:
+ if isinstance(value, types):
+ value = converter(value, **convert_opts)
+ break
else:
- return None
+ 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)
@@ -506,27 +548,15 @@
self.types = {}
self.relations = set()
self.indexes = {}
- self._rql = None
- self._commit = None
-
- def _put(self, type, item):
- self.items.append(item)
- return len(self.items) - 1
def create_entity(self, etype, **data):
data = attrdict(data)
- data['eid'] = eid = self._put(etype, data)
+ data['eid'] = eid = len(self.items)
+ self.items.append(data)
self.eids[eid] = data
self.types.setdefault(etype, []).append(eid)
return data
- @deprecated("[3.11] add is deprecated, use create_entity instead")
- def add(self, etype, item):
- assert isinstance(item, dict), 'item is not a dict but a %s' % type(item)
- data = self.create_entity(etype, **item)
- item['eid'] = data['eid']
- return item
-
def relate(self, eid_from, rtype, eid_to, **kwargs):
"""Add new relation"""
relation = eid_from, rtype, eid_to
@@ -534,32 +564,12 @@
return relation
def commit(self):
- """this commit method do nothing by default
-
- This is voluntary to use the frequent autocommit feature in CubicWeb
- when you are using hooks or another
-
- If you want override commit method, please set it by the
- constructor
- """
- pass
+ """this commit method does nothing by default"""
+ return
def flush(self):
- """The method is provided so that all stores share a common API.
- It just tries to call the commit method.
- """
- print 'starting flush'
- try:
- self.commit()
- except:
- print 'failed to flush'
- else:
- print 'flush done'
-
- def rql(self, *args):
- if self._rql is not None:
- return self._rql(*args)
- return []
+ """The method is provided so that all stores share a common API"""
+ pass
@property
def nb_inserted_entities(self):
@@ -573,62 +583,47 @@
class RQLObjectStore(ObjectStore):
"""ObjectStore that works with an actual RQL repository (production mode)"""
- _rql = None # bw compat
- def __init__(self, session=None, commit=None):
- ObjectStore.__init__(self)
- if session is None:
- sys.exit('please provide a session of run this script with cubicweb-ctl shell and pass cnx as session')
- if not hasattr(session, 'set_cnxset'):
- if hasattr(session, 'request'):
- # connection object
- cnx = session
- session = session.request()
- else: # object is already a request
- cnx = session.cnx
- session.set_cnxset = lambda : None
- commit = commit or cnx.commit
- else:
- session.set_cnxset()
- self.session = session
- self._commit = commit or session.commit
+ 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):
- txuuid = self._commit()
- self.session.set_cnxset()
- return txuuid
+ return self._commit()
def rql(self, *args):
- if self._rql is not None:
- return self._rql(*args)
- return self.session.execute(*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.session.create_entity(*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 _put(self, type, item):
- query = 'INSERT %s X' % type
- if item:
- query += ': ' + ', '.join('X %s %%(%s)s' % (k, k)
- for k in item)
- return self.rql(query, item)[0][0]
-
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 session.find(*args, **kwargs).entities() instead")
+ @deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
def find_entities(self, *args, **kwargs):
- return self.session.find(*args, **kwargs).entities()
+ return self._cnx.find(*args, **kwargs).entities()
- @deprecated("[3.19] use session.find(*args, **kwargs).one() instead")
+ @deprecated("[3.19] use cnx.find(*args, **kwargs).one() instead")
def find_one_entity(self, *args, **kwargs):
- return self.session.find(*args, **kwargs).one()
+ return self._cnx.find(*args, **kwargs).one()
# the import controller ########################################################
@@ -755,7 +750,6 @@
class NoHookRQLObjectStore(RQLObjectStore):
"""ObjectStore that works with an actual RQL repository (production mode)"""
- _rql = None # bw compat
def __init__(self, session, metagen=None, baseurl=None):
super(NoHookRQLObjectStore, self).__init__(session)
@@ -768,7 +762,6 @@
self._nb_inserted_entities = 0
self._nb_inserted_types = 0
self._nb_inserted_relations = 0
- self.rql = session.execute
# deactivate security
session.read_security = False
session.write_security = False
@@ -821,9 +814,6 @@
def nb_inserted_relations(self):
return self._nb_inserted_relations
- def _put(self, type, item):
- raise RuntimeError('use create entity')
-
class MetaGenerator(object):
META_RELATIONS = (META_RTYPES
@@ -1056,10 +1046,6 @@
nb_threads=self.nb_threads_statement,
support_copy_from=self.support_copy_from,
encoding=self.dbencoding)
- except:
- print 'failed to flush'
- else:
- print 'flush done'
finally:
_entities_sql.clear()
_relations_sql.clear()
--- a/dbapi.py Sun Nov 30 21:24:36 2014 +0100
+++ b/dbapi.py Mon Dec 01 11:13:10 2014 +0100
@@ -447,11 +447,8 @@
class LogCursor(Cursor):
"""override the standard cursor to log executed queries"""
- def execute(self, operation, parameters=None, eid_key=None, build_descr=True):
+ def execute(self, operation, parameters=None, build_descr=True):
"""override the standard cursor to log executed queries"""
- if eid_key is not None:
- warn('[3.8] eid_key is deprecated, you can safely remove this argument',
- DeprecationWarning, stacklevel=2)
tstart, cstart = time(), clock()
rset = Cursor.execute(self, operation, parameters, build_descr=build_descr)
self.connection.executed_queries.append((operation, parameters,
--- a/debian/control Sun Nov 30 21:24:36 2014 +0100
+++ b/debian/control Mon Dec 01 11:13:10 2014 +0100
@@ -15,7 +15,7 @@
python-unittest2 | python (>= 2.7),
python-logilab-mtconverter,
python-rql,
- python-yams (>= 0.39.1),
+ python-yams (>= 0.40.0),
python-lxml,
Standards-Version: 3.9.1
Homepage: http://www.cubicweb.org
@@ -152,7 +152,8 @@
gettext,
python-logilab-mtconverter (>= 0.8.0),
python-logilab-common (>= 0.62.0),
- python-yams (>= 0.39.1),
+ python-markdown,
+ python-yams (>= 0.40.0),
python-rql (>= 0.31.2),
python-lxml
Recommends:
--- a/devtools/fake.py Sun Nov 30 21:24:36 2014 +0100
+++ b/devtools/fake.py Mon Dec 01 11:13:10 2014 +0100
@@ -65,8 +65,8 @@
super(FakeRequest, self).__init__(*args, **kwargs)
self._session_data = {}
- def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
- super(FakeRequest, self).set_cookie(name, value, maxage, expires, secure)
+ def set_cookie(self, name, value, maxage=300, expires=None, secure=False, httponly=False):
+ super(FakeRequest, self).set_cookie(name, value, maxage, expires, secure, httponly)
cookie = self.get_response_header('Set-Cookie')
self._headers_in.setHeader('Cookie', cookie)
--- a/devtools/htmlparser.py Sun Nov 30 21:24:36 2014 +0100
+++ b/devtools/htmlparser.py Mon Dec 01 11:13:10 2014 +0100
@@ -88,8 +88,6 @@
try:
return etree.fromstring(pdata, self.parser)
except etree.XMLSyntaxError as exc:
- def save_in(fname=''):
- file(fname, 'w').write(data)
new_exc = AssertionError(u'invalid document: %s' % exc)
new_exc.position = exc.position
raise new_exc
@@ -176,23 +174,6 @@
return super(XMLSyntaxValidator, self)._parse(data)
-class XMLDemotingValidator(XMLValidator):
- """ some views produce html instead of xhtml, using demote_to_html
-
- this is typically related to the use of external dependencies
- which do not produce valid xhtml (google maps, ...)
- """
- __metaclass__ = class_deprecated
- __deprecation_warning__ = '[3.10] this is now handled in testlib.py'
-
- def preprocess_data(self, data):
- if data.startswith('<?xml'):
- self.parser = etree.XMLParser()
- else:
- self.parser = etree.HTMLParser()
- return data
-
-
class HTMLValidator(Validator):
def __init__(self):
--- a/devtools/testlib.py Sun Nov 30 21:24:36 2014 +0100
+++ b/devtools/testlib.py Mon Dec 01 11:13:10 2014 +0100
@@ -513,9 +513,9 @@
This method will be called by the database handler once the config has
been properly bootstrapped.
"""
- source = config.system_source_config
- cls.admlogin = unicode(source['db-user'])
- cls.admpassword = source['db-password']
+ admincfg = config.default_admin_config
+ cls.admlogin = unicode(admincfg['login'])
+ cls.admpassword = admincfg['password']
# uncomment the line below if you want rql queries to be logged
#config.global_set_option('query-log-file',
# '/tmp/test_rql_log.' + `os.getpid()`)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/3.20.rst Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,22 @@
+What's new in CubicWeb 3.20
+===========================
+
+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/book/en/_themes/cubicweb/layout.html Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,63 @@
+{% extends "basic/layout.html" %}
+
+{%- block extrahead %}
+<link rel="stylesheet" href="{{ pathto('_static/font-nobile.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
+<link rel="stylesheet" href="{{ pathto('_static/font-neuton.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
+<!--[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/book/en/_themes/cubicweb/static/cubicweb.css_t Mon Dec 01 11:13:10 2014 +0100
@@ -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/book/en/_themes/cubicweb/static/cubicweb.ico Mon Dec 01 11:13:10 2014 +0100
@@ -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/book/en/_themes/cubicweb/static/logo-cubicweb-small.svg Mon Dec 01 11:13:10 2014 +0100
@@ -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/book/en/_themes/cubicweb/static/logo-cubicweb.svg Mon Dec 01 11:13:10 2014 +0100
@@ -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/book/en/_themes/cubicweb/theme.conf Mon Dec 01 11:13:10 2014 +0100
@@ -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 =
--- a/doc/book/en/admin/setup.rst Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/admin/setup.rst Mon Dec 01 11:13:10 2014 +0100
@@ -139,7 +139,7 @@
.. _`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/>`_,
+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.
@@ -152,13 +152,15 @@
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>`_ from this source making
-sure to choose the correct architecture and version of Python.
+<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::
--- a/doc/book/en/annexes/faq.rst Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/annexes/faq.rst Mon Dec 01 11:13:10 2014 +0100
@@ -197,9 +197,6 @@
except NoSelectableObject:
continue
-Don't forget the 'from __future__ import with_statement' at the module
-top-level if you're using python 2.5.
-
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'>
--- a/doc/book/en/conf.py Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/conf.py Mon Dec 01 11:13:10 2014 +0100
@@ -52,8 +52,14 @@
# 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', 'logilab.common.sphinx_ext']
+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 = []
@@ -117,8 +123,9 @@
# 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 = 'standard_theme'
-html_theme_path = ['.']
+
+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
--- a/doc/book/en/devrepo/datamodel/baseschema.rst Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/devrepo/datamodel/baseschema.rst Mon Dec 01 11:13:10 2014 +0100
@@ -22,7 +22,7 @@
Entity types used to manage workflows
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* _`Workflow`, workflow entity, linked to some entity types which may use this workflow
+* :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
--- a/doc/book/en/devrepo/datamodel/definition.rst Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/devrepo/datamodel/definition.rst Mon Dec 01 11:13:10 2014 +0100
@@ -1,4 +1,4 @@
- .. -*- coding: utf-8 -*-
+.. -*- coding: utf-8 -*-
.. _datamodel_definition:
@@ -523,6 +523,202 @@
.. _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(ComputedRelationType):
+ 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
-------------------------------
--- a/doc/book/en/devrepo/index.rst Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/devrepo/index.rst Mon Dec 01 11:13:10 2014 +0100
@@ -22,4 +22,4 @@
migration.rst
profiling.rst
fti.rst
-
+ dataimport
--- a/doc/book/en/devweb/views/embedding.rst Sun Nov 30 21:24:36 2014 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Embedding external pages
-------------------------
-
-(:mod:`cubicweb.web.views.embedding`)
-
-including external content
-
--- a/doc/book/en/standard_theme/layout.html Sun Nov 30 21:24:36 2014 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-{% extends "basic/layout.html" %}
-
-{% block header %}
-<div class="header">
- <a href="http://www.cubicweb.org">
- <img alt="cubicweb logo" src="{{ pathto('_static/cubicweb.png', 1) }}"/>
- </a>
-</div>
-{% endblock %}
-
-{# puts the sidebar into "sidebar1" block i.e. before the document body #}
-{% block sidebar1 %}{{ sidebar() }}{% endblock %}
-{% block sidebar2 %}{% endblock %}
Binary file doc/book/en/standard_theme/static/contents.png has changed
--- a/doc/book/en/standard_theme/static/lglb-sphinx-doc.css Sun Nov 30 21:24:36 2014 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,359 +0,0 @@
-/**
- * Sphinx stylesheet -- CubicWeb theme
- * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- *
- * Inspired from sphinxdoc original theme and logilab theme.
- */
-
-@import url("basic.css");
-
-/* -- page layout ----------------------------------------------------------- */
-
-body {
- font-family: 'Bitstream Vera Sans', 'Lucida Grande', 'Lucida Sans Unicode',
- 'Geneva', 'Verdana', sans-serif;
- font-size: 14px;
- line-height: 150%;
- text-align: center;
- padding: 0;
-}
-
-div.document {
- text-align: left;
-}
-
-div.bodywrapper {
- margin: 0 0 0 230px;
- border-left: 1px solid #CCBCA7;
-}
-
-div.body {
- margin: 0;
- padding: 0.5em 20px 20px 20px;
-}
-
-div.header {
- text-align: left;
- }
-
-div.related {
- background-color: #FF7700;
- color: white;
- font-weight: bolder;
- font-size: 1em;
-}
-
-div.related a {
- color: white;
-}
-
-div.related ul {
- height: 2em;
- border-top: 1px solid #CCBCA7;
- border-bottom: 1px solid #CCBCA7;
-}
-
-div.related ul li {
- margin: 0;
- padding: 0;
- height: 2em;
- float: left;
-}
-
-div.related ul li.right {
- float: right;
- margin-right: 5px;
-}
-
-div.related ul li a {
- margin: 0;
- padding: 0 5px 0 5px;
- line-height: 1.75em;
-}
-
-div.sphinxsidebarwrapper {
- padding: 0;
-}
-
-div.sphinxsidebar {
- margin: 0;
- padding: 5px 10px 5px 10px;
- width: 210px;
- float: left;
- font-size: 1em;
- text-align: left;
-}
-
-div.sphinxsidebar h3, div.sphinxsidebar h4 {
- font-size: 1.2em;
- font-style: italic;
-}
-
-div.sphinxsidebar ul {
- padding-left: 1.5em;
- margin-top: 15px;
- padding: 0;
- line-height: 130%;
- font-weight: bold;
-}
-
-div.sphinxsidebar ul ul {
- margin-left: 20px;
- font-weight: normal;
-}
-
-div.sphinxsidebar li {
- margin: 0;
-}
-
-div.sphinxsidebar input {
- border: 1px solid #CCBCA7;
- font-family: sans-serif;
- font-size: 1em;
-}
-
-div.footer {
- color: orangered;
- padding: 3px 8px 3px 0;
- clear: both;
- font-size: 0.8em;
- text-align: center;
-}
-
-div.footer a {
- text-decoration: underline;
-}
-
-/* -- body styles ----------------------------------------------------------- */
-
-p {
- margin: 0.8em 0 0 0;
-}
-
-ul, ol {
- margin: 0;
-}
-
-li {
- margin: 0.2em 0 0 0;
-}
-
-a {
- color: orangered;
- text-decoration: none;
-}
-
-div.sphinxsidebar a {
- color: black;
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-h1 {
- margin: 0;
- padding: 0.7em 0 0.3em 0;
- font-size: 1.5em;
- border-bottom: 1px dotted;
-}
-
-h2 {
- margin: 1.3em 0 0.2em 0;
- font-size: 1.35em;
- padding: 0;
- color: #303030;
-}
-
-h3 {
- margin: 1em 0 -0.3em 0;
- font-size: 1.2em;
- color: #202020;
-}
-
-div.body h1 a {
- color: #404040!important;
-}
-
-div.body h2 a {
- color: #303030!important;
-}
-
-div.body h3 a {
- color: #202020!important;
-}
-
-div.body h4 a, div.body h5 a, div.body h6 a {
- color: #000000!important;
-}
-
-h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor {
- display: none;
- margin: 0 0 0 0.3em;
- padding: 0 0.2em 0 0.2em;
- color: #AAA!important;
-}
-
-h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor,
-h5:hover a.anchor, h6:hover a.anchor {
- display: inline;
-}
-
-h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover,
-h5 a.anchor:hover, h6 a.anchor:hover {
- color: #777;
- background-color: #EEE;
-}
-
-a.headerlink {
- color: #C60F0F!important;
- font-size: 1em;
- margin-left: 6px;
- padding: 0 4px 0 4px;
- text-decoration: none!important;
-}
-
-a.headerlink:hover {
- background-color: #C0C0C0;
- color: #FFFFFF!important;
-}
-
-cite, code, tt {
- font-family: 'Consolas', 'Deja Vu Sans Mono',
- 'Bitstream Vera Sans Mono', monospace;
- font-size: 0.95em;
- letter-spacing: 0.01em;
-}
-
-tt {
- background-color: #F0F0F0;
- border-bottom: 1px solid #D0D0D0;
-}
-
-tt.descname, tt.descclassname, tt.xref {
- background-color: #F0F0F0;
- font-weight: normal;
- font-size: 1em;
- border: 1px solid #D0D0D0;
- border: 0;
-}
-
-hr {
- border: 1px solid #CC8B00;
- margin: 2em;
-}
-
-a tt {
- border: 0;
- color: #B45300;
-}
-
-a:hover tt {
- color: #4BACFF;
-}
-
-pre {
- font-family: 'Consolas', 'Deja Vu Sans Mono',
- 'Bitstream Vera Sans Mono', monospace;
- font-size: 0.95em;
- letter-spacing: 0.015em;
- line-height: 120%;
- padding: 0.5em;
- border: 1px solid #CCBCA7;
- background-color: #F0F0F0;
-}
-
-pre a {
- color: inherit;
- text-decoration: underline;
-}
-
-td.linenos pre {
- padding: 0.5em 0;
-}
-
-div.quotebar {
- background-color: #F8F8F8;
- max-width: 250px;
- float: right;
- padding: 2px 7px;
- border: 1px solid #C0C0C0;
-}
-
-div.topic {
- background-color: #F8F8F8;
-}
-
-table {
- border-collapse: collapse;
- margin: 0.8em -0.5em 0em -0.5em;
-}
-
-table td, table th {
- padding: 0.2em 0.5em 0.2em 0.5em;
-}
-
-div.admonition, div.warning {
- font-size: 0.9em;
- margin: 1em 0 1em 0;
- padding: 0;
-}
-
-div.admonition {
- border: 1px solid #86989B;
- background-color: #EBEBFF;
-}
-
-div.warning {
- border: 1px solid #940000;
- background-color: #FFEBEB;
-}
-
-div.admonition p, div.warning p {
- margin: 0.5em 1em 0.5em 1em;
- padding: 0;
-}
-
-div.admonition pre, div.warning pre {
- margin: 0.4em 1em 0.4em 1em;
-}
-
-div.admonition p.admonition-title,
-div.warning p.admonition-title {
- margin: 0;
- padding: 0.1em 0 0.1em 0.5em;
- color: #FFFFFF;
- font-weight: bold;
-}
-
-div.admonition p.admonition-title {
- border-bottom: 1px solid #86989B;
- background-color: #8C88B5;
-}
-
-div.warning p.admonition-title {
- background-color: #CF0000;
- border-bottom: 1px solid #940000;
-}
-
-div.admonition ul, div.admonition ol,
-div.warning ul, div.warning ol {
- margin: 0.1em 0.5em 0.5em 3em;
- padding: 0;
-}
-
-div.versioninfo {
- margin: 1em 0 0 0;
- border: 1px solid #C0C0C0;
- background-color: #DDEAF0;
- padding: 8px;
- line-height: 1.3em;
- font-size: 0.9em;
-}
-
-/* TOC trees */
-
-li.toctree-l1 {
- margin-top: 0.4em;
- }
\ No newline at end of file
Binary file doc/book/en/standard_theme/static/logilab_logo.png has changed
Binary file doc/book/en/standard_theme/static/navigation.png has changed
--- a/doc/book/en/standard_theme/theme.conf Sun Nov 30 21:24:36 2014 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = lglb-sphinx-doc.css
-pygments_style = friendly
--- a/doc/book/en/tutorials/textreports/index.rst Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/book/en/tutorials/textreports/index.rst Mon Dec 01 11:13:10 2014 +0100
@@ -8,6 +8,6 @@
Three additional restructuredtext roles are defined by |cubicweb|:
-.. autodocfunction:: cubicweb.ext.rest.eid_reference_role
-.. autodocfunction:: cubicweb.ext.rest.rql_role
-.. autodocfunction:: cubicweb.ext.rest.bookmark_role
+.. autofunction:: cubicweb.ext.rest.eid_reference_role
+.. autofunction:: cubicweb.ext.rest.rql_role
+.. autofunction:: cubicweb.ext.rest.bookmark_role
--- a/doc/tools/pyjsrest.py Sun Nov 30 21:24:36 2014 +0100
+++ b/doc/tools/pyjsrest.py Mon Dec 01 11:13:10 2014 +0100
@@ -2,8 +2,6 @@
"""
Parser for Javascript comments.
"""
-from __future__ import with_statement
-
import os.path as osp
import sys, os, getopt, re
--- a/entities/__init__.py Sun Nov 30 21:24:36 2014 +0100
+++ b/entities/__init__.py Mon Dec 01 11:13:10 2014 +0100
@@ -31,7 +31,6 @@
instances have access to their issuing cursor
"""
__regid__ = 'Any'
- __implements__ = ()
@classproperty
def cw_etype(cls):
--- a/entity.py Sun Nov 30 21:24:36 2014 +0100
+++ b/entity.py Mon Dec 01 11:13:10 2014 +0100
@@ -22,7 +22,6 @@
from warnings import warn
from functools import partial
-from logilab.common import interface
from logilab.common.decorators import cached
from logilab.common.deprecation import deprecated
from logilab.common.registry import yes
@@ -520,7 +519,11 @@
rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
else:
rql = 'INSERT %s X' % (cls.__regid__)
- created = execute(rql, qargs).get_entity(0, 0)
+ try:
+ created = execute(rql, qargs).get_entity(0, 0)
+ 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
@@ -1367,100 +1370,6 @@
def clear_all_caches(self):
return self.cw_clear_all_caches()
- @property
- @deprecated('[3.10] use entity.cw_edited')
- def edited_attributes(self):
- return self.cw_edited
-
- @property
- @deprecated('[3.10] use entity.cw_edited.skip_security')
- def skip_security_attributes(self):
- return self.cw_edited.skip_security
-
- @property
- @deprecated('[3.10] use entity.cw_edited.skip_security')
- def _cw_skip_security_attributes(self):
- return self.cw_edited.skip_security
-
- @property
- @deprecated('[3.10] use entity.cw_edited.querier_pending_relations')
- def querier_pending_relations(self):
- return self.cw_edited.querier_pending_relations
-
- @deprecated('[3.10] use key in entity.cw_attr_cache')
- def __contains__(self, key):
- return key in self.cw_attr_cache
-
- @deprecated('[3.10] iter on entity.cw_attr_cache')
- def __iter__(self):
- return iter(self.cw_attr_cache)
-
- @deprecated('[3.10] use entity.cw_attr_cache[attr]')
- def __getitem__(self, key):
- return self.cw_attr_cache[key]
-
- @deprecated('[3.10] use entity.cw_attr_cache.get(attr[, default])')
- def get(self, key, default=None):
- return self.cw_attr_cache.get(key, default)
-
- @deprecated('[3.10] use entity.cw_attr_cache.clear()')
- def clear(self):
- self.cw_attr_cache.clear()
- # XXX clear cw_edited ?
-
- @deprecated('[3.10] use entity.cw_edited[attr] = value or entity.cw_attr_cache[attr] = value')
- def __setitem__(self, attr, value):
- """override __setitem__ to update self.cw_edited.
-
- Typically, a before_[update|add]_hook could do::
-
- entity['generated_attr'] = generated_value
-
- and this way, cw_edited will be updated accordingly. Also, add
- the attribute to skip_security since we don't want to check security
- for such attributes set by hooks.
- """
- try:
- self.cw_edited[attr] = value
- except AttributeError:
- self.cw_attr_cache[attr] = value
-
- @deprecated('[3.10] use del entity.cw_edited[attr]')
- def __delitem__(self, attr):
- """override __delitem__ to update self.cw_edited on cleanup of
- undesired changes introduced in the entity's dict. For example, see the
- code snippet below from the `forge` cube:
-
- .. sourcecode:: python
-
- edited = self.entity.cw_edited
- has_load_left = 'load_left' in edited
- if 'load' in edited and self.entity.load_left is None:
- self.entity.load_left = self.entity['load']
- elif not has_load_left and edited:
- # cleanup, this may cause undesired changes
- del self.entity['load_left']
- """
- del self.cw_edited[attr]
-
- @deprecated('[3.10] use entity.cw_edited.setdefault(attr, default)')
- def setdefault(self, attr, default):
- """override setdefault to update self.cw_edited"""
- return self.cw_edited.setdefault(attr, default)
-
- @deprecated('[3.10] use entity.cw_edited.pop(attr[, default])')
- def pop(self, attr, *args):
- """override pop to update self.cw_edited on cleanup of
- undesired changes introduced in the entity's dict. See `__delitem__`
- """
- return self.cw_edited.pop(attr, *args)
-
- @deprecated('[3.10] use entity.cw_edited.update(values)')
- def update(self, values):
- """override update to update self.cw_edited. See `__setitem__`
- """
- self.cw_edited.update(values)
-
# attribute and relation descriptors ##########################################
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/markdown.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,27 @@
+from __future__ import absolute_import
+import markdown
+
+import logging
+
+log = logging.getLogger(__name__)
+
+
+def markdown_publish(context, data):
+ """publish a string formatted as MarkDown Text to HTML
+
+ :type context: a cubicweb application object
+
+ :type data: str
+ :param data: some MarkDown text
+
+ :rtype: unicode
+ :return:
+ the data formatted as HTML or the original data if an error occurred
+ """
+ md = markdown.Markdown()
+ try:
+ return md.convert(data)
+ except:
+ import traceback; traceback.print_exc()
+ log.exception("Error while converting Markdown to HTML")
+ return data
--- a/ext/rest.py Sun Nov 30 21:24:36 2014 +0100
+++ b/ext/rest.py Mon Dec 01 11:13:10 2014 +0100
@@ -99,9 +99,9 @@
**options)], []
def rql_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
- """:rql:`<rql-expr>` or :rql:`<rql-expr>:<vid>`
+ """``:rql:`<rql-expr>``` or ``:rql:`<rql-expr>:<vid>```
- Example: :rql:`Any X,Y WHERE X is CWUser, X login Y:table`
+ Example: ``:rql:`Any X,Y WHERE X is CWUser, X login Y:table```
Replace the directive with the output of applying the view to the resultset
returned by the query.
@@ -132,9 +132,9 @@
return [nodes.raw('', content, format='html')], []
def bookmark_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
- """:bookmark:`<bookmark-eid>` or :bookmark:`<eid>:<vid>`
+ """``:bookmark:`<bookmark-eid>``` or ``:bookmark:`<eid>:<vid>```
- Example: :bookmark:`1234:table`
+ Example: ``:bookmark:`1234:table```
Replace the directive with the output of applying the view to the resultset
returned by the query stored in the bookmark. By default, the view is the one
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/synccomputed.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,227 @@
+# 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/>.
+"""Hooks for synchronizing computed attributes"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from collections import defaultdict
+
+from rql import nodes
+
+from cubicweb.server import hook
+
+
+class RecomputeAttributeOperation(hook.DataOperationMixIn, hook.Operation):
+ """Operation to recompute caches of computed attribute at commit time,
+ depending on what's have been modified in the transaction and avoiding to
+ recompute twice the same attribute
+ """
+ containercls = dict
+ def add_data(self, computed_attribute, eid=None):
+ try:
+ self._container[computed_attribute].add(eid)
+ except KeyError:
+ self._container[computed_attribute] = set((eid,))
+
+ def precommit_event(self):
+ for computed_attribute_rdef, eids in self.get_data().iteritems():
+ attr = computed_attribute_rdef.rtype
+ formula = computed_attribute_rdef.formula
+ rql = formula.replace('Any ', 'Any X, ', 1)
+ kwargs = None
+ # add constraint on X to the formula
+ if None in eids : # recompute for all etype if None is found
+ rql += ', X is %s' % computed_attribute_rdef.subject
+ elif len(eids) == 1:
+ rql += ', X eid %(x)s'
+ kwargs = {'x': eids.pop()}
+ else:
+ rql += ', X eid IN (%s)' % ', '.join((str(eid) for eid in eids))
+ update_rql = 'SET X %s %%(value)s WHERE X eid %%(x)s' % attr
+ for eid, value in self.cnx.execute(rql, kwargs):
+ self.cnx.execute(update_rql, {'value': value, 'x': eid})
+
+
+class EntityWithCACreatedHook(hook.Hook):
+ """When creating an entity that has some computed attribute, those
+ attributes have to be computed.
+
+ Concret class of this hook are generated at registration time by
+ introspecting the schema.
+ """
+ __abstract__ = True
+ events = ('after_add_entity',)
+ # list of computed attribute rdefs that have to be recomputed
+ computed_attributes = None
+
+ def __call__(self):
+ for rdef in self.computed_attributes:
+ RecomputeAttributeOperation.get_instance(self._cw).add_data(
+ rdef, self.entity.eid)
+
+
+class RelationInvolvedInCAModifiedHook(hook.Hook):
+ """When some relation used in a computed attribute is updated, those
+ attributes have to be recomputed.
+
+ Concret class of this hook are generated at registration time by
+ introspecting the schema.
+ """
+ __abstract__ = True
+ events = ('after_add_relation', 'before_delete_relation')
+ # list of (computed attribute rdef, optimize_on) that have to be recomputed
+ optimized_computed_attributes = None
+
+ def __call__(self):
+ for rdef, optimize_on in self.optimized_computed_attributes:
+ if optimize_on is None:
+ eid = None
+ else:
+ eid = getattr(self, optimize_on)
+ RecomputeAttributeOperation.get_instance(self._cw).add_data(rdef, eid)
+
+
+class AttributeInvolvedInCAModifiedHook(hook.Hook):
+ """When some attribute used in a computed attribute is updated, those
+ attributes have to be recomputed.
+
+ Concret class of this hook are generated at registration time by
+ introspecting the schema.
+ """
+ __abstract__ = True
+ events = ('after_update_entity',)
+ # list of (computed attribute rdef, attributes of this entity type involved)
+ # that may have to be recomputed
+ attributes_computed_attributes = None
+
+ def __call__(self):
+ edited_attributes = frozenset(self.entity.cw_edited)
+ for rdef, used_attributes in self.attributes_computed_attributes.iteritems():
+ if edited_attributes.intersection(used_attributes):
+ # XXX optimize if the modified attributes belong to the same
+ # entity as the computed attribute
+ RecomputeAttributeOperation.get_instance(self._cw).add_data(rdef)
+
+
+# code generation at registration time #########################################
+
+def _optimize_on(formula_select, rtype):
+ """Given a formula and some rtype, tells whether on update of the given
+ relation, formula may be recomputed only for rhe relation's subject
+ ('eidfrom' returned), object ('eidto' returned) or None.
+
+ Optimizing is only possible when X is used as direct subject/object of this
+ relation, else we may miss some necessary update.
+ """
+ for rel in formula_select.get_nodes(nodes.Relation):
+ if rel.r_type == rtype:
+ sub = rel.get_variable_parts()[0]
+ obj = rel.get_variable_parts()[1]
+ if sub.name == 'X':
+ return 'eidfrom'
+ elif obj.name == 'X':
+ return 'eidto'
+ else:
+ return None
+
+
+class _FormulaDependenciesMatrix(object):
+ """This class computes and represents the dependencies of computed attributes
+ towards relations and attributes
+ """
+
+ def __init__(self, schema):
+ """Analyzes the schema to compute the dependencies"""
+ # entity types holding some computed attribute {etype: [computed rdefs]}
+ self.computed_attribute_by_etype = defaultdict(list)
+ # depending entity types {dep. etype: {computed rdef: dep. etype attributes}}
+ self.computed_attribute_by_etype_attrs = defaultdict(lambda: defaultdict(set))
+ # depending relations def {dep. rdef: [computed rdefs]
+ self.computed_attribute_by_relation = defaultdict(list) # by rdef
+ # Walk through all attributes definitions
+ for rdef in schema.iter_computed_attributes():
+ self.computed_attribute_by_etype[rdef.subject.type].append(rdef)
+ # extract the relations it depends upon - `rdef.formula_select` is
+ # expected to have been set by finalize_computed_attributes
+ select = rdef.formula_select
+ for rel_node in select.get_nodes(nodes.Relation):
+ rschema = schema.rschema(rel_node.r_type)
+ lhs, rhs = rel_node.get_variable_parts()
+ for sol in select.solutions:
+ subject_etype = sol[lhs.name]
+ if isinstance(rhs, nodes.VariableRef):
+ object_etypes = set(sol[rhs.name] for sol in select.solutions)
+ else:
+ object_etypes = rschema.objects(subject_etype)
+ for object_etype in object_etypes:
+ if rschema.final:
+ attr_for_computations = self.computed_attribute_by_etype_attrs[subject_etype]
+ attr_for_computations[rdef].add(rschema.type)
+ else:
+ depend_on_rdef = rschema.rdefs[subject_etype, object_etype]
+ self.computed_attribute_by_relation[depend_on_rdef].append(rdef)
+
+ def generate_entity_creation_hooks(self):
+ for etype, computed_attributes in self.computed_attribute_by_etype.iteritems():
+ regid = 'computed_attribute.%s_created' % etype
+ selector = hook.is_instance(etype)
+ yield type('%sCreatedHook' % etype,
+ (EntityWithCACreatedHook,),
+ {'__regid__': regid,
+ '__select__': hook.Hook.__select__ & selector,
+ 'computed_attributes': computed_attributes})
+
+ def generate_relation_change_hooks(self):
+ for rdef, computed_attributes in self.computed_attribute_by_relation.iteritems():
+ regid = 'computed_attribute.%s_modified' % rdef.rtype
+ selector = hook.match_rtype(rdef.rtype.type,
+ frometypes=(rdef.subject.type,),
+ toetypes=(rdef.object.type,))
+ optimized_computed_attributes = []
+ for computed_rdef in computed_attributes:
+ optimized_computed_attributes.append(
+ (computed_rdef,
+ _optimize_on(computed_rdef.formula_select, rdef.rtype))
+ )
+ yield type('%sModifiedHook' % rdef.rtype,
+ (RelationInvolvedInCAModifiedHook,),
+ {'__regid__': regid,
+ '__select__': hook.Hook.__select__ & selector,
+ 'optimized_computed_attributes': optimized_computed_attributes})
+
+ def generate_entity_update_hooks(self):
+ for etype, attributes_computed_attributes in self.computed_attribute_by_etype_attrs.iteritems():
+ regid = 'computed_attribute.%s_updated' % etype
+ selector = hook.is_instance(etype)
+ yield type('%sModifiedHook' % etype,
+ (AttributeInvolvedInCAModifiedHook,),
+ {'__regid__': regid,
+ '__select__': hook.Hook.__select__ & selector,
+ 'attributes_computed_attributes': attributes_computed_attributes})
+
+
+def registration_callback(vreg):
+ vreg.register_all(globals().values(), __name__)
+ dependencies = _FormulaDependenciesMatrix(vreg.schema)
+ for hook_class in dependencies.generate_entity_creation_hooks():
+ vreg.register(hook_class)
+ for hook_class in dependencies.generate_relation_change_hooks():
+ vreg.register(hook_class)
+ for hook_class in dependencies.generate_entity_update_hooks():
+ vreg.register(hook_class)
--- a/hooks/syncschema.py Sun Nov 30 21:24:36 2014 +0100
+++ b/hooks/syncschema.py Mon Dec 01 11:13:10 2014 +0100
@@ -27,7 +27,8 @@
_ = unicode
from copy import copy
-from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
+from yams.schema import (BASE_TYPES, BadSchemaDefinition,
+ RelationSchema, RelationDefinitionSchema)
from yams import buildobjs as ybo, schema2sql as y2sql, convert_default_value
from logilab.common.decorators import clear_cache
@@ -38,6 +39,7 @@
CONSTRAINTS, ETYPE_NAME_MAP, display_name)
from cubicweb.server import hook, schemaserial as ss
from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.hooks.synccomputed import RecomputeAttributeOperation
# core entity and relation types which can't be removed
CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
@@ -70,14 +72,14 @@
table = SQL_PREFIX + etype
column = SQL_PREFIX + rtype
try:
- cnx.system_sql(str('ALTER TABLE %s ADD %s integer'
- % (table, column)), rollback_on_failure=False)
+ cnx.system_sql(str('ALTER TABLE %s ADD %s integer' % (table, column)),
+ rollback_on_failure=False)
cnx.info('added column %s to table %s', column, table)
except Exception:
# silent exception here, if this error has not been raised because the
# column already exists, index creation will fail anyway
cnx.exception('error while adding column %s to table %s',
- table, column)
+ table, column)
# create index before alter table which may expectingly fail during test
# (sqlite) while index creation should never fail (test for index existence
# is done by the dbhelper)
@@ -166,8 +168,8 @@
# drop index if any
source.drop_index(cnx, table, column)
if source.dbhelper.alter_column_support:
- cnx.system_sql('ALTER TABLE %s DROP COLUMN %s'
- % (table, column), rollback_on_failure=False)
+ cnx.system_sql('ALTER TABLE %s DROP COLUMN %s' % (table, column),
+ rollback_on_failure=False)
self.info('dropped column %s from table %s', column, table)
else:
# not supported by sqlite for instance
@@ -307,7 +309,7 @@
class CWRTypeUpdateOp(MemSchemaOperation):
"""actually update some properties of a relation definition"""
rschema = entity = values = None # make pylint happy
- oldvalus = None
+ oldvalues = None
def precommit_event(self):
rschema = self.rschema
@@ -388,6 +390,21 @@
# XXX revert changes on database
+class CWComputedRTypeUpdateOp(MemSchemaOperation):
+ """actually update some properties of a computed relation definition"""
+ rschema = entity = rule = None # make pylint happy
+ old_rule = None
+
+ def precommit_event(self):
+ # update the in-memory schema first
+ self.old_rule = self.rschema.rule
+ self.rschema.rule = self.rule
+
+ def revertprecommit_event(self):
+ # revert changes on in memory schema
+ self.rschema.rule = self.old_rule
+
+
class CWAttributeAddOp(MemSchemaOperation):
"""an attribute relation (CWAttribute) has been added:
* add the necessary column
@@ -407,12 +424,19 @@
description=entity.description, cardinality=entity.cardinality,
constraints=get_constraints(self.cnx, entity),
order=entity.ordernum, eid=entity.eid, **kwargs)
- self.cnx.vreg.schema.add_relation_def(rdefdef)
+ try:
+ self.cnx.vreg.schema.add_relation_def(rdefdef)
+ except BadSchemaDefinition:
+ # rdef has been infered then explicitly added (current consensus is
+ # not clear at all versus infered relation handling (and much
+ # probably buggy)
+ rdef = self.cnx.vreg.schema.rschema(rdefdef.name).rdefs[rdefdef.subject, rdefdef.object]
+ assert rdef.infered
self.cnx.execute('SET X ordernum Y+1 '
- 'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
- 'X ordernum >= %(order)s, NOT X eid %(x)s',
- {'x': entity.eid, 'se': fromentity.eid,
- 'order': entity.ordernum or 0})
+ 'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
+ 'X ordernum >= %(order)s, NOT X eid %(x)s',
+ {'x': entity.eid, 'se': fromentity.eid,
+ 'order': entity.ordernum or 0})
return rdefdef
def precommit_event(self):
@@ -427,6 +451,9 @@
'indexed': entity.indexed,
'fulltextindexed': entity.fulltextindexed,
'internationalizable': entity.internationalizable}
+ # entity.formula may not exist yet if we're migrating to 3.20
+ if hasattr(entity, 'formula'):
+ props['formula'] = entity.formula
# update the in-memory schema first
rdefdef = self.init_rdef(**props)
# then make necessary changes to the system source database
@@ -447,8 +474,8 @@
column = SQL_PREFIX + rdefdef.name
try:
cnx.system_sql(str('ALTER TABLE %s ADD %s %s'
- % (table, column, attrtype)),
- rollback_on_failure=False)
+ % (table, column, attrtype)),
+ rollback_on_failure=False)
self.info('added column %s to table %s', column, table)
except Exception as ex:
# the column probably already exists. this occurs when
@@ -479,6 +506,12 @@
default = convert_default_value(self.rdefdef, default)
cnx.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
{'default': default})
+ # if attribute is computed, compute it
+ if getattr(entity, 'formula', None):
+ # add rtype attribute for RelationDefinitionSchema api compat, this
+ # is what RecomputeAttributeOperation expect
+ rdefdef.rtype = rdefdef.name
+ RecomputeAttributeOperation.get_instance(cnx).add_data(rdefdef)
def revertprecommit_event(self):
# revert changes on in memory schema
@@ -616,6 +649,8 @@
self.null_allowed_changed = True
if 'fulltextindexed' in self.values:
UpdateFTIndexOp.get_instance(cnx).add_data(rdef.subject)
+ if 'formula' in self.values:
+ RecomputeAttributeOperation.get_instance(cnx).add_data(rdef)
def revertprecommit_event(self):
if self.rdef is None:
@@ -977,7 +1012,26 @@
MemSchemaCWRTypeDel(self._cw, rtype=name)
-class AfterAddCWRTypeHook(DelCWRTypeHook):
+class AfterAddCWComputedRTypeHook(SyncSchemaHook):
+ """after a CWComputedRType entity has been added:
+ * register an operation to add the relation type to the instance's
+ schema on commit
+
+ We don't know yet this point if a table is necessary
+ """
+ __regid__ = 'syncaddcwcomputedrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+ events = ('after_add_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ rtypedef = ybo.ComputedRelation(name=entity.name,
+ eid=entity.eid,
+ rule=entity.rule)
+ MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
+
+
+class AfterAddCWRTypeHook(SyncSchemaHook):
"""after a CWRType entity has been added:
* register an operation to add the relation type to the instance's
schema on commit
@@ -985,6 +1039,7 @@
We don't know yet this point if a table is necessary
"""
__regid__ = 'syncaddcwrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
events = ('after_add_entity',)
def __call__(self):
@@ -997,9 +1052,10 @@
MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
-class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
+class BeforeUpdateCWRTypeHook(SyncSchemaHook):
"""check name change, handle final"""
__regid__ = 'syncupdatecwrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
events = ('before_update_entity',)
def __call__(self):
@@ -1017,6 +1073,23 @@
values=newvalues)
+class BeforeUpdateCWComputedRTypeHook(SyncSchemaHook):
+ """check name change, handle final"""
+ __regid__ = 'syncupdatecwcomputedrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+ events = ('before_update_entity',)
+
+ def __call__(self):
+ entity = self.entity
+ check_valid_changes(self._cw, entity)
+ if 'rule' in entity.cw_edited:
+ old, new = entity.cw_edited.oldnewvalue('rule')
+ if old != new:
+ rschema = self._cw.vreg.schema.rschema(entity.name)
+ CWComputedRTypeUpdateOp(self._cw, rschema=rschema,
+ entity=entity, rule=new)
+
+
class AfterDelRelationTypeHook(SyncSchemaHook):
"""before deleting a CWAttribute or CWRelation entity:
* if this is a final or inlined relation definition, instantiate an
@@ -1053,6 +1126,24 @@
RDefDelOp(cnx, rdef=rdef)
+# CWComputedRType hooks #######################################################
+
+class DelCWComputedRTypeHook(SyncSchemaHook):
+ """before deleting a CWComputedRType entity:
+ * check that we don't remove a core relation type
+ * instantiate an operation to delete the relation type on commit
+ """
+ __regid__ = 'syncdelcwcomputedrtype'
+ __select__ = SyncSchemaHook.__select__ & is_instance('CWComputedRType')
+ events = ('before_delete_entity',)
+
+ def __call__(self):
+ name = self.entity.name
+ if name in CORE_TYPES:
+ raise validation_error(self.entity, {None: _("can't be deleted")})
+ MemSchemaCWRTypeDel(self._cw, rtype=name)
+
+
# CWAttribute / CWRelation hooks ###############################################
class AfterAddCWAttributeHook(SyncSchemaHook):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/test/data-computed/schema.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,31 @@
+# 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/>.
+from yams.buildobjs import EntityType, String, Int, SubjectRelation
+
+THISYEAR = 2014
+
+class Person(EntityType):
+ name = String()
+ salaire = Int()
+ birth_year = Int(required=True)
+ travaille = SubjectRelation('Societe')
+ age = Int(formula='Any %d - D WHERE X birth_year D' % THISYEAR)
+
+class Societe(EntityType):
+ nom = String()
+ salaire_total = Int(formula='Any SUM(SA) GROUPBY X WHERE P travaille X, P salaire SA')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/test/unittest_synccomputed.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,139 @@
+# 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/>.
+"""unit tests for computed attributes/relations hooks"""
+
+from unittest import TestCase
+
+from yams.buildobjs import EntityType, String, Int, SubjectRelation
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.schema import build_schema_from_namespace
+
+
+class FormulaDependenciesMatrixTC(TestCase):
+
+ def simple_schema(self):
+ THISYEAR = 2014
+
+ class Person(EntityType):
+ name = String()
+ salary = Int()
+ birth_year = Int(required=True)
+ works_for = SubjectRelation('Company')
+ age = Int(formula='Any %d - D WHERE X birth_year D' % THISYEAR)
+
+ class Company(EntityType):
+ name = String()
+ total_salary = Int(formula='Any SUM(SA) GROUPBY X WHERE P works_for X, P salary SA')
+
+ schema = build_schema_from_namespace(vars().items())
+ return schema
+
+ def setUp(self):
+ from cubicweb.hooks.synccomputed import _FormulaDependenciesMatrix
+ self.schema = self.simple_schema()
+ self.dependencies = _FormulaDependenciesMatrix(self.schema)
+
+ def test_computed_attributes_by_etype(self):
+ comp_by_etype = self.dependencies.computed_attribute_by_etype
+ self.assertEqual(len(comp_by_etype), 2)
+ values = comp_by_etype['Person']
+ self.assertEqual(len(values), 1)
+ self.assertEqual(values[0].rtype, 'age')
+ values = comp_by_etype['Company']
+ self.assertEqual(len(values), 1)
+ self.assertEqual(values[0].rtype, 'total_salary')
+
+ def test_computed_attribute_by_relation(self):
+ comp_by_rdef = self.dependencies.computed_attribute_by_relation
+ self.assertEqual(len(comp_by_rdef), 1)
+ key, values = iter(comp_by_rdef.iteritems()).next()
+ self.assertEqual(key.rtype, 'works_for')
+ self.assertEqual(len(values), 1)
+ self.assertEqual(values[0].rtype, 'total_salary')
+
+ def test_computed_attribute_by_etype_attrs(self):
+ comp_by_attr = self.dependencies.computed_attribute_by_etype_attrs
+ self.assertEqual(len(comp_by_attr), 1)
+ values = comp_by_attr['Person']
+ self.assertEqual(len(values), 2)
+ values = set((rdef.formula, tuple(v))
+ for rdef, v in values.iteritems())
+ self.assertEquals(values,
+ set((('Any 2014 - D WHERE X birth_year D', tuple(('birth_year',))),
+ ('Any SUM(SA) GROUPBY X WHERE P works_for X, P salary SA', tuple(('salary',)))))
+ )
+
+
+class ComputedAttributeTC(CubicWebTC):
+ appid = 'data-computed'
+
+ def setup_entities(self, req):
+ self.societe = req.create_entity('Societe', nom=u'Foo')
+ req.create_entity('Person', name=u'Titi', salaire=1000,
+ travaille=self.societe, birth_year=2001)
+ self.tata = req.create_entity('Person', name=u'Tata', salaire=2000,
+ travaille=self.societe, birth_year=1990)
+
+
+ def test_update_on_add_remove_relation(self):
+ """check the rewriting of a computed attribute"""
+ with self.admin_access.web_request() as req:
+ self.setup_entities(req)
+ req.cnx.commit()
+ rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+ self.assertEqual(rset[0][0], 3000)
+ # Add relation.
+ toto = req.create_entity('Person', name=u'Toto', salaire=1500,
+ travaille=self.societe, birth_year=1988)
+ req.cnx.commit()
+ rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+ self.assertEqual(rset[0][0], 4500)
+ # Delete relation.
+ toto.cw_set(travaille=None)
+ req.cnx.commit()
+ rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+ self.assertEqual(rset[0][0], 3000)
+
+ def test_recompute_on_attribute_update(self):
+ """check the modification of an attribute triggers the update of the
+ computed attributes that depend on it"""
+ with self.admin_access.web_request() as req:
+ self.setup_entities(req)
+ req.cnx.commit()
+ rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+ self.assertEqual(rset[0][0], 3000)
+ # Update attribute.
+ self.tata.cw_set(salaire=1000)
+ req.cnx.commit()
+ rset = req.execute('Any S WHERE X salaire_total S, X nom "Foo"')
+ self.assertEqual(rset[0][0], 2000)
+
+ def test_init_on_entity_creation(self):
+ """check the computed attribute is initialized on entity creation"""
+ with self.admin_access.web_request() as req:
+ p = req.create_entity('Person', name=u'Tata', salaire=2000,
+ birth_year=1990)
+ req.cnx.commit()
+ rset = req.execute('Any A, X WHERE X age A, X name "Tata"')
+ self.assertEqual(rset[0][0], 2014 - 1990)
+
+
+if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
--- a/i18n/de.po Sun Nov 30 21:24:36 2014 +0100
+++ b/i18n/de.po Mon Dec 01 11:13:10 2014 +0100
@@ -483,6 +483,12 @@
msgid "Entities"
msgstr "Entitäten"
+#, python-format
+msgid ""
+"Entity %(eid)s has changed since you started to edit it. Reload the page and "
+"reapply your changes."
+msgstr ""
+
msgid "Entity and relation supported by this source"
msgstr ""
--- a/i18n/en.po Sun Nov 30 21:24:36 2014 +0100
+++ b/i18n/en.po Mon Dec 01 11:13:10 2014 +0100
@@ -461,6 +461,12 @@
msgid "Entities"
msgstr ""
+#, python-format
+msgid ""
+"Entity %(eid)s has changed since you started to edit it. Reload the page and "
+"reapply your changes."
+msgstr ""
+
msgid "Entity and relation supported by this source"
msgstr ""
--- a/i18n/es.po Sun Nov 30 21:24:36 2014 +0100
+++ b/i18n/es.po Mon Dec 01 11:13:10 2014 +0100
@@ -492,6 +492,12 @@
msgid "Entities"
msgstr "Entidades"
+#, python-format
+msgid ""
+"Entity %(eid)s has changed since you started to edit it. Reload the page and "
+"reapply your changes."
+msgstr ""
+
msgid "Entity and relation supported by this source"
msgstr "Entidades y relaciones aceptadas por esta fuente"
--- a/i18n/fr.po Sun Nov 30 21:24:36 2014 +0100
+++ b/i18n/fr.po Mon Dec 01 11:13:10 2014 +0100
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2012-02-15 16:08+0100\n"
+"PO-Revision-Date: 2014-06-24 13:29+0200\n"
"Last-Translator: Logilab Team <contact@logilab.fr>\n"
"Language-Team: fr <contact@logilab.fr>\n"
"Language: \n"
@@ -486,6 +486,12 @@
msgid "Entities"
msgstr "entités"
+#, python-format
+msgid ""
+"Entity %(eid)s has changed since you started to edit it. Reload the page and "
+"reapply your changes."
+msgstr "L'entité %(eid)s a été modifiée depuis votre demande d'édition. Veuillez recharger cette page et réappliquer vos changements."
+
msgid "Entity and relation supported by this source"
msgstr "Entités et relations supportés par cette source"
--- a/migration.py Sun Nov 30 21:24:36 2014 +0100
+++ b/migration.py Mon Dec 01 11:13:10 2014 +0100
@@ -247,12 +247,13 @@
local_ctx = self._create_context()
try:
import readline
- from rlcompleter import Completer
+ from cubicweb.toolsutils import CWShellCompleter
except ImportError:
# readline not available
pass
else:
- readline.set_completer(Completer(local_ctx).complete)
+ rql_completer = CWShellCompleter(local_ctx)
+ readline.set_completer(rql_completer.complete)
readline.parse_and_bind('tab: complete')
home_key = 'HOME'
if sys.platform == 'win32':
--- a/misc/migration/bootstrapmigration_repository.py Sun Nov 30 21:24:36 2014 +0100
+++ b/misc/migration/bootstrapmigration_repository.py Mon Dec 01 11:13:10 2014 +0100
@@ -63,6 +63,14 @@
replace_eid_sequence_with_eid_numrange(session)
+if applcubicwebversion < (3, 20, 0) and cubicwebversion >= (3, 20, 0):
+ ss._IGNORED_PROPS.append('formula')
+ add_attribute('CWAttribute', 'formula', commit=False)
+ ss._IGNORED_PROPS.remove('formula')
+ commit()
+ add_entity_type('CWComputedRType')
+ commit()
+
if applcubicwebversion < (3, 17, 0) and cubicwebversion >= (3, 17, 0):
try:
add_cube('sioc', update_database=False)
@@ -260,3 +268,18 @@
if applcubicwebversion < (3, 2, 0) and cubicwebversion >= (3, 2, 0):
add_cube('card', update_database=False)
+
+def sync_constraint_types():
+ """Make sure the repository knows about all constraint types defined in the code"""
+ from cubicweb.schema import CONSTRAINTS
+ repo_constraints = set(row[0] for row in rql('Any N WHERE X is CWConstraintType, X name N'))
+
+ for cstrtype in set(CONSTRAINTS) - repo_constraints:
+ if cstrtype == 'BoundConstraint':
+ # was renamed to BoundaryConstraint, we don't need the old name
+ continue
+ rql('INSERT CWConstraintType X: X name %(name)s', {'name': cstrtype})
+
+ commit()
+
+sync_constraint_types()
--- a/mttransforms.py Sun Nov 30 21:24:36 2014 +0100
+++ b/mttransforms.py Mon Dec 01 11:13:10 2014 +0100
@@ -28,7 +28,7 @@
register_pygments_transforms)
from cubicweb.utils import UStringIO
-from cubicweb.uilib import rest_publish, html_publish
+from cubicweb.uilib import rest_publish, markdown_publish, html_publish
HTML_MIMETYPES = ('text/html', 'text/xhtml', 'application/xhtml+xml')
@@ -40,6 +40,12 @@
def _convert(self, trdata):
return rest_publish(trdata.appobject, trdata.decode())
+class markdown_to_html(Transform):
+ inputs = ('text/markdown', 'text/x-markdown')
+ output = 'text/html'
+ def _convert(self, trdata):
+ return markdown_publish(trdata.appobject, trdata.decode())
+
class html_to_html(Transform):
inputs = HTML_MIMETYPES
output = 'text/html'
@@ -53,6 +59,7 @@
ENGINE = TransformEngine()
ENGINE.add_transform(rest_to_html())
+ENGINE.add_transform(markdown_to_html())
ENGINE.add_transform(html_to_html())
try:
--- a/predicates.py Sun Nov 30 21:24:36 2014 +0100
+++ b/predicates.py Mon Dec 01 11:13:10 2014 +0100
@@ -188,7 +188,6 @@
from warnings import warn
from operator import eq
-from logilab.common.interface import implements as implements_iface
from logilab.common.registry import Predicate, objectify_predicate, yes
from yams.schema import BASE_TYPES, role_name
--- a/req.py Sun Nov 30 21:24:36 2014 +0100
+++ b/req.py Mon Dec 01 11:13:10 2014 +0100
@@ -485,12 +485,16 @@
raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
% {'value': value, 'format': format})
+ def _base_url(self, secure=None):
+ if secure:
+ return self.vreg.config.get('https-url') or self.vreg.config['base-url']
+ return self.vreg.config['base-url']
+
def base_url(self, secure=None):
"""return the root url of the instance
"""
- if secure:
- return self.vreg.config.get('https-url') or self.vreg.config['base-url']
- return self.vreg.config['base-url']
+ url = self._base_url(secure=secure)
+ return url if url is None else url.rstrip('/') + '/'
# abstract methods to override according to the web front-end #############
--- a/rqlrewrite.py Sun Nov 30 21:24:36 2014 +0100
+++ b/rqlrewrite.py Mon Dec 01 11:13:10 2014 +0100
@@ -31,7 +31,7 @@
from logilab.common.graph import has_path
from cubicweb import Unauthorized
-
+from cubicweb.schema import RRQLExpression
def cleanup_solutions(rqlst, solutions):
for sol in solutions:
@@ -208,11 +208,21 @@
because it create an unresolvable query (eg no solutions found)
"""
+class VariableFromSubQuery(Exception):
+ """flow control exception to indicate that a variable is coming from a
+ subquery, and let parent act accordingly
+ """
+ def __init__(self, variable):
+ self.variable = variable
+
class RQLRewriter(object):
- """insert some rql snippets into another rql syntax tree
+ """Insert some rql snippets into another rql syntax tree, for security /
+ relation vocabulary. This implies that it should only restrict results of
+ the original query, not generate new ones. Hence, inserted snippets are
+ inserted under an EXISTS node.
- this class *isn't thread safe*
+ This class *isn't thread safe*.
"""
def __init__(self, session):
@@ -338,7 +348,7 @@
def rewrite(self, select, snippets, kwargs, existingvars=None):
"""
snippets: (varmap, list of rql expression)
- with varmap a *tuple* (select var, snippet var)
+ with varmap a *dict* {select var: snippet var}
"""
self.select = select
# remove_solutions used below require a copy
@@ -350,7 +360,7 @@
self.pending_keys = []
self.existingvars = existingvars
# we have to annotate the rqlst before inserting snippets, even though
- # we'll have to redo it latter
+ # we'll have to redo it later
self.annotate(select)
self.insert_snippets(snippets)
if not self.exists_snippet and self.u_varname:
@@ -362,7 +372,7 @@
assert len(newsolutions) >= len(solutions), (
'rewritten rql %s has lost some solutions, there is probably '
'something wrong in your schema permission (for instance using a '
- 'RQLExpression which insert a relation which doesn\'t exists in '
+ 'RQLExpression which inserts a relation which doesn\'t exist in '
'the schema)\nOrig solutions: %s\nnew solutions: %s' % (
select, solutions, newsolutions))
if len(newsolutions) > len(solutions):
@@ -382,11 +392,10 @@
continue
self.insert_varmap_snippets(varmap, rqlexprs, varexistsmap)
- def insert_varmap_snippets(self, varmap, rqlexprs, varexistsmap):
+ def init_from_varmap(self, varmap, varexistsmap=None):
self.varmap = varmap
self.revvarmap = {}
self.varinfos = []
- self._insert_scope = None
for i, (selectvar, snippetvar) in enumerate(varmap):
assert snippetvar in 'SOX'
self.revvarmap[snippetvar] = (selectvar, i)
@@ -399,25 +408,35 @@
try:
vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo
except KeyError:
- # variable may have been moved to a newly inserted subquery
- # we should insert snippet in that subquery
- subquery = self.select.aliases[selectvar].query
- assert len(subquery.children) == 1
- subselect = subquery.children[0]
- RQLRewriter(self.session).rewrite(subselect, [(varmap, rqlexprs)],
- self.kwargs)
- return
+ vi['stinfo'] = sti = self._subquery_variable(selectvar)
if varexistsmap is None:
# build an index for quick access to relations
vi['rhs_rels'] = {}
- for rel in sti['rhsrelations']:
+ for rel in sti.get('rhsrelations', []):
vi['rhs_rels'].setdefault(rel.r_type, []).append(rel)
vi['lhs_rels'] = {}
- for rel in sti['relations']:
- if not rel in sti['rhsrelations']:
+ for rel in sti.get('relations', []):
+ if not rel in sti.get('rhsrelations', []):
vi['lhs_rels'].setdefault(rel.r_type, []).append(rel)
else:
vi['rhs_rels'] = vi['lhs_rels'] = {}
+
+ def _subquery_variable(self, selectvar):
+ raise VariableFromSubQuery(selectvar)
+
+ def insert_varmap_snippets(self, varmap, rqlexprs, varexistsmap):
+ try:
+ self.init_from_varmap(varmap, varexistsmap)
+ except VariableFromSubQuery, ex:
+ # variable may have been moved to a newly inserted subquery
+ # we should insert snippet in that subquery
+ subquery = self.select.aliases[ex.variable].query
+ assert len(subquery.children) == 1, subquery
+ subselect = subquery.children[0]
+ RQLRewriter(self.session).rewrite(subselect, [(varmap, rqlexprs)],
+ self.kwargs)
+ return
+ self._insert_scope = None
previous = None
inserted = False
for rqlexpr in rqlexprs:
@@ -450,6 +469,11 @@
finally:
self.existingvars = existing
+ def _inserted_root(self, new):
+ if not isinstance(new, (n.Exists, n.Not)):
+ new = n.Exists(new)
+ return new
+
def _insert_snippet(self, varmap, previous, new):
"""insert `new` snippet into the syntax tree, which have been rewritten
using `varmap`. In cases where an action is protected by several rql
@@ -474,8 +498,7 @@
self.insert_pending()
#self._insert_scope = None
return new
- if not isinstance(new, (n.Exists, n.Not)):
- new = n.Exists(new)
+ new = self._inserted_root(new)
if previous is None:
insert_scope.add_restriction(new)
else:
@@ -869,3 +892,40 @@
if self._insert_scope is None:
return self.select
return self._insert_scope.stmt
+
+
+class RQLRelationRewriter(RQLRewriter):
+ """Insert some rql snippets into another rql syntax tree, replacing computed
+ relations by their associated rule.
+
+ This class *isn't thread safe*.
+ """
+ def __init__(self, session):
+ super(RQLRelationRewriter, self).__init__(session)
+ self.rules = {}
+ for rschema in self.schema.iter_computed_relations():
+ self.rules[rschema.type] = RRQLExpression(rschema.rule)
+
+ def rewrite(self, union, kwargs=None):
+ self.kwargs = kwargs
+ self.removing_ambiguity = False
+ self.existingvars = None
+ self.pending_keys = None
+ for relation in union.iget_nodes(n.Relation):
+ if relation.r_type in self.rules:
+ self.select = relation.stmt
+ self.solutions = solutions = self.select.solutions[:]
+ self.current_expr = self.rules[relation.r_type]
+ self._insert_scope = relation.scope
+ self.rewritten = {}
+ lhs, rhs = relation.get_variable_parts()
+ varmap = {lhs.name: 'S', rhs.name: 'O'}
+ self.init_from_varmap(tuple(sorted(varmap.items())))
+ self.insert_snippet(varmap, self.current_expr.snippet_rqlst)
+ self.select.remove_node(relation)
+
+ def _subquery_variable(self, selectvar):
+ return self.select.aliases[selectvar].stinfo
+
+ def _inserted_root(self, new):
+ return new
--- a/schema.py Sun Nov 30 21:24:36 2014 +0100
+++ b/schema.py Mon Dec 01 11:13:10 2014 +0100
@@ -37,9 +37,11 @@
RelationDefinitionSchema, PermissionMixIn, role_name
from yams.constraints import BaseConstraint, FormatConstraint
from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
- obsolete as yobsolete, cleanup_sys_modules)
+ obsolete as yobsolete, cleanup_sys_modules,
+ fill_schema_from_namespace)
from rql import parse, nodes, RQLSyntaxError, TypeResolverException
+from rql.analyze import ETypeResolver
import cubicweb
from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
@@ -81,7 +83,7 @@
# set of entity and relation types used to build the schema
SCHEMA_TYPES = set((
- 'CWEType', 'CWRType', 'CWAttribute', 'CWRelation',
+ 'CWEType', 'CWRType', 'CWComputedRType', 'CWAttribute', 'CWRelation',
'CWConstraint', 'CWConstraintType', 'CWUniqueTogetherConstraint',
'RQLExpression',
'specializes',
@@ -106,6 +108,11 @@
ybo.ETYPE_PROPERTIES += ('eid',)
ybo.RTYPE_PROPERTIES += ('eid',)
+def build_schema_from_namespace(items):
+ schema = CubicWebSchema('noname')
+ fill_schema_from_namespace(schema, items, register_base_types=False)
+ return schema
+
# Bases for manipulating RQL in schema #########################################
def guess_rrqlexpr_mainvars(expression):
@@ -118,7 +125,8 @@
if 'U' in defined:
mainvars.add('U')
if not mainvars:
- raise Exception('unable to guess selection variables')
+ raise BadSchemaDefinition('unable to guess selection variables in %r'
+ % expression)
return mainvars
def split_expression(rqlstring):
@@ -136,6 +144,44 @@
return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
+def _check_valid_formula(rdef, formula_rqlst):
+ """Check the formula is a valid RQL query with some restriction (no union,
+ single selected node, etc.), raise BadSchemaDefinition if not
+ """
+ if len(formula_rqlst.children) != 1:
+ raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
+ 'can not use UNION in formula %(form)r' %
+ {'attr' : rdef.rtype,
+ 'etype' : rdef.subject.type,
+ 'form' : rdef.formula})
+ select = formula_rqlst.children[0]
+ if len(select.selection) != 1:
+ raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
+ 'can only select one term in formula %(form)r' %
+ {'attr' : rdef.rtype,
+ 'etype' : rdef.subject.type,
+ 'form' : rdef.formula})
+ term = select.selection[0]
+ types = set(term.get_type(sol) for sol in select.solutions)
+ if len(types) != 1:
+ raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
+ 'multiple possible types (%(types)s) for formula %(form)r' %
+ {'attr' : rdef.rtype,
+ 'etype' : rdef.subject.type,
+ 'types' : list(types),
+ 'form' : rdef.formula})
+ computed_type = types.pop()
+ expected_type = rdef.object.type
+ if computed_type != expected_type:
+ raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
+ 'computed attribute type (%(comp_type)s) mismatch with '
+ 'specified type (%(attr_type)s)' %
+ {'attr' : rdef.rtype,
+ 'etype' : rdef.subject.type,
+ 'comp_type' : computed_type,
+ 'attr_type' : expected_type})
+
+
class RQLExpression(object):
"""Base class for RQL expression used in schema (constraints and
permissions)
@@ -146,6 +192,7 @@
# to be defined in concrete classes
rqlst = None
predefined_variables = None
+ full_rql = None
def __init__(self, expression, mainvars, eid):
"""
@@ -1001,6 +1048,59 @@
def schema_by_eid(self, eid):
return self._eid_index[eid]
+ def iter_computed_attributes(self):
+ for relation in self.relations():
+ for rdef in relation.rdefs.itervalues():
+ if rdef.final and rdef.formula is not None:
+ yield rdef
+
+ def iter_computed_relations(self):
+ for relation in self.relations():
+ if relation.rule:
+ yield relation
+
+ def finalize(self):
+ super(CubicWebSchema, self).finalize()
+ self.finalize_computed_attributes()
+ self.finalize_computed_relations()
+
+ def finalize_computed_attributes(self):
+ """Check computed attributes validity (if any), else raise
+ `BadSchemaDefinition`
+ """
+ analyzer = ETypeResolver(self)
+ for rdef in self.iter_computed_attributes():
+ rqlst = parse(rdef.formula)
+ select = rqlst.children[0]
+ analyzer.visit(select)
+ _check_valid_formula(rdef, rqlst)
+ rdef.formula_select = select # avoid later recomputation
+
+
+ def finalize_computed_relations(self):
+ """Build relation definitions for computed relations
+
+ The subject and object types are infered using rql analyzer.
+ """
+ analyzer = ETypeResolver(self)
+ for rschema in self.iter_computed_relations():
+ # XXX rule is valid if both S and O are defined and not in an exists
+ rqlexpr = RRQLExpression(rschema.rule)
+ rqlst = rqlexpr.snippet_rqlst
+ analyzer.visit(rqlst)
+ couples = set((sol['S'], sol['O']) for sol in rqlst.solutions)
+ for subjtype, objtype in couples:
+ if self[objtype].final:
+ raise BadSchemaDefinition('computed relations cannot be final')
+ rdef = ybo.RelationDefinition(
+ subjtype, rschema.type, objtype)
+ rdef.infered = True
+ self.add_relation_def(rdef)
+
+ def rebuild_infered_relations(self):
+ super(CubicWebSchema, self).rebuild_infered_relations()
+ self.finalize_computed_relations()
+
# additional cw specific constraints ###########################################
@@ -1263,6 +1363,7 @@
# only defining here to prevent pylint from complaining
info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
+
set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
--- a/schemas/bootstrap.py Sun Nov 30 21:24:36 2014 +0100
+++ b/schemas/bootstrap.py Mon Dec 01 11:13:10 2014 +0100
@@ -57,6 +57,16 @@
final = Boolean(description=_('automatic'))
+class CWComputedRType(EntityType):
+ """define a virtual relation type, used to build the instance schema"""
+ __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+ name = String(required=True, indexed=True, internationalizable=True,
+ unique=True, maxsize=64)
+ description = RichString(internationalizable=True,
+ description=_('semantic description of this relation type'))
+ rule = String(required=True)
+
+
class CWAttribute(EntityType):
"""define a final relation: link a final relation type from a non final
entity to a final entity type.
@@ -80,6 +90,7 @@
description=_('subject/object cardinality'))
ordernum = Int(description=('control subject entity\'s relations order'), default=0)
+ formula = String(maxsize=2048)
indexed = Boolean(description=_('create an index for quick search on this attribute'))
fulltextindexed = Boolean(description=_('index this attribute\'s value in the plain text index'))
internationalizable = Boolean(description=_('is this attribute\'s value translatable'))
--- a/server/__init__.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/__init__.py Mon Dec 01 11:13:10 2014 +0100
@@ -331,6 +331,7 @@
mhandler.cmd_exec_event_script('pre%s' % event, apphome=True)
# enter instance'schema into the database
serialize_schema(cnx, schema)
+ cnx.commit()
# execute cubicweb's post<event> script
mhandler.cmd_exec_event_script('post%s' % event)
# execute cubes'post<event> script if any
--- a/server/hook.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/hook.py Mon Dec 01 11:13:10 2014 +0100
@@ -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.
@@ -415,10 +415,6 @@
for event in ALL_HOOKS:
CWRegistryStore.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
-@deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)')
-def entity_oldnewvalue(entity, attr):
- return entity.cw_edited.oldnewvalue(attr)
-
# some hook specific predicates #################################################
@@ -763,10 +759,6 @@
def handle_event(self, event):
"""delegate event handling to the opertaion"""
- if event == 'postcommit_event' and hasattr(self, 'commit_event'):
- warn('[3.10] %s: commit_event method has been replaced by postcommit_event'
- % self.__class__, DeprecationWarning)
- self.commit_event() # pylint: disable=E1101
getattr(self, event)()
def precommit_event(self):
@@ -903,58 +895,6 @@
return self._container
-@deprecated('[3.10] use opcls.get_instance(cnx, **opkwargs).add_data(value)')
-def set_operation(cnx, datakey, value, opcls, containercls=set, **opkwargs):
- """Function to ease applying a single operation on a set of data, avoiding
- to create as many as operation as they are individual modification. You
- should try to use this instead of creating on operation for each `value`,
- since handling operations becomes coslty on massive data import.
-
- Arguments are:
-
- * `cnx`, the current connection
-
- * `datakey`, a specially forged key that will be used as key in
- cnx.transaction_data
-
- * `value` that is the actual payload of an individual operation
-
- * `opcls`, the class of the operation. An instance is created on the first
- call for the given key, and then subsequent calls will simply add the
- payload to the container (hence `opkwargs` is only used on that first
- call)
-
- * `containercls`, the container class that should be instantiated to hold
- payloads. An instance is created on the first call for the given key, and
- then subsequent calls will add the data to the existing container. Default
- to a set. Give `list` if you want to keep arrival ordering.
-
- * more optional parameters to give to the operation (here the rtype which do not
- vary accross operations).
-
- The body of the operation must then iterate over the values that have been mapped
- in the transaction_data dictionary to the forged key, e.g.:
-
- .. sourcecode:: python
-
- for value in self._cw.transaction_data.pop(datakey):
- ...
-
- .. Note::
- **poping** the key from `transaction_data` is not an option, else you may
- get unexpected data loss in some case of nested hooks.
- """
- try:
- # Search for cnx.transaction_data[`datakey`] (expected to be a set):
- # if found, simply append `value`
- _container_add(cnx.transaction_data[datakey], value)
- except KeyError:
- # else, initialize it to containercls([`value`]) and instantiate the given
- # `opcls` operation class with additional keyword arguments
- opcls(cnx, **opkwargs)
- cnx.transaction_data[datakey] = containercls()
- _container_add(cnx.transaction_data[datakey], value)
-
class LateOperation(Operation):
"""special operation which should be called after all possible (ie non late)
--- a/server/migractions.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/migractions.py Mon Dec 01 11:13:10 2014 +0100
@@ -320,7 +320,6 @@
"""cached group mapping"""
return ss.group_mapping(self.cnx)
- @cached
def cstrtype_mapping(self):
"""cached constraint types mapping"""
return ss.cstrtype_mapping(self.cnx)
@@ -579,6 +578,9 @@
"""
subjtype, objtype = str(subjtype), str(objtype)
rschema = self.fs_schema.rschema(rtype)
+ if rschema.rule:
+ raise ExecutionError('Cannot synchronize a relation definition for a '
+ 'computed relation (%s)' % rschema)
reporschema = self.repo.schema.rschema(rschema)
if (subjtype, rschema, objtype) in self._synchronized:
return
@@ -1018,11 +1020,13 @@
if rtype in reposchema:
print 'warning: relation type %s is already known, skip addition' % (
rtype)
+ elif rschema.rule:
+ ss.execschemarql(execute, rschema, ss.crschema2rql(rschema))
else:
# register the relation into CWRType and insert necessary relation
# definitions
ss.execschemarql(execute, rschema, ss.rschema2rql(rschema, addrdef=False))
- if addrdef:
+ if not rschema.rule and addrdef:
self.commit()
gmap = self.group_mapping()
cmap = self.cstrtype_mapping()
@@ -1057,8 +1061,12 @@
def cmd_drop_relation_type(self, rtype, commit=True):
"""unregister an existing relation type"""
- # unregister the relation from CWRType
- self.rqlexec('DELETE CWRType X WHERE X name %r' % rtype,
+ rschema = self.repo.schema[rtype]
+ if rschema.rule:
+ etype = 'CWComputedRType'
+ else:
+ etype = 'CWRType'
+ self.rqlexec('DELETE %s X WHERE X name %r' % (etype, rtype),
ask_confirm=self.verbosity>=2)
if commit:
self.commit()
@@ -1086,6 +1094,9 @@
schema definition file
"""
rschema = self.fs_schema.rschema(rtype)
+ if rschema.rule:
+ raise ExecutionError('Cannot add a relation definition for a '
+ 'computed relation (%s)' % rschema)
if not rtype in self.repo.schema:
self.cmd_add_relation_type(rtype, addrdef=False, commit=True)
if (subjtype, objtype) in self.repo.schema.rschema(rtype).rdefs:
@@ -1113,6 +1124,9 @@
def cmd_drop_relation_definition(self, subjtype, rtype, objtype, commit=True):
"""unregister an existing relation definition"""
rschema = self.repo.schema.rschema(rtype)
+ if rschema.rule:
+ raise ExecutionError('Cannot drop a relation definition for a '
+ 'computed relation (%s)' % rschema)
# unregister the definition from CWAttribute or CWRelation
if rschema.final:
etype = 'CWAttribute'
--- a/server/querier.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/querier.py Mon Dec 01 11:13:10 2014 +0100
@@ -28,6 +28,7 @@
from yams import BASE_TYPES
from cubicweb import ValidationError, Unauthorized, UnknownEid
+from cubicweb.rqlrewrite import RQLRelationRewriter
from cubicweb import Binary, server
from cubicweb.rset import ResultSet
@@ -72,7 +73,44 @@
except AttributeError:
return cnx.entity_metas(term.eval(args))['type']
-def check_read_access(cnx, rqlst, solution, args):
+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
+ """
+ # use `term_etype` since we've to deal with rewritten constants here,
+ # when used as an external source by another repository.
+ # XXX what about local read security w/ those rewritten constants...
+ # XXX constants can also happen in some queries generated by req.find()
+ DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
+ schema = cnx.repo.schema
+ user = cnx.user
+ if select.where is not None:
+ for rel in select.where.iget_nodes(Relation):
+ for solution in select.solutions:
+ # XXX has_text may have specific perm ?
+ if rel.r_type in READ_ONLY_RTYPES:
+ continue
+ rschema = schema.rschema(rel.r_type)
+ if rschema.final:
+ eschema = schema.eschema(term_etype(cnx, rel.children[0],
+ solution, args))
+ rdef = eschema.rdef(rschema)
+ else:
+ rdef = rschema.rdef(term_etype(cnx, rel.children[0],
+ solution, args),
+ term_etype(cnx, rel.children[1].children[0],
+ solution, args))
+ if not user.matching_groups(rdef.get_groups('read')):
+ if DBG:
+ print ('check_read_access: %s %s does not match %s' %
+ (rdef, user.groups, rdef.get_groups('read')))
+ # XXX rqlexpr not allowed
+ raise Unauthorized('read', rel.r_type)
+ if DBG:
+ print ('check_read_access: %s %s matches %s' %
+ (rdef, user.groups, rdef.get_groups('read')))
+
+def get_local_checks(cnx, rqlst, solution):
"""Check that the given user has credentials to access data read by the
query and return a dict defining necessary "local checks" (i.e. rql
expression in read permission defined in the schema) where no group grants
@@ -80,50 +118,27 @@
Returned dictionary's keys are variable names and values the rql expressions
for this variable (with the given solution).
+
+ Raise :exc:`Unauthorized` if access is known to be defined, i.e. if there is
+ no matching group and no local permissions.
"""
- # use `term_etype` since we've to deal with rewritten constants here,
- # when used as an external source by another repository.
- # XXX what about local read security w/ those rewritten constants...
DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
schema = cnx.repo.schema
- if rqlst.where is not None:
- for rel in rqlst.where.iget_nodes(Relation):
- # XXX has_text may have specific perm ?
- if rel.r_type in READ_ONLY_RTYPES:
- continue
- rschema = schema.rschema(rel.r_type)
- if rschema.final:
- eschema = schema.eschema(term_etype(cnx, rel.children[0],
- solution, args))
- rdef = eschema.rdef(rschema)
- else:
- rdef = rschema.rdef(term_etype(cnx, rel.children[0],
- solution, args),
- term_etype(cnx, rel.children[1].children[0],
- solution, args))
- if not cnx.user.matching_groups(rdef.get_groups('read')):
- if DBG:
- print ('check_read_access: %s %s does not match %s' %
- (rdef, cnx.user.groups, rdef.get_groups('read')))
- # XXX rqlexpr not allowed
- raise Unauthorized('read', rel.r_type)
- if DBG:
- print ('check_read_access: %s %s matches %s' %
- (rdef, cnx.user.groups, rdef.get_groups('read')))
+ user = cnx.user
localchecks = {}
# iterate on defined_vars and not on solutions to ignore column aliases
for varname in rqlst.defined_vars:
eschema = schema.eschema(solution[varname])
if eschema.final:
continue
- if not cnx.user.matching_groups(eschema.get_groups('read')):
+ if not user.matching_groups(eschema.get_groups('read')):
erqlexprs = eschema.get_rqlexprs('read')
if not erqlexprs:
ex = Unauthorized('read', solution[varname])
ex.var = varname
if DBG:
print ('check_read_access: %s %s %s %s' %
- (varname, eschema, cnx.user.groups, eschema.get_groups('read')))
+ (varname, eschema, user.groups, eschema.get_groups('read')))
raise ex
# don't insert security on variable only referenced by 'NOT X relation Y' or
# 'NOT EXISTS(X relation Y)'
@@ -133,7 +148,8 @@
if (not schema.rschema(r.r_type).final
and ((isinstance(r.parent, Exists) and r.parent.neged(strict=True))
or isinstance(r.parent, Not)))])
- != len(varinfo['relations'])):
+ !=
+ len(varinfo['relations'])):
localchecks[varname] = erqlexprs
return localchecks
@@ -258,7 +274,7 @@
newsolutions = []
for solution in rqlst.solutions:
try:
- localcheck = check_read_access(cnx, rqlst, solution, self.args)
+ localcheck = get_local_checks(cnx, rqlst, solution)
except Unauthorized as ex:
msg = 'remove %s from solutions since %s has no %s access to %s'
msg %= (solution, cnx.user.login, ex.args[0], ex.args[1])
@@ -573,10 +589,14 @@
if cnx.read_security:
for select in rqlst.children:
check_no_password_selected(select)
+ check_relations_read_access(cnx, select, args)
# on select query, always copy the cached rqlst so we don't have to
# bother modifying it. This is not necessary on write queries since
# a new syntax tree is built from them.
rqlst = rqlst.copy()
+ # Rewrite computed relations
+ rewriter = RQLRelationRewriter(cnx)
+ rewriter.rewrite(rqlst, args)
self._annotate(rqlst)
if args:
# different SQL generated when some argument is None or not (IS
--- a/server/repository.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/repository.py Mon Dec 01 11:13:10 2014 +0100
@@ -651,8 +651,8 @@
query_attrs)
return rset.rows
- def connect(self, login, **kwargs):
- """open a session for a given user
+ def new_session(self, login, **kwargs):
+ """open a new session for a given user
raise `AuthenticationError` if the authentication failed
raise `ConnectionError` if we can't open a connection
@@ -678,7 +678,11 @@
# commit connection at this point in case write operation has been
# done during `session_open` hooks
cnx.commit()
- return session.sessionid
+ return session
+
+ def connect(self, login, **kwargs):
+ """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):
@@ -1161,7 +1165,7 @@
def glob_add_entity(self, cnx, edited):
"""add an entity to the repository
- the entity eid should originaly be None and a unique eid is assigned to
+ the entity eid should originally be None and a unique eid is assigned to
the entity instance
"""
entity = edited.entity
--- a/server/schemaserial.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/schemaserial.py Mon Dec 01 11:13:10 2014 +0100
@@ -87,6 +87,27 @@
"""
repo = cnx.repo
dbhelper = repo.system_source.dbhelper
+
+ # Computed Rtype
+ with cnx.ensure_cnx_set:
+ tables = set(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")
+ has_computed_attributes = True
+ except Exception:
+ cnx.rollback()
+ 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'")
@@ -100,6 +121,7 @@
copiedeids = set()
permsidx = deserialize_ertype_permissions(cnx)
schema.reading_from_database = True
+ # load every entity types
for eid, etype, desc in cnx.execute(
'Any X, N, D WHERE X is CWEType, X name N, X description D',
build_descr=False):
@@ -148,6 +170,7 @@
eschema = schema.add_entity_type(
ybo.EntityType(name=etype, description=desc, eid=eid))
set_perms(eschema, permsidx)
+ # load inheritance relations
for etype, stype in cnx.execute(
'Any XN, ETN WHERE X is CWEType, X name XN, X specializes ET, ET name ETN',
build_descr=False):
@@ -155,6 +178,7 @@
stype = ETYPE_NAME_MAP.get(stype, stype)
schema.eschema(etype)._specialized_type = stype
schema.eschema(stype)._specialized_by.append(etype)
+ # 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, '
'X symmetric S, X inlined I, X fulltext_container FTC', build_descr=False):
@@ -163,6 +187,7 @@
ybo.RelationType(name=rtype, description=desc,
symmetric=bool(sym), inlined=bool(il),
fulltext_container=ftc, eid=eid))
+ # remains to load every relation definitions (ie relations and attributes)
cstrsidx = deserialize_rdef_constraints(cnx)
pendingrdefs = []
# closure to factorize common code of attribute/relation rdef addition
@@ -193,29 +218,37 @@
# Get the type parameters for additional base types.
try:
extra_props = dict(cnx.execute('Any X, XTP WHERE X is CWAttribute, '
- 'X extra_props XTP'))
+ 'X extra_props XTP'))
except Exception:
cnx.critical('Previous CRITICAL notification about extra_props is not '
- 'a problem if you are migrating to cubicweb 3.17')
+ 'a problem if you are migrating to cubicweb 3.17')
extra_props = {} # not yet in the schema (introduced by 3.17 migration)
- for values in cnx.execute(
- 'Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT WHERE X is CWAttribute,'
- 'X relation_type RT, X cardinality CARD, X ordernum ORD, X indexed IDX,'
- 'X description DESC, X internationalizable I18N, X defaultval DFLT,'
- 'X fulltextindexed FTIDX, X from_entity SE, X to_entity OE',
- build_descr=False):
- rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values
- typeparams = extra_props.get(rdefeid)
- typeparams = json.load(typeparams) if typeparams else {}
+
+ # load attributes
+ rql = ('Any X,SE,RT,OE,CARD,ORD,DESC,IDX,FTIDX,I18N,DFLT%(fm)s '
+ 'WHERE X is CWAttribute, X relation_type RT, X cardinality CARD,'
+ ' X ordernum ORD, X indexed IDX, X description DESC, '
+ ' X internationalizable I18N, X defaultval DFLT,%(fmsnip)s'
+ ' X fulltextindexed FTIDX, X from_entity SE, X to_entity OE')
+ if has_computed_attributes:
+ rql = rql % {'fm': ',FM', 'fmsnip': 'X formula FM,'}
+ else:
+ rql = rql % {'fm': '', 'fmsnip': ''}
+ for values in cnx.execute(rql, build_descr=False):
+ attrs = dict(zip(
+ ('rdefeid', 'seid', 'reid', 'oeid', 'cardinality',
+ 'order', 'description', 'indexed', 'fulltextindexed',
+ 'internationalizable', 'default', 'formula'), values))
+ typeparams = extra_props.get(attrs['rdefeid'])
+ attrs.update(json.load(typeparams) if typeparams else {})
+ default = attrs['default']
if default is not None:
if isinstance(default, Binary):
# while migrating from 3.17 to 3.18, we still have to
# handle String defaults
- default = default.unzpickle()
- _add_rdef(rdefeid, seid, reid, oeid,
- cardinality=card, description=desc, order=ord,
- indexed=idx, fulltextindexed=ftidx, internationalizable=i18n,
- default=default, **typeparams)
+ attrs['default'] = default.unzpickle()
+ _add_rdef(**attrs)
+ # load relations
for values in cnx.execute(
'Any X,SE,RT,OE,CARD,ORD,DESC,C WHERE X is CWRelation, X relation_type RT,'
'X cardinality CARD, X ordernum ORD, X description DESC, '
@@ -252,6 +285,7 @@
eschema._unique_together.append(tuple(sorted(unique_together)))
schema.infer_specialization_rules()
cnx.commit()
+ schema.finalize()
schema.reading_from_database = False
@@ -309,19 +343,14 @@
"""synchronize schema and permissions in the database according to
current schema
"""
- quiet = os.environ.get('APYCOT_ROOT')
- if not quiet:
- _title = '-> storing the schema in the database '
- print _title,
+ _title = '-> storing the schema in the database '
+ print _title,
execute = cnx.execute
eschemas = schema.entities()
- if not quiet:
- pb_size = (len(eschemas + schema.relations())
- + len(CONSTRAINTS)
- + len([x for x in eschemas if x.specializes()]))
- pb = ProgressBar(pb_size, title=_title)
- else:
- pb = None
+ pb_size = (len(eschemas + schema.relations())
+ + len(CONSTRAINTS)
+ + len([x for x in eschemas if x.specializes()]))
+ pb = ProgressBar(pb_size, title=_title)
groupmap = group_mapping(cnx, interactive=False)
# serialize all entity types, assuring CWEType is serialized first for proper
# is / is_instance_of insertion
@@ -346,6 +375,11 @@
if pb is not None:
pb.update()
continue
+ if rschema.rule:
+ execschemarql(execute, rschema, crschema2rql(rschema))
+ if pb is not None:
+ pb.update()
+ continue
execschemarql(execute, rschema, rschema2rql(rschema, addrdef=False))
if rschema.symmetric:
rdefs = [rdef for k, rdef in rschema.rdefs.iteritems()
@@ -366,8 +400,7 @@
execute(rql, kwargs, build_descr=False)
if pb is not None:
pb.update()
- if not quiet:
- print
+ print
# high level serialization functions
@@ -469,7 +502,7 @@
# rtype serialization
def rschema2rql(rschema, cstrtypemap=None, addrdef=True, groupmap=None):
- """return a list of rql insert statements to enter a relation schema
+ """generate rql insert statements to enter a relation schema
in the database as an CWRType entity
"""
if rschema.type == 'has_text':
@@ -496,10 +529,22 @@
relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
return relations, values
+def crschema2rql(crschema):
+ relations, values = crschema_relations_values(crschema)
+ yield 'INSERT CWComputedRType X: %s' % ','.join(relations), values
+
+def crschema_relations_values(crschema):
+ values = _ervalues(crschema)
+ values['rule'] = unicode(crschema.rule)
+ # XXX why oh why?
+ del values['final']
+ relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
+ return relations, values
+
# rdef serialization
def rdef2rql(rdef, cstrtypemap, groupmap=None):
- # don't serialize infered relations
+ # don't serialize inferred relations
if rdef.infered:
return
relations, values = _rdef_values(rdef)
@@ -518,12 +563,14 @@
for rql, args in _erperms2rql(rdef, groupmap):
yield rql, args
+_IGNORED_PROPS = ['eid', 'constraints', 'uid', 'infered', 'permissions']
+
def _rdef_values(rdef):
amap = {'order': 'ordernum', 'default': 'defaultval'}
values = {}
extra = {}
for prop in rdef.rproperty_defs(rdef.object):
- if prop in ('eid', 'constraints', 'uid', 'infered', 'permissions'):
+ if prop in _IGNORED_PROPS:
continue
value = getattr(rdef, prop)
if prop not in KNOWN_RPROPERTIES:
@@ -592,9 +639,13 @@
yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
def updaterschema2rql(rschema, eid):
- relations, values = rschema_relations_values(rschema)
- values['x'] = eid
- yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
+ if rschema.rule:
+ yield ('SET X rule %(r)s WHERE X eid %(x)s',
+ {'x': eid, 'r': unicode(rschema.rule)})
+ else:
+ relations, values = rschema_relations_values(rschema)
+ values['x'] = eid
+ yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
def updaterdef2rql(rdef, eid):
relations, values = _rdef_values(rdef)
--- a/server/serverctl.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/serverctl.py Mon Dec 01 11:13:10 2014 +0100
@@ -482,7 +482,6 @@
def run(self, args):
appid = args[0]
config = ServerConfiguration.config_for(appid)
- config.quick_start = True
repo, cnx = repo_cnx(config)
with cnx:
used = set(n for n, in cnx.execute('Any SN WHERE S is CWSource, S name SN'))
@@ -505,6 +504,12 @@
continue
break
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:
@@ -515,11 +520,13 @@
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)
+ cnx.create_entity('CWSource', name=sourceuri, type=unicode(type),
+ config=cfgstr, parser=unicode(parser), url=unicode(url))
cnx.commit()
--- a/server/session.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/session.py Mon Dec 01 11:13:10 2014 +0100
@@ -1010,15 +1010,12 @@
@_with_cnx_set
@_open_only
- def execute(self, rql, kwargs=None, eid_key=None, build_descr=True):
+ def execute(self, rql, kwargs=None, build_descr=True):
"""db-api like method directly linked to the querier execute method.
See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
"""
self._session_timestamp.touch()
- if eid_key is not None:
- warn('[3.8] eid_key is deprecated, you can safely remove this argument',
- DeprecationWarning, stacklevel=2)
rset = self._execute(self, rql, kwargs, build_descr)
rset.req = self
self._session_timestamp.touch()
--- a/server/sources/__init__.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sources/__init__.py Mon Dec 01 11:13:10 2014 +0100
@@ -105,7 +105,7 @@
self.support_relations['identity'] = False
self.eid = eid
self.public_config = source_config.copy()
- self.public_config.setdefault('use-cwuri-as-url', self.use_cwuri_as_url)
+ self.public_config['use-cwuri-as-url'] = self.use_cwuri_as_url
self.remove_sensitive_information(self.public_config)
self.uri = source_config.pop('uri')
set_log_methods(self, getLogger('cubicweb.sources.'+self.uri))
--- a/server/sources/datafeed.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sources/datafeed.py Mon Dec 01 11:13:10 2014 +0100
@@ -83,6 +83,13 @@
'help': ('Timeout of HTTP GET requests, when synchronizing a source.'),
'group': 'datafeed-source', 'level': 2,
}),
+ ('use-cwuri-as-url',
+ {'type': 'yn',
+ 'default': None, # explicitly unset
+ 'help': ('Use cwuri (i.e. external URL) for link to the entity '
+ 'instead of its local URL.'),
+ 'group': 'datafeed-source', 'level': 1,
+ }),
)
def check_config(self, source_entity):
@@ -107,6 +114,12 @@
self.synchro_interval = timedelta(seconds=typed_config['synchronization-interval'])
self.max_lock_lifetime = timedelta(seconds=typed_config['max-lock-lifetime'])
self.http_timeout = typed_config['http-timeout']
+ # if typed_config['use-cwuri-as-url'] is set, we have to update
+ # use_cwuri_as_url attribute and public configuration dictionary
+ # accordingly
+ if typed_config['use-cwuri-as-url'] is not None:
+ self.use_cwuri_as_url = typed_config['use-cwuri-as-url']
+ self.public_config['use-cwuri-as-url'] = self.use_cwuri_as_url
def init(self, activated, source_entity):
super(DataFeedSource, self).init(activated, source_entity)
@@ -285,12 +298,39 @@
self.stats = {'created': set(), 'updated': set(), 'checked': set()}
def normalize_url(self, url):
- from cubicweb.sobjects import URL_MAPPING # available after registration
+ """Normalize an url by looking if there is a replacement for it in
+ `cubicweb.sobjects.URL_MAPPING`.
+
+ This dictionary allow to redirect from one host to another, which may be
+ useful for example in case of test instance using production data, while
+ you don't want to load the external source nor to hack your `/etc/hosts`
+ file.
+ """
+ # local import mandatory, it's available after registration
+ from cubicweb.sobjects import URL_MAPPING
for mappedurl in URL_MAPPING:
if url.startswith(mappedurl):
return url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
return url
+ def retrieve_url(self, url, data=None, headers=None):
+ """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
+ """
+ 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)
+ return _OPENER.open(req, timeout=self.source.http_timeout)
+ if url.startswith('file://'):
+ return URLLibResponseAdapter(open(url[7:]), url)
+ return URLLibResponseAdapter(StringIO.StringIO(url), url)
+
def add_schema_config(self, schemacfg, checkonly=False):
"""added CWSourceSchemaConfig, modify mapping accordingly"""
msg = schemacfg._cw._("this parser doesn't use a mapping")
@@ -427,14 +467,7 @@
return error
def parse(self, url):
- if url.startswith('http'):
- url = self.normalize_url(url)
- self.source.info('GET %s', url)
- stream = _OPENER.open(url, timeout=self.source.http_timeout)
- elif url.startswith('file://'):
- stream = open(url[7:])
- else:
- stream = StringIO.StringIO(url)
+ stream = self.retrieve_url(url)
return self.parse_etree(etree.parse(stream).getroot())
def parse_etree(self, document):
@@ -455,6 +488,27 @@
return exists(extid[7:])
return False
+
+class URLLibResponseAdapter(object):
+ """Thin wrapper to be used to fake a value returned by urllib2.urlopen"""
+ def __init__(self, stream, url, code=200):
+ self._stream = stream
+ self._url = url
+ self.code = code
+
+ def read(self, *args):
+ return self._stream.read(*args)
+
+ def geturl(self):
+ return self._url
+
+ def getcode(self):
+ return self.code
+
+ def info(self):
+ from mimetools import Message
+ return Message(StringIO.StringIO())
+
# use a cookie enabled opener to use session cookie if any
_OPENER = urllib2.build_opener()
try:
--- a/server/sources/ldapfeed.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sources/ldapfeed.py Mon Dec 01 11:13:10 2014 +0100
@@ -126,7 +126,7 @@
}),
('user-attrs-map',
{'type' : 'named',
- 'default': {'uid': 'login', 'gecos': 'email', 'userPassword': 'upassword'},
+ 'default': {'uid': 'login'},
'help': 'map from ldap user attributes to cubicweb attributes (with Active Directory, you want to use sAMAccountName:login,mail:email,givenName:firstname,sn:surname)',
'group': 'ldap-source', 'level': 1,
}),
--- a/server/sources/native.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sources/native.py Mon Dec 01 11:13:10 2014 +0100
@@ -318,10 +318,16 @@
'want trusted authentication for the database connection',
'group': 'native-source', 'level': 2,
}),
+ ('db-statement-timeout',
+ {'type': 'int',
+ 'default': 0,
+ 'help': 'sql statement timeout, in milliseconds (postgres only)',
+ 'group': 'native-source', 'level': 2,
+ }),
)
def __init__(self, repo, source_config, *args, **kwargs):
- SQLAdapterMixIn.__init__(self, source_config)
+ SQLAdapterMixIn.__init__(self, source_config, repairing=repo.config.repairing)
self.authentifiers = [LoginPasswordAuthentifier(self)]
if repo.config['allow-email-login']:
self.authentifiers.insert(0, EmailPasswordAuthentifier(self))
--- a/server/sqlutils.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/sqlutils.py Mon Dec 01 11:13:10 2014 +0100
@@ -47,7 +47,7 @@
return subprocess.call(cmd)
-def sqlexec(sqlstmts, cursor_or_execute, withpb=not os.environ.get('APYCOT_ROOT'),
+def sqlexec(sqlstmts, cursor_or_execute, withpb=True,
pbtitle='', delimiter=';', cnx=None):
"""execute sql statements ignoring DROP/ CREATE GROUP or USER statements
error.
@@ -299,7 +299,7 @@
"""
cnx_wrap = ConnectionWrapper
- def __init__(self, source_config):
+ def __init__(self, source_config, repairing=False):
try:
self.dbdriver = source_config['db-driver'].lower()
dbname = source_config['db-name']
@@ -328,6 +328,14 @@
if self.dbdriver == 'sqlite':
self.cnx_wrap = SqliteConnectionWrapper
self.dbhelper.dbname = abspath(self.dbhelper.dbname)
+ if not repairing:
+ statement_timeout = int(source_config.get('db-statement-timeout', 0))
+ if statement_timeout > 0:
+ def set_postgres_timeout(cnx):
+ cnx.cursor().execute('SET statement_timeout to %d' % statement_timeout)
+ cnx.commit()
+ postgres_hooks = SQL_CONNECT_HOOKS['postgres']
+ postgres_hooks.append(set_postgres_timeout)
def wrapped_connection(self):
"""open and return a connection to the database, wrapped into a class
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data-cwep002/schema.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,35 @@
+# 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/>.
+
+from yams.buildobjs import EntityType, RelationDefinition, Int, ComputedRelation
+
+class Person(EntityType):
+ salary = Int()
+
+class works_for(RelationDefinition):
+ subject = 'Person'
+ object = 'Company'
+ cardinality = '?*'
+
+class Company(EntityType):
+ total_salary = Int(formula='Any SUM(SA) GROUPBY X WHERE '
+ 'P works_for X, P salary SA')
+
+class has_employee(ComputedRelation):
+ rule = 'O works_for S'
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/datacomputed/migratedapp/schema.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,57 @@
+# 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/>.
+
+from yams.buildobjs import (EntityType, RelationDefinition, ComputedRelation,
+ Int, Float)
+
+
+class Employee(EntityType):
+ pass
+
+
+class employees(RelationDefinition):
+ subject = 'Company'
+ object = 'Employee'
+
+
+class associates(RelationDefinition):
+ subject = 'Company'
+ object = 'Employee'
+
+
+class works_for(ComputedRelation):
+ rule = 'O employees S, NOT EXISTS (O associates S)'
+
+
+class Company(EntityType):
+ score = Float(formula='Any AVG(NN) WHERE X employees E, N concerns E, N note NN')
+ score100 = Float(formula='Any AVG(NN) WHERE X employees E, N concerns E, N note100 NN')
+
+
+class Note(EntityType):
+ note = Int()
+ note100 = Int(formula='Any N*100 WHERE X note N')
+
+
+class concerns(RelationDefinition):
+ subject = 'Note'
+ object = 'Employee'
+
+
+class whatever(ComputedRelation):
+ rule = 'S employees E, O associates E'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/datacomputed/schema.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,54 @@
+# 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/>.
+
+from yams.buildobjs import EntityType, RelationDefinition, ComputedRelation, Int, Float
+
+
+class Employee(EntityType):
+ pass
+
+
+class employees(RelationDefinition):
+ subject = 'Company'
+ object = 'Employee'
+
+
+class associates(RelationDefinition):
+ subject = 'Company'
+ object = 'Employee'
+
+
+class Company(EntityType):
+ score100 = Float(formula='Any AVG(NN) WHERE X employees E, N concerns E, N note100 NN')
+
+class Note(EntityType):
+ note = Int()
+ note20 = Int(formula='Any N*20 WHERE X note N')
+ note100 = Int(formula='Any N*20 WHERE X note N')
+
+class concerns(RelationDefinition):
+ subject = 'Note'
+ object = 'Employee'
+
+
+class notes(ComputedRelation):
+ rule = 'S employees E, O concerns E'
+
+
+class whatever(ComputedRelation):
+ rule = 'S employees E, O concerns E'
--- a/server/test/unittest_datafeed.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_datafeed.py Mon Dec 01 11:13:10 2014 +0100
@@ -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/>.
+import mimetools
from datetime import timedelta
+from contextlib import contextmanager
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.server.sources import datafeed
@@ -25,19 +27,14 @@
class DataFeedTC(CubicWebTC):
def setup_database(self):
with self.admin_access.repo_cnx() as cnx:
- cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
- parser=u'testparser', url=u'ignored',
- config=u'synchronization-interval=1min')
- cnx.commit()
+ with self.base_parser(cnx):
+ cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+ parser=u'testparser', url=u'ignored',
+ config=u'synchronization-interval=1min')
+ cnx.commit()
- def test(self):
- self.assertIn('myfeed', self.repo.sources_by_uri)
- dfsource = self.repo.sources_by_uri['myfeed']
- self.assertEqual(dfsource.latest_retrieval, None)
- self.assertEqual(dfsource.synchro_interval, timedelta(seconds=60))
- self.assertFalse(dfsource.fresh())
-
-
+ @contextmanager
+ def base_parser(self, session):
class AParser(datafeed.DataFeedParser):
__regid__ = 'testparser'
def process(self, url, raise_on_error=False):
@@ -50,7 +47,24 @@
entity.cw_edited.update(sourceparams['item'])
with self.temporary_appobjects(AParser):
- with self.repo.internal_cnx() as cnx:
+ if 'myfeed' in self.repo.sources_by_uri:
+ yield self.repo.sources_by_uri['myfeed']._get_parser(session)
+ else:
+ yield
+
+ def test(self):
+ self.assertIn('myfeed', self.repo.sources_by_uri)
+ dfsource = self.repo.sources_by_uri['myfeed']
+ self.assertNotIn('use_cwuri_as_url', dfsource.__dict__)
+ self.assertEqual({'type': u'datafeed', 'uri': u'myfeed', 'use-cwuri-as-url': True},
+ dfsource.public_config)
+ self.assertEqual(dfsource.use_cwuri_as_url, True)
+ self.assertEqual(dfsource.latest_retrieval, None)
+ self.assertEqual(dfsource.synchro_interval, timedelta(seconds=60))
+ self.assertFalse(dfsource.fresh())
+
+ with self.repo.internal_cnx() as cnx:
+ with self.base_parser(cnx):
stats = dfsource.pull_data(cnx, force=True)
cnx.commit()
# test import stats
@@ -119,6 +133,28 @@
self.assertFalse(cnx.execute('Card X WHERE X title "cubicweb.org"'))
self.assertFalse(cnx.execute('Any X WHERE X has_text "cubicweb.org"'))
+ def test_parser_retrieve_url_local(self):
+ with self.admin_access.repo_cnx() as cnx:
+ with self.base_parser(cnx) as parser:
+ value = parser.retrieve_url('a string')
+ self.assertEqual(200, value.getcode())
+ self.assertEqual('a string', value.geturl())
+ self.assertIsInstance(value.info(), mimetools.Message)
+
+
+class DataFeedConfigTC(CubicWebTC):
+
+ def test_use_cwuri_as_url_override(self):
+ with self.admin_access.client_cnx() as cnx:
+ cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+ parser=u'testparser', url=u'ignored',
+ config=u'use-cwuri-as-url=no')
+ cnx.commit()
+ dfsource = self.repo.sources_by_uri['myfeed']
+ self.assertEqual(dfsource.use_cwuri_as_url, False)
+ self.assertEqual({'type': u'datafeed', 'uri': u'myfeed', 'use-cwuri-as-url': False},
+ dfsource.public_config)
+
if __name__ == '__main__':
from logilab.common.testlib import unittest_main
unittest_main()
--- a/server/test/unittest_migractions.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_migractions.py Mon Dec 01 11:13:10 2014 +0100
@@ -18,46 +18,50 @@
"""unit tests for module cubicweb.server.migractions"""
from datetime import date
-from os.path import join
+import os.path as osp
from contextlib import contextmanager
from logilab.common.testlib import unittest_main, Tags, tag
from yams.constraints import UniqueConstraint
-from cubicweb import ConfigurationError, ValidationError
+from cubicweb import ConfigurationError, ValidationError, ExecutionError
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.server.sqlutils import SQL_PREFIX
from cubicweb.server.migractions import ServerMigrationHelper
import cubicweb.devtools
+
+HERE = osp.dirname(osp.abspath(__file__))
+
migrschema = None
def tearDownModule(*args):
global migrschema
del migrschema
if hasattr(MigrationCommandsTC, 'origschema'):
del MigrationCommandsTC.origschema
+ if hasattr(MigrationCommandsComputedTC, 'origschema'):
+ del MigrationCommandsComputedTC.origschema
-class MigrationCommandsTC(CubicWebTC):
+class MigrationTC(CubicWebTC):
configcls = cubicweb.devtools.TestServerConfiguration
tags = CubicWebTC.tags | Tags(('server', 'migration', 'migractions'))
def _init_repo(self):
- super(MigrationCommandsTC, self)._init_repo()
+ super(MigrationTC, self)._init_repo()
# we have to read schema from the database to get eid for schema entities
self.repo.set_schema(self.repo.deserialize_schema(), resetvreg=False)
# hack to read the schema from data/migrschema
config = self.config
- config.appid = join('data', 'migratedapp')
- config._apphome = self.datapath('migratedapp')
+ config.appid = osp.join(self.appid, 'migratedapp')
+ config._apphome = osp.join(HERE, config.appid)
global migrschema
migrschema = config.load_schema()
- config.appid = 'data'
- config._apphome = self.datadir
- assert 'Folder' in migrschema
+ config.appid = self.appid
+ config._apphome = osp.join(HERE, self.appid)
def setUp(self):
CubicWebTC.setUp(self)
@@ -73,6 +77,26 @@
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})
+ 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(','))
+
+
+class MigrationCommandsTC(MigrationTC):
+
+ def _init_repo(self):
+ super(MigrationCommandsTC, self)._init_repo()
+ assert 'Folder' in migrschema
+
def test_add_attribute_bool(self):
with self.mh() as (cnx, mh):
self.assertNotIn('yesno', self.schema)
@@ -135,8 +159,7 @@
self.assertEqual(self.schema['shortpara'].subjects(), ('Note', ))
self.assertEqual(self.schema['shortpara'].objects(), ('String', ))
# test created column is actually a varchar(64)
- notesql = mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' and name='%sNote'" % SQL_PREFIX)[0][0]
- fields = dict(x.strip().split()[:2] for x in notesql.split('(', 1)[1].rsplit(')', 1)[0].split(','))
+ fields = self.table_schema(mh, '%sNote' % SQL_PREFIX)
self.assertEqual(fields['%sshortpara' % SQL_PREFIX], 'varchar(64)')
# test default value set on existing entities
self.assertEqual(cnx.execute('Note X').get_entity(0, 0).shortpara, 'hop')
@@ -656,16 +679,167 @@
self.assertEqual(self.schema['Note'].specializes(), None)
self.assertEqual(self.schema['Text'].specializes(), None)
-
def test_add_symmetric_relation_type(self):
with self.mh() as (cnx, mh):
- same_as_sql = mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
- "and name='same_as_relation'")
- self.assertFalse(same_as_sql)
+ self.assertFalse(self.table_sql(mh, 'same_as_relation'))
mh.cmd_add_relation_type('same_as')
- same_as_sql = mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' "
- "and name='same_as_relation'")
- self.assertTrue(same_as_sql)
+ self.assertTrue(self.table_sql(mh, 'same_as_relation'))
+
+
+class MigrationCommandsComputedTC(MigrationTC):
+ """ Unit tests for computed relations and attributes
+ """
+ appid = 'datacomputed'
+
+ def setUp(self):
+ MigrationTC.setUp(self)
+ # ensure vregistry is reloaded, needed by generated hooks for computed
+ # attributes
+ self.repo.vreg.set_schema(self.repo.schema)
+
+ def test_computed_relation_add_relation_definition(self):
+ self.assertNotIn('works_for', self.schema)
+ with self.mh() as (cnx, mh):
+ with self.assertRaises(ExecutionError) as exc:
+ mh.cmd_add_relation_definition('Employee', 'works_for',
+ 'Company')
+ self.assertEqual(str(exc.exception),
+ 'Cannot add a relation definition for a computed '
+ 'relation (works_for)')
+
+ def test_computed_relation_drop_relation_definition(self):
+ self.assertIn('notes', self.schema)
+ with self.mh() as (cnx, mh):
+ with self.assertRaises(ExecutionError) as exc:
+ mh.cmd_drop_relation_definition('Company', 'notes', 'Note')
+ self.assertEqual(str(exc.exception),
+ 'Cannot drop a relation definition for a computed '
+ 'relation (notes)')
+
+ def test_computed_relation_add_relation_type(self):
+ self.assertNotIn('works_for', self.schema)
+ with self.mh() as (cnx, mh):
+ mh.cmd_add_relation_type('works_for')
+ self.assertIn('works_for', self.schema)
+ self.assertEqual(self.schema['works_for'].rule,
+ 'O employees S, NOT EXISTS (O associates S)')
+ self.assertEqual(self.schema['works_for'].objects(), ('Company',))
+ self.assertEqual(self.schema['works_for'].subjects(), ('Employee',))
+ self.assertFalse(self.table_sql(mh, 'works_for_relation'))
+ e = cnx.create_entity('Employee')
+ a = cnx.create_entity('Employee')
+ cnx.create_entity('Company', employees=e, associates=a)
+ cnx.commit()
+ company = cnx.execute('Company X').get_entity(0, 0)
+ self.assertEqual([e.eid],
+ [x.eid for x in company.reverse_works_for])
+ mh.rollback()
+
+ def test_computed_relation_drop_relation_type(self):
+ self.assertIn('notes', self.schema)
+ with self.mh() as (cnx, mh):
+ mh.cmd_drop_relation_type('notes')
+ self.assertNotIn('notes', self.schema)
+
+ def test_computed_relation_sync_schema_props_perms(self):
+ self.assertIn('whatever', self.schema)
+ with self.mh() as (cnx, mh):
+ mh.cmd_sync_schema_props_perms('whatever')
+ self.assertEqual(self.schema['whatever'].rule,
+ 'S employees E, O associates E')
+ self.assertEqual(self.schema['whatever'].objects(), ('Company',))
+ self.assertEqual(self.schema['whatever'].subjects(), ('Company',))
+ self.assertFalse(self.table_sql(mh, 'whatever_relation'))
+
+ def test_computed_relation_sync_schema_props_perms_on_rdef(self):
+ self.assertIn('whatever', self.schema)
+ with self.mh() as (cnx, mh):
+ with self.assertRaises(ExecutionError) as exc:
+ mh.cmd_sync_schema_props_perms(
+ ('Company', 'whatever', 'Person'))
+ self.assertEqual(str(exc.exception),
+ 'Cannot synchronize a relation definition for a computed '
+ 'relation (whatever)')
+
+ # computed attributes migration ############################################
+
+ def setup_add_score(self):
+ with self.admin_access.client_cnx() as cnx:
+ assert not cnx.execute('Company X')
+ c = cnx.create_entity('Company')
+ e1 = cnx.create_entity('Employee', reverse_employees=c)
+ n1 = cnx.create_entity('Note', note=2, concerns=e1)
+ e2 = cnx.create_entity('Employee', reverse_employees=c)
+ n2 = cnx.create_entity('Note', note=4, concerns=e2)
+ cnx.commit()
+
+ def assert_score_initialized(self, mh):
+ 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([[3.0]],
+ mh.rqlexec('Any CS WHERE C score CS, C is Company').rows)
+
+ def test_computed_attribute_add_relation_type(self):
+ self.assertNotIn('score', self.schema)
+ self.setup_add_score()
+ with self.mh() as (cnx, mh):
+ mh.cmd_add_relation_type('score')
+ self.assertIn('score', self.schema)
+ self.assertEqual(self.schema['score'].objects(), ('Float',))
+ self.assertEqual(self.schema['score'].subjects(), ('Company',))
+ self.assert_score_initialized(mh)
+
+ def test_computed_attribute_add_attribute(self):
+ self.assertNotIn('score', self.schema)
+ self.setup_add_score()
+ with self.mh() as (cnx, mh):
+ mh.cmd_add_attribute('Company', 'score')
+ self.assertIn('score', self.schema)
+ self.assert_score_initialized(mh)
+
+ 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)
+
+ def test_computed_attribute_drop_type(self):
+ self.assertIn('note20', self.schema)
+ with self.mh() as (cnx, mh):
+ mh.cmd_drop_relation_type('note20')
+ self.assert_computed_attribute_dropped()
+
+ def test_computed_attribute_drop_relation_definition(self):
+ self.assertIn('note20', self.schema)
+ with self.mh() as (cnx, mh):
+ mh.cmd_drop_relation_definition('Note', 'note20', 'Int')
+ self.assert_computed_attribute_dropped()
+
+ def test_computed_attribute_drop_attribute(self):
+ self.assertIn('note20', self.schema)
+ with self.mh() as (cnx, mh):
+ mh.cmd_drop_attribute('Note', 'note20')
+ self.assert_computed_attribute_dropped()
+
+ def test_computed_attribute_sync_schema_props_perms_rtype(self):
+ self.assertIn('note100', self.schema)
+ with self.mh() as (cnx, mh):
+ mh.cmd_sync_schema_props_perms('note100')
+ self.assertEqual(self.schema['note100'].rdefs['Note', 'Int'].formula,
+ 'Any N*100 WHERE X note N')
+
+ def test_computed_attribute_sync_schema_props_perms_rdef(self):
+ self.setup_add_score()
+ with self.mh() as (cnx, mh):
+ mh.cmd_sync_schema_props_perms(('Note', 'note100', 'Int'))
+ self.assertEqual([[200], [400]],
+ cnx.execute('Any N ORDERBY N WHERE X note100 N').rows)
+ self.assertEqual([[300]],
+ cnx.execute('Any CS WHERE C score100 CS, C is Company').rows)
+
if __name__ == '__main__':
unittest_main()
--- a/server/test/unittest_querier.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_querier.py Mon Dec 01 11:13:10 2014 +0100
@@ -173,11 +173,11 @@
'ET': 'CWEType', 'ETN': 'String'}])
rql, solutions = partrqls[1]
self.assertRQLEqual(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, '
- 'X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, '
- ' CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, '
- ' CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, Comment, '
- ' Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Frozable, '
- ' Note, Old, Personne, RQLExpression, Societe, State, SubDivision, '
+ 'X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWComputedRType, '
+ ' CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, '
+ ' CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, '
+ ' Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, '
+ ' Frozable, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, '
' SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
self.assertListEqual(sorted(solutions),
sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'},
@@ -186,6 +186,7 @@
{'X': 'Comment', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'Division', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'CWCache', 'ETN': 'String', 'ET': 'CWEType'},
+ {'X': 'CWComputedRType', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'CWConstraint', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'CWConstraintType', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'CWEType', 'ETN': 'String', 'ET': 'CWEType'},
@@ -603,18 +604,18 @@
'WHERE RT name N, RDEF relation_type RT '
'HAVING COUNT(RDEF) > 10')
self.assertListEqual(rset.rows,
- [[u'description_format', 12],
- [u'description', 13],
- [u'name', 18],
- [u'created_by', 44],
- [u'creation_date', 44],
- [u'cw_source', 44],
- [u'cwuri', 44],
- [u'in_basket', 44],
- [u'is', 44],
- [u'is_instance_of', 44],
- [u'modification_date', 44],
- [u'owned_by', 44]])
+ [[u'description_format', 13],
+ [u'description', 14],
+ [u'name', 19],
+ [u'created_by', 45],
+ [u'creation_date', 45],
+ [u'cw_source', 45],
+ [u'cwuri', 45],
+ [u'in_basket', 45],
+ [u'is', 45],
+ [u'is_instance_of', 45],
+ [u'modification_date', 45],
+ [u'owned_by', 45]])
def test_select_aggregat_having_dumb(self):
# dumb but should not raise an error
--- a/server/test/unittest_repository.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_repository.py Mon Dec 01 11:13:10 2014 +0100
@@ -280,7 +280,7 @@
self.assertListEqual(['relation_type',
'from_entity', 'to_entity',
'constrained_by',
- 'cardinality', 'ordernum',
+ 'cardinality', 'ordernum', 'formula',
'indexed', 'fulltextindexed', 'internationalizable',
'defaultval', 'extra_props',
'description', 'description_format'],
--- a/server/test/unittest_schemaserial.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_schemaserial.py Mon Dec 01 11:13:10 2014 +0100
@@ -25,6 +25,7 @@
from cubicweb import Binary
from cubicweb.schema import CubicWebSchemaLoader
from cubicweb.devtools import TestServerConfiguration
+from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.server.schemaserial import (updateeschema2rql, updaterschema2rql, rschema2rql,
eschema2rql, rdef2rql, specialize2rql,
@@ -221,7 +222,7 @@
'inlined': False}),
('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,'
- 'X description %(description)s,X fulltextindexed %(fulltextindexed)s,'
+ 'X description %(description)s,X formula %(formula)s,X fulltextindexed %(fulltextindexed)s,'
'X indexed %(indexed)s,X internationalizable %(internationalizable)s,'
'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,'
'X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
@@ -234,6 +235,7 @@
'ordernum': 5,
'defaultval': None,
'indexed': False,
+ 'formula': None,
'cardinality': u'?1'}),
('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X '
'WHERE CT eid %(ct)s, EDEF eid %(x)s',
@@ -247,7 +249,7 @@
'value': u"u'?1', u'11'"}),
('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,'
- 'X description %(description)s,X fulltextindexed %(fulltextindexed)s,'
+ 'X description %(description)s,X formula %(formula)s,X fulltextindexed %(fulltextindexed)s,'
'X indexed %(indexed)s,X internationalizable %(internationalizable)s,'
'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE '
'WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
@@ -260,6 +262,7 @@
'ordernum': 5,
'defaultval': None,
'indexed': False,
+ 'formula': None,
'cardinality': u'?1'}),
('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X '
'WHERE CT eid %(ct)s, EDEF eid %(x)s',
@@ -272,7 +275,7 @@
'ct': u'StaticVocabularyConstraint_eid',
'value': (u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', "
"u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'")})],
- list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)))
+ list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)))
def test_rschema2rql_custom_type(self):
expected = [('INSERT CWRType X: X description %(description)s,X final %(final)s,'
@@ -286,13 +289,14 @@
'symmetric': False}),
('INSERT CWAttribute X: X cardinality %(cardinality)s,'
'X defaultval %(defaultval)s,X description %(description)s,'
- 'X extra_props %(extra_props)s,X indexed %(indexed)s,'
+ 'X extra_props %(extra_props)s,X formula %(formula)s,X indexed %(indexed)s,'
'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,'
'X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
{'cardinality': u'?1',
'defaultval': None,
'description': u'',
'extra_props': '{"jungle_speed": 42}',
+ 'formula': None,
'indexed': False,
'oe': None,
'ordernum': 4,
@@ -312,7 +316,7 @@
def test_rdef2rql(self):
self.assertListEqual([
('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,'
- 'X description %(description)s,X fulltextindexed %(fulltextindexed)s,'
+ 'X description %(description)s,X formula %(formula)s,X fulltextindexed %(fulltextindexed)s,'
'X indexed %(indexed)s,X internationalizable %(internationalizable)s,'
'X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,'
'X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
@@ -325,6 +329,7 @@
'ordernum': 3,
'defaultval': Binary.zpickle(u'text/plain'),
'indexed': False,
+ 'formula': None,
'cardinality': u'?1'}),
('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X '
'WHERE CT eid %(ct)s, EDEF eid %(x)s',
@@ -424,7 +429,19 @@
# self.assertListEqual(perms2rql(schema, self.GROUP_MAPPING),
# ['INSERT CWEType X: X name 'Societe', X final FALSE'])
+class ComputedAttributeAndRelationTC(CubicWebTC):
+ appid = 'data-cwep002'
+ def test(self):
+ # force to read schema from the database
+ 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('O works_for S',
+ schema['has_employee'].rule)
+ self.assertEqual([('Company', 'Int')], list(schema['total_salary'].rdefs))
+ self.assertEqual('Any SUM(SA) GROUPBY X WHERE P works_for X, P salary SA',
+ schema['total_salary'].rdefs['Company', 'Int'].formula)
if __name__ == '__main__':
unittest_main()
--- a/server/test/unittest_security.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_security.py Mon Dec 01 11:13:10 2014 +0100
@@ -22,7 +22,7 @@
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb import Unauthorized, ValidationError, QueryError, Binary
from cubicweb.schema import ERQLExpression
-from cubicweb.server.querier import check_read_access
+from cubicweb.server.querier import get_local_checks, check_relations_read_access
from cubicweb.server.utils import _CRYPTO_CTX
@@ -37,18 +37,33 @@
class LowLevelSecurityFunctionTC(BaseSecurityTC):
- def test_check_read_access(self):
- rql = u'Personne U where U nom "managers"'
+ def test_check_relation_read_access(self):
+ rql = u'Personne U WHERE U nom "managers"'
+ rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
+ nom = self.repo.schema['Personne'].rdef('nom')
+ with self.temporary_permissions((nom, {'read': ('users', 'managers')})):
+ 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:
+ self.assertRaises(Unauthorized,
+ check_relations_read_access,
+ cnx, rqlst, {})
+ self.assertRaises(Unauthorized, cnx.execute, rql)
+
+ def test_get_local_checks(self):
+ rql = u'Personne U WHERE U nom "managers"'
rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
with self.admin_access.repo_cnx() as cnx:
self.repo.vreg.solutions(cnx, rqlst, None)
solution = rqlst.solutions[0]
- check_read_access(cnx, rqlst, solution, {})
+ localchecks = get_local_checks(cnx, rqlst, solution)
+ self.assertEqual({}, localchecks)
with self.new_access('anon').repo_cnx() as cnx:
self.assertRaises(Unauthorized,
- check_read_access,
- cnx, rqlst, solution, {})
+ get_local_checks,
+ cnx, rqlst, solution)
self.assertRaises(Unauthorized, cnx.execute, rql)
def test_upassword_not_selectable(self):
--- a/server/test/unittest_undo.py Sun Nov 30 21:24:36 2014 +0100
+++ b/server/test/unittest_undo.py Mon Dec 01 11:13:10 2014 +0100
@@ -106,13 +106,20 @@
self.assertEqual(a4.eid_from, self.totoeid)
self.assertEqual(a4.eid_to, self.toto(cnx).in_group[0].eid)
self.assertEqual(a4.order, 4)
- for i, rtype in ((1, 'owned_by'), (2, 'owned_by'),
- (4, 'in_state'), (5, 'created_by')):
+ for i, rtype in ((1, 'owned_by'), (2, 'owned_by')):
a = actions[i]
self.assertEqual(a.action, 'A')
self.assertEqual(a.eid_from, self.totoeid)
self.assertEqual(a.rtype, rtype)
self.assertEqual(a.order, i+1)
+ self.assertEqual(set((actions[4].rtype, actions[5].rtype)),
+ set(('in_state', 'created_by')))
+ for i in (4, 5):
+ a = actions[i]
+ self.assertEqual(a.action, 'A')
+ self.assertEqual(a.eid_from, self.totoeid)
+ self.assertEqual(a.order, i+1)
+
# test undoable_transactions
txs = cnx.undoable_transactions()
self.assertEqual(len(txs), 1)
--- a/sobjects/notification.py Sun Nov 30 21:24:36 2014 +0100
+++ b/sobjects/notification.py Mon Dec 01 11:13:10 2014 +0100
@@ -80,15 +80,8 @@
# this is usually the method to call
def render_and_send(self, **kwargs):
- """generate and send an email message for this view"""
- delayed = kwargs.pop('delay_to_commit', None)
- for recipients, msg in self.render_emails(**kwargs):
- if delayed is None:
- self.send(recipients, msg)
- elif delayed:
- self.send_on_commit(recipients, msg)
- else:
- self.send_now(recipients, msg)
+ """generate and send email messages for this view"""
+ self._cw.vreg.config.sendmails(self.render_emails(**kwargs))
def cell_call(self, row, col=0, **kwargs):
self.w(self._cw._(self.content) % self.context(**kwargs))
@@ -146,16 +139,11 @@
continue
msg = format_mail(self.user_data, [emailaddr], content, subject,
config=self._cw.vreg.config, msgid=msgid, references=refs)
- yield [emailaddr], msg
+ yield msg, [emailaddr]
finally:
- # ensure we have a cnxset since commit will fail if there is
- # some operation but no cnxset. This may occurs in this very
- # specific case (eg SendMailOp)
- with cnx.ensure_cnx_set:
- cnx.commit()
self._cw = req
- # recipients / email sending ###############################################
+ # recipients handling ######################################################
def recipients(self):
"""return a list of either 2-uple (email, language) or user entity to
@@ -166,13 +154,6 @@
row=self.cw_row or 0, col=self.cw_col or 0)
return finder.recipients()
- def send_now(self, recipients, msg):
- self._cw.vreg.config.sendmails([(msg, recipients)])
-
- def send_on_commit(self, recipients, msg):
- SendMailOp(self._cw, recipients=recipients, msg=msg)
- send = send_on_commit
-
# email generation helpers #################################################
def construct_message_id(self, eid):
--- a/test/data/rewrite/schema.py Sun Nov 30 21:24:36 2014 +0100
+++ b/test/data/rewrite/schema.py Mon Dec 01 11:13:10 2014 +0100
@@ -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.
@@ -15,9 +15,15 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-from yams.buildobjs import EntityType, RelationDefinition, String, SubjectRelation
+from yams.buildobjs import (EntityType, RelationDefinition, String, SubjectRelation,
+ ComputedRelation, Int)
from cubicweb.schema import ERQLExpression
+
+class Person(EntityType):
+ name = String()
+
+
class Affaire(EntityType):
__permissions__ = {
'read': ('managers',
@@ -82,3 +88,37 @@
object = 'CWUser'
inlined = True
cardinality = '1*'
+
+class Contribution(EntityType):
+ code = Int()
+
+class ArtWork(EntityType):
+ name = String()
+
+class Role(EntityType):
+ name = String()
+
+class contributor(RelationDefinition):
+ subject = 'Contribution'
+ object = 'Person'
+ cardinality = '1*'
+ inlined = True
+
+class manifestation(RelationDefinition):
+ subject = 'Contribution'
+ object = 'ArtWork'
+
+class role(RelationDefinition):
+ subject = 'Contribution'
+ object = 'Role'
+
+class illustrator_of(ComputedRelation):
+ rule = ('C is Contribution, C contributor S, C manifestation O, '
+ 'C role R, R name "illustrator"')
+
+class participated_in(ComputedRelation):
+ rule = 'S contributor O'
+
+class match(RelationDefinition):
+ subject = 'ArtWork'
+ object = 'Note'
--- a/test/unittest_dataimport.py Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_dataimport.py Mon Dec 01 11:13:10 2014 +0100
@@ -1,6 +1,88 @@
+# -*- 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):
--- a/test/unittest_dbapi.py Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_dbapi.py Mon Dec 01 11:13:10 2014 +0100
@@ -78,7 +78,7 @@
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.base_url(), 'http://perdu.com/')
self.assertEqual(req.from_controller(), 'view')
self.assertEqual(req.relative_path(), '')
req.ajax_replace_url('domid') # don't crash
--- a/test/unittest_entity.py Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_entity.py Mon Dec 01 11:13:10 2014 +0100
@@ -587,6 +587,16 @@
# should be default groups but owners, i.e. managers, users, guests
self.assertEqual(len(unrelated), 3)
+ def test_markdown_printable_value_string(self):
+ with self.admin_access.web_request() as req:
+ e = req.create_entity('Card', title=u'rest markdown',
+ content=u'This is [an example](http://example.com/ "Title") inline link`',
+ content_format=u'text/markdown')
+ self.assertEqual(
+ u'<p>This is <a href="http://example.com/" '
+ u'title="Title">an example</a> inline link`</p>',
+ e.printable_value('content'))
+
def test_printable_value_string(self):
with self.admin_access.web_request() as req:
e = req.create_entity('Card', title=u'rest test',
--- a/test/unittest_rqlrewrite.py Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_rqlrewrite.py Mon Dec 01 11:13:10 2014 +0100
@@ -19,6 +19,7 @@
from logilab.common.testlib import unittest_main, TestCase
from logilab.common.testlib import mock_object
from yams import BadSchemaDefinition
+from yams.buildobjs import RelationDefinition
from rql import parse, nodes, RQLHelper
from cubicweb import Unauthorized, rqlrewrite
@@ -31,10 +32,8 @@
config = TestServerConfiguration(RQLRewriteTC.datapath('rewrite'))
config.bootstrap_cubes()
schema = config.load_schema()
- from yams.buildobjs import RelationDefinition
schema.add_relation_def(RelationDefinition(subject='Card', name='in_state',
object='State', cardinality='1*'))
-
rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
'has_text': 'fti'})
repotest.do_monkey_patch()
@@ -49,11 +48,11 @@
2: 'Card',
3: 'Affaire'}[eid]
-def rewrite(rqlst, snippets_map, kwargs, existingvars=None):
+def _prepare_rewriter(rewriter_cls, kwargs):
class FakeVReg:
schema = schema
@staticmethod
- def solutions(sqlcursor, mainrqlst, kwargs):
+ def solutions(sqlcursor, rqlst, kwargs):
rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs)
class rqlhelper:
@staticmethod
@@ -62,8 +61,10 @@
@staticmethod
def simplify(mainrqlst, needcopy=False):
rqlhelper.simplify(rqlst, needcopy)
- rewriter = rqlrewrite.RQLRewriter(
- mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
+ return rewriter_cls(mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
+
+def rewrite(rqlst, snippets_map, kwargs, existingvars=None):
+ rewriter = _prepare_rewriter(rqlrewrite.RQLRewriter, kwargs)
snippets = []
for v, exprs in sorted(snippets_map.items()):
rqlexprs = [isinstance(snippet, basestring)
@@ -87,7 +88,7 @@
except KeyError:
vrefmaps[stmt] = {vref.name: set( (vref,) )}
selects.append(stmt)
- assert node in selects
+ assert node in selects, (node, selects)
for stmt in selects:
for var in stmt.defined_vars.itervalues():
assert var.stinfo['references']
@@ -591,5 +592,223 @@
finally:
RQLRewriter.insert_snippets = orig_insert_snippets
+
+class RQLRelationRewriterTC(TestCase):
+ # XXX valid rules: S and O specified, not in a SET, INSERT, DELETE scope
+ # valid uses: no outer join
+
+ # Basic tests
+ def test_base_rule(self):
+ rules = {'participated_in': 'S contributor O'}
+ rqlst = rqlhelper.parse('Any X WHERE X participated_in S')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any X WHERE X contributor S',
+ rqlst.as_string())
+
+ def test_complex_rule_1(self):
+ rules = {'illustrator_of': ('C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, '
+ 'R name "illustrator"')}
+ rqlst = rqlhelper.parse('Any A,B WHERE A illustrator_of B')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE C is Contribution, '
+ 'C contributor A, C manifestation B, '
+ 'C role D, D name "illustrator"',
+ rqlst.as_string())
+
+ def test_complex_rule_2(self):
+ rules = {'illustrator_of': ('C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, '
+ 'R name "illustrator"')}
+ rqlst = rqlhelper.parse('Any A WHERE EXISTS(A illustrator_of B)')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A WHERE EXISTS(C is Contribution, '
+ 'C contributor A, C manifestation B, '
+ 'C role D, D name "illustrator")',
+ rqlst.as_string())
+
+
+ def test_rewrite2(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('Any A,B WHERE A illustrator_of B, C require_permission R, S'
+ 'require_state O')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE C require_permission R, S require_state O, '
+ 'D is Contribution, D contributor A, D manifestation B, D role E, '
+ 'E name "illustrator"',
+ rqlst.as_string())
+
+ def test_rewrite3(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('Any A,B WHERE E require_permission T, A illustrator_of B')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE E require_permission T, '
+ 'C is Contribution, C contributor A, C manifestation B, '
+ 'C role D, D name "illustrator"',
+ rqlst.as_string())
+
+ def test_rewrite4(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('Any A,B WHERE C require_permission R, A illustrator_of B')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE C require_permission R, '
+ 'D is Contribution, D contributor A, D manifestation B, '
+ 'D role E, E name "illustrator"',
+ rqlst.as_string())
+
+ def test_rewrite5(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('Any A,B WHERE C require_permission R, A illustrator_of B, '
+ 'S require_state O')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE C require_permission R, S require_state O, '
+ 'D is Contribution, D contributor A, D manifestation B, D role E, '
+ 'E name "illustrator"',
+ rqlst.as_string())
+
+ # Tests for the with clause
+ def test_rewrite_with(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('Any A,B WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WITH A,B BEING '
+ '(Any X,Y WHERE A is Contribution, A contributor X, '
+ 'A manifestation Y, A role B, B name "illustrator")',
+ rqlst.as_string())
+
+ def test_rewrite_with2(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('Any A,B WHERE T require_permission C WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE T require_permission C '
+ 'WITH A,B BEING (Any X,Y WHERE A is Contribution, '
+ 'A contributor X, A manifestation Y, A role B, B name "illustrator")',
+ rqlst.as_string())
+
+ def test_rewrite_with3(self):
+ rules = {'participated_in': 'S contributor O'}
+ rqlst = rqlhelper.parse('Any A,B WHERE A participated_in B '
+ 'WITH A, B BEING(Any X,Y WHERE X contributor Y)')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE A contributor B WITH A,B BEING '
+ '(Any X,Y WHERE X contributor Y)',
+ rqlst.as_string())
+
+ def test_rewrite_with4(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('Any A,B WHERE A illustrator_of B '
+ 'WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE C is Contribution, '
+ 'C contributor A, C manifestation B, C role D, '
+ 'D name "illustrator" WITH A,B BEING '
+ '(Any X,Y WHERE A is Contribution, A contributor X, '
+ 'A manifestation Y, A role B, B name "illustrator")',
+ rqlst.as_string())
+
+ # Tests for the union
+ def test_rewrite_union(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('(Any A,B WHERE A illustrator_of B) UNION'
+ '(Any X,Y WHERE X is CWUser, Z manifestation Y)')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('(Any A,B WHERE C is Contribution, '
+ 'C contributor A, C manifestation B, C role D, '
+ 'D name "illustrator") UNION (Any X,Y WHERE X is CWUser, Z manifestation Y)',
+ rqlst.as_string())
+
+ def test_rewrite_union2(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('(Any Y WHERE Y match W) UNION '
+ '(Any A WHERE A illustrator_of B) UNION '
+ '(Any Y WHERE Y is ArtWork)')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('(Any Y WHERE Y match W) '
+ 'UNION (Any A WHERE C is Contribution, C contributor A, '
+ 'C manifestation B, C role D, D name "illustrator") '
+ 'UNION (Any Y WHERE Y is ArtWork)',
+ rqlst.as_string())
+
+ # Tests for the exists clause
+ def test_rewrite_exists(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('(Any A,B WHERE A illustrator_of B, '
+ 'EXISTS(B is ArtWork))')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE EXISTS(B is ArtWork), '
+ 'C is Contribution, C contributor A, C manifestation B, C role D, '
+ 'D name "illustrator"',
+ rqlst.as_string())
+
+ def test_rewrite_exists2(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('(Any A,B WHERE B contributor A, EXISTS(A illustrator_of W))')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE B contributor A, '
+ 'EXISTS(C is Contribution, C contributor A, C manifestation W, '
+ 'C role D, D name "illustrator")',
+ rqlst.as_string())
+
+ def test_rewrite_exists3(self):
+ rules = {'illustrator_of': 'C is Contribution, C contributor S, '
+ 'C manifestation O, C role R, R name "illustrator"'}
+ rqlst = rqlhelper.parse('(Any A,B WHERE A illustrator_of B, EXISTS(A illustrator_of W))')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any A,B WHERE EXISTS(C is Contribution, C contributor A, '
+ 'C manifestation W, C role D, D name "illustrator"), '
+ 'E is Contribution, E contributor A, E manifestation B, E role F, '
+ 'F name "illustrator"',
+ rqlst.as_string())
+
+ # Test for GROUPBY
+ def test_rewrite_groupby(self):
+ rules = {'participated_in': 'S contributor O'}
+ rqlst = rqlhelper.parse('Any SUM(SA) GROUPBY S WHERE P participated_in S, P manifestation SA')
+ rule_rewrite(rqlst, rules)
+ self.assertEqual('Any SUM(SA) GROUPBY S WHERE P manifestation SA, P contributor S',
+ rqlst.as_string())
+
+
+class RQLRelationRewriterTC(CubicWebTC):
+
+ appid = 'data/rewrite'
+
+ def test_base_rule(self):
+ with self.admin_access.client_cnx() as cnx:
+ art = cnx.create_entity('ArtWork', name=u'Les travailleurs de la Mer')
+ role = cnx.create_entity('Role', name=u'illustrator')
+ vic = cnx.create_entity('Person', name=u'Victor Hugo')
+ contrib = cnx.create_entity('Contribution', code=96, contributor=vic,
+ manifestation=art, role=role)
+ rset = cnx.execute('Any X WHERE X illustrator_of S')
+ self.assertEqual([u'Victor Hugo'],
+ [result.name for result in rset.entities()])
+ rset = cnx.execute('Any S WHERE X illustrator_of S, X eid %(x)s',
+ {'x': vic.eid})
+ self.assertEqual([u'Les travailleurs de la Mer'],
+ [result.name for result in rset.entities()])
+
+
+def rule_rewrite(rqlst, kwargs=None):
+ rewriter = _prepare_rewriter(rqlrewrite.RQLRelationRewriter, kwargs)
+ rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map},
+ kwargs=kwargs)
+ rewriter.rewrite(rqlst)
+ for select in rqlst.children:
+ test_vrefs(select)
+ return rewriter.rewritten
+
+
if __name__ == '__main__':
unittest_main()
--- a/test/unittest_schema.py Sun Nov 30 21:24:36 2014 +0100
+++ b/test/unittest_schema.py Mon Dec 01 11:13:10 2014 +0100
@@ -26,14 +26,16 @@
from yams import ValidationError, BadSchemaDefinition
from yams.constraints import SizeConstraint, StaticVocabularyConstraint
-from yams.buildobjs import RelationDefinition, EntityType, RelationType
+from yams.buildobjs import (RelationDefinition, EntityType, RelationType,
+ Int, String, SubjectRelation, ComputedRelation)
from yams.reader import fill_schema
from cubicweb.schema import (
CubicWebSchema, CubicWebEntitySchema, CubicWebSchemaLoader,
RQLConstraint, RQLUniqueConstraint, RQLVocabularyConstraint,
RQLExpression, ERQLExpression, RRQLExpression,
- normalize_expression, order_eschemas, guess_rrqlexpr_mainvars)
+ normalize_expression, order_eschemas, guess_rrqlexpr_mainvars,
+ build_schema_from_namespace)
from cubicweb.devtools import TestServerConfiguration as TestConfiguration
from cubicweb.devtools.testlib import CubicWebTC
@@ -161,9 +163,10 @@
entities = sorted([str(e) for e in schema.entities()])
expected_entities = ['Ami', 'BaseTransition', 'BigInt', 'Bookmark', 'Boolean', 'Bytes', 'Card',
'Date', 'Datetime', 'Decimal',
- 'CWCache', 'CWConstraint', 'CWConstraintType', 'CWDataImport',
- 'CWEType', 'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation',
- 'CWPermission', 'CWProperty', 'CWRType',
+ 'CWCache', 'CWComputedRType', 'CWConstraint',
+ 'CWConstraintType', 'CWDataImport', 'CWEType',
+ 'CWAttribute', 'CWGroup', 'EmailAddress',
+ 'CWRelation', 'CWPermission', 'CWProperty', 'CWRType',
'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig',
'CWUniqueTogetherConstraint', 'CWUser',
'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
@@ -190,7 +193,7 @@
'ean', 'ecrit_par', 'eid', 'end_timestamp', 'evaluee', 'expression', 'exprtype', 'extra_props',
- 'fabrique_par', 'final', 'firstname', 'for_user', 'fournit',
+ 'fabrique_par', 'final', 'firstname', 'for_user', 'formula', 'fournit',
'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed',
'has_group_permission', 'has_text',
@@ -207,7 +210,7 @@
'parser', 'path', 'pkey', 'prefered_form', 'prenom', 'primary_email',
- 'read_permission', 'relation_type', 'relations', 'require_group',
+ 'read_permission', 'relation_type', 'relations', 'require_group', 'rule',
'specializes', 'start_timestamp', 'state_of', 'status', 'subworkflow', 'subworkflow_exit', 'subworkflow_state', 'surname', 'symmetric', 'synopsis',
@@ -281,6 +284,88 @@
'add': ('managers',),
'delete': ('managers',)})
+ def test_computed_attribute(self):
+ """Check schema finalization for computed attributes."""
+ class Person(EntityType):
+ salary = Int()
+
+ class works_for(RelationDefinition):
+ subject = 'Person'
+ object = 'Company'
+ cardinality = '?*'
+
+ class Company(EntityType):
+ total_salary = Int(formula='Any SUM(SA) GROUPBY X WHERE '
+ 'P works_for X, P salary SA')
+ good_schema = build_schema_from_namespace(vars().items())
+
+ class Company(EntityType):
+ total_salary = String(formula='Any SUM(SA) GROUPBY X WHERE '
+ 'P works_for X, P salary SA')
+
+ with self.assertRaises(BadSchemaDefinition) as exc:
+ bad_schema = build_schema_from_namespace(vars().items())
+
+ self.assertEqual(str(exc.exception),
+ 'computed attribute total_salary on Company: '
+ 'computed attribute type (Int) mismatch with '
+ 'specified type (String)')
+
+
+class SchemaReaderComputedRelationAndAttributesTest(TestCase):
+
+ def test_infer_computed_relation(self):
+ class Person(EntityType):
+ name = String()
+
+ class Company(EntityType):
+ name = String()
+
+ class Service(EntityType):
+ name = String()
+
+ class works_for(RelationDefinition):
+ subject = 'Person'
+ object = 'Company'
+
+ class produce(RelationDefinition):
+ subject = ('Person', 'Company')
+ object = 'Service'
+
+ class achete(RelationDefinition):
+ subject = 'Person'
+ object = 'Service'
+
+ class produces_and_buys(ComputedRelation):
+ rule = 'S produce O, S achete O'
+
+ class produces_and_buys2(ComputedRelation):
+ rule = 'S works_for SO, SO produce O'
+
+ class reproduce(ComputedRelation):
+ rule = 'S produce O'
+
+ schema = build_schema_from_namespace(vars().items())
+
+ # check object/subject type
+ self.assertEqual([('Person','Service')],
+ 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())
+ # check relations as marked infered
+ self.assertTrue(
+ schema['produces_and_buys'].rdefs[('Person','Service')].infered)
+
+ del schema
+ class autoname(ComputedRelation):
+ rule = 'S produce X, X name O'
+
+ with self.assertRaises(BadSchemaDefinition) as cm:
+ build_schema_from_namespace(vars().items())
+ self.assertEqual(str(cm.exception), 'computed relations cannot be final')
+
class BadSchemaTC(TestCase):
def setUp(self):
@@ -395,6 +480,7 @@
('cw_source', 'Bookmark', 'CWSource', 'object'),
('cw_source', 'CWAttribute', 'CWSource', 'object'),
('cw_source', 'CWCache', 'CWSource', 'object'),
+ ('cw_source', 'CWComputedRType', 'CWSource', 'object'),
('cw_source', 'CWConstraint', 'CWSource', 'object'),
('cw_source', 'CWConstraintType', 'CWSource', 'object'),
('cw_source', 'CWDataImport', 'CWSource', 'object'),
@@ -454,5 +540,6 @@
sorted([(r.rtype.type, r.subject.type, r.object.type, role)
for r, role in sorted(schema[etype].composite_rdef_roles)])
+
if __name__ == '__main__':
unittest_main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_toolsutils.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,57 @@
+# 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/>.
+
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.toolsutils import RQLExecuteMatcher
+
+
+class RQLExecuteMatcherTests(TestCase):
+ def matched_query(self, text):
+ match = RQLExecuteMatcher.match(text)
+ if match is None:
+ return None
+ return match['rql_query']
+
+ def test_unknown_function_dont_match(self):
+ self.assertIsNone(self.matched_query('foo'))
+ self.assertIsNone(self.matched_query('rql('))
+ self.assertIsNone(self.matched_query('hell("")'))
+ self.assertIsNone(self.matched_query('eval("rql(\'bla\''))
+
+ def test_rql_other_parameters_dont_match(self):
+ self.assertIsNone(self.matched_query('rql("Any X WHERE X eid %(x)s")'))
+ self.assertIsNone(self.matched_query('rql("Any X WHERE X eid %(x)s", {'))
+ self.assertIsNone(self.matched_query('session.execute("Any X WHERE X eid %(x)s")'))
+ self.assertIsNone(self.matched_query('session.execute("Any X WHERE X eid %(x)s", {'))
+
+ def test_rql_function_match(self):
+ for func_expr in ('rql', 'session.execute'):
+ query = self.matched_query('%s("Any X WHERE X is ' % func_expr)
+ self.assertEqual(query, 'Any X WHERE X is ')
+
+ def test_offseted_rql_function_match(self):
+ """check indentation is allowed"""
+ for func_expr in (' rql', ' session.execute'):
+ query = self.matched_query('%s("Any X WHERE X is ' % func_expr)
+ self.assertEqual(query, 'Any X WHERE X is ')
+
+
+if __name__ == '__main__':
+ unittest_main()
--- a/toolsutils.py Sun Nov 30 21:24:36 2014 +0100
+++ b/toolsutils.py Mon Dec 01 11:13:10 2014 +0100
@@ -25,7 +25,12 @@
import subprocess
from os import listdir, makedirs, environ, chmod, walk, remove
from os.path import exists, join, abspath, normpath
-
+import re
+from rlcompleter import Completer
+try:
+ import readline
+except ImportError: # readline not available, no completion
+ pass
try:
from os import symlink
except ImportError:
@@ -263,3 +268,155 @@
password = getpass('password: ')
return connect(login=user, password=password, host=optconfig.host, database=appid)
+
+## cwshell helpers #############################################################
+
+class AbstractMatcher(object):
+ """Abstract class for CWShellCompleter's matchers.
+
+ A matcher should implement a ``possible_matches`` method. This
+ method has to return the list of possible completions for user's input.
+ Because of the python / readline interaction, each completion should
+ be a superset of the user's input.
+
+ NOTE: readline tokenizes user's input and only passes last token to
+ completers.
+ """
+
+ def possible_matches(self, text):
+ """return possible completions for user's input.
+
+ Parameters:
+ text: the user's input
+
+ Return:
+ a list of completions. Each completion includes the original input.
+ """
+ raise NotImplementedError()
+
+
+class RQLExecuteMatcher(AbstractMatcher):
+ """Custom matcher for rql queries.
+
+ If user's input starts with ``rql(`` or ``session.execute(`` and
+ the corresponding rql query is incomplete, suggest some valid completions.
+ """
+ query_match_rgx = re.compile(
+ r'(?P<func_prefix>\s*(?:rql)' # match rql, possibly indented
+ r'|' # or
+ r'\s*(?:\w+\.execute))' # match .execute, possibly indented
+ # end of <func_prefix>
+ r'\(' # followed by a parenthesis
+ r'(?P<quote_delim>["\'])' # a quote or double quote
+ r'(?P<parameters>.*)') # and some content
+
+ def __init__(self, local_ctx, req):
+ self.local_ctx = local_ctx
+ self.req = req
+ self.schema = req.vreg.schema
+ self.rsb = req.vreg['components'].select('rql.suggestions', req)
+
+ @staticmethod
+ def match(text):
+ """check if ``text`` looks like a call to ``rql`` or ``session.execute``
+
+ Parameters:
+ text: the user's input
+
+ Returns:
+ None if it doesn't match, the query structure otherwise.
+ """
+ query_match = RQLExecuteMatcher.query_match_rgx.match(text)
+ if query_match is None:
+ return None
+ parameters_text = query_match.group('parameters')
+ quote_delim = query_match.group('quote_delim')
+ # first parameter is fully specified, no completion needed
+ if re.match(r"(.*?)%s" % quote_delim, parameters_text) is not None:
+ return None
+ func_prefix = query_match.group('func_prefix')
+ return {
+ # user's input
+ 'text': text,
+ # rql( or session.execute(
+ 'func_prefix': func_prefix,
+ # offset of rql query
+ 'rql_offset': len(func_prefix) + 2,
+ # incomplete rql query
+ 'rql_query': parameters_text,
+ }
+
+ def possible_matches(self, text):
+ """call ``rql.suggestions`` component to complete user's input.
+ """
+ # readline will only send last token, but we need the entire user's input
+ user_input = readline.get_line_buffer()
+ query_struct = self.match(user_input)
+ if query_struct is None:
+ return []
+ else:
+ # we must only send completions of the last token => compute where it
+ # starts relatively to the rql query itself.
+ completion_offset = readline.get_begidx() - query_struct['rql_offset']
+ rql_query = query_struct['rql_query']
+ return [suggestion[completion_offset:]
+ for suggestion in self.rsb.build_suggestions(rql_query)]
+
+
+class DefaultMatcher(AbstractMatcher):
+ """Default matcher: delegate to standard's `rlcompleter.Completer`` class
+ """
+ def __init__(self, local_ctx):
+ self.completer = Completer(local_ctx)
+
+ def possible_matches(self, text):
+ if "." in text:
+ return self.completer.attr_matches(text)
+ else:
+ return self.completer.global_matches(text)
+
+
+class CWShellCompleter(object):
+ """Custom auto-completion helper for cubicweb-ctl shell.
+
+ ``CWShellCompleter`` provides a ``complete`` method suitable for
+ ``readline.set_completer``.
+
+ Attributes:
+ matchers: the list of ``AbstractMatcher`` instances that will suggest
+ possible completions
+
+ The completion process is the following:
+
+ - readline calls the ``complete`` method with user's input,
+ - the ``complete`` method asks for each known matchers if
+ it can suggest completions for user's input.
+ """
+
+ def __init__(self, local_ctx):
+ # list of matchers to ask for possible matches on completion
+ self.matchers = [DefaultMatcher(local_ctx)]
+ self.matchers.insert(0, RQLExecuteMatcher(local_ctx, local_ctx['session']))
+
+ def complete(self, text, state):
+ """readline's completer method
+
+ cf http://docs.python.org/2/library/readline.html#readline.set_completer
+ for more details.
+
+ Implementation inspired by `rlcompleter.Completer`
+ """
+ if state == 0:
+ # reset self.matches
+ self.matches = []
+ for matcher in self.matchers:
+ matches = matcher.possible_matches(text)
+ if matches:
+ self.matches = matches
+ break
+ else:
+ return None # no matcher able to handle `text`
+ try:
+ return self.matches[state]
+ except IndexError:
+ return None
--- a/uilib.py Sun Nov 30 21:24:36 2014 +0100
+++ b/uilib.py Mon Dec 01 11:13:10 2014 +0100
@@ -163,6 +163,8 @@
# text publishing #############################################################
+from cubicweb.ext.markdown import markdown_publish # pylint: disable=W0611
+
try:
from cubicweb.ext.rest import rest_publish # pylint: disable=W0611
except ImportError:
@@ -170,6 +172,7 @@
"""default behaviour if docutils was not found"""
return xml_escape(data)
+
TAG_PROG = re.compile(r'</?.*?>', re.U)
def remove_html_tags(text):
"""Removes HTML tags from text
--- a/utils.py Sun Nov 30 21:24:36 2014 +0100
+++ b/utils.py Mon Dec 01 11:13:10 2014 +0100
@@ -17,7 +17,7 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""Some utilities for CubicWeb server/clients."""
-from __future__ import division, with_statement
+from __future__ import division
__docformat__ = "restructuredtext en"
@@ -420,6 +420,8 @@
self.head = req.html_headers
# main stream
self.body = UStringIO()
+ # this method will be assigned to self.w in views
+ self.write = self.body.write
self.doctype = u''
self._htmlattrs = [('lang', req.lang)]
# keep main_stream's reference on req for easier text/html demoting
@@ -445,11 +447,6 @@
warn('[3.17] xhtml is no more supported',
DeprecationWarning, stacklevel=2)
- def write(self, data):
- """StringIO interface: this method will be assigned to self.w
- """
- self.body.write(data)
-
@property
def htmltag(self):
attrs = ' '.join('%s="%s"' % (attr, xml_escape(value))
--- a/view.py Sun Nov 30 21:24:36 2014 +0100
+++ b/view.py Mon Dec 01 11:13:10 2014 +0100
@@ -20,7 +20,6 @@
__docformat__ = "restructuredtext en"
_ = unicode
-import types, new
from cStringIO import StringIO
from warnings import warn
from functools import partial
@@ -290,12 +289,6 @@
clabel = vtitle
return u'%s (%s)' % (clabel, self._cw.property_value('ui.site-title'))
- @deprecated('[3.10] use vreg["etypes"].etype_class(etype).cw_create_url(req)')
- def create_url(self, etype, **kwargs):
- """ return the url of the entity creation form for a given entity type"""
- return self._cw.vreg["etypes"].etype_class(etype).cw_create_url(
- self._cw, **kwargs)
-
def field(self, label, value, row=True, show_label=True, w=None, tr=True,
table=False):
"""read-only field"""
@@ -501,36 +494,10 @@
class ReloadableMixIn(object):
"""simple mixin for reloadable parts of UI"""
- def user_callback(self, cb, args, msg=None, nonify=False):
- """register the given user callback and return a URL to call it ready to be
- inserted in html
- """
- self._cw.add_js('cubicweb.ajax.js')
- if nonify:
- _cb = cb
- def cb(*args):
- _cb(*args)
- cbname = self._cw.register_onetime_callback(cb, *args)
- return self.build_js(cbname, xml_escape(msg or ''))
-
- def build_update_js_call(self, cbname, msg):
- rql = self.cw_rset.printable_rql()
- return "javascript: %s" % js.userCallbackThenUpdateUI(
- cbname, self.__regid__, rql, msg, self.__registry__, self.domid)
-
- def build_reload_js_call(self, cbname, msg):
- return "javascript: %s" % js.userCallbackThenReloadPage(cbname, msg)
-
- build_js = build_update_js_call # expect updatable component by default
-
@property
def domid(self):
return domid(self.__regid__)
- @deprecated('[3.10] use .domid property')
- def div_id(self):
- return self.domid
-
class Component(ReloadableMixIn, View):
"""base class for components"""
@@ -548,10 +515,6 @@
def domid(self):
return '%sComponent' % domid(self.__regid__)
- @deprecated('[3.10] use .cssclass property')
- def div_class(self):
- return self.cssclass
-
class Adapter(AppObject):
"""base class for adapters"""
--- a/web/application.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/application.py Mon Dec 01 11:13:10 2014 +0100
@@ -23,6 +23,7 @@
from time import clock, time
from contextlib import contextmanager
from warnings import warn
+import json
import httplib
@@ -223,7 +224,7 @@
sessioncookie = self.session_cookie(req)
secure = req.https and req.base_url().startswith('https://')
req.set_cookie(sessioncookie, session.sessionid,
- maxage=None, secure=secure)
+ maxage=None, secure=secure, httponly=True)
if not session.anonymous_session:
self.session_manager.postlogin(req, session)
return session
@@ -589,8 +590,10 @@
status = httplib.INTERNAL_SERVER_ERROR
if isinstance(ex, PublishException) and ex.status is not None:
status = ex.status
- req.status_out = status
- json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+ if req.status_out < 400:
+ # don't overwrite it if it's already set
+ req.status_out = status
+ json_dumper = getattr(ex, 'dumps', lambda : json.dumps({'reason': unicode(ex)}))
return json_dumper()
# special case handling
--- a/web/component.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/component.py Mon Dec 01 11:13:10 2014 +0100
@@ -218,6 +218,10 @@
def render(self, w):
w(tags.a(self.label, href=self.href, **self.attrs))
+ def __repr__(self):
+ return '<%s: href=%r label=%r %r>' % (self.__class__.__name__,
+ self.href, self.label, self.attrs)
+
class Separator(object):
"""a menu separator.
@@ -270,7 +274,7 @@
layout_id = None # to be defined in concret class
layout_args = {}
- def layout_render(self, w):
+ def layout_render(self, w, **kwargs):
getlayout = self._cw.vreg['components'].select
layout = getlayout(self.layout_id, self._cw, **self.layout_select_args())
layout.render(w)
@@ -331,19 +335,8 @@
title = None
layout_id = 'component_layout'
- # XXX support kwargs for compat with old boxes which gets the view as
- # argument
def render(self, w, **kwargs):
- if hasattr(self, 'call'):
- warn('[3.10] should not anymore implement call on %s, see new CtxComponent api'
- % self.__class__, DeprecationWarning)
- self.w = w
- def wview(__vid, rset=None, __fallback_vid=None, **kwargs):
- self._cw.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
- self.wview = wview
- self.call(**kwargs) # pylint: disable=E1101
- return
- self.layout_render(w)
+ self.layout_render(w, **kwargs)
def layout_select_args(self):
args = super(CtxComponent, self).layout_select_args()
@@ -410,19 +403,6 @@
def separator(self):
return Separator()
- @deprecated('[3.10] use action_link() / link()')
- def box_action(self, action): # XXX action_link
- return self.build_link(self._cw._(action.title), action.url())
-
- @deprecated('[3.10] use action_link() / link()')
- def build_link(self, title, url, **kwargs):
- if self._cw.selected(url):
- try:
- kwargs['klass'] += ' selected'
- except KeyError:
- kwargs['klass'] = 'selected'
- return tags.a(title, href=url, **kwargs)
-
class EntityCtxComponent(CtxComponent):
"""base class for boxes related to a single entity"""
@@ -725,7 +705,6 @@
def entity_call(self, entity, view=None):
raise NotImplementedError()
-
class RelatedObjectsVComponent(EntityVComponent):
"""a section to display some related entities"""
__select__ = EntityVComponent.__select__ & partial_has_related_entities()
--- a/web/cors.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/cors.py Mon Dec 01 11:13:10 2014 +0100
@@ -109,6 +109,6 @@
'%s != %s' % (host, myhost))
raise CORSFailed('Host header and hostname do not match')
# include "Vary: Origin" header (see 6.4)
- req.set_header('Vary', 'Origin')
+ req.headers_out.addHeader('Vary', 'Origin')
return origin
--- a/web/data/cubicweb.ajax.js Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.ajax.js Mon Dec 01 11:13:10 2014 +0100
@@ -88,8 +88,8 @@
});
var AJAX_PREFIX_URL = 'ajax';
-var JSON_BASE_URL = baseuri() + 'json?';
-var AJAX_BASE_URL = baseuri() + AJAX_PREFIX_URL + '?';
+var JSON_BASE_URL = BASE_URL + 'json?';
+var AJAX_BASE_URL = BASE_URL + AJAX_PREFIX_URL + '?';
jQuery.extend(cw.ajax, {
@@ -122,9 +122,7 @@
* (e.g. http://..../data??resource1.js,resource2.js)
*/
_modconcatLikeUrl: function(url) {
- var base = baseuri();
- if (!base.endswith('/')) { base += '/'; }
- var modconcat_rgx = new RegExp('(' + base + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
+ var modconcat_rgx = new RegExp('(' + BASE_URL + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
return modconcat_rgx.exec(url);
},
@@ -285,8 +283,6 @@
setFormsTarget(node);
}
_loadDynamicFragments(node);
- // XXX [3.7] jQuery.one is now used instead jQuery.bind,
- // jquery.treeview.js can be unpatched accordingly.
jQuery(cw).trigger('server-response', [true, node]);
jQuery(node).trigger('server-response', [true, node]);
}
@@ -379,8 +375,8 @@
* dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
*/
function loadRemote(url, form, reqtype, sync) {
- if (!url.toLowerCase().startswith(baseuri().toLowerCase())) {
- url = baseuri() + url;
+ if (!url.toLowerCase().startswith(BASE_URL.toLowerCase())) {
+ url = BASE_URL + url;
}
if (!sync) {
var deferred = new Deferred();
@@ -609,7 +605,7 @@
var fck = new FCKeditor(this.id);
fck.Config['CustomConfigurationsPath'] = fckconfigpath;
fck.Config['DefaultLanguage'] = fcklang;
- fck.BasePath = baseuri() + "fckeditor/";
+ fck.BasePath = BASE_URL + "fckeditor/";
fck.ReplaceTextarea();
} else {
cw.log('fckeditor could not be found.');
--- a/web/data/cubicweb.edition.js Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.edition.js Mon Dec 01 11:13:10 2014 +0100
@@ -67,7 +67,7 @@
rql: rql_for_eid(eid),
'__notemplate': 1
};
- var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
+ var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(BASE_URL + 'view', args, 'post', 'append');
d.addCallback(function() {
_showMatchingSelect(eid, jQuery('#' + divId));
});
--- a/web/data/cubicweb.facets.js Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.facets.js Mon Dec 01 11:13:10 2014 +0100
@@ -69,7 +69,7 @@
}
var $focusLink = $('#focusLink');
if ($focusLink.length) {
- var url = baseuri()+ 'view?rql=' + encodeURIComponent(rql);
+ var url = BASE_URL + 'view?rql=' + encodeURIComponent(rql);
if (vid) {
url += '&vid=' + encodeURIComponent(vid);
}
--- a/web/data/cubicweb.htmlhelpers.js Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.htmlhelpers.js Mon Dec 01 11:13:10 2014 +0100
@@ -12,20 +12,13 @@
/**
* .. function:: baseuri()
*
- * returns the document's baseURI. (baseuri() uses document.baseURI if
- * available and inspects the <base> tag manually otherwise.)
+ * returns the document's baseURI.
*/
-function baseuri() {
- if (typeof BASE_URL === 'undefined') {
- // backward compatibility, BASE_URL might be undefined
- var uri = document.baseURI;
- if (uri) { // some browsers don't define baseURI
- return uri.toLowerCase();
- }
- return jQuery('base').attr('href').toLowerCase();
- }
- return BASE_URL;
-}
+baseuri = cw.utils.deprecatedFunction(
+ "[3.20] baseuri() is deprecated, use BASE_URL instead",
+ function () {
+ return BASE_URL;
+ });
/**
* .. function:: setProgressCursor()
@@ -107,18 +100,6 @@
}
/**
- * .. function:: popupLoginBox()
- *
- * toggles visibility of login popup div
- */
-// XXX used exactly ONCE in basecomponents
-popupLoginBox = cw.utils.deprecatedFunction(
- function() {
- $('#popupLoginBox').toggleClass('hidden');
- jQuery('#__login:visible').focus();
-});
-
-/**
* .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
*
* returns the list of elements in the document matching the tag name
--- a/web/data/cubicweb.js Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.js Mon Dec 01 11:13:10 2014 +0100
@@ -208,91 +208,40 @@
},
/**
- * .. function:: formContents(elem \/* = document.body *\/)
+ * .. function:: formContents(elem)
*
- * this implementation comes from MochiKit
+ * cannot use jQuery.serializeArray() directly because of FCKeditor
*/
- formContents: function (elem /* = document.body */ ) {
- var names = [];
- var values = [];
- if (typeof(elem) == "undefined" || elem === null) {
- elem = document.body;
- } else {
- elem = cw.getNode(elem);
- }
- cw.utils.nodeWalkDepthFirst(elem, function (elem) {
- var name = elem.name;
- if (name && name.length) {
- if (elem.disabled) {
- return null;
- }
- var tagName = elem.tagName.toUpperCase();
- if (tagName === "INPUT" && (elem.type == "radio" || elem.type == "checkbox") && !elem.checked) {
- return null;
- }
- if (tagName === "SELECT") {
- if (elem.type == "select-one") {
- if (elem.selectedIndex >= 0) {
- var opt = elem.options[elem.selectedIndex];
- var v = opt.value;
- if (!v) {
- var h = opt.outerHTML;
- // internet explorer sure does suck.
- if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
- v = opt.text;
- }
- }
- names.push(name);
- values.push(v);
+ formContents: function (elem) {
+ var $elem, array, names, values;
+ $elem = cw.jqNode(elem);
+ array = $elem.serializeArray();
+
+ if (typeof FCKeditor !== 'undefined') {
+ $elem.find('textarea').each(function (idx, textarea) {
+ var fck = FCKeditorAPI.GetInstance(textarea.id);
+ if (fck) {
+ array = jQuery.map(array, function (dict) {
+ if (dict.name === textarea.name) {
+ // filter out the textarea's - likely empty - value ...
return null;
}
- // no form elements?
- names.push(name);
- values.push("");
- return null;
- } else {
- var opts = elem.options;
- if (!opts.length) {
- names.push(name);
- values.push("");
- return null;
- }
- for (var i = 0; i < opts.length; i++) {
- var opt = opts[i];
- if (!opt.selected) {
- continue;
- }
- var v = opt.value;
- if (!v) {
- var h = opt.outerHTML;
- // internet explorer sure does suck.
- if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
- v = opt.text;
- }
- }
- names.push(name);
- values.push(v);
- }
- return null;
- }
+ return dict;
+ });
+ // ... so we can put the HTML coming from FCKeditor instead.
+ array.push({
+ name: textarea.name,
+ value: fck.GetHTML()
+ });
}
- if (tagName === "FORM" || tagName === "P" || tagName === "SPAN" || tagName === "DIV") {
- return elem.childNodes;
- }
- var value = elem.value;
- if (tagName === "TEXTAREA") {
- if (typeof(FCKeditor) != 'undefined') {
- var fck = FCKeditorAPI.GetInstance(elem.id);
- if (fck) {
- value = fck.GetHTML();
- }
- }
- }
- names.push(name);
- values.push(value || '');
- return null;
- }
- return elem.childNodes;
+ });
+ }
+
+ names = [];
+ values = [];
+ jQuery.each(array, function (idx, dict) {
+ names.push(dict.name);
+ values.push(dict.value);
});
return [names, values];
},
--- a/web/data/cubicweb.timeline-bundle.js Sun Nov 30 21:24:36 2014 +0100
+++ b/web/data/cubicweb.timeline-bundle.js Mon Dec 01 11:13:10 2014 +0100
@@ -3,8 +3,8 @@
* :organization: Logilab
*/
-var SimileAjax_urlPrefix = baseuri() + 'data/';
-var Timeline_urlPrefix = baseuri() + 'data/';
+var SimileAjax_urlPrefix = BASE_URL + 'data/';
+var Timeline_urlPrefix = BASE_URL + 'data/';
/*
* Simile Ajax API
--- a/web/formfields.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/formfields.py Mon Dec 01 11:13:10 2014 +0100
@@ -349,13 +349,7 @@
def initial_typed_value(self, form, load_bytes):
if self.value is not _MARKER:
if callable(self.value):
- # pylint: disable=E1102
- if support_args(self.value, 'form', 'field'):
- return self.value(form, self)
- else:
- warn("[3.10] field's value callback must now take form and "
- "field as argument (%s)" % self, DeprecationWarning)
- return self.value(form)
+ return self.value(form, self)
return self.value
formattr = '%s_%s_default' % (self.role, self.name)
if self.eidparam and self.role is not None:
@@ -529,6 +523,7 @@
"""
widget = fw.TextArea
size = 45
+ placeholder = None
def __init__(self, name=None, max_length=None, **kwargs):
self.max_length = max_length # must be set before super call
@@ -547,6 +542,9 @@
elif isinstance(self.widget, fw.TextInput):
self.init_text_input(self.widget)
+ if self.placeholder:
+ self.widget.attrs.setdefault('placeholder', self.placeholder)
+
def init_text_input(self, widget):
if self.max_length:
widget.attrs.setdefault('size', min(self.size, self.max_length))
@@ -557,6 +555,11 @@
widget.attrs.setdefault('cols', 60)
widget.attrs.setdefault('rows', 5)
+ def set_placeholder(self, placeholder):
+ self.placeholder = placeholder
+ if self.widget and self.placeholder:
+ self.widget.attrs.setdefault('placeholder', self.placeholder)
+
class PasswordField(StringField):
"""Use this field to edit password (`Password` yams type, encoded python
@@ -776,11 +779,13 @@
actually contains some text.
If the stream format is one of text/plain, text/html, text/rest,
+ text/markdown
then a :class:`~cubicweb.web.formwidgets.TextArea` will be additionaly
displayed, allowing to directly the file's content when desired, instead
of choosing a file from user's file system.
"""
- editable_formats = ('text/plain', 'text/html', 'text/rest')
+ editable_formats = (
+ 'text/plain', 'text/html', 'text/rest', 'text/markdown')
def render(self, form, renderer):
wdgs = [super(EditableFileField, self).render(form, renderer)]
--- a/web/formwidgets.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/formwidgets.py Mon Dec 01 11:13:10 2014 +0100
@@ -210,6 +210,8 @@
attrs['id'] = field.dom_id(form, self.suffix)
if self.settabindex and not 'tabindex' in attrs:
attrs['tabindex'] = form._cw.next_tabindex()
+ if 'placeholder' in attrs:
+ attrs['placeholder'] = form._cw._(attrs['placeholder'])
return attrs
def values(self, form, field):
--- a/web/http_headers.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/http_headers.py Mon Dec 01 11:13:10 2014 +0100
@@ -2,8 +2,6 @@
# http://twistedmatrix.com/trac/wiki/TwistedWeb2
-from __future__ import generators
-
import types, time
from calendar import timegm
import base64
@@ -934,9 +932,13 @@
#### Cookies. Blech!
class Cookie(object):
- # __slots__ = ['name', 'value', 'path', 'domain', 'ports', 'expires', 'discard', 'secure', 'comment', 'commenturl', 'version']
+ # __slots__ = ['name', 'value', 'path', 'domain', 'ports', 'expires',
+ # 'discard', 'secure', 'httponly', 'comment', 'commenturl',
+ # 'version']
- def __init__(self, name, value, path=None, domain=None, ports=None, expires=None, discard=False, secure=False, comment=None, commenturl=None, version=0):
+ def __init__(self, name, value, path=None, domain=None, ports=None,
+ expires=None, discard=False, secure=False, httponly=False,
+ comment=None, commenturl=None, version=0):
self.name = name
self.value = value
self.path = path
@@ -945,6 +947,7 @@
self.expires = expires
self.discard = discard
self.secure = secure
+ self.httponly = httponly
self.comment = comment
self.commenturl = commenturl
self.version = version
@@ -955,7 +958,8 @@
if self.domain is not None: s+=", domain=%r" % (self.domain,)
if self.ports is not None: s+=", ports=%r" % (self.ports,)
if self.expires is not None: s+=", expires=%r" % (self.expires,)
- if self.secure is not False: s+=", secure=%r" % (self.secure,)
+ if self.secure: s+=", secure"
+ if self.httponly: s+=", HttpOnly"
if self.comment is not None: s+=", comment=%r" % (self.comment,)
if self.commenturl is not None: s+=", commenturl=%r" % (self.commenturl,)
if self.version != 0: s+=", version=%r" % (self.version,)
@@ -1213,6 +1217,8 @@
out.append("domain=%s" % cookie.domain)
if cookie.secure:
out.append("secure")
+ if cookie.httponly:
+ out.append("HttpOnly")
setCookies.append('; '.join(out))
return setCookies
@@ -1240,6 +1246,8 @@
out.append("Port=%s" % quoteString(",".join([str(x) for x in cookie.ports])))
if cookie.secure:
out.append("Secure")
+ if cookie.httponly:
+ out.append("HttpOnly")
out.append('Version="1"')
setCookies.append('; '.join(out))
return setCookies
@@ -1339,6 +1347,9 @@
h = self._headers.get(name, None)
r = self.handler.generate(name, h)
if r is not None:
+ assert isinstance(r, list)
+ for v in r:
+ assert isinstance(v, str)
self._raw_headers[name] = r
return r
@@ -1377,6 +1388,9 @@
Value should be a list of strings, each being one header of the
given name.
"""
+ assert isinstance(value, list)
+ for v in value:
+ assert isinstance(v, str)
name = name.lower()
self._raw_headers[name] = value
self._headers[name] = _RecalcNeeded
--- a/web/request.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/request.py Mon Dec 01 11:13:10 2014 +0100
@@ -139,6 +139,11 @@
self.setup_params(form)
#: received body
self.content = StringIO()
+ # prepare output header
+ #: Header used for the final response
+ self.headers_out = Headers()
+ #: HTTP status use by the final response
+ self.status_out = 200
# set up language based on request headers or site default (we don't
# have a user yet, and might not get one)
self.set_user_language(None)
@@ -152,11 +157,6 @@
#: page id, set by htmlheader template
self.pageid = None
self._set_pageid()
- # prepare output header
- #: Header used for the final response
- self.headers_out = Headers()
- #: HTTP status use by the final response
- self.status_out = 200
def _set_pageid(self):
"""initialize self.pageid
@@ -179,7 +179,7 @@
self.ajax_request = value
json_request = property(_get_json_request, _set_json_request)
- def base_url(self, secure=None):
+ def _base_url(self, secure=None):
"""return the root url of the instance
secure = False -> base-url
@@ -192,7 +192,7 @@
if secure:
base_url = self.vreg.config.get('https-url')
if base_url is None:
- base_url = super(_CubicWebRequestBase, self).base_url()
+ base_url = super(_CubicWebRequestBase, self)._base_url()
return base_url
@property
@@ -439,10 +439,6 @@
"""
self.add_js('cubicweb.ajax.js')
jsfunc = kwargs.pop('jsfunc', 'userCallbackThenReloadPage')
- if 'msg' in kwargs:
- warn('[3.10] msg should be given as positional argument',
- DeprecationWarning, stacklevel=2)
- args = (kwargs.pop('msg'),) + args
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)
@@ -569,7 +565,7 @@
except KeyError:
return SimpleCookie()
- def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
+ def set_cookie(self, name, value, maxage=300, expires=None, secure=False, httponly=False):
"""set / update a cookie
by default, cookie will be available for the next 5 minutes.
@@ -595,7 +591,7 @@
expires = None
# make sure cookie is set on the correct path
cookie = Cookie(str(name), str(value), self.base_url_path(),
- expires=expires, secure=secure)
+ expires=expires, secure=secure, httponly=httponly)
self.headers_out.addHeader('Set-cookie', cookie)
def remove_cookie(self, name, bwcompat=None):
@@ -786,10 +782,6 @@
if 'Expires' not in self.headers_out:
# Expires header seems to be required by IE7 -- Are you sure ?
self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
- if self.http_method() == 'HEAD':
- self.status_out = 200
- # XXX replace by True once validate_cache bw compat method is dropped
- return 200
# /!\ no raise, the function returns and we keep processing the request
else:
# overwrite headers_out to forge a brand new not-modified response
@@ -1005,6 +997,7 @@
pass
if vreg.config.get('language-negociation', False):
# 2. http accept-language
+ self.headers_out.addHeader('Vary', 'Accept-Language')
for lang in self.header_accept_language():
if lang in self.translations:
self.set_language(lang)
--- a/web/test/unittest_form.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_form.py Mon Dec 01 11:13:10 2014 +0100
@@ -16,7 +16,10 @@
# 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 time
+
from xml.etree.ElementTree import fromstring
+from lxml import html
from logilab.common.testlib import unittest_main, mock_object
@@ -104,7 +107,9 @@
req.form['__linkto'] = 'in_group:%s:subject' % geid
form = self.vreg['forms'].select('edition', req, entity=e)
form.content_type = 'text/html'
- pageinfo = self._check_html(form.render(), form, template=None)
+ data = []
+ form.render(w=data.append)
+ pageinfo = self._check_html(u'\n'.join(data), form, template=None)
inputs = pageinfo.find_tag('select', False)
ok = False
for selectnode in pageinfo.matching_nodes('select', name='from_in_group-subject:A'):
@@ -124,6 +129,24 @@
self.assertIn('content_format', data)
+ def test_form_generation_time(self):
+ with self.admin_access.web_request() as req:
+ e = req.create_entity('BlogEntry', title=u'cubicweb.org', content=u"hop")
+ expected_field_name = '__form_generation_time:%d' % e.eid
+
+ ts_before = time.time()
+ form = self.vreg['forms'].select('edition', req, entity=e)
+ ts_after = time.time()
+
+ data = []
+ form.render(action='edit', w=data.append)
+ html_form = html.fromstring(''.join(data)).forms[0]
+ fields = dict(html_form.form_values())
+ self.assertIn(expected_field_name, fields)
+ ts = float(fields[expected_field_name])
+ self.assertTrue(ts_before < ts < ts_after)
+
+
# form tests ##############################################################
def test_form_inheritance(self):
@@ -133,14 +156,18 @@
creation_date = DateTimeField(widget=DateTimePicker)
form = CustomChangeStateForm(req, redirect_path='perdu.com',
entity=req.user)
- form.render(formvalues=dict(state=123, trcomment=u'',
+ data = []
+ form.render(w=data.append,
+ formvalues=dict(state=123, trcomment=u'',
trcomment_format=u'text/plain'))
def test_change_state_form(self):
with self.admin_access.web_request() as req:
form = ChangeStateForm(req, redirect_path='perdu.com',
entity=req.user)
- form.render(formvalues=dict(state=123, trcomment=u'',
+ data = []
+ form.render(w=data.append,
+ formvalues=dict(state=123, trcomment=u'',
trcomment_format=u'text/plain'))
# fields tests ############################################################
@@ -168,6 +195,7 @@
self._test_richtextfield(req, '''<select id="description_format-subject:%(eid)s" name="description_format-subject:%(eid)s" size="1" style="display: block" tabindex="1">
<option value="text/cubicweb-page-template">text/cubicweb-page-template</option>
<option selected="selected" value="text/html">text/html</option>
+<option value="text/markdown">text/markdown</option>
<option value="text/plain">text/plain</option>
<option value="text/rest">text/rest</option>
</select><textarea cols="80" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2" tabindex="2"></textarea>''')
--- a/web/test/unittest_http.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_http.py Mon Dec 01 11:13:10 2014 +0100
@@ -227,7 +227,7 @@
hout = [('etag', 'rhino/really-not-babar'),
]
req = _test_cache(hin, hout, method='HEAD')
- self.assertCache(200, req.status_out, 'modifier HEAD verb')
+ self.assertCache(None, req.status_out, 'modifier HEAD verb')
# not modified
hin = [('if-none-match', 'babar'),
]
--- a/web/test/unittest_views_basecontrollers.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_views_basecontrollers.py Mon Dec 01 11:13:10 2014 +0100
@@ -23,7 +23,11 @@
from urlparse import parse_qs as url_parse_query
except ImportError:
from cgi import parse_qs as url_parse_query
+
+import lxml
+
from logilab.common.testlib import unittest_main
+
from logilab.common.decorators import monkeypatch
from cubicweb import Binary, NoSelectableObject, ValidationError
@@ -81,6 +85,49 @@
self.assertEqual({'login-subject': 'the value "admin" is already used, use another one'},
cm.exception.errors)
+ def test_simultaneous_edition_only_one_commit(self):
+ """ Allow two simultaneous edit view of the same entity as long as only one commits
+ """
+ with self.admin_access.web_request() as req:
+ e = req.create_entity('BlogEntry', title=u'cubicweb.org', content=u"hop")
+ expected_path = e.rest_path()
+ req.cnx.commit()
+ form = self.vreg['views'].select('edition', req, rset=e.as_rset(), row=0)
+ html_form = lxml.html.fromstring(form.render(w=None, action='edit')).forms[0]
+
+ with self.admin_access.web_request() as req2:
+ form2 = self.vreg['views'].select('edition', req, rset=e.as_rset(), row=0)
+
+ with self.admin_access.web_request(**dict(html_form.form_values())) as req:
+ path, args = self.expect_redirect_handle_request(req, path='edit')
+ self.assertEqual(path, expected_path)
+
+ def test_simultaneous_edition_refuse_second_commit(self):
+ """ Disallow committing changes to an entity edited in between """
+ with self.admin_access.web_request() as req:
+ e = req.create_entity('BlogEntry', title=u'cubicweb.org', content=u"hop")
+ eid = e.eid
+ req.cnx.commit()
+ form = self.vreg['views'].select('edition', req, rset=e.as_rset(), row=0)
+ html_form = lxml.html.fromstring(form.render(w=None, action='edit')).forms[0]
+
+ with self.admin_access.web_request() as req2:
+ e = req2.entity_from_eid(eid)
+ e.cw_set(content = u"hip")
+ req2.cnx.commit()
+
+ form_field_name = "content-subject:%d" % eid
+ form_values = dict(html_form.form_values())
+ assert form_field_name in form_values
+ form_values[form_field_name] = u'yep'
+ with self.admin_access.web_request(**form_values) as req:
+ with self.assertRaises(ValidationError) as cm:
+ self.ctrl_publish(req)
+ reported_eid, dict_info = cm.exception.args
+ self.assertEqual(reported_eid, eid)
+ self.assertIn(None, dict_info)
+ self.assertIn("has changed since you started to edit it.", dict_info[None])
+
def test_user_editing_itself(self):
"""checking that a manager user can edit itself
"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_forms.py Mon Dec 01 11:13:10 2014 +0100
@@ -0,0 +1,36 @@
+# 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/>.
+
+from cubicweb.devtools.testlib import CubicWebTC
+
+class InlinedFormTC(CubicWebTC):
+
+ def test_linked_to(self):
+ req = self.request()
+ formview = req.vreg['views'].select(
+ 'inline-creation', req,
+ etype='File', rtype='described_by_test', role='subject',
+ peid=123,
+ petype='Salesterm')
+ self.assertEqual({('described_by_test', 'object'): [123]},
+ formview.form.linked_to)
+
+if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
+
--- a/web/test/unittest_views_json.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_views_json.py Mon Dec 01 11:13:10 2014 +0100
@@ -70,6 +70,11 @@
self.assertEqual(data[0]['name'], 'guests')
self.assertEqual(data[1]['name'], 'managers')
+ rset = req.execute('Any G WHERE G is CWGroup, G name "foo"')
+ data = self.view('ejsonexport', rset, req=req)
+ self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json'])
+ self.assertEqual(data, [])
+
class NotAnonymousJsonViewsTC(JsonViewsTC):
anonymize = False
--- a/web/test/unittest_viewselector.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_viewselector.py Mon Dec 01 11:13:10 2014 +0100
@@ -110,6 +110,7 @@
self.assertListEqual(self.pviews(req, rset),
[('csvexport', csvexport.CSVRsetView),
('ecsvexport', csvexport.CSVEntityView),
+ ('ejsonexport', json.JsonEntityView),
('jsonexport', json.JsonRsetView),
])
--- a/web/test/unittest_web.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/test/unittest_web.py Mon Dec 01 11:13:10 2014 +0100
@@ -94,15 +94,37 @@
self.assertEqual(webreq.status_code, 200)
self.assertDictEqual(expect, loads(webreq.content))
+
class LanguageTC(CubicWebServerTC):
def test_language_neg(self):
headers = {'Accept-Language': 'fr'}
webreq = self.web_request(headers=headers)
self.assertIn('lang="fr"', webreq.read())
+ vary = [h.lower().strip() for h in webreq.getheader('Vary').split(',')]
+ self.assertIn('accept-language', vary)
headers = {'Accept-Language': 'en'}
webreq = self.web_request(headers=headers)
self.assertIn('lang="en"', webreq.read())
+ vary = [h.lower().strip() for h in webreq.getheader('Vary').split(',')]
+ self.assertIn('accept-language', vary)
+
+ def test_response_codes(self):
+ with self.admin_access.client_cnx() as cnx:
+ admin_eid = cnx.user.eid
+ # guest can't see admin
+ webreq = self.web_request('/%d' % admin_eid)
+ self.assertEqual(webreq.status, 403)
+
+ # but admin can
+ self.web_login()
+ webreq = self.web_request('/%d' % admin_eid)
+ self.assertEqual(webreq.status, 200)
+
+ def test_session_cookie_httponly(self):
+ webreq = self.web_request()
+ self.assertIn('HttpOnly', webreq.getheader('set-cookie'))
+
class LogQueriesTC(CubicWebServerTC):
@classmethod
--- a/web/views/ajaxedit.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/ajaxedit.py Mon Dec 01 11:13:10 2014 +0100
@@ -36,8 +36,6 @@
cw_property_defs = {} # don't want to inherit this from Box
expected_kwargs = form_params = ('rtype', 'target')
- build_js = component.EditRelationMixIn.build_reload_js_call
-
def cell_call(self, row, col, rtype=None, target=None, etype=None):
self.rtype = rtype or self._cw.form['rtype']
self.target = target or self._cw.form['target']
--- a/web/views/authentication.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/authentication.py Mon Dec 01 11:13:10 2014 +0100
@@ -148,11 +148,13 @@
raise :exc:`cubicweb.AuthenticationError` if authentication failed
(no authentication info found or wrong user/password)
"""
+ has_auth = False
for retriever in self.authinforetrievers:
try:
login, authinfo = retriever.authentication_information(req)
except NoAuthInfo:
continue
+ has_auth = True
try:
session = self._authenticate(login, authinfo)
except AuthenticationError:
@@ -161,9 +163,9 @@
for retriever_ in self.authinforetrievers:
retriever_.authenticated(retriever, req, session, login, authinfo)
return session, login
- # false if no authentication info found, eg this is not an
+ # false if no authentication info found, i.e. this is not an
# authentication failure
- if 'login' in locals():
+ if has_auth:
req.set_message(req._('authentication failure'))
login, authinfo = self.anoninfo
if login:
--- a/web/views/editcontroller.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/editcontroller.py Mon Dec 01 11:13:10 2014 +0100
@@ -22,6 +22,8 @@
from warnings import warn
from collections import defaultdict
+from datetime import datetime
+
from logilab.common.deprecation import deprecated
from logilab.common.graph import ordered_nodes
@@ -266,6 +268,7 @@
if eid is None: # creation or copy
entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery)
elif rqlquery.edited: # edition of an existant entity
+ self.check_concurrent_edition(formparams, eid)
self._update_entity(eid, rqlquery)
if is_main_entity:
self.notify_edited(entity)
@@ -362,6 +365,23 @@
else:
self._cw.set_message(self._cw._('entity deleted'))
+
+ def check_concurrent_edition(self, formparams, eid):
+ req = self._cw
+ try:
+ form_ts = datetime.fromtimestamp(float(formparams['__form_generation_time']))
+ except KeyError:
+ # Backward and tests compatibility : if no timestamp consider edition OK
+ return
+ if req.execute("Any X WHERE X modification_date > %(fts)s, X eid %(eid)s",
+ {'eid': eid, 'fts': form_ts}):
+ # We only mark the message for translation but the actual
+ # translation will be handled by the Validation mechanism...
+ msg = _("Entity %(eid)s has changed since you started to edit it."
+ " Reload the page and reapply your changes.")
+ # ... this is why we pass the formats' dict as a third argument.
+ raise ValidationError(eid, {None: msg}, {'eid' : eid})
+
def _action_apply(self):
self._default_publish()
self.reset()
--- a/web/views/formrenderers.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/formrenderers.py Mon Dec 01 11:13:10 2014 +0100
@@ -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.
@@ -142,12 +142,7 @@
help = []
descr = field.help
if callable(descr):
- if support_args(descr, 'form', 'field'):
- descr = descr(form, field)
- else:
- warn("[3.10] field's help callback must now take form and field as argument (%s)"
- % field, DeprecationWarning)
- descr = descr(form)
+ descr = descr(form, field)
if descr:
help.append('<div class="helper">%s</div>' % self._cw._(descr))
example = field.example_format(self._cw)
--- a/web/views/forms.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/forms.py Mon Dec 01 11:13:10 2014 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 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.
@@ -41,16 +41,20 @@
but you'll use this one rarely.
"""
+
__docformat__ = "restructuredtext en"
+
from warnings import warn
+import time
+
from logilab.common import dictattr, tempattr
from logilab.common.decorators import iclassmethod, cached
from logilab.common.textutils import splitstrip
from logilab.common.deprecation import deprecated
-from cubicweb import ValidationError
+from cubicweb import ValidationError, neg_role
from cubicweb.utils import support_args
from cubicweb.predicates import non_final_entity, match_kwargs, one_line_rset
from cubicweb.web import RequestError, ProcessFormError
@@ -196,24 +200,10 @@
Extra keyword arguments will be given to renderer's :meth:`render` method.
"""
w = kwargs.pop('w', None)
- if w is None:
- warn('[3.10] you should specify "w" to form.render() named arguments',
- DeprecationWarning, stacklevel=2)
- data = []
- w = data.append
- else:
- data = None
self.build_context(formvalues)
if renderer is None:
renderer = self.default_renderer()
- if support_args(renderer.render, 'w'):
- renderer.render(w, self, kwargs)
- else:
- warn('[3.10] you should add "w" as first argument o %s.render()'
- % renderer.__class__, DeprecationWarning)
- w(renderer.render(self, kwargs))
- if data is not None:
- return '\n'.join(data)
+ renderer.render(w, self, kwargs)
def default_renderer(self):
return self._cw.vreg['formrenderers'].select(
@@ -362,7 +352,9 @@
self.uicfg_affk = self._cw.vreg['uicfg'].select(
'autoform_field_kwargs', self._cw, entity=self.edited_entity)
self.add_hidden('__type', self.edited_entity.cw_etype, eidparam=True)
+
self.add_hidden('eid', self.edited_entity.eid)
+ self.add_generation_time()
# mainform default to true in parent, hence default to True
if kwargs.get('mainform', True) or kwargs.get('mainentity', False):
self.add_hidden(u'__maineid', self.edited_entity.eid)
@@ -376,6 +368,11 @@
msgid = self._cw.set_redirect_message(msg)
self.add_hidden('_cwmsgid', msgid)
+ def add_generation_time(self):
+ # NB repr is critical to avoid truncation of the timestamp
+ self.add_hidden('__form_generation_time', repr(time.time()),
+ eidparam=True)
+
def add_linkto_hidden(self):
"""add the __linkto hidden field used to directly attach the new object
to an existing other one when the relation between those two is not
@@ -399,12 +396,21 @@
@property
@cached
def linked_to(self):
- # if current form is not the main form, exit immediately
+ linked_to = {}
+ # case where this is an embeded creation form
+ try:
+ eid = int(self.cw_extra_kwargs['peid'])
+ except KeyError:
+ pass
+ else:
+ ltrtype = self.cw_extra_kwargs['rtype']
+ ltrole = neg_role(self.cw_extra_kwargs['role'])
+ linked_to[(ltrtype, ltrole)] = [eid]
+ # now consider __linkto if the current form is the main form
try:
self.field_by_name('__maineid')
except form.FieldNotFound:
- return {}
- linked_to = {}
+ return linked_to
for linkto in self._cw.list_form_param('__linkto'):
ltrtype, eid, ltrole = linkto.split(':')
linked_to.setdefault((ltrtype, ltrole), []).append(int(eid))
--- a/web/views/ibreadcrumbs.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/ibreadcrumbs.py Mon Dec 01 11:13:10 2014 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 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.
@@ -64,16 +64,9 @@
"""
parent = self.parent_entity()
if parent is not None:
- if recurs is True:
- _recurs = set()
- warn('[3.10] recurs argument should be a set() or None',
- DeprecationWarning, stacklevel=2)
- elif recurs:
+ if recurs:
_recurs = recurs
else:
- if recurs is False:
- warn('[3.10] recurs argument should be a set() or None',
- DeprecationWarning, stacklevel=2)
_recurs = set()
if _recurs and parent.eid in _recurs:
self.error('cycle in breadcrumbs for entity %s' % self.entity)
--- a/web/views/idownloadable.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/idownloadable.py Mon Dec 01 11:13:10 2014 +0100
@@ -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.
@@ -34,23 +34,6 @@
from cubicweb.web.views import primary, baseviews
-@deprecated('[3.10] use a custom IDownloadable adapter instead')
-def download_box(w, entity, title=None, label=None, footer=u''):
- req = entity._cw
- w(u'<div class="sideBox">')
- if title is None:
- title = req._('download')
- w(u'<div class="sideBoxTitle downloadBoxTitle"><span>%s</span></div>'
- % xml_escape(title))
- w(u'<div class="sideBox downloadBox"><div class="sideBoxBody">')
- w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
- % (xml_escape(entity.cw_adapt_to('IDownloadable').download_url()),
- req.uiprops['DOWNLOAD_ICON'],
- req._('download icon'), xml_escape(label or entity.dc_title())))
- w(u'%s</div>' % footer)
- w(u'</div></div>\n')
-
-
class DownloadBox(component.EntityCtxComponent):
"""add download box"""
__regid__ = 'download_box' # no download box for images
@@ -175,10 +158,6 @@
self.w(u'<a href="%s">%s</a> [<a href="%s">%s</a>]' %
(url, name, durl, self._cw._('download')))
-IDownloadableLineView = class_renamed(
- 'IDownloadableLineView', IDownloadableOneLineView,
- '[3.10] IDownloadableLineView is deprecated, use IDownloadableOneLineView')
-
class AbstractEmbeddedView(EntityView):
__abstract__ = True
--- a/web/views/json.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/json.py Mon Dec 01 11:13:10 2014 +0100
@@ -21,7 +21,7 @@
_ = unicode
from cubicweb.utils import json_dumps
-from cubicweb.predicates import any_rset
+from cubicweb.predicates import any_rset, empty_rset
from cubicweb.view import EntityView, AnyRsetView
from cubicweb.web.application import anonymized_request
from cubicweb.web.views import basecontrollers
@@ -106,6 +106,7 @@
- ``__cwetype__`` : entity type
"""
__regid__ = 'ejsonexport'
+ __select__ = EntityView.__select__ | empty_rset()
title = _('json-entities-export-view')
def call(self):
--- a/web/views/primary.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/primary.py Mon Dec 01 11:13:10 2014 +0100
@@ -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.
@@ -131,16 +131,7 @@
boxes = None
if boxes or hasattr(self, 'render_side_related'):
self.w(u'<table width="100%"><tr><td style="width: 75%">')
- if hasattr(self, 'render_entity_summary'):
- warn('[3.10] render_entity_summary method is deprecated (%s)' % self,
- DeprecationWarning)
- self.render_entity_summary(entity) # pylint: disable=E1101
- summary = self.summary(entity)
- if summary:
- warn('[3.10] summary method is deprecated (%s)' % self,
- DeprecationWarning)
- self.w(u'<div class="summary">%s</div>' % summary)
self.w(u'<div class="mainInfo">')
self.content_navigation_components('navcontenttop')
self.render_entity_attributes(entity)
@@ -189,10 +180,6 @@
def render_entity_toolbox(self, entity):
self.content_navigation_components('ctxtoolbar')
- def summary(self, entity):
- """default implementation return an empty string"""
- return u''
-
def render_entity_attributes(self, entity):
"""Renders all attributes and relations in the 'attributes' section.
"""
@@ -263,23 +250,10 @@
explicit box appobjects selectable in this context.
"""
for box in boxes:
- if isinstance(box, tuple):
- try:
- label, rset, vid, dispctrl = box
- except ValueError:
- label, rset, vid = box
- dispctrl = {}
- warn('[3.10] box views should now be a RsetBox instance, '
- 'please update %s' % self.__class__.__name__,
- DeprecationWarning)
- self.w(u'<div class="sideBox">')
- self.wview(vid, rset, title=label, initargs={'dispctrl': dispctrl})
- self.w(u'</div>')
- else:
- try:
- box.render(w=self.w, row=self.cw_row)
- except TypeError:
- box.render(w=self.w)
+ try:
+ box.render(w=self.w, row=self.cw_row)
+ except TypeError:
+ box.render(w=self.w)
def _prepare_side_boxes(self, entity):
sideboxes = []
--- a/web/views/startup.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/startup.py Mon Dec 01 11:13:10 2014 +0100
@@ -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.
@@ -156,14 +156,6 @@
return u'[<a href="%s" title="%s">+</a>]' % (
xml_escape(url), self._cw.__('New %s' % etype))
- @deprecated('[3.11] display_folders method is deprecated, backport it if needed')
- def display_folders(self):
- return False
-
- @deprecated('[3.11] folders method is deprecated, backport it if needed')
- def folders(self):
- self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by category'))
- self._cw.vreg['views'].select('tree', self._cw).render(w=self.w, maxlevel=1)
class IndexView(ManageView):
--- a/web/views/workflow.py Sun Nov 30 21:24:36 2014 +0100
+++ b/web/views/workflow.py Mon Dec 01 11:13:10 2014 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 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.
@@ -169,14 +169,7 @@
title = _('Workflow history')
def render_body(self, w):
- if hasattr(self, 'cell_call'):
- warn('[3.10] %s should now implement render_body instead of cell_call'
- % self.__class__, DeprecationWarning)
- self.w = w
- # pylint: disable=E1101
- self.cell_call(self.entity.cw_row, self.entity.cw_col)
- else:
- self.entity.view('wfhistory', w=w, title=None)
+ self.entity.view('wfhistory', w=w, title=None)
class InContextWithStateView(EntityView):
--- a/wsgi/request.py Sun Nov 30 21:24:36 2014 +0100
+++ b/wsgi/request.py Mon Dec 01 11:13:10 2014 +0100
@@ -70,7 +70,7 @@
if k.startswith('HTTP_'))
if 'CONTENT_TYPE' in environ:
headers_in['Content-Type'] = environ['CONTENT_TYPE']
- https = environ["wsgi.url_scheme"] == 'https'
+ https = self.is_secure()
if self.path.startswith('/https/'):
self.path = self.path[6:]
self.environ['PATH_INFO'] = self.path
@@ -118,32 +118,8 @@
## wsgi request helpers ###################################################
- def instance_uri(self):
- """Return the instance's base URI (no PATH_INFO or QUERY_STRING)
-
- see python2.5's wsgiref.util.instance_uri code
- """
- environ = self.environ
- url = environ['wsgi.url_scheme'] + '://'
- if environ.get('HTTP_HOST'):
- url += environ['HTTP_HOST']
- else:
- url += environ['SERVER_NAME']
- if environ['wsgi.url_scheme'] == 'https':
- if environ['SERVER_PORT'] != '443':
- url += ':' + environ['SERVER_PORT']
- else:
- if environ['SERVER_PORT'] != '80':
- url += ':' + environ['SERVER_PORT']
- url += quote(environ.get('SCRIPT_NAME') or '/')
- return url
-
- def get_full_path(self):
- return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + self.environ.get('QUERY_STRING', '')) or '')
-
def is_secure(self):
- return 'wsgi.url_scheme' in self.environ \
- and self.environ['wsgi.url_scheme'] == 'https'
+ return self.environ['wsgi.url_scheme'] == 'https'
def get_posted_data(self):
# The WSGI spec says 'QUERY_STRING' may be absent.