# HG changeset patch # User Pierre-Yves David # Date 1358443808 -3600 # Node ID ab57000bff7b13c779caaea7adc55f1d5d58f302 # Parent 7021bba2dcf29b24932b44d6db0e7040f2f737a7# Parent 7812093e21f7fc7a7bc91ccc6070bcfa1202b7ac merge with stable diff -r 7812093e21f7 -r ab57000bff7b __init__.py --- a/__init__.py Thu Jan 17 17:12:06 2013 +0100 +++ b/__init__.py Thu Jan 17 18:30:08 2013 +0100 @@ -199,3 +199,26 @@ CW_EVENT_MANAGER.bind(event, func, *args, **kwargs) return func return _decorator + + +from yams.schema import role_name as rname + +def validation_error(entity, errors, substitutions=None, i18nvalues=None): + """easy way to retrieve a :class:`cubicweb.ValidationError` for an entity or eid. + + You may also have 2-tuple as error keys, :func:`yams.role_name` will be + called automatically for them. + + Messages in errors **should not be translated yet**, though marked for + internationalization. You may give an additional substition dictionary that + will be used for interpolation after the translation. + """ + if substitutions is None: + # set empty dict else translation won't be done for backward + # compatibility reason (see ValidationError.translate method) + substitutions = {} + for key in errors.keys(): + if isinstance(key, tuple): + errors[rname(*key)] = errors.pop(key) + return ValidationError(getattr(entity, 'eid', entity), errors, + substitutions, i18nvalues) diff -r 7812093e21f7 -r ab57000bff7b __pkginfo__.py --- a/__pkginfo__.py Thu Jan 17 17:12:06 2013 +0100 +++ b/__pkginfo__.py Thu Jan 17 18:30:08 2013 +0100 @@ -43,7 +43,7 @@ 'logilab-common': '>= 0.58.0', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.31.2', - 'yams': '>= 0.34.0', + 'yams': '>= 0.36.0', #gettext # for xgettext, msgcat, etc... # web dependancies 'simplejson': '>= 2.0.9', diff -r 7812093e21f7 -r ab57000bff7b _exceptions.py --- a/_exceptions.py Thu Jan 17 17:12:06 2013 +0100 +++ b/_exceptions.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -19,7 +19,7 @@ __docformat__ = "restructuredtext en" -from yams import ValidationError +from yams import ValidationError as ValidationError # abstract exceptions ######################################################### diff -r 7812093e21f7 -r ab57000bff7b appobject.py --- a/appobject.py Thu Jan 17 17:12:06 2013 +0100 +++ b/appobject.py Thu Jan 17 18:30:08 2013 +0100 @@ -31,25 +31,25 @@ """ __docformat__ = "restructuredtext en" -import types from logging import getLogger -from warnings import warn from logilab.common.deprecation import deprecated, class_renamed from logilab.common.decorators import classproperty from logilab.common.logging_ext import set_log_methods from logilab.common.registry import yes -from cubicweb.cwconfig import CubicWebConfiguration # XXX for bw compat from logilab.common.registry import objectify_predicate, traced_selection, Predicate -objectify_selector = deprecated('[3.15] objectify_selector has been renamed to objectify_predicates in logilab.common.registry')(objectify_predicate) -traced_selection = deprecated('[3.15] traced_selection has been moved to logilab.common.registry')(traced_selection) -Selector = class_renamed( - 'Selector', Predicate, - '[3.15] Selector has been renamed to Predicate in logilab.common.registry') +objectify_selector = deprecated('[3.15] objectify_selector has been ' + 'renamed to objectify_predicates in ' + 'logilab.common.registry')(objectify_predicate) +traced_selection = deprecated('[3.15] traced_selection has been ' + 'moved to logilab.common.registry')(traced_selection) +Selector = class_renamed('Selector', Predicate, + '[3.15] Selector has been renamed to Predicate ' + 'in logilab.common.registry') @deprecated('[3.15] lltrace decorator can now be removed') def lltrace(func): diff -r 7812093e21f7 -r ab57000bff7b cwconfig.py --- a/cwconfig.py Thu Jan 17 17:12:06 2013 +0100 +++ b/cwconfig.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -671,54 +671,6 @@ cubicweb_appobject_path = set(['entities']) cube_appobject_path = set(['entities']) - @classmethod - def build_vregistry_path(cls, templpath, evobjpath=None, tvobjpath=None): - """given a list of directories, return a list of sub files and - directories that should be loaded by the instance objects registry. - - :param evobjpath: - optional list of sub-directories (or files without the .py ext) of - the cubicweb library that should be tested and added to the output list - if they exists. If not give, default to `cubicweb_appobject_path` class - attribute. - :param tvobjpath: - optional list of sub-directories (or files without the .py ext) of - directories given in `templpath` that should be tested and added to - the output list if they exists. If not give, default to - `cube_appobject_path` class attribute. - """ - vregpath = cls.build_vregistry_cubicweb_path(evobjpath) - vregpath += cls.build_vregistry_cube_path(templpath, tvobjpath) - return vregpath - - @classmethod - def build_vregistry_cubicweb_path(cls, evobjpath=None): - vregpath = [] - if evobjpath is None: - evobjpath = cls.cubicweb_appobject_path - # NOTE: for the order, see http://www.cubicweb.org/ticket/2330799 - # it is clearly a workaround - for subdir in sorted(evobjpath, key=lambda x:x != 'entities'): - path = join(CW_SOFTWARE_ROOT, subdir) - if exists(path): - vregpath.append(path) - return vregpath - - @classmethod - def build_vregistry_cube_path(cls, templpath, tvobjpath=None): - vregpath = [] - if tvobjpath is None: - tvobjpath = cls.cube_appobject_path - for directory in templpath: - # NOTE: for the order, see http://www.cubicweb.org/ticket/2330799 - for subdir in sorted(tvobjpath, key=lambda x:x != 'entities'): - path = join(directory, subdir) - if exists(path): - vregpath.append(path) - elif exists(path + '.py'): - vregpath.append(path + '.py') - return vregpath - def __init__(self, debugmode=False): if debugmode: # in python 2.7, DeprecationWarning are not shown anymore by default @@ -766,12 +718,57 @@ # configure simpleTal logger logging.getLogger('simpleTAL').setLevel(logging.ERROR) - def vregistry_path(self): + def appobjects_path(self): """return a list of files or directories where the registry will look for application objects. By default return nothing in NoApp config. """ return [] + def build_appobjects_path(self, templpath, evobjpath=None, tvobjpath=None): + """given a list of directories, return a list of sub files and + directories that should be loaded by the instance objects registry. + + :param evobjpath: + optional list of sub-directories (or files without the .py ext) of + the cubicweb library that should be tested and added to the output list + if they exists. If not give, default to `cubicweb_appobject_path` class + attribute. + :param tvobjpath: + optional list of sub-directories (or files without the .py ext) of + directories given in `templpath` that should be tested and added to + the output list if they exists. If not give, default to + `cube_appobject_path` class attribute. + """ + vregpath = self.build_appobjects_cubicweb_path(evobjpath) + vregpath += self.build_appobjects_cube_path(templpath, tvobjpath) + return vregpath + + def build_appobjects_cubicweb_path(self, evobjpath=None): + vregpath = [] + if evobjpath is None: + evobjpath = self.cubicweb_appobject_path + # NOTE: for the order, see http://www.cubicweb.org/ticket/2330799 + # it is clearly a workaround + for subdir in sorted(evobjpath, key=lambda x:x != 'entities'): + path = join(CW_SOFTWARE_ROOT, subdir) + if exists(path): + vregpath.append(path) + return vregpath + + def build_appobjects_cube_path(self, templpath, tvobjpath=None): + vregpath = [] + if tvobjpath is None: + tvobjpath = self.cube_appobject_path + for directory in templpath: + # NOTE: for the order, see http://www.cubicweb.org/ticket/2330799 + for subdir in sorted(tvobjpath, key=lambda x:x != 'entities'): + path = join(directory, subdir) + if exists(path): + vregpath.append(path) + elif exists(path + '.py'): + vregpath.append(path + '.py') + return vregpath + apphome = None def load_site_cubicweb(self, paths=None): @@ -1177,14 +1174,14 @@ self.exception('localisation support error for language %s', language) - def vregistry_path(self): + def appobjects_path(self): """return a list of files or directories where the registry will look for application objects """ templpath = list(reversed(self.cubes_path())) if self.apphome: # may be unset in tests templpath.append(self.apphome) - return self.build_vregistry_path(templpath) + return self.build_appobjects_path(templpath) def set_sources_mode(self, sources): if not 'all' in sources: diff -r 7812093e21f7 -r ab57000bff7b cwvreg.py --- a/cwvreg.py Thu Jan 17 17:12:06 2013 +0100 +++ b/cwvreg.py Thu Jan 17 18:30:08 2013 +0100 @@ -588,7 +588,7 @@ """set instance'schema and load application objects""" self._set_schema(schema) # now we can load application's web objects - self.reload(self.config.vregistry_path(), force_reload=False) + self.reload(self.config.appobjects_path(), force_reload=False) # map lowered entity type names to their actual name self.case_insensitive_etypes = {} for eschema in self.schema.entities(): @@ -598,7 +598,7 @@ clear_cache(eschema, 'meta_attributes') def reload_if_needed(self): - path = self.config.vregistry_path() + path = self.config.appobjects_path() if self.is_reload_needed(path): self.reload(path) @@ -614,7 +614,7 @@ cfg = self.config for cube in cfg.expand_cubes(cubes, with_recommends=True): if not cube in cubes: - cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)]) + cpath = cfg.build_appobjects_cube_path([cfg.cube_dir(cube)]) cleanup_sys_modules(cpath) self.register_objects(path) CW_EVENT_MANAGER.emit('after-registry-reload') diff -r 7812093e21f7 -r ab57000bff7b dataimport.py --- a/dataimport.py Thu Jan 17 17:12:06 2013 +0100 +++ b/dataimport.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -66,13 +66,18 @@ """ __docformat__ = "restructuredtext en" +import csv import sys -import csv +import threading import traceback +import cPickle import os.path as osp -from StringIO import StringIO +from collections import defaultdict +from contextlib import contextmanager from copy import copy -from datetime import datetime +from datetime import date, datetime +from time import asctime +from StringIO import StringIO from logilab.common import shellutils, attrdict from logilab.common.date import strptime @@ -80,9 +85,11 @@ from logilab.common.deprecation import deprecated from cubicweb import QueryError +from cubicweb.utils import make_uid from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES +from cubicweb.server.edition import EditedEntity +from cubicweb.server.sqlutils import SQL_PREFIX from cubicweb.server.utils import eschema_eid -from cubicweb.server.edition import EditedEntity def count_lines(stream_or_filename): @@ -117,15 +124,33 @@ print ' %s rows imported' % rowcount def ucsvreader(stream, encoding='utf-8', separator=',', quote='"', - skipfirst=False): + skipfirst=False, ignore_errors=False): """A csv reader that accepts files with any encoding and outputs unicode strings """ it = iter(csv.reader(stream, delimiter=separator, quotechar=quote)) - if skipfirst: - it.next() - for row in it: - yield [item.decode(encoding) for item in row] + if not ignore_errors: + if skipfirst: + it.next() + for row in it: + yield [item.decode(encoding) for item in row] + else: + # Skip first line + try: + row = it.next() + except csv.Error: + pass + # Safe version, that can cope with error in CSV file + while True: + try: + row = it.next() + # End of CSV, break + except StopIteration: + break + # Error in CSV, ignore line and continue + except csv.Error: + continue + yield [item.decode(encoding) for item in row] def callfunc_every(func, number, iterable): """yield items of `iterable` one by one and call function `func` @@ -299,6 +324,149 @@ if k is not None and len(v) > 1] +# sql generator utility functions ############################################# + + +def _import_statements(sql_connect, statements, nb_threads=3, + dump_output_dir=None, + support_copy_from=True, encoding='utf-8'): + """ + Import a bunch of sql statements, using different threads. + """ + try: + chunksize = (len(statements) / nb_threads) + 1 + threads = [] + for i in xrange(nb_threads): + chunks = statements[i*chunksize:(i+1)*chunksize] + thread = threading.Thread(target=_execmany_thread, + args=(sql_connect, chunks, + dump_output_dir, + support_copy_from, + encoding)) + thread.start() + threads.append(thread) + for t in threads: + t.join() + except Exception: + print 'Error in import statements' + +def _execmany_thread_not_copy_from(cu, statement, data, table=None, + columns=None, encoding='utf-8'): + """ Execute thread without copy from + """ + cu.executemany(statement, data) + +def _execmany_thread_copy_from(cu, statement, data, table, + columns, encoding='utf-8'): + """ Execute thread with copy from + """ + buf = _create_copyfrom_buffer(data, columns, encoding) + if buf is None: + _execmany_thread_not_copy_from(cu, statement, data) + else: + if columns is None: + cu.copy_from(buf, table, null='NULL') + else: + cu.copy_from(buf, table, null='NULL', columns=columns) + +def _execmany_thread(sql_connect, statements, dump_output_dir=None, + support_copy_from=True, encoding='utf-8'): + """ + Execute sql statement. If 'INSERT INTO', try to use 'COPY FROM' command, + or fallback to execute_many. + """ + if support_copy_from: + execmany_func = _execmany_thread_copy_from + else: + execmany_func = _execmany_thread_not_copy_from + cnx = sql_connect() + cu = cnx.cursor() + try: + for statement, data in statements: + table = None + columns = None + try: + if not statement.startswith('INSERT INTO'): + cu.executemany(statement, data) + continue + table = statement.split()[2] + if isinstance(data[0], (tuple, list)): + columns = None + else: + columns = data[0].keys() + execmany_func(cu, statement, data, table, columns, encoding) + except Exception: + print 'unable to copy data into table %s', table + # Error in import statement, save data in dump_output_dir + if dump_output_dir is not None: + pdata = {'data': data, 'statement': statement, + 'time': asctime(), 'columns': columns} + filename = make_uid() + try: + with open(osp.join(dump_output_dir, + '%s.pickle' % filename), 'w') as fobj: + fobj.write(cPickle.dumps(pdata)) + except IOError: + print 'ERROR while pickling in', dump_output_dir, filename+'.pickle' + pass + cnx.rollback() + raise + finally: + cnx.commit() + cu.close() + +def _create_copyfrom_buffer(data, columns, encoding='utf-8', replace_sep=None): + """ + Create a StringIO buffer for 'COPY FROM' command. + Deals with Unicode, Int, Float, Date... + """ + # 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])) + for row in data: + # Iterate over the different columns and the different values + # and try to convert them to a correct datatype. + # If an error is raised, do not continue. + formatted_row = [] + for col in columns: + value = row[col] + 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)): + # Do not use strftime, as it yields issue + # with date < 1900 + value = '%04d-%02d-%02d' % (value.year, + value.month, + value.day) + else: + return None + # We push the value to the new formatted row + # if the value is not None and could be converted to a string. + formatted_row.append(value) + rows.append('\t'.join(formatted_row)) + return StringIO('\n'.join(rows)) + + # object stores ################################################################# class ObjectStore(object): @@ -753,3 +921,261 @@ return self.session.user.eid def gen_owned_by(self, entity): return self.session.user.eid + + +########################################################################### +## SQL object store ####################################################### +########################################################################### +class SQLGenObjectStore(NoHookRQLObjectStore): + """Controller of the data import process. This version is based + on direct insertions throught SQL command (COPY FROM or execute many). + + >>> store = SQLGenObjectStore(session) + >>> store.create_entity('Person', ...) + >>> store.flush() + """ + + def __init__(self, session, dump_output_dir=None, nb_threads_statement=3): + """ + Initialize a SQLGenObjectStore. + + Parameters: + + - session: session on the cubicweb instance + - dump_output_dir: a directory to dump failed statements + for easier recovery. Default is None (no dump). + - nb_threads_statement: number of threads used + for SQL insertion (default is 3). + """ + super(SQLGenObjectStore, self).__init__(session) + ### hijack default source + self.source = SQLGenSourceWrapper( + self.source, session.vreg.schema, + dump_output_dir=dump_output_dir, + nb_threads_statement=nb_threads_statement) + ### XXX This is done in super().__init__(), but should be + ### redone here to link to the correct source + self.add_relation = self.source.add_relation + self.indexes_etypes = {} + + def flush(self): + """Flush data to the database""" + self.source.flush() + + def relate(self, subj_eid, rtype, obj_eid, subjtype=None): + if subj_eid is None or obj_eid is None: + return + # XXX Could subjtype be inferred ? + self.source.add_relation(self.session, subj_eid, rtype, obj_eid, + self.rschema(rtype).inlined, subjtype) + + def drop_indexes(self, etype): + """Drop indexes for a given entity type""" + if etype not in self.indexes_etypes: + cu = self.session.cnxset['system'] + def index_to_attr(index): + """turn an index name to (database) attribute name""" + return index.replace(etype.lower(), '').replace('idx', '').strip('_') + indices = [(index, index_to_attr(index)) + for index in self.source.dbhelper.list_indices(cu, etype) + # Do not consider 'cw_etype_pkey' index + if not index.endswith('key')] + self.indexes_etypes[etype] = indices + for index, attr in self.indexes_etypes[etype]: + self.session.system_sql('DROP INDEX %s' % index) + + def create_indexes(self, etype): + """Recreate indexes for a given entity type""" + for index, attr in self.indexes_etypes.get(etype, []): + sql = 'CREATE INDEX %s ON cw_%s(%s)' % (index, etype, attr) + self.session.system_sql(sql) + + +########################################################################### +## SQL Source ############################################################# +########################################################################### + +class SQLGenSourceWrapper(object): + + def __init__(self, system_source, schema, + dump_output_dir=None, nb_threads_statement=3): + self.system_source = system_source + self._sql = threading.local() + # Explicitely backport attributes from system source + self._storage_handler = self.system_source._storage_handler + self.preprocess_entity = self.system_source.preprocess_entity + self.sqlgen = self.system_source.sqlgen + self.copy_based_source = self.system_source.copy_based_source + self.uri = self.system_source.uri + self.eid = self.system_source.eid + # Directory to write temporary files + self.dump_output_dir = dump_output_dir + # Allow to execute code with SQLite backend that does + # not support (yet...) copy_from + # XXX Should be dealt with in logilab.database + spcfrom = system_source.dbhelper.dbapi_module.support_copy_from + self.support_copy_from = spcfrom + self.dbencoding = system_source.dbhelper.dbencoding + self.nb_threads_statement = nb_threads_statement + # initialize thread-local data for main thread + self.init_thread_locals() + self._inlined_rtypes_cache = {} + self._fill_inlined_rtypes_cache(schema) + self.schema = schema + self.do_fti = False + + def _fill_inlined_rtypes_cache(self, schema): + cache = self._inlined_rtypes_cache + for eschema in schema.entities(): + for rschema in eschema.ordered_relations(): + if rschema.inlined: + cache[eschema.type] = SQL_PREFIX + rschema.type + + def init_thread_locals(self): + """initializes thread-local data""" + self._sql.entities = defaultdict(list) + self._sql.relations = {} + self._sql.inlined_relations = {} + # keep track, for each eid of the corresponding data dict + self._sql.eid_insertdicts = {} + + def flush(self): + print 'starting flush' + _entities_sql = self._sql.entities + _relations_sql = self._sql.relations + _inlined_relations_sql = self._sql.inlined_relations + _insertdicts = self._sql.eid_insertdicts + try: + # try, for each inlined_relation, to find if we're also creating + # the host entity (i.e. the subject of the relation). + # In that case, simply update the insert dict and remove + # the need to make the + # UPDATE statement + for statement, datalist in _inlined_relations_sql.iteritems(): + new_datalist = [] + # for a given inlined relation, + # browse each couple to be inserted + for data in datalist: + keys = data.keys() + # For inlined relations, it exists only two case: + # (rtype, cw_eid) or (cw_eid, rtype) + if keys[0] == 'cw_eid': + rtype = keys[1] + else: + rtype = keys[0] + updated_eid = data['cw_eid'] + if updated_eid in _insertdicts: + _insertdicts[updated_eid][rtype] = data[rtype] + else: + # could not find corresponding insert dict, keep the + # UPDATE query + new_datalist.append(data) + _inlined_relations_sql[statement] = new_datalist + _import_statements(self.system_source.get_connection, + _entities_sql.items() + + _relations_sql.items() + + _inlined_relations_sql.items(), + dump_output_dir=self.dump_output_dir, + nb_threads=self.nb_threads_statement, + support_copy_from=self.support_copy_from, + encoding=self.dbencoding) + except: + print 'failed to flush' + finally: + _entities_sql.clear() + _relations_sql.clear() + _insertdicts.clear() + _inlined_relations_sql.clear() + print 'flush done' + + def add_relation(self, session, subject, rtype, object, + inlined=False, subjtype=None): + if inlined: + _sql = self._sql.inlined_relations + data = {'cw_eid': subject, SQL_PREFIX + rtype: object} + if subjtype is None: + # Try to infer it + targets = [t.type for t in + self.schema.rschema(rtype).targets()] + if len(targets) == 1: + subjtype = targets[0] + else: + raise ValueError('You should give the subject etype for ' + 'inlined relation %s' + ', as it cannot be inferred' % rtype) + statement = self.sqlgen.update(SQL_PREFIX + subjtype, + data, ['cw_eid']) + else: + _sql = self._sql.relations + data = {'eid_from': subject, 'eid_to': object} + statement = self.sqlgen.insert('%s_relation' % rtype, data) + if statement in _sql: + _sql[statement].append(data) + else: + _sql[statement] = [data] + + def add_entity(self, session, entity): + with self._storage_handler(entity, 'added'): + attrs = self.preprocess_entity(entity) + rtypes = self._inlined_rtypes_cache.get(entity.__regid__, ()) + if isinstance(rtypes, str): + rtypes = (rtypes,) + for rtype in rtypes: + if rtype not in attrs: + attrs[rtype] = None + sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs) + self._sql.eid_insertdicts[entity.eid] = attrs + self._append_to_entities(sql, attrs) + + def _append_to_entities(self, sql, attrs): + self._sql.entities[sql].append(attrs) + + def _handle_insert_entity_sql(self, session, sql, attrs): + # We have to overwrite the source given in parameters + # as here, we directly use the system source + attrs['source'] = 'system' + attrs['asource'] = self.system_source.uri + self._append_to_entities(sql, attrs) + + def _handle_is_relation_sql(self, session, sql, attrs): + self._append_to_entities(sql, attrs) + + def _handle_is_instance_of_sql(self, session, sql, attrs): + self._append_to_entities(sql, attrs) + + def _handle_source_relation_sql(self, session, sql, attrs): + self._append_to_entities(sql, attrs) + + # XXX add_info is similar to the one in NativeSQLSource. It is rewritten + # here to correctly used the _handle_xxx of the SQLGenSourceWrapper. This + # part should be rewritten in a more clearly way. + def add_info(self, session, entity, source, extid, complete): + """add type and source info for an eid into the system table""" + # begin by inserting eid/type/source/extid into the entities table + if extid is not None: + assert isinstance(extid, str) + extid = b64encode(extid) + uri = 'system' if source.copy_based_source else source.uri + attrs = {'type': entity.__regid__, 'eid': entity.eid, 'extid': extid, + 'source': uri, 'asource': source.uri, 'mtime': datetime.utcnow()} + self._handle_insert_entity_sql(session, self.sqlgen.insert('entities', attrs), attrs) + # insert core relations: is, is_instance_of and cw_source + try: + self._handle_is_relation_sql(session, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, eschema_eid(session, entity.e_schema))) + except IndexError: + # during schema serialization, skip + pass + else: + for eschema in entity.e_schema.ancestors() + [entity.e_schema]: + self._handle_is_relation_sql(session, + 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, eschema_eid(session, eschema))) + if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10 + self._handle_is_relation_sql(session, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, source.eid)) + # now we can update the full text index + if self.do_fti and self.need_fti_indexation(entity.__regid__): + if complete: + entity.complete(entity.e_schema.indexable_attributes()) + self.index_entity(session, entity=entity) diff -r 7812093e21f7 -r ab57000bff7b dbapi.py --- a/dbapi.py Thu Jan 17 17:12:06 2013 +0100 +++ b/dbapi.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -80,9 +80,8 @@ class ConnectionProperties(object): - def __init__(self, cnxtype=None, lang=None, close=True, log=False): + def __init__(self, cnxtype=None, close=True, log=False): self.cnxtype = cnxtype or 'pyro' - self.lang = lang self.log_queries = log self.close_on_del = close @@ -283,6 +282,8 @@ return '' % self.sessionid class DBAPIRequest(RequestSessionBase): + #: Request language identifier eg: 'en' + lang = None def __init__(self, vreg, session=None): super(DBAPIRequest, self).__init__(vreg) @@ -292,9 +293,6 @@ self.translations = vreg.config.translations except AttributeError: self.translations = {} - #: Request language identifier eg: 'en' - self.lang = None - self.set_default_language(vreg) #: cache entities built during the request self._eid_cache = {} if session is not None: @@ -304,6 +302,7 @@ # established self.session = None self.cnx = self.user = _NeedAuthAccessMock() + self.set_default_language(vreg) def from_controller(self): return 'view' @@ -320,7 +319,7 @@ self.cnx = session.cnx self.execute = session.cnx.cursor(self).execute if user is None: - user = self.cnx.user(self, {'lang': self.lang}) + user = self.cnx.user(self) if user is not None: self.user = user self.set_entity_cache(user) @@ -333,19 +332,15 @@ def set_default_language(self, vreg): try: - self.lang = vreg.property_value('ui.language') + lang = vreg.property_value('ui.language') except Exception: # property may not be registered - self.lang = 'en' - # use req.__ to translate a message without registering it to the catalog + lang = 'en' try: - gettext, pgettext = self.translations[self.lang] - self._ = self.__ = gettext - self.pgettext = pgettext + self.set_language(lang) except KeyError: # this occurs usually during test execution self._ = self.__ = unicode self.pgettext = lambda x, y: unicode(y) - self.debug('request default language: %s', self.lang) # server-side service call ################################################# @@ -606,9 +601,9 @@ # then init cubes config.init_cubes(cubes) # then load appobjects into the registry - vpath = config.build_vregistry_path(reversed(config.cubes_path()), - evobjpath=esubpath, - tvobjpath=subpath) + vpath = config.build_appobjects_path(reversed(config.cubes_path()), + evobjpath=esubpath, + tvobjpath=subpath) self.vreg.register_objects(vpath) def use_web_compatible_requests(self, baseurl, sitetitle=None): @@ -678,11 +673,6 @@ # session data methods ##################################################### @check_not_closed - def set_session_props(self, **props): - """raise `BadConnectionId` if the connection is no more valid""" - self._repo.set_session_props(self.sessionid, props) - - @check_not_closed def get_shared_data(self, key, default=None, pop=False, txdata=False): """return value associated to key in the session's data dictionary or session's transaction's data if `txdata` is true. diff -r 7812093e21f7 -r ab57000bff7b debian/control --- a/debian/control Thu Jan 17 17:12:06 2013 +0100 +++ b/debian/control Thu Jan 17 18:30:08 2013 +0100 @@ -107,7 +107,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.34.0), python-rql (>= 0.31.2), python-lxml +Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.36.0), python-rql (>= 0.31.2), python-lxml Recommends: python-simpletal (>= 4.0), python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core diff -r 7812093e21f7 -r ab57000bff7b devtools/__init__.py --- a/devtools/__init__.py Thu Jan 17 17:12:06 2013 +0100 +++ b/devtools/__init__.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -355,7 +355,7 @@ def _restore_database(self, backup_coordinates, config): """Actual restore of the current database. - Use the value tostored in db_cache as input """ + Use the value stored in db_cache as input """ raise NotImplementedError() def get_repo(self, startup=False): @@ -466,7 +466,6 @@ ``pre_setup_func`` to setup the database. This function backup any database it build""" - if self.has_cache(test_db_id): return #test_db_id, 'already in cache' if test_db_id is DEFAULT_EMPTY_DB_ID: @@ -723,7 +722,7 @@ dbfile = self.absolute_dbfile() self._cleanup_database(dbfile) shutil.copy(backup_coordinates, dbfile) - repo = self.get_repo() + self.get_repo() def init_test_database(self): """initialize a fresh sqlite databse used for testing purpose""" diff -r 7812093e21f7 -r ab57000bff7b devtools/devctl.py --- a/devtools/devctl.py Thu Jan 17 17:12:06 2013 +0100 +++ b/devtools/devctl.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -91,7 +91,7 @@ if mod.__file__ is None: # odd/rare but real continue - for path in config.vregistry_path(): + for path in config.appobjects_path(): if mod.__file__.startswith(path): del sys.modules[name] break @@ -302,10 +302,11 @@ from logilab.common.fileutils import ensure_fs_mode from logilab.common.shellutils import globfind, find, rm from logilab.common.modutils import get_module_files - from cubicweb.i18n import extract_from_tal, execute + from cubicweb.i18n import extract_from_tal, execute2 tempdir = tempfile.mkdtemp(prefix='cw-') cwi18ndir = WebConfiguration.i18n_lib_dir() - print '-> extract schema messages.' + print '-> extract messages:', + print 'schema', schemapot = osp.join(tempdir, 'schema.pot') potfiles = [schemapot] potfiles.append(schemapot) @@ -314,7 +315,7 @@ schemapotstream = file(schemapot, 'w') generate_schema_pot(schemapotstream.write, cubedir=None) schemapotstream.close() - print '-> extract TAL messages.' + print 'TAL', tali18nfile = osp.join(tempdir, 'tali18n.py') extract_from_tal(find(osp.join(BASEDIR, 'web'), ('.py', '.pt')), tali18nfile) @@ -329,26 +330,29 @@ ('tal', [tali18nfile], None), ('js', jsfiles, 'java'), ]: - cmd = 'xgettext --no-location --omit-header -k_ -o %s %s' + potfile = osp.join(tempdir, '%s.pot' % id) + cmd = ['xgettext', '--no-location', '--omit-header', '-k_'] if lang is not None: - cmd += ' -L %s' % lang - potfile = osp.join(tempdir, '%s.pot' % id) - execute(cmd % (potfile, ' '.join('"%s"' % f for f in files))) + cmd.extend(['-L', lang]) + cmd.extend(['-o', potfile]) + cmd.extend(files) + execute2(cmd) if osp.exists(potfile): potfiles.append(potfile) else: print '-> WARNING: %s file was not generated' % potfile print '-> merging %i .pot files' % len(potfiles) cubicwebpot = osp.join(tempdir, 'cubicweb.pot') - execute('msgcat -o %s %s' - % (cubicwebpot, ' '.join('"%s"' % f for f in potfiles))) + cmd = ['msgcat', '-o', cubicwebpot] + potfiles + execute2(cmd) print '-> merging main pot file with existing translations.' chdir(cwi18ndir) toedit = [] for lang in CubicWebNoAppConfiguration.cw_languages(): target = '%s.po' % lang - execute('msgmerge -N --sort-output -o "%snew" "%s" "%s"' - % (target, target, cubicwebpot)) + cmd = ['msgmerge', '-N', '--sort-output', '-o', + target+'new', target, cubicwebpot] + execute2(cmd) ensure_fs_mode(target) shutil.move('%snew' % target, target) toedit.append(osp.abspath(target)) @@ -382,16 +386,21 @@ def update_cubes_catalogs(cubes): + from subprocess import CalledProcessError for cubedir in cubes: if not osp.isdir(cubedir): print '-> ignoring %s that is not a directory.' % cubedir continue try: toedit = update_cube_catalogs(cubedir) + except CalledProcessError, exc: + print '\n*** error while updating catalogs for cube', cubedir + print 'cmd:\n%s' % exc.cmd + print 'stdout:\n%s\nstderr:\n%s' % exc.data except Exception: import traceback traceback.print_exc() - print '-> error while updating catalogs for cube', cubedir + print '*** error while updating catalogs for cube', cubedir return False else: # instructions pour la suite @@ -408,7 +417,7 @@ import tempfile from logilab.common.fileutils import ensure_fs_mode from logilab.common.shellutils import find, rm - from cubicweb.i18n import extract_from_tal, execute + from cubicweb.i18n import extract_from_tal, execute2 cube = osp.basename(osp.normpath(cubedir)) tempdir = tempfile.mkdtemp() print underline_title('Updating i18n catalogs for cube %s' % cube) @@ -421,7 +430,8 @@ potfiles = [osp.join('i18n', 'static-messages.pot')] else: potfiles = [] - print '-> extract schema messages' + print '-> extracting messages:', + print 'schema', schemapot = osp.join(tempdir, 'schema.pot') potfiles.append(schemapot) # explicit close necessary else the file may not be yet flushed when @@ -429,50 +439,55 @@ schemapotstream = file(schemapot, 'w') generate_schema_pot(schemapotstream.write, cubedir) schemapotstream.close() - print '-> extract TAL messages' + print 'TAL', tali18nfile = osp.join(tempdir, 'tali18n.py') ptfiles = find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST+('test',)) extract_from_tal(ptfiles, tali18nfile) - print '-> extract Javascript messages' + print 'Javascript' jsfiles = [jsfile for jsfile in find('.', '.js') if osp.basename(jsfile).startswith('cub')] if jsfiles: tmppotfile = osp.join(tempdir, 'js.pot') - execute('xgettext --no-location --omit-header -k_ -L java ' - '--from-code=utf-8 -o %s %s' % (tmppotfile, ' '.join(jsfiles))) + cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-L', 'java', + '--from-code=utf-8', '-o', tmppotfile] + jsfiles + execute2(cmd) # no pot file created if there are no string to translate if osp.exists(tmppotfile): potfiles.append(tmppotfile) - print '-> create cube-specific catalog' + print '-> creating cube-specific catalog' tmppotfile = osp.join(tempdir, 'generated.pot') cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',)) cubefiles.append(tali18nfile) - execute('xgettext --no-location --omit-header -k_ -o %s %s' - % (tmppotfile, ' '.join('"%s"' % f for f in cubefiles))) + cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-o', tmppotfile] + cmd.extend(cubefiles) + execute2(cmd) if osp.exists(tmppotfile): # doesn't exists of no translation string found potfiles.append(tmppotfile) potfile = osp.join(tempdir, 'cube.pot') - print '-> merging %i .pot files:' % len(potfiles) - execute('msgcat -o %s %s' % (potfile, - ' '.join('"%s"' % f for f in potfiles))) + print '-> merging %i .pot files' % len(potfiles) + cmd = ['msgcat', '-o', potfile] + cmd.extend(potfiles) + execute2(cmd) if not osp.exists(potfile): print 'no message catalog for cube', cube, 'nothing to translate' # cleanup rm(tempdir) return () - print '-> merging main pot file with existing translations:' + print '-> merging main pot file with existing translations:', chdir('i18n') toedit = [] for lang in CubicWebNoAppConfiguration.cw_languages(): - print '-> language', lang + print lang, cubepo = '%s.po' % lang if not osp.exists(cubepo): shutil.copy(potfile, cubepo) else: - execute('msgmerge -N -s -o %snew %s %s' % (cubepo, cubepo, potfile)) + cmd = ['msgmerge','-N','-s','-o', cubepo+'new', cubepo, potfile] + execute2(cmd) ensure_fs_mode(cubepo) shutil.move('%snew' % cubepo, cubepo) toedit.append(osp.abspath(cubepo)) + print # cleanup rm(tempdir) return toedit diff -r 7812093e21f7 -r ab57000bff7b devtools/fake.py --- a/devtools/fake.py Thu Jan 17 17:12:06 2013 +0100 +++ b/devtools/fake.py Thu Jan 17 18:30:08 2013 +0100 @@ -155,6 +155,12 @@ def set_entity_cache(self, entity): pass + def security_enabled(self, read=False, write=False): + class FakeCM(object): + def __enter__(self): pass + def __exit__(self, exctype, exc, traceback): pass + return FakeCM() + # for use with enabled_security context manager read_security = write_security = True def init_security(self, *args): diff -r 7812093e21f7 -r ab57000bff7b devtools/repotest.py --- a/devtools/repotest.py Thu Jan 17 17:12:06 2013 +0100 +++ b/devtools/repotest.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b devtools/testlib.py --- a/devtools/testlib.py Thu Jan 17 17:12:06 2013 +0100 +++ b/devtools/testlib.py Thu Jan 17 18:30:08 2013 +0100 @@ -48,7 +48,7 @@ from cubicweb.utils import json from cubicweb.sobjects import notification from cubicweb.web import Redirect, application -from cubicweb.server.session import Session, security_enabled +from cubicweb.server.session import Session from cubicweb.server.hook import SendMailOp from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS from cubicweb.devtools import BASE_URL, fake, htmlparser, DEFAULT_EMPTY_DB_ID @@ -451,7 +451,7 @@ finally: self.session.set_cnxset() # ensure cnxset still set after commit - # # server side db api ####################################################### + # server side db api ####################################################### def sexecute(self, rql, args=None, eid_key=None): if eid_key is not None: @@ -1060,7 +1060,7 @@ """this method populates the database with `how_many` entities of each possible type. It also inserts random relations between them """ - with security_enabled(self.session, read=False, write=False): + with self.session.security_enabled(read=False, write=False): self._auto_populate(how_many) def _auto_populate(self, how_many): diff -r 7812093e21f7 -r ab57000bff7b doc/3.16.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/3.16.rst Thu Jan 17 18:30:08 2013 +0100 @@ -0,0 +1,25 @@ +What's new in CubicWeb 3.16? +============================ + +New functionnalities +-------------------- + +* Add a new dataimport store (`SQLGenObjectStore`). This store enables a fast + import of data (entity creation, link creation) in CubicWeb, by directly + flushing information in SQL. This may only be used with PostgreSQL, as it + requires the 'COPY FROM' command. + +API changes +----------- + +Unintrusive API changes +----------------------- + +* Drop of `cubicweb.web.uicfg.AutoformSectionRelationTags.bw_tag_map`, + deprecated since 3,6. + +User interface changes +---------------------- + +* Remove changelog view, as nor cubicweb nor known cubes/applications were properly + feeding related files diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/annexes/faq.rst --- a/doc/book/en/annexes/faq.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/annexes/faq.rst Thu Jan 17 18:30:08 2013 +0100 @@ -364,7 +364,7 @@ >>> crypted = crypt_password('joepass') >>> rset = rql('Any U WHERE U is CWUser, U login "joe"') >>> joe = rset.get_entity(0,0) - >>> joe.set_attributes(upassword=Binary(crypted)) + >>> joe.cw_set(upassword=Binary(crypted)) Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file. diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/annexes/rql/debugging.rst --- a/doc/book/en/annexes/rql/debugging.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/annexes/rql/debugging.rst Thu Jan 17 18:30:08 2013 +0100 @@ -15,6 +15,8 @@ .. autodata:: cubicweb.server.DBG_SQL .. autodata:: cubicweb.server.DBG_REPO .. autodata:: cubicweb.server.DBG_MS +.. autodata:: cubicweb.server.DBG_HOOKS +.. autodata:: cubicweb.server.DBG_OPS .. autodata:: cubicweb.server.DBG_MORE .. autodata:: cubicweb.server.DBG_ALL @@ -31,6 +33,14 @@ .. autofunction:: cubicweb.server.set_debug +Another example showing how to debug hooks at a specific code site: + +.. sourcecode:: python + + from cubicweb.server import debuged, DBG_HOOKS + with debugged(DBG_HOOKS): + person.cw_set(works_for=company) + Detect largest RQL queries ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devrepo/dataimport.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/book/en/devrepo/dataimport.rst Thu Jan 17 18:30:08 2013 +0100 @@ -0,0 +1,58 @@ +. -*- coding: utf-8 -*- + +.. _dataimport: + +Dataimport +========== + +*CubicWeb* is designed to manipulate huge of amount of data, and provides helper functions to do so. +These functions insert data within different levels of the *CubicWeb* API, +allowing different speed/security tradeoffs. Those keeping all the *CubicWeb* hooks +and security will be slower but the possible errors in insertion +(bad data types, integrity error, ...) will be raised. + +These dataimport function are provided in the file `dataimport.py`. + +All the stores have the following API:: + + >>> store = ObjectStore() + >>> user = store.create_entity('CWUser', login=u'johndoe') + >>> group = store.create_entity('CWUser', name=u'unknown') + >>> store.relate(user.eid, 'in_group', group.eid) + + +ObjectStore +----------- + +This store keeps objects in memory for *faster* validation. It may be useful +in development mode. However, as it will not enforce the constraints of the schema, +it may miss some problems. + + + +RQLObjectStore +-------------- + +This store works with an actual RQL repository, and it may be used in production mode. + + +NoHookRQLObjectStore +-------------------- + +This store works similarly to the *RQLObjectStore* but bypasses some *CubicWeb* hooks to be faster. + + +SQLGenObjectStore +----------------- + +This store relies on *COPY FROM*/execute many sql commands to directly push data using SQL commands +rather than using the whole *CubicWeb* API. For now, **it only works with PostgresSQL** as it requires +the *COPY FROM* command. + +The API is similar to the other stores, but **it requires a flush** after some imports to copy data +in the database (these flushes may be multiples through the processes, or be done only once at the +end if there is no memory issue):: + + >>> store = SQLGenObjectStore(session) + >>> store.create_entity('Person', ...) + >>> store.flush() diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devrepo/entityclasses/application-logic.rst --- a/doc/book/en/devrepo/entityclasses/application-logic.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/devrepo/entityclasses/application-logic.rst Thu Jan 17 18:30:08 2013 +0100 @@ -38,7 +38,7 @@ object was built. Setting an attribute or relation value can be done in the context of a -Hook/Operation, using the obj.set_relations(x=42) notation or a plain +Hook/Operation, using the obj.cw_set(x=42) notation or a plain RQL SET expression. In views, it would be preferable to encapsulate the necessary logic in diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devrepo/entityclasses/data-as-objects.rst --- a/doc/book/en/devrepo/entityclasses/data-as-objects.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/devrepo/entityclasses/data-as-objects.rst Thu Jan 17 18:30:08 2013 +0100 @@ -16,50 +16,47 @@ `Formatting and output generation`: -* `view(__vid, __registry='views', **kwargs)`, applies the given view to the entity +* :meth:`view(__vid, __registry='views', **kwargs)`, applies the given view to the entity (and returns an unicode string) -* `absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url +* :meth:`absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url -* `rest_path()`, returns a relative REST URL to get the entity +* :meth:`rest_path()`, returns a relative REST URL to get the entity -* `printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`, +* :meth:`printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`, returns a string enabling the display of an attribute value in a given format (the value is automatically recovered if necessary) `Data handling`: -* `as_rset()`, converts the entity into an equivalent result set simulating the +* :meth:`as_rset()`, converts the entity into an equivalent result set simulating the request `Any X WHERE X eid _eid_` -* `complete(skip_bytes=True)`, executes a request that recovers at +* :meth:`complete(skip_bytes=True)`, executes a request that recovers at once all the missing attributes of an entity -* `get_value(name)`, returns the value associated to the attribute name given +* :meth:`get_value(name)`, returns the value associated to the attribute name given in parameter -* `related(rtype, role='subject', limit=None, entities=False)`, +* :meth:`related(rtype, role='subject', limit=None, entities=False)`, returns a list of entities related to the current entity by the relation given in parameter -* `unrelated(rtype, targettype, role='subject', limit=None)`, +* :meth:`unrelated(rtype, targettype, role='subject', limit=None)`, returns a result set corresponding to the entities not (yet) related to the current entity by the relation given in parameter and satisfying its constraints -* `set_attributes(**kwargs)`, updates the attributes list with the corresponding - values given named parameters +* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the + corresponding values given named parameters. To set a relation where this + entity is the object of the relation, use `reverse_` as argument + name. Values may be an entity, a list of entities, or None (meaning that all + relations of the given type from or to this object should be deleted). -* `set_relations(**kwargs)`, add relations to the given object. To - set a relation where this entity is the object of the relation, - use `reverse_` as argument name. Values may be an - entity, a list of entities, or None (meaning that all relations of - the given type from or to this object should be deleted). - -* `copy_relations(ceid)`, copies the relations of the entities having the eid +* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid given in the parameters on the current entity -* `delete()` allows to delete the entity +* :meth:`cw_delete()` allows to delete the entity The :class:`AnyEntity` class @@ -81,40 +78,30 @@ `Standard meta-data (Dublin Core)`: -* `dc_title()`, returns a unicode string corresponding to the +* :meth:`dc_title()`, returns a unicode string corresponding to the meta-data `Title` (used by default is the first non-meta attribute of the entity schema) -* `dc_long_title()`, same as dc_title but can return a more +* :meth:`dc_long_title()`, same as dc_title but can return a more detailed title -* `dc_description(format='text/plain')`, returns a unicode string +* :meth:`dc_description(format='text/plain')`, returns a unicode string corresponding to the meta-data `Description` (looks for a description attribute by default) -* `dc_authors()`, returns a unicode string corresponding to the meta-data +* :meth:`dc_authors()`, returns a unicode string corresponding to the meta-data `Authors` (owners by default) -* `dc_creator()`, returns a unicode string corresponding to the +* :meth:`dc_creator()`, returns a unicode string corresponding to the creator of the entity -* `dc_date(date_format=None)`, returns a unicode string corresponding to +* :meth:`dc_date(date_format=None)`, returns a unicode string corresponding to the meta-data `Date` (update date by default) -* `dc_type(form='')`, returns a string to display the entity type by +* :meth:`dc_type(form='')`, returns a string to display the entity type by specifying the preferred form (`plural` for a plural form) -* `dc_language()`, returns the language used by the entity - - -`Misc methods`: - -* `after_deletion_path`, return (path, parameters) which should be - used as redirect information when this entity is being deleted - -* `pre_web_edit`, callback called by the web editcontroller when an - entity will be created/modified, to let a chance to do some entity - specific stuff (does nothing by default) +* :meth:`dc_language()`, returns the language used by the entity Inheritance ----------- diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devrepo/repo/hooks.rst --- a/doc/book/en/devrepo/repo/hooks.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/devrepo/repo/hooks.rst Thu Jan 17 18:30:08 2013 +0100 @@ -206,10 +206,11 @@ Reminder ~~~~~~~~ -You should never use the `entity.foo = 42` notation to update an -entity. It will not do what you expect (updating the -database). Instead, use the :meth:`set_attributes` and -:meth:`set_relations` methods. +You should never use the `entity.foo = 42` notation to update an entity. It will +not do what you expect (updating the database). Instead, use the +:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's +:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or +'before_update_entity' event. How to choose between a before and an after event ? diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devrepo/testing.rst --- a/doc/book/en/devrepo/testing.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/devrepo/testing.rst Thu Jan 17 18:30:08 2013 +0100 @@ -70,13 +70,13 @@ def test_cannot_create_cycles(self): # direct obvious cycle - self.assertRaises(ValidationError, self.kw1.set_relations, + self.assertRaises(ValidationError, self.kw1.cw_set, subkeyword_of=self.kw1) # testing indirect cycles kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, ' 'SK subkeyword_of K WHERE C name "classif1", K eid %s' % self.kw1.eid).get_entity(0,0) - self.kw1.set_relations(subkeyword_of=kw3) + self.kw1.cw_set(subkeyword_of=kw3) self.assertRaises(ValidationError, self.commit) The test class defines a :meth:`setup_database` method which populates the @@ -192,10 +192,10 @@ description=u'cubicweb is beautiful') blog_entry_1 = req.create_entity('BlogEntry', title=u'hop', content=u'cubicweb hop') - blog_entry_1.set_relations(entry_of=cubicweb_blog) + blog_entry_1.cw_set(entry_of=cubicweb_blog) blog_entry_2 = req.create_entity('BlogEntry', title=u'yes', content=u'cubicweb yes') - blog_entry_2.set_relations(entry_of=cubicweb_blog) + blog_entry_2.cw_set(entry_of=cubicweb_blog) self.assertEqual(len(MAILBOX), 0) self.commit() self.assertEqual(len(MAILBOX), 2) diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devweb/index.rst --- a/doc/book/en/devweb/index.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/devweb/index.rst Thu Jan 17 18:30:08 2013 +0100 @@ -10,6 +10,7 @@ publisher controllers request + searchbar views/index rtags ajax diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devweb/searchbar.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/book/en/devweb/searchbar.rst Thu Jan 17 18:30:08 2013 +0100 @@ -0,0 +1,41 @@ +.. _searchbar: + +RQL search bar +-------------- + +The RQL search bar is a visual component, hidden by default, the tiny *search* +input being enough for common use cases. + +An autocompletion helper is provided to help you type valid queries, both +in terms of syntax and in terms of schema validity. + +.. autoclass:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder + + +How search is performed ++++++++++++++++++++++++ + +You can use the *rql search bar* to either type RQL queries, plain text queries +or standard shortcuts such as ** or * *. + +Ultimately, all queries are translated to rql since it's the only +language understood on the server (data) side. To transform the user +query into RQL, CubicWeb uses the so-called *magicsearch component*, +defined in :mod:`cubicweb.web.views.magicsearch`, which in turn +delegates to a number of query preprocessor that are responsible of +interpreting the user query and generating corresponding RQL. + +The code of the main processor loop is easy to understand: + +.. sourcecode:: python + + for proc in self.processors: + try: + return proc.process_query(uquery, req) + except (RQLSyntaxError, BadRQLQuery): + pass + +The idea is simple: for each query processor, try to translate the +query. If it fails, try with the next processor, if it succeeds, +we're done and the RQL query will be executed. + diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devweb/views/index.rst diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/devweb/views/views.rst diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/tutorials/advanced/part02_security.rst --- a/doc/book/en/tutorials/advanced/part02_security.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/tutorials/advanced/part02_security.rst Thu Jan 17 18:30:08 2013 +0100 @@ -196,7 +196,7 @@ for eid in self.get_data(): entity = self.session.entity_from_eid(eid) if entity.visibility == 'parent': - entity.set_attributes(visibility=u'authenticated') + entity.cw_set(visibility=u'authenticated') class SetVisibilityHook(hook.Hook): __regid__ = 'sytweb.setvisibility' @@ -215,7 +215,7 @@ parent = self._cw.entity_from_eid(self.eidto) child = self._cw.entity_from_eid(self.eidfrom) if child.visibility == 'parent': - child.set_attributes(visibility=parent.visibility) + child.cw_set(visibility=parent.visibility) Notice: @@ -344,7 +344,7 @@ self.assertEquals(len(req.execute('Folder X')), 0) # restricted... # may_be_read_by propagation self.restore_connection() - folder.set_relations(may_be_read_by=toto) + folder.cw_set(may_be_read_by=toto) self.commit() photo1.clear_all_caches() self.failUnless(photo1.may_be_read_by) diff -r 7812093e21f7 -r ab57000bff7b doc/book/en/tutorials/advanced/part04_ui-base.rst --- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Thu Jan 17 17:12:06 2013 +0100 +++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Thu Jan 17 18:30:08 2013 +0100 @@ -294,6 +294,7 @@ folder in which the current file (e.g. `self.entity`) is located. .. Note:: + The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look at the value returned by its `parent_entity` method. It also provides a @@ -331,6 +332,7 @@ navigate through the web site to see if everything is ok... .. Note:: + In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while in the two other, it refers to the **instance** (if you can't see the difference, reread CubicWeb's concept chapter !). @@ -363,4 +365,4 @@ .. _`several improvments`: http://www.cubicweb.org/blogentry/1179899 .. _`3.8`: http://www.cubicweb.org/blogentry/917107 .. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642 -.. _`an earlier post`: http://www.cubicweb.org/867464 \ No newline at end of file +.. _`an earlier post`: http://www.cubicweb.org/867464 diff -r 7812093e21f7 -r ab57000bff7b entities/authobjs.py --- a/entities/authobjs.py Thu Jan 17 17:12:06 2013 +0100 +++ b/entities/authobjs.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -77,6 +77,19 @@ self._properties = dict((p.pkey, p.value) for p in self.reverse_for_user) return self._properties + def prefered_language(self, language=None): + """return language used by this user, if explicitly defined (eg not + using http negociation) + """ + language = language or self.property_value('ui.language') + vreg = self._cw.vreg + try: + vreg.config.translations[language] + except KeyError: + language = vreg.property_value('ui.language') + assert language in vreg.config.translations[language], language + return language + def property_value(self, key): try: # properties stored on the user aren't correctly typed @@ -101,7 +114,7 @@ kwargs['for_user'] = self self._cw.create_entity('CWProperty', **kwargs) else: - prop.set_attributes(value=value) + prop.cw_set(value=value) def matching_groups(self, groups): """return the number of the given group(s) in which the user is diff -r 7812093e21f7 -r ab57000bff7b entities/sources.py --- a/entities/sources.py Thu Jan 17 17:12:06 2013 +0100 +++ b/entities/sources.py Thu Jan 17 18:30:08 2013 +0100 @@ -51,7 +51,7 @@ continue raise cfgstr = unicode(generate_source_config(sconfig), self._cw.encoding) - self.set_attributes(config=cfgstr) + self.cw_set(config=cfgstr) class CWSource(_CWSourceCfgMixIn, AnyEntity): @@ -181,5 +181,5 @@ def write_log(self, session, **kwargs): if 'status' not in kwargs: kwargs['status'] = getattr(self, '_status', u'success') - self.set_attributes(log=u'
'.join(self._logs), **kwargs) + self.cw_set(log=u'
'.join(self._logs), **kwargs) self._logs = [] diff -r 7812093e21f7 -r ab57000bff7b entities/test/unittest_base.py --- a/entities/test/unittest_base.py Thu Jan 17 17:12:06 2013 +0100 +++ b/entities/test/unittest_base.py Thu Jan 17 18:30:08 2013 +0100 @@ -70,7 +70,7 @@ email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0) email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0) email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0) - email1.set_relations(prefered_form=email2) + email1.cw_set(prefered_form=email2) self.assertEqual(email1.prefered.eid, email2.eid) self.assertEqual(email2.prefered.eid, email2.eid) self.assertEqual(email3.prefered.eid, email3.eid) @@ -104,10 +104,10 @@ e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0) self.assertEqual(e.dc_title(), 'member') self.assertEqual(e.name(), 'member') - e.set_attributes(firstname=u'bouah') + e.cw_set(firstname=u'bouah') self.assertEqual(e.dc_title(), 'member') self.assertEqual(e.name(), u'bouah') - e.set_attributes(surname=u'lôt') + e.cw_set(surname=u'lôt') self.assertEqual(e.dc_title(), 'member') self.assertEqual(e.name(), u'bouah lôt') diff -r 7812093e21f7 -r ab57000bff7b entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Thu Jan 17 17:12:06 2013 +0100 +++ b/entities/test/unittest_wfobjs.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,7 +20,6 @@ from cubicweb import ValidationError from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.server.session import security_enabled def add_wf(self, etype, name=None, default=False): @@ -63,7 +62,7 @@ # gnark gnark bar = wf.add_state(u'bar') self.commit() - bar.set_attributes(name=u'foo') + bar.cw_set(name=u'foo') with self.assertRaises(ValidationError) as cm: self.commit() self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'}) @@ -86,7 +85,7 @@ # gnark gnark biz = wf.add_transition(u'biz', (bar,), foo) self.commit() - biz.set_attributes(name=u'baz') + biz.cw_set(name=u'baz') with self.assertRaises(ValidationError) as cm: self.commit() self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'}) @@ -126,8 +125,9 @@ self.assertEqual(trs[0].destination(None).name, u'deactivated') # test a std user get no possible transition cnx = self.login('member') + req = self.request() # fetch the entity using the new session - trs = list(cnx.user().cw_adapt_to('IWorkflowable').possible_transitions()) + trs = list(req.user.cw_adapt_to('IWorkflowable').possible_transitions()) self.assertEqual(len(trs), 0) cnx.close() @@ -154,7 +154,7 @@ wf = add_wf(self, 'CWUser') s = wf.add_state(u'foo', initial=True) self.commit() - with security_enabled(self.session, write=False): + with self.session.security_enabled(write=False): with self.assertRaises(ValidationError) as cm: self.session.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', {'x': self.user().eid, 's': s.eid}) @@ -173,7 +173,7 @@ def test_goback_transition(self): req = self.request() - wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow + wf = req.user.cw_adapt_to('IWorkflowable').current_workflow asleep = wf.add_state('asleep') wf.add_transition('rest', (wf.state_by_name('activated'), wf.state_by_name('deactivated')), @@ -516,7 +516,7 @@ ['rest']) self.assertEqual(parse_hist(iworkflowable.workflow_history), [('asleep', 'asleep', 'rest', None)]) - user.set_attributes(surname=u'toto') # fulfill condition + user.cw_set(surname=u'toto') # fulfill condition self.commit() iworkflowable.fire_transition('rest') self.commit() @@ -556,13 +556,12 @@ def setUp(self): CubicWebTC.setUp(self) - self.wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow - self.session.set_cnxset() + req = self.request() + self.wf = req.user.cw_adapt_to('IWorkflowable').current_workflow self.s_activated = self.wf.state_by_name('activated').eid self.s_deactivated = self.wf.state_by_name('deactivated').eid self.s_dummy = self.wf.add_state(u'dummy').eid self.wf.add_transition(u'dummy', (self.s_deactivated,), self.s_dummy) - req = self.request() ueid = self.create_user(req, 'stduser', commit=False).eid # test initial state is set rset = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', @@ -623,23 +622,22 @@ cnx.close() def test_transition_checking3(self): - cnx = self.login('stduser') - session = self.session - user = cnx.user(session) - iworkflowable = user.cw_adapt_to('IWorkflowable') - iworkflowable.fire_transition('deactivate') - cnx.commit() - session.set_cnxset() - with self.assertRaises(ValidationError) as cm: + with self.login('stduser') as cnx: + session = self.session + user = self.user() + iworkflowable = user.cw_adapt_to('IWorkflowable') iworkflowable.fire_transition('deactivate') - self.assertEqual(self._cleanup_msg(cm.exception.errors['by_transition-subject']), - u"transition isn't allowed from") - cnx.rollback() - session.set_cnxset() - # get back now - iworkflowable.fire_transition('activate') - cnx.commit() - cnx.close() + session.commit() + session.set_cnxset() + with self.assertRaises(ValidationError) as cm: + iworkflowable.fire_transition('deactivate') + self.assertEqual(self._cleanup_msg(cm.exception.errors['by_transition-subject']), + u"transition isn't allowed from") + session.rollback() + session.set_cnxset() + # get back now + iworkflowable.fire_transition('activate') + session.commit() if __name__ == '__main__': diff -r 7812093e21f7 -r ab57000bff7b entity.py --- a/entity.py Thu Jan 17 17:12:06 2013 +0100 +++ b/entity.py Thu Jan 17 18:30:08 2013 +0100 @@ -452,26 +452,13 @@ return mainattr, needcheck @classmethod - def cw_instantiate(cls, execute, **kwargs): - """add a new entity of this given type - - Example (in a shell session): - - >>> companycls = vreg['etypes'].etype_class(('Company') - >>> personcls = vreg['etypes'].etype_class(('Person') - >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab') - >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe', - ... works_for=c) - - You can also set relation where the entity has 'object' role by - prefixing the relation by 'reverse_'. - """ - rql = 'INSERT %s X' % cls.__regid__ + def _cw_build_entity_query(cls, kwargs): relations = [] restrictions = set() - pending_relations = [] + pendingrels = [] eschema = cls.e_schema qargs = {} + attrcache = {} for attr, value in kwargs.items(): if attr.startswith('reverse_'): attr = attr[len('reverse_'):] @@ -487,10 +474,13 @@ value = iter(value).next() else: # prepare IN clause - pending_relations.append( (attr, role, value) ) + pendingrels.append( (attr, role, value) ) continue if rschema.final: # attribute relations.append('X %s %%(%s)s' % (attr, attr)) + attrcache[attr] = value + elif value is None: + pendingrels.append( (attr, role, value) ) else: rvar = attr.upper() if role == 'object': @@ -503,19 +493,51 @@ if hasattr(value, 'eid'): value = value.eid qargs[attr] = value + rql = u'' if relations: - rql = '%s: %s' % (rql, ', '.join(relations)) + rql += ', '.join(relations) if restrictions: - rql = '%s WHERE %s' % (rql, ', '.join(restrictions)) - created = execute(rql, qargs).get_entity(0, 0) - for attr, role, values in pending_relations: + rql += ' WHERE %s' % ', '.join(restrictions) + return rql, qargs, pendingrels, attrcache + + @classmethod + def _cw_handle_pending_relations(cls, eid, pendingrels, execute): + for attr, role, values in pendingrels: if role == 'object': restr = 'Y %s X' % attr else: restr = 'X %s Y' % attr + if values is None: + execute('DELETE %s WHERE X eid %%(x)s' % restr, {'x': eid}) + continue execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % ( restr, ','.join(str(getattr(r, 'eid', r)) for r in values)), - {'x': created.eid}, build_descr=False) + {'x': eid}, build_descr=False) + + @classmethod + def cw_instantiate(cls, execute, **kwargs): + """add a new entity of this given type + + Example (in a shell session): + + >>> companycls = vreg['etypes'].etype_class(('Company') + >>> personcls = vreg['etypes'].etype_class(('Person') + >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab') + >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe', + ... works_for=c) + + You can also set relations where the entity has 'object' role by + prefixing the relation name by 'reverse_'. Also, relation values may be + an entity or eid, a list of entities or eids. + """ + rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs) + if rql: + rql = 'INSERT %s X: %s' % (cls.__regid__, rql) + else: + rql = 'INSERT %s X' % (cls.__regid__) + created = execute(rql, qargs).get_entity(0, 0) + created._cw_update_attr_cache(attrcache) + cls._cw_handle_pending_relations(created.eid, pendingrels, execute) return created def __init__(self, req, rset=None, row=None, col=0): @@ -535,6 +557,24 @@ def __cmp__(self, other): raise NotImplementedError('comparison not implemented for %s' % self.__class__) + def _cw_update_attr_cache(self, attrcache): + # if context is a repository session, don't consider dont-cache-attrs as + # the instance already hold modified values and loosing them could + # introduce severe problems + if self._cw.is_request: + for attr in self._cw.get_shared_data('%s.dont-cache-attrs' % self.eid, + default=(), txdata=True, pop=True): + attrcache.pop(attr, None) + self.cw_attr_cache.pop(attr, None) + self.cw_attr_cache.update(attrcache) + + def _cw_dont_cache_attribute(self, attr): + """repository side method called when some attribute have been + transformed by a hook, hence original value should not be cached by + client + """ + self._cw.transaction_data.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr) + def __json_encode__(self): """custom json dumps hook to dump the entity's eid which is not part of dict structure itself @@ -1217,54 +1257,41 @@ # raw edition utilities ################################################### - def set_attributes(self, **kwargs): # XXX cw_set_attributes + def cw_set(self, **kwargs): + """update this entity using given attributes / relation, working in the + same fashion as :meth:`cw_instantiate`. + + Example (in a shell session): + + >>> c = rql('Any X WHERE X is Company').get_entity(0, 0) + >>> p = rql('Any X WHERE X is Person').get_entity(0, 0) + >>> c.set(name=u'Logilab') + >>> p.set(firstname=u'John', lastname=u'Doe', works_for=c) + + You can also set relations where the entity has 'object' role by + prefixing the relation name by 'reverse_'. Also, relation values may be + an entity or eid, a list of entities or eids, or None (meaning that all + relations of the given type from or to this object should be deleted). + """ _check_cw_unsafe(kwargs) assert kwargs assert self.cw_is_saved(), "should not call set_attributes while entity "\ "hasn't been saved yet" - relations = ['X %s %%(%s)s' % (key, key) for key in kwargs] - # and now update the database - kwargs['x'] = self.eid - self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), - kwargs) - kwargs.pop('x') + rql, qargs, pendingrels, attrcache = self._cw_build_entity_query(kwargs) + if rql: + rql = 'SET ' + rql + qargs['x'] = self.eid + if ' WHERE ' in rql: + rql += ', X eid %(x)s' + else: + rql += ' WHERE X eid %(x)s' + self._cw.execute(rql, qargs) # update current local object _after_ the rql query to avoid # interferences between the query execution itself and the cw_edited / # skip_security machinery - self.cw_attr_cache.update(kwargs) - - def set_relations(self, **kwargs): # XXX cw_set_relations - """add relations to the given object. To set a relation where this entity - is the object of the relation, use 'reverse_' as argument name. - - Values may be an entity or eid, a list of entities or eids, or None - (meaning that all relations of the given type from or to this object - should be deleted). - """ - # XXX update cache - _check_cw_unsafe(kwargs) - for attr, values in kwargs.iteritems(): - if attr.startswith('reverse_'): - restr = 'Y %s X' % attr[len('reverse_'):] - else: - restr = 'X %s Y' % attr - if values is None: - self._cw.execute('DELETE %s WHERE X eid %%(x)s' % restr, - {'x': self.eid}) - continue - if not isinstance(values, (tuple, list, set, frozenset)): - values = (values,) - eids = [] - for val in values: - try: - eids.append(str(val.eid)) - except AttributeError: - try: - eids.append(str(typed_eid(val))) - except (ValueError, TypeError): - raise Exception('expected an Entity or eid, got %s' % val) - self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % ( - restr, ','.join(eids)), {'x': self.eid}) + self._cw_update_attr_cache(attrcache) + self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute) + # XXX update relation cache def cw_delete(self, **kwargs): assert self.has_eid(), self.eid @@ -1279,6 +1306,21 @@ # deprecated stuff ######################################################### + @deprecated('[3.16] use cw_set() instead') + def set_attributes(self, **kwargs): # XXX cw_set_attributes + self.cw_set(**kwargs) + + @deprecated('[3.16] use cw_set() instead') + def set_relations(self, **kwargs): # XXX cw_set_relations + """add relations to the given object. To set a relation where this entity + is the object of the relation, use 'reverse_' as argument name. + + Values may be an entity or eid, a list of entities or eids, or None + (meaning that all relations of the given type from or to this object + should be deleted). + """ + self.cw_set(**kwargs) + @deprecated('[3.13] use entity.cw_clear_all_caches()') def clear_all_caches(self): return self.cw_clear_all_caches() diff -r 7812093e21f7 -r ab57000bff7b ext/tal.py --- a/ext/tal.py Thu Jan 17 17:12:06 2013 +0100 +++ b/ext/tal.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -261,7 +261,7 @@ return wrapped def _compiled_template(self, instance): - for fileordirectory in instance.config.vregistry_path(): + for fileordirectory in instance.config.appobjects_path(): filepath = join(fileordirectory, self.filename) if isdir(fileordirectory) and exists(filepath): return compile_template_file(filepath) diff -r 7812093e21f7 -r ab57000bff7b hooks/__init__.py diff -r 7812093e21f7 -r ab57000bff7b hooks/bookmark.py --- a/hooks/bookmark.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/bookmark.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b hooks/email.py --- a/hooks/email.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/email.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,9 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""hooks to ensure use_email / primary_email relations consistency +"""hooks to ensure use_email / primary_email relations consistency""" -""" __docformat__ = "restructuredtext en" from cubicweb.server import hook diff -r 7812093e21f7 -r ab57000bff7b hooks/integrity.py --- a/hooks/integrity.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/integrity.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,12 +20,11 @@ """ __docformat__ = "restructuredtext en" +_ = unicode from threading import Lock -from yams.schema import role_name - -from cubicweb import ValidationError +from cubicweb import validation_error from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES, RQLConstraint, RQLUniqueConstraint) from cubicweb.predicates import is_instance @@ -87,11 +86,11 @@ continue if not session.execute(self.base_rql % rtype, {'x': eid}): etype = session.describe(eid)[0] - _ = session._ msg = _('at least one relation %(rtype)s is required on ' '%(etype)s (%(eid)s)') - msg %= {'rtype': _(rtype), 'etype': _(etype), 'eid': eid} - raise ValidationError(eid, {role_name(rtype, self.role): msg}) + raise validation_error(eid, {(rtype, self.role): msg}, + {'rtype': rtype, 'etype': etype, 'eid': eid}, + ['rtype', 'etype']) class _CheckSRelationOp(_CheckRequiredRelationOperation): @@ -231,9 +230,9 @@ rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr) rset = self._cw.execute(rql, {'val': val}) if rset and rset[0][0] != entity.eid: - msg = self._cw._('the value "%s" is already used, use another one') - qname = role_name(attr, 'subject') - raise ValidationError(entity.eid, {qname: msg % val}) + msg = _('the value "%s" is already used, use another one') + raise validation_error(entity, {(attr, 'subject'): msg}, + (val,)) class DontRemoveOwnersGroupHook(IntegrityHook): @@ -246,15 +245,12 @@ def __call__(self): entity = self.entity if self.event == 'before_delete_entity' and entity.name == 'owners': - msg = self._cw._('can\'t be deleted') - raise ValidationError(entity.eid, {None: msg}) + raise validation_error(entity, {None: _("can't be deleted")}) elif self.event == 'before_update_entity' \ and 'name' in entity.cw_edited: oldname, newname = entity.cw_edited.oldnewvalue('name') if oldname == 'owners' and newname != oldname: - qname = role_name('name', 'subject') - msg = self._cw._('can\'t be changed') - raise ValidationError(entity.eid, {qname: msg}) + raise validation_error(entity, {('name', 'subject'): _("can't be changed")}) class TidyHtmlFields(IntegrityHook): diff -r 7812093e21f7 -r ab57000bff7b hooks/metadata.py --- a/hooks/metadata.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/metadata.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b hooks/syncschema.py --- a/hooks/syncschema.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/syncschema.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -24,6 +24,7 @@ """ __docformat__ = "restructuredtext en" +_ = unicode from copy import copy from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema @@ -31,7 +32,7 @@ from logilab.common.decorators import clear_cache -from cubicweb import ValidationError +from cubicweb import validation_error from cubicweb.predicates import is_instance from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS, ETYPE_NAME_MAP, display_name) @@ -127,10 +128,9 @@ if attr in ro_attrs: origval, newval = entity.cw_edited.oldnewvalue(attr) if newval != origval: - errors[attr] = session._("can't change the %s attribute") % \ - display_name(session, attr) + errors[attr] = _("can't change this attribute") if errors: - raise ValidationError(entity.eid, errors) + raise validation_error(entity, errors) class _MockEntity(object): # XXX use a named tuple with python 2.6 @@ -913,7 +913,7 @@ # final entities can't be deleted, don't care about that name = self.entity.name if name in CORE_TYPES: - raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')}) + raise validation_error(self.entity, {None: _("can't be deleted")}) # delete every entities of this type if name not in ETYPE_NAME_MAP: self._cw.execute('DELETE %s X' % name) @@ -983,7 +983,7 @@ def __call__(self): name = self.entity.name if name in CORE_TYPES: - raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')}) + raise validation_error(self.entity, {None: _("can't be deleted")}) # delete relation definitions using this relation type self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s', {'x': self.entity.eid}) diff -r 7812093e21f7 -r ab57000bff7b hooks/syncsession.py --- a/hooks/syncsession.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/syncsession.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -18,9 +18,9 @@ """Core hooks: synchronize living session on persistent data changes""" __docformat__ = "restructuredtext en" +_ = unicode -from yams.schema import role_name -from cubicweb import UnknownProperty, ValidationError, BadConnectionId +from cubicweb import UnknownProperty, BadConnectionId, validation_error from cubicweb.predicates import is_instance from cubicweb.server import hook @@ -165,13 +165,11 @@ try: value = session.vreg.typed_value(key, value) except UnknownProperty: - qname = role_name('pkey', 'subject') - msg = session._('unknown property key %s') % key - raise ValidationError(self.entity.eid, {qname: msg}) + msg = _('unknown property key %s') + raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,)) except ValueError, ex: - qname = role_name('value', 'subject') - raise ValidationError(self.entity.eid, - {qname: session._(str(ex))}) + raise validation_error(self.entity, + {('value', 'subject'): str(ex)}) if not session.user.matching_groups('managers'): session.add_relation(self.entity.eid, 'for_user', session.user.eid) else: @@ -196,8 +194,7 @@ except UnknownProperty: return except ValueError, ex: - qname = role_name('value', 'subject') - raise ValidationError(entity.eid, {qname: session._(str(ex))}) + raise validation_error(entity, {('value', 'subject'): str(ex)}) if entity.for_user: for session_ in get_user_sessions(session.repo, entity.for_user[0].eid): _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties, @@ -237,10 +234,8 @@ key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V', {'x': eidfrom})[0] if session.vreg.property_info(key)['sitewide']: - qname = role_name('for_user', 'subject') - msg = session._("site-wide property can't be set for user") - raise ValidationError(eidfrom, - {qname: msg}) + msg = _("site-wide property can't be set for user") + raise validation_error(eidfrom, {('for_user', 'subject'): msg}) for session_ in get_user_sessions(session.repo, self.eidto): _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties, key=key, value=value) diff -r 7812093e21f7 -r ab57000bff7b hooks/syncsources.py --- a/hooks/syncsources.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/syncsources.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -17,12 +17,13 @@ # with CubicWeb. If not, see . """hooks for repository sources synchronization""" +_ = unicode + from socket import gethostname from logilab.common.decorators import clear_cache -from yams.schema import role_name -from cubicweb import ValidationError +from cubicweb import validation_error from cubicweb.predicates import is_instance from cubicweb.server import SOURCE_TYPES, hook @@ -46,12 +47,15 @@ try: sourcecls = SOURCE_TYPES[self.entity.type] except KeyError: - msg = self._cw._('unknown source type') - raise ValidationError(self.entity.eid, - {role_name('type', 'subject'): msg}) - sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config, - fail_if_unknown=not self._cw.vreg.config.repairing) - SourceAddedOp(self._cw, entity=self.entity) + msg = _('Unknown source type') + raise validation_error(self.entity, {('type', 'subject'): msg}) + # ignore creation of the system source done during database + # initialisation, as config for this source is in a file and handling + # is done separatly (no need for the operation either) + if self.entity.name != 'system': + sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config, + fail_if_unknown=not self._cw.vreg.config.repairing) + SourceAddedOp(self._cw, entity=self.entity) class SourceRemovedOp(hook.Operation): @@ -65,7 +69,8 @@ events = ('before_delete_entity',) def __call__(self): if self.entity.name == 'system': - raise ValidationError(self.entity.eid, {None: 'cant remove system source'}) + msg = _("You cannot remove the system source") + raise validation_error(self.entity, {None: msg}) SourceRemovedOp(self._cw, uri=self.entity.name) @@ -116,11 +121,18 @@ __select__ = SourceHook.__select__ & is_instance('CWSource') events = ('before_update_entity',) def __call__(self): - if 'config' in self.entity.cw_edited: - SourceConfigUpdatedOp.get_instance(self._cw).add_data(self.entity) if 'name' in self.entity.cw_edited: oldname, newname = self.entity.cw_edited.oldnewvalue('name') + if oldname == 'system': + msg = _("You cannot rename the system source") + raise validation_error(self.entity, {('name', 'subject'): msg}) SourceRenamedOp(self._cw, oldname=oldname, newname=newname) + if 'config' in self.entity.cw_edited: + if self.entity.name == 'system' and self.entity.config: + msg = _("Configuration of the system source goes to " + "the 'sources' file, not in the database") + raise validation_error(self.entity, {('config', 'subject'): msg}) + SourceConfigUpdatedOp.get_instance(self._cw).add_data(self.entity) class SourceHostConfigUpdatedHook(SourceHook): @@ -154,8 +166,8 @@ events = ('before_add_relation',) def __call__(self): if not self._cw.added_in_transaction(self.eidfrom): - msg = self._cw._("can't change this relation") - raise ValidationError(self.eidfrom, {self.rtype: msg}) + msg = _("You can't change this relation") + raise validation_error(self.eidfrom, {self.rtype: msg}) class SourceMappingChangedOp(hook.DataOperationMixIn, hook.Operation): diff -r 7812093e21f7 -r ab57000bff7b hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/test/unittest_hooks.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -67,10 +67,9 @@ entity = self.request().create_entity('Workflow', name=u'wf1', description_format=u'text/html', description=u'yo') - entity.set_attributes(name=u'wf2') + entity.cw_set(name=u'wf2') self.assertEqual(entity.description, u'yo') - entity.set_attributes(description=u'R&D

yo') - entity.cw_attr_cache.pop('description') + entity.cw_set(description=u'R&D

yo') self.assertEqual(entity.description, u'R&D

yo

') def test_metadata_cwuri(self): @@ -166,13 +165,12 @@ self.execute, 'INSERT CWRType X: X name "in_group"') def test_validation_unique_constraint(self): - self.assertRaises(ValidationError, - self.execute, 'INSERT CWUser X: X login "admin"') - try: + with self.assertRaises(ValidationError) as cm: self.execute('INSERT CWUser X: X login "admin"') - except ValidationError, ex: - self.assertIsInstance(ex.entity, int) - self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'}) + ex = cm.exception + ex.translate(unicode) + self.assertIsInstance(ex.entity, int) + self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'}) if __name__ == '__main__': diff -r 7812093e21f7 -r ab57000bff7b hooks/test/unittest_syncschema.py --- a/hooks/test/unittest_syncschema.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/test/unittest_syncschema.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -294,7 +294,7 @@ def test_change_fulltext_container(self): req = self.request() target = req.create_entity(u'EmailAddress', address=u'rick.roll@dance.com') - target.set_relations(reverse_use_email=req.user) + target.cw_set(reverse_use_email=req.user) self.commit() rset = req.execute('Any X WHERE X has_text "rick.roll"') self.assertIn(req.user.eid, [item[0] for item in rset]) diff -r 7812093e21f7 -r ab57000bff7b hooks/test/unittest_syncsession.py --- a/hooks/test/unittest_syncsession.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/test/unittest_syncsession.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -31,9 +31,11 @@ def test_unexistant_cwproperty(self): with self.assertRaises(ValidationError) as cm: self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U') + cm.exception.translate(unicode) self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'}) with self.assertRaises(ValidationError) as cm: self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop"') + cm.exception.translate(unicode) self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'}) def test_site_wide_cwproperty(self): diff -r 7812093e21f7 -r ab57000bff7b hooks/workflow.py --- a/hooks/workflow.py Thu Jan 17 17:12:06 2013 +0100 +++ b/hooks/workflow.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -18,12 +18,12 @@ """Core hooks: workflow related hooks""" __docformat__ = "restructuredtext en" +_ = unicode from datetime import datetime -from yams.schema import role_name -from cubicweb import RepositoryError, ValidationError +from cubicweb import RepositoryError, validation_error from cubicweb.predicates import is_instance, adaptable from cubicweb.server import hook @@ -92,9 +92,8 @@ if mainwf.eid == self.wfeid: deststate = mainwf.initial if not deststate: - qname = role_name('custom_workflow', 'subject') - msg = session._('workflow has no initial state') - raise ValidationError(entity.eid, {qname: msg}) + msg = _('workflow has no initial state') + raise validation_error(entity, {('custom_workflow', 'subject'): msg}) if mainwf.state_by_eid(iworkflowable.current_state.eid): # nothing to do return @@ -119,9 +118,8 @@ outputs = set() for ep in tr.subworkflow_exit: if ep.subwf_state.eid in outputs: - qname = role_name('subworkflow_exit', 'subject') - msg = self.session._("can't have multiple exits on the same state") - raise ValidationError(self.treid, {qname: msg}) + msg = _("can't have multiple exits on the same state") + raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg}) outputs.add(ep.subwf_state.eid) @@ -137,13 +135,12 @@ wftr = iworkflowable.subworkflow_input_transition() if wftr is None: # inconsistency detected - qname = role_name('to_state', 'subject') - msg = session._("state doesn't belong to entity's current workflow") - raise ValidationError(self.trinfo.eid, {'to_state': msg}) + msg = _("state doesn't belong to entity's current workflow") + raise validation_error(self.trinfo, {('to_state', 'subject'): msg}) tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state']) if tostate is not None: # reached an exit point - msg = session._('exiting from subworkflow %s') + msg = _('exiting from subworkflow %s') msg %= session._(iworkflowable.current_workflow.name) session.transaction_data[(forentity.eid, 'subwfentrytr')] = True iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr) @@ -186,9 +183,8 @@ try: foreid = entity.cw_attr_cache['wf_info_for'] except KeyError: - qname = role_name('wf_info_for', 'subject') - msg = session._('mandatory relation') - raise ValidationError(entity.eid, {qname: msg}) + msg = _('mandatory relation') + raise validation_error(entity, {('wf_info_for', 'subject'): msg}) forentity = session.entity_from_eid(foreid) # see comment in the TrInfo entity definition entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for) @@ -201,13 +197,13 @@ else: wf = iworkflowable.current_workflow if wf is None: - msg = session._('related entity has no workflow set') - raise ValidationError(entity.eid, {None: msg}) + msg = _('related entity has no workflow set') + raise validation_error(entity, {None: msg}) # then check it has a state set fromstate = iworkflowable.current_state if fromstate is None: - msg = session._('related entity has no state') - raise ValidationError(entity.eid, {None: msg}) + msg = _('related entity has no state') + raise validation_error(entity, {None: msg}) # True if we are coming back from subworkflow swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None) cowpowers = (session.user.is_in_group('managers') @@ -219,47 +215,42 @@ # no transition set, check user is a manager and destination state # is specified (and valid) if not cowpowers: - qname = role_name('by_transition', 'subject') - msg = session._('mandatory relation') - raise ValidationError(entity.eid, {qname: msg}) + msg = _('mandatory relation') + raise validation_error(entity, {('by_transition', 'subject'): msg}) deststateeid = entity.cw_attr_cache.get('to_state') if not deststateeid: - qname = role_name('by_transition', 'subject') - msg = session._('mandatory relation') - raise ValidationError(entity.eid, {qname: msg}) + msg = _('mandatory relation') + raise validation_error(entity, {('by_transition', 'subject'): msg}) deststate = wf.state_by_eid(deststateeid) if deststate is None: - qname = role_name('to_state', 'subject') - msg = session._("state doesn't belong to entity's workflow") - raise ValidationError(entity.eid, {qname: msg}) + msg = _("state doesn't belong to entity's workflow") + raise validation_error(entity, {('to_state', 'subject'): msg}) else: # check transition is valid and allowed, unless we're coming back # from subworkflow tr = session.entity_from_eid(treid) if swtr is None: - qname = role_name('by_transition', 'subject') + qname = ('by_transition', 'subject') if tr is None: - msg = session._("transition doesn't belong to entity's workflow") - raise ValidationError(entity.eid, {qname: msg}) + msg = _("transition doesn't belong to entity's workflow") + raise validation_error(entity, {qname: msg}) if not tr.has_input_state(fromstate): - msg = session._("transition %(tr)s isn't allowed from %(st)s") % { - 'tr': session._(tr.name), 'st': session._(fromstate.name)} - raise ValidationError(entity.eid, {qname: msg}) + msg = _("transition %(tr)s isn't allowed from %(st)s") + raise validation_error(entity, {qname: msg}, { + 'tr': tr.name, 'st': fromstate.name}, ['tr', 'st']) if not tr.may_be_fired(foreid): - msg = session._("transition may not be fired") - raise ValidationError(entity.eid, {qname: msg}) + msg = _("transition may not be fired") + raise validation_error(entity, {qname: msg}) deststateeid = entity.cw_attr_cache.get('to_state') if deststateeid is not None: if not cowpowers and deststateeid != tr.destination(forentity).eid: - qname = role_name('by_transition', 'subject') - msg = session._("transition isn't allowed") - raise ValidationError(entity.eid, {qname: msg}) + msg = _("transition isn't allowed") + raise validation_error(entity, {('by_transition', 'subject'): msg}) if swtr is None: deststate = session.entity_from_eid(deststateeid) if not cowpowers and deststate is None: - qname = role_name('to_state', 'subject') - msg = session._("state doesn't belong to entity's workflow") - raise ValidationError(entity.eid, {qname: msg}) + msg = _("state doesn't belong to entity's workflow") + raise validation_error(entity, {('to_state', 'subject'): msg}) else: deststateeid = tr.destination(forentity).eid # everything is ok, add missing information on the trinfo entity @@ -307,20 +298,18 @@ iworkflowable = entity.cw_adapt_to('IWorkflowable') mainwf = iworkflowable.main_workflow if mainwf is None: - msg = session._('entity has no workflow set') - raise ValidationError(entity.eid, {None: msg}) + msg = _('entity has no workflow set') + raise validation_error(entity, {None: msg}) for wf in mainwf.iter_workflows(): if wf.state_by_eid(self.eidto): break else: - qname = role_name('in_state', 'subject') - msg = session._("state doesn't belong to entity's workflow. You may " - "want to set a custom workflow for this entity first.") - raise ValidationError(self.eidfrom, {qname: msg}) + msg = _("state doesn't belong to entity's workflow. You may " + "want to set a custom workflow for this entity first.") + raise validation_error(self.eidfrom, {('in_state', 'subject'): msg}) if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid: - qname = role_name('in_state', 'subject') - msg = session._("state doesn't belong to entity's current workflow") - raise ValidationError(self.eidfrom, {qname: msg}) + msg = _("state doesn't belong to entity's current workflow") + raise validation_error(self.eidfrom, {('in_state', 'subject'): msg}) class SetModificationDateOnStateChange(WorkflowHook): @@ -335,7 +324,7 @@ return entity = self._cw.entity_from_eid(self.eidfrom) try: - entity.set_attributes(modification_date=datetime.now()) + entity.cw_set(modification_date=datetime.now()) except RepositoryError, ex: # usually occurs if entity is coming from a read-only source # (eg ldap user) diff -r 7812093e21f7 -r ab57000bff7b i18n.py --- a/i18n.py Thu Jan 17 17:12:06 2013 +0100 +++ b/i18n.py Thu Jan 17 18:30:08 2013 +0100 @@ -54,19 +54,16 @@ w('msgid "%s"\n' % msgid[0]) w('msgstr ""\n\n') - -def execute(cmd): - """display the command, execute it and raise an Exception if returned - status != 0 - """ - from subprocess import call - # use getcwdu as cmd may be unicode and cwd may contains non-ascii - # characters - print cmd.replace(os.getcwdu() + os.sep, '') - status = call(cmd, shell=True) - if status != 0: - raise Exception('status = %s' % status) - +def execute2(args): + # XXX replace this with check_output in Python 2.7 + from subprocess import Popen, PIPE, CalledProcessError + p = Popen(args, stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + if p.returncode != 0: + exc = CalledProcessError(p.returncode, args[0]) + exc.cmd = args + exc.data = (out, err) + raise exc def available_catalogs(i18ndir=None): if i18ndir is None: @@ -81,6 +78,7 @@ def compile_i18n_catalogs(sourcedirs, destdir, langs): """generate .mo files for a set of languages into the `destdir` i18n directory """ + from subprocess import CalledProcessError from logilab.common.fileutils import ensure_fs_mode print '-> compiling message catalogs to %s' % destdir errors = [] @@ -93,17 +91,21 @@ mergedpo = join(destdir, '%s_merged.po' % lang) try: # merge instance/cubes messages catalogs with the stdlib's one - execute('msgcat --use-first --sort-output --strict -o "%s" %s' - % (mergedpo, ' '.join('"%s"' % f for f in pofiles))) + cmd = ['msgcat', '--use-first', '--sort-output', '--strict', + '-o', mergedpo] + pofiles + execute2(cmd) # make sure the .mo file is writeable and compiles with *msgfmt* applmo = join(destdir, lang, 'LC_MESSAGES', 'cubicweb.mo') try: ensure_fs_mode(applmo) except OSError: pass # suppose not exists - execute('msgfmt "%s" -o "%s"' % (mergedpo, applmo)) - except Exception, ex: - errors.append('while handling language %s: %s' % (lang, ex)) + execute2(['msgfmt', mergedpo, '-o', applmo]) + except CalledProcessError, exc: + errors.append(u'while handling language %s:\ncmd:\n%s\nstdout:\n%s\nstderr:\n%s\n' % + (lang, exc.cmd, repr(exc.data[0]), repr(exc.data[1]))) + except Exception, exc: + errors.append(u'while handling language %s: %s' % (lang, exc)) try: # clean everything os.unlink(mergedpo) diff -r 7812093e21f7 -r ab57000bff7b i18n/de.po --- a/i18n/de.po Thu Jan 17 17:12:06 2013 +0100 +++ b/i18n/de.po Thu Jan 17 18:30:08 2013 +0100 @@ -50,6 +50,14 @@ msgstr "" #, python-format +msgid "%(KEY-cstr)s constraint failed for value %(KEY-value)r" +msgstr "" + +#, python-format +msgid "%(KEY-value)r doesn't match the %(KEY-regexp)r regular expression" +msgstr "" + +#, python-format msgid "%(attr)s set to %(newvalue)s" msgstr "%(attr)s geändert in %(newvalue)s" @@ -58,10 +66,6 @@ msgstr "%(attr)s geändert von %(oldvalue)s in %(newvalue)s" #, python-format -msgid "%(cstr)s constraint failed for value %(value)r" -msgstr "%(cstr)s Einschränkung verletzt für Wert %(value)r" - -#, python-format msgid "%(etype)s by %(author)s" msgstr "" @@ -74,10 +78,6 @@ msgstr "%(subject)s %(etype)s #%(eid)s (%(login)s)" #, python-format -msgid "%(value)r doesn't match the %(regexp)r regular expression" -msgstr "%(value)r entspricht nicht dem regulären Ausdruck %(regexp)r" - -#, python-format msgid "%d days" msgstr "%d Tage" @@ -427,6 +427,11 @@ msgid "Click to sort on this column" msgstr "" +msgid "" +"Configuration of the system source goes to the 'sources' file, not in the " +"database" +msgstr "" + #, python-format msgid "Created %(etype)s : %(entity)s" msgstr "" @@ -902,6 +907,9 @@ msgid "UniqueConstraint" msgstr "eindeutige Einschränkung" +msgid "Unknown source type" +msgstr "" + msgid "Unreachable objects" msgstr "unzugängliche Objekte" @@ -960,6 +968,15 @@ msgid "You can use any of the following substitutions in your text" msgstr "Sie können die folgenden Ersetzungen in Ihrem Text verwenden:" +msgid "You can't change this relation" +msgstr "" + +msgid "You cannot remove the system source" +msgstr "" + +msgid "You cannot rename the system source" +msgstr "" + msgid "" "You have no access to this view or it can not be used to display the current " "data." @@ -1006,9 +1023,6 @@ msgid "abstract base class for transitions" msgstr "abstrakte Basisklasse für Übergänge" -msgid "action menu" -msgstr "" - msgid "action(s) on this selection" msgstr "Aktionen(en) bei dieser Auswahl" @@ -1392,11 +1406,7 @@ msgid "can't be deleted" msgstr "kann nicht entfernt werden" -#, python-format -msgid "can't change the %s attribute" -msgstr "Kann das Attribut %s nicht ändern." - -msgid "can't change this relation" +msgid "can't change this attribute" msgstr "" #, python-format @@ -2569,6 +2579,9 @@ msgid "foaf" msgstr "FOAF" +msgid "focus on this selection" +msgstr "" + msgid "follow" msgstr "dem Link folgen" @@ -2859,8 +2872,8 @@ msgstr "Unzulässiger Wert für Überschrift" #, python-format -msgid "incorrect value (%(value)s) for type \"%(type)s\"" -msgstr "Wert %(value)s ungültig für den Typ \"%(type)s\"" +msgid "incorrect value (%(KEY-value)r) for type \"%(KEY-type)s\"" +msgstr "" msgid "index this attribute's value in the plain text index" msgstr "indizieren des Wertes dieses Attributs im Volltext-Index" @@ -2938,8 +2951,8 @@ msgstr "Ungültige Aktion %r" #, python-format -msgid "invalid value %(value)s, it must be one of %(choices)s" -msgstr "Wert %(value)s ungültig, er muss zwischen %(choices)s" +msgid "invalid value %(KEY-value)s, it must be one of %(KEY-choices)s" +msgstr "" msgid "is" msgstr "vom Typ" @@ -4174,6 +4187,9 @@ msgid "toggle check boxes" msgstr "Kontrollkästchen umkehren" +msgid "toggle filter" +msgstr "filter verbergen/zeigen" + msgid "tr_count" msgstr "" @@ -4316,9 +4332,6 @@ msgid "unknown property key %s" msgstr "Unbekannter Eigentumsschlüssel %s" -msgid "unknown source type" -msgstr "" - msgid "unknown vocabulary:" msgstr "Unbekanntes Wörterbuch : " @@ -4464,15 +4477,7 @@ msgstr "Wert" #, python-format -msgid "value %(value)s must be %(op)s %(boundary)s" -msgstr "" - -#, python-format -msgid "value %(value)s must be <= %(boundary)s" -msgstr "" - -#, python-format -msgid "value %(value)s must be >= %(boundary)s" +msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s" msgstr "" msgid "value associated to this key is not editable manually" @@ -4481,11 +4486,11 @@ "werden." #, python-format -msgid "value should have maximum size of %s but found %s" +msgid "value should have maximum size of %(KEY-max)s but found %(KEY-size)s" msgstr "" #, python-format -msgid "value should have minimum size of %s but found %s" +msgid "value should have minimum size of %(KEY-min)s but found %(KEY-size)s" msgstr "" msgid "vcard" @@ -4630,6 +4635,12 @@ msgid "you should un-inline relation %s which is supported and may be crossed " msgstr "" +#~ msgid "%(cstr)s constraint failed for value %(value)r" +#~ msgstr "%(cstr)s Einschränkung verletzt für Wert %(value)r" + +#~ msgid "%(value)r doesn't match the %(regexp)r regular expression" +#~ msgstr "%(value)r entspricht nicht dem regulären Ausdruck %(regexp)r" + #~ msgid "" #~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does " #~ "not exists anymore in the schema." @@ -4637,8 +4648,11 @@ #~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, " #~ "diese Relation existiert nicht mehr in dem Schema." -#~ msgid "log out first" -#~ msgstr "Melden Sie sich zuerst ab." - -#~ msgid "week" -#~ msgstr "Woche" +#~ msgid "can't change the %s attribute" +#~ msgstr "Kann das Attribut %s nicht ändern." + +#~ msgid "incorrect value (%(value)s) for type \"%(type)s\"" +#~ msgstr "Wert %(value)s ungültig für den Typ \"%(type)s\"" + +#~ msgid "invalid value %(value)s, it must be one of %(choices)s" +#~ msgstr "Wert %(value)s ungültig, er muss zwischen %(choices)s" diff -r 7812093e21f7 -r ab57000bff7b i18n/en.po --- a/i18n/en.po Thu Jan 17 17:12:06 2013 +0100 +++ b/i18n/en.po Thu Jan 17 18:30:08 2013 +0100 @@ -42,6 +42,14 @@ msgstr "" #, python-format +msgid "%(KEY-cstr)s constraint failed for value %(KEY-value)r" +msgstr "" + +#, python-format +msgid "%(KEY-value)r doesn't match the %(KEY-regexp)r regular expression" +msgstr "" + +#, python-format msgid "%(attr)s set to %(newvalue)s" msgstr "" @@ -50,10 +58,6 @@ msgstr "" #, python-format -msgid "%(cstr)s constraint failed for value %(value)r" -msgstr "" - -#, python-format msgid "%(etype)s by %(author)s" msgstr "" @@ -66,10 +70,6 @@ msgstr "" #, python-format -msgid "%(value)r doesn't match the %(regexp)r regular expression" -msgstr "" - -#, python-format msgid "%d days" msgstr "" @@ -405,6 +405,11 @@ msgid "Click to sort on this column" msgstr "" +msgid "" +"Configuration of the system source goes to the 'sources' file, not in the " +"database" +msgstr "" + #, python-format msgid "Created %(etype)s : %(entity)s" msgstr "" @@ -878,6 +883,9 @@ msgid "UniqueConstraint" msgstr "unique constraint" +msgid "Unknown source type" +msgstr "" + msgid "Unreachable objects" msgstr "" @@ -929,6 +937,15 @@ msgid "You can use any of the following substitutions in your text" msgstr "" +msgid "You can't change this relation" +msgstr "" + +msgid "You cannot remove the system source" +msgstr "" + +msgid "You cannot rename the system source" +msgstr "" + msgid "" "You have no access to this view or it can not be used to display the current " "data." @@ -968,9 +985,6 @@ msgid "abstract base class for transitions" msgstr "" -msgid "action menu" -msgstr "" - msgid "action(s) on this selection" msgstr "" @@ -1349,11 +1363,7 @@ msgid "can't be deleted" msgstr "" -#, python-format -msgid "can't change the %s attribute" -msgstr "" - -msgid "can't change this relation" +msgid "can't change this attribute" msgstr "" #, python-format @@ -2516,6 +2526,9 @@ msgid "foaf" msgstr "" +msgid "focus on this selection" +msgstr "" + msgid "follow" msgstr "" @@ -2788,7 +2801,7 @@ msgstr "" #, python-format -msgid "incorrect value (%(value)s) for type \"%(type)s\"" +msgid "incorrect value (%(KEY-value)r) for type \"%(KEY-type)s\"" msgstr "" msgid "index this attribute's value in the plain text index" @@ -2865,7 +2878,7 @@ msgstr "" #, python-format -msgid "invalid value %(value)s, it must be one of %(choices)s" +msgid "invalid value %(KEY-value)s, it must be one of %(KEY-choices)s" msgstr "" msgid "is" @@ -4074,6 +4087,9 @@ msgid "toggle check boxes" msgstr "" +msgid "toggle filter" +msgstr "" + msgid "tr_count" msgstr "transition number" @@ -4216,9 +4232,6 @@ msgid "unknown property key %s" msgstr "" -msgid "unknown source type" -msgstr "" - msgid "unknown vocabulary:" msgstr "" @@ -4355,26 +4368,18 @@ msgstr "" #, python-format -msgid "value %(value)s must be %(op)s %(boundary)s" -msgstr "" - -#, python-format -msgid "value %(value)s must be <= %(boundary)s" -msgstr "" - -#, python-format -msgid "value %(value)s must be >= %(boundary)s" +msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s" msgstr "" msgid "value associated to this key is not editable manually" msgstr "" #, python-format -msgid "value should have maximum size of %s but found %s" +msgid "value should have maximum size of %(KEY-max)s but found %(KEY-size)s" msgstr "" #, python-format -msgid "value should have minimum size of %s but found %s" +msgid "value should have minimum size of %(KEY-min)s but found %(KEY-size)s" msgstr "" msgid "vcard" diff -r 7812093e21f7 -r ab57000bff7b i18n/es.po --- a/i18n/es.po Thu Jan 17 17:12:06 2013 +0100 +++ b/i18n/es.po Thu Jan 17 18:30:08 2013 +0100 @@ -51,6 +51,14 @@ "\"role=subject\" o \"role=object\" debe ser especificado en las opciones" #, python-format +msgid "%(KEY-cstr)s constraint failed for value %(KEY-value)r" +msgstr "" + +#, python-format +msgid "%(KEY-value)r doesn't match the %(KEY-regexp)r regular expression" +msgstr "" + +#, python-format msgid "%(attr)s set to %(newvalue)s" msgstr "%(attr)s modificado a %(newvalue)s" @@ -59,10 +67,6 @@ msgstr "%(attr)s modificado de %(oldvalue)s a %(newvalue)s" #, python-format -msgid "%(cstr)s constraint failed for value %(value)r" -msgstr "el valor %(value)r no satisface la condición %(cstr)s" - -#, python-format msgid "%(etype)s by %(author)s" msgstr "%(etype)s por %(author)s" @@ -75,10 +79,6 @@ msgstr "%(subject)s %(etype)s #%(eid)s (%(login)s)" #, python-format -msgid "%(value)r doesn't match the %(regexp)r regular expression" -msgstr "%(value)r no corresponde a la expresión regular %(regexp)r" - -#, python-format msgid "%d days" msgstr "%d días" @@ -427,6 +427,11 @@ msgid "Click to sort on this column" msgstr "" +msgid "" +"Configuration of the system source goes to the 'sources' file, not in the " +"database" +msgstr "" + #, python-format msgid "Created %(etype)s : %(entity)s" msgstr "" @@ -905,6 +910,9 @@ msgid "UniqueConstraint" msgstr "Restricción de Unicidad" +msgid "Unknown source type" +msgstr "" + msgid "Unreachable objects" msgstr "Objetos inaccesibles" @@ -965,6 +973,15 @@ "Puede realizar cualquiera de las siguientes sustituciones en el contenido de " "su email." +msgid "You can't change this relation" +msgstr "" + +msgid "You cannot remove the system source" +msgstr "" + +msgid "You cannot rename the system source" +msgstr "" + msgid "" "You have no access to this view or it can not be used to display the current " "data." @@ -1016,9 +1033,6 @@ msgid "abstract base class for transitions" msgstr "Clase de base abstracta para la transiciones" -msgid "action menu" -msgstr "" - msgid "action(s) on this selection" msgstr "Acción(es) en esta selección" @@ -1403,12 +1417,8 @@ msgid "can't be deleted" msgstr "No puede ser eliminado" -#, python-format -msgid "can't change the %s attribute" -msgstr "no puede modificar el atributo %s" - -msgid "can't change this relation" -msgstr "no puede modificar esta relación" +msgid "can't change this attribute" +msgstr "" #, python-format msgid "can't connect to source %s, some data may be missing" @@ -2611,6 +2621,9 @@ msgid "foaf" msgstr "Amigo de un Amigo, FOAF" +msgid "focus on this selection" +msgstr "" + msgid "follow" msgstr "Seguir la liga" @@ -2901,8 +2914,8 @@ msgstr "Valor del Captcha incorrecto" #, python-format -msgid "incorrect value (%(value)s) for type \"%(type)s\"" -msgstr "valor %(value)s incorrecto para el tipo \"%(type)s\"" +msgid "incorrect value (%(KEY-value)r) for type \"%(KEY-type)s\"" +msgstr "" msgid "index this attribute's value in the plain text index" msgstr "Indexar el valor de este atributo en el índice de texto simple" @@ -2981,8 +2994,8 @@ msgstr "Acción %r invalida" #, python-format -msgid "invalid value %(value)s, it must be one of %(choices)s" -msgstr "Valor %(value)s incorrecto, debe estar entre %(choices)s" +msgid "invalid value %(KEY-value)s, it must be one of %(KEY-choices)s" +msgstr "" msgid "is" msgstr "es" @@ -4224,6 +4237,9 @@ msgid "toggle check boxes" msgstr "Cambiar valor" +msgid "toggle filter" +msgstr "esconder/mostrar el filtro" + msgid "tr_count" msgstr "n° de transición" @@ -4366,9 +4382,6 @@ msgid "unknown property key %s" msgstr "Clave de Propiedad desconocida: %s" -msgid "unknown source type" -msgstr "tipo de fuente desconocida" - msgid "unknown vocabulary:" msgstr "Vocabulario desconocido: " @@ -4514,26 +4527,18 @@ msgstr "Vampr" #, python-format -msgid "value %(value)s must be %(op)s %(boundary)s" -msgstr "" - -#, python-format -msgid "value %(value)s must be <= %(boundary)s" -msgstr "" - -#, python-format -msgid "value %(value)s must be >= %(boundary)s" +msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s" msgstr "" msgid "value associated to this key is not editable manually" msgstr "El valor asociado a este elemento no es editable manualmente" #, python-format -msgid "value should have maximum size of %s but found %s" +msgid "value should have maximum size of %(KEY-max)s but found %(KEY-size)s" msgstr "" #, python-format -msgid "value should have minimum size of %s but found %s" +msgid "value should have minimum size of %(KEY-min)s but found %(KEY-size)s" msgstr "" msgid "vcard" @@ -4681,6 +4686,12 @@ "usted debe quitar la puesta en línea de la relación %s que es aceptada y " "puede ser cruzada" +#~ msgid "%(cstr)s constraint failed for value %(value)r" +#~ msgstr "el valor %(value)r no satisface la condición %(cstr)s" + +#~ msgid "%(value)r doesn't match the %(regexp)r regular expression" +#~ msgstr "%(value)r no corresponde a la expresión regular %(regexp)r" + #~ msgid "" #~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does " #~ "not exists anymore in the schema." @@ -4688,17 +4699,17 @@ #~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta " #~ "relación ya no existe en el esquema." -#~ msgid "day" -#~ msgstr "día" - -#~ msgid "log out first" -#~ msgstr "Desconéctese primero" - -#~ msgid "month" -#~ msgstr "mes" - -#~ msgid "today" -#~ msgstr "hoy" - -#~ msgid "week" -#~ msgstr "sem." +#~ msgid "can't change the %s attribute" +#~ msgstr "no puede modificar el atributo %s" + +#~ msgid "can't change this relation" +#~ msgstr "no puede modificar esta relación" + +#~ msgid "incorrect value (%(value)s) for type \"%(type)s\"" +#~ msgstr "valor %(value)s incorrecto para el tipo \"%(type)s\"" + +#~ msgid "invalid value %(value)s, it must be one of %(choices)s" +#~ msgstr "Valor %(value)s incorrecto, debe estar entre %(choices)s" + +#~ msgid "unknown source type" +#~ msgstr "tipo de fuente desconocida" diff -r 7812093e21f7 -r ab57000bff7b i18n/fr.po --- a/i18n/fr.po Thu Jan 17 17:12:06 2013 +0100 +++ b/i18n/fr.po Thu Jan 17 18:30:08 2013 +0100 @@ -50,6 +50,15 @@ "\"role=subject\" ou \"role=object\" doit être specifié dans les options" #, python-format +msgid "%(KEY-cstr)s constraint failed for value %(KEY-value)r" +msgstr "la valeur %(KEY-value)r ne satisfait pas la contrainte %(KEY-cstr)s" + +#, python-format +msgid "%(KEY-value)r doesn't match the %(KEY-regexp)r regular expression" +msgstr "" +"%(KEY-value)r ne correspond pas à l'expression régulière %(KEY-regexp)r" + +#, python-format msgid "%(attr)s set to %(newvalue)s" msgstr "%(attr)s modifié à %(newvalue)s" @@ -58,10 +67,6 @@ msgstr "%(attr)s modifié de %(oldvalue)s à %(newvalue)s" #, python-format -msgid "%(cstr)s constraint failed for value %(value)r" -msgstr "la valeur %(value)r ne satisfait pas la contrainte %(cstr)s" - -#, python-format msgid "%(etype)s by %(author)s" msgstr "%(etype)s par %(author)s" @@ -74,10 +79,6 @@ msgstr "%(subject)s %(etype)s #%(eid)s (%(login)s)" #, python-format -msgid "%(value)r doesn't match the %(regexp)r regular expression" -msgstr "%(value)r ne correspond pas à l'expression régulière %(regexp)r" - -#, python-format msgid "%d days" msgstr "%d jours" @@ -427,6 +428,11 @@ msgid "Click to sort on this column" msgstr "Cliquer pour trier sur cette colonne" +msgid "" +"Configuration of the system source goes to the 'sources' file, not in the " +"database" +msgstr "" + #, python-format msgid "Created %(etype)s : %(entity)s" msgstr "Entité %(etype)s crée : %(entity)s" @@ -907,6 +913,9 @@ msgid "UniqueConstraint" msgstr "contrainte d'unicité" +msgid "Unknown source type" +msgstr "Type de source inconnue" + msgid "Unreachable objects" msgstr "Objets inaccessibles" @@ -967,6 +976,15 @@ "Vous pouvez utiliser n'importe quelle substitution parmi la liste suivante " "dans le contenu de votre courriel." +msgid "You can't change this relation" +msgstr "Vous ne pouvez pas modifier cette relation" + +msgid "You cannot remove the system source" +msgstr "Vous ne pouvez pas supprimer la source système" + +msgid "You cannot rename the system source" +msgstr "Vous ne pouvez pas renommer la source système" + msgid "" "You have no access to this view or it can not be used to display the current " "data." @@ -1018,9 +1036,6 @@ msgid "abstract base class for transitions" msgstr "classe de base abstraite pour les transitions" -msgid "action menu" -msgstr "actions" - msgid "action(s) on this selection" msgstr "action(s) sur cette sélection" @@ -1260,7 +1275,7 @@ msgstr "anonyme" msgid "anyrsetview" -msgstr "vues \"tous les rset\"" +msgstr "vues pour tout rset" msgid "april" msgstr "avril" @@ -1311,7 +1326,7 @@ msgstr "mauvaise valeur" msgid "badly formatted url" -msgstr "" +msgstr "URL mal formattée" msgid "base url" msgstr "url de base" @@ -1399,7 +1414,7 @@ msgstr "impossible d'interpréter les types d'entités :" msgid "can only have one url" -msgstr "" +msgstr "ne supporte qu'une seule URL" msgid "can't be changed" msgstr "ne peut-être modifié" @@ -1407,12 +1422,8 @@ msgid "can't be deleted" msgstr "ne peut-être supprimé" -#, python-format -msgid "can't change the %s attribute" -msgstr "ne peut changer l'attribut %s" - -msgid "can't change this relation" -msgstr "ne peut modifier cette relation" +msgid "can't change this attribute" +msgstr "cet attribut ne peut pas être modifié" #, python-format msgid "can't connect to source %s, some data may be missing" @@ -2621,6 +2632,9 @@ msgid "foaf" msgstr "foaf" +msgid "focus on this selection" +msgstr "afficher cette sélection" + msgid "follow" msgstr "suivre le lien" @@ -2909,8 +2923,8 @@ msgstr "valeur de captcha incorrecte" #, python-format -msgid "incorrect value (%(value)s) for type \"%(type)s\"" -msgstr "valeur %(value)s incorrecte pour le type \"%(type)s\"" +msgid "incorrect value (%(KEY-value)r) for type \"%(KEY-type)s\"" +msgstr "la valeur %(KEY-value)s est incorrecte pour le type \"%(KEY-type)s\"" msgid "index this attribute's value in the plain text index" msgstr "indexer la valeur de cet attribut dans l'index plein texte" @@ -2989,8 +3003,9 @@ msgstr "action %r invalide" #, python-format -msgid "invalid value %(value)s, it must be one of %(choices)s" -msgstr "valeur %(value)s incorrect, doit être parmi %(choices)s" +msgid "invalid value %(KEY-value)s, it must be one of %(KEY-choices)s" +msgstr "" +"la valeur %(KEY-value)s est incorrecte, elle doit être parmi %(KEY-choices)s" msgid "is" msgstr "de type" @@ -4235,7 +4250,10 @@ msgstr "à faire par" msgid "toggle check boxes" -msgstr "inverser les cases à cocher" +msgstr "afficher/masquer les cases à cocher" + +msgid "toggle filter" +msgstr "afficher/masquer le filtre" msgid "tr_count" msgstr "n° de transition" @@ -4379,14 +4397,11 @@ msgid "unknown property key %s" msgstr "clé de propriété inconnue : %s" -msgid "unknown source type" -msgstr "type de source inconnu" - msgid "unknown vocabulary:" msgstr "vocabulaire inconnu : " msgid "unsupported protocol" -msgstr "" +msgstr "protocole non supporté" msgid "upassword" msgstr "mot de passe" @@ -4525,27 +4540,23 @@ msgstr "valeur" #, python-format -msgid "value %(value)s must be %(op)s %(boundary)s" -msgstr "la valeur %(value)s doit être %(op)s %(boundary)s" - -#, python-format -msgid "value %(value)s must be <= %(boundary)s" -msgstr "la valeur %(value)s doit être <= %(boundary)s" - -#, python-format -msgid "value %(value)s must be >= %(boundary)s" -msgstr "la valeur %(value)s doit être >= %(boundary)s" +msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s" +msgstr "la valeur %(KEY-value)s n'est pas %(KEY-op)s %(KEY-boundary)s" msgid "value associated to this key is not editable manually" msgstr "la valeur associée à cette clé n'est pas éditable manuellement" #, python-format -msgid "value should have maximum size of %s but found %s" -msgstr "la taille maximum est %s mais cette valeur est de taille %s" +msgid "value should have maximum size of %(KEY-max)s but found %(KEY-size)s" +msgstr "" +"la taille maximum est %(KEY-max)s mais cette valeur est de taille " +"%(KEY-size)s" #, python-format -msgid "value should have minimum size of %s but found %s" -msgstr "la taille minimum est %s mais cette valeur est de taille %s" +msgid "value should have minimum size of %(KEY-min)s but found %(KEY-size)s" +msgstr "" +"la taille minimum est %(KEY-min)s mais cette valeur est de taille " +"%(KEY-size)s" msgid "vcard" msgstr "vcard" @@ -4699,6 +4710,9 @@ #~ msgid "day" #~ msgstr "jour" +#~ msgid "jump to selection" +#~ msgstr "afficher cette sélection" + #~ msgid "log out first" #~ msgstr "déconnecter vous d'abord" diff -r 7812093e21f7 -r ab57000bff7b misc/migration/3.10.0_Any.py --- a/misc/migration/3.10.0_Any.py Thu Jan 17 17:12:06 2013 +0100 +++ b/misc/migration/3.10.0_Any.py Thu Jan 17 18:30:08 2013 +0100 @@ -34,5 +34,5 @@ for x in rql('Any X,XK WHERE X pkey XK, ' 'X pkey ~= "boxes.%" OR ' 'X pkey ~= "contentnavigation.%"').entities(): - x.set_attributes(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1]) + x.cw_set(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1]) diff -r 7812093e21f7 -r ab57000bff7b misc/migration/3.11.0_Any.py --- a/misc/migration/3.11.0_Any.py Thu Jan 17 17:12:06 2013 +0100 +++ b/misc/migration/3.11.0_Any.py Thu Jan 17 18:30:08 2013 +0100 @@ -81,5 +81,5 @@ rset = session.execute('Any V WHERE X is CWProperty, X value V, X pkey %(k)s', {'k': pkey}) timestamp = int(rset[0][0]) - sourceentity.set_attributes(latest_retrieval=datetime.fromtimestamp(timestamp)) + sourceentity.cw_set(latest_retrieval=datetime.fromtimestamp(timestamp)) session.execute('DELETE CWProperty X WHERE X pkey %(k)s', {'k': pkey}) diff -r 7812093e21f7 -r ab57000bff7b misc/migration/3.14.0_Any.py --- a/misc/migration/3.14.0_Any.py Thu Jan 17 17:12:06 2013 +0100 +++ b/misc/migration/3.14.0_Any.py Thu Jan 17 18:30:08 2013 +0100 @@ -9,5 +9,5 @@ expression = rqlcstr.value mainvars = guess_rrqlexpr_mainvars(expression) yamscstr = CONSTRAINTS[rqlcstr.type](expression, mainvars) - rqlcstr.set_attributes(value=yamscstr.serialize()) + rqlcstr.cw_set(value=yamscstr.serialize()) print 'updated', rqlcstr.type, rqlcstr.value.strip() diff -r 7812093e21f7 -r ab57000bff7b misc/migration/3.15.0_Any.py --- a/misc/migration/3.15.0_Any.py Thu Jan 17 17:12:06 2013 +0100 +++ b/misc/migration/3.15.0_Any.py Thu Jan 17 18:30:08 2013 +0100 @@ -4,7 +4,7 @@ config = source.dictconfig host = config.pop('host', u'ldap') protocol = config.pop('protocol', u'ldap') - source.set_attributes(url=u'%s://%s' % (protocol, host)) + source.cw_set(url=u'%s://%s' % (protocol, host)) source.update_config(skip_unknown=True, **config) commit() diff -r 7812093e21f7 -r ab57000bff7b misc/migration/postcreate.py --- a/misc/migration/postcreate.py Thu Jan 17 17:12:06 2013 +0100 +++ b/misc/migration/postcreate.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b misc/scripts/chpasswd.py --- a/misc/scripts/chpasswd.py Thu Jan 17 17:12:06 2013 +0100 +++ b/misc/scripts/chpasswd.py Thu Jan 17 18:30:08 2013 +0100 @@ -42,7 +42,7 @@ crypted = crypt_password(pass1) cwuser = rset.get_entity(0,0) -cwuser.set_attributes(upassword=Binary(crypted)) +cwuser.cw_set(upassword=Binary(crypted)) commit() print("password updated.") diff -r 7812093e21f7 -r ab57000bff7b misc/scripts/ldapuser2ldapfeed.py --- a/misc/scripts/ldapuser2ldapfeed.py Thu Jan 17 17:12:06 2013 +0100 +++ b/misc/scripts/ldapuser2ldapfeed.py Thu Jan 17 18:30:08 2013 +0100 @@ -87,7 +87,7 @@ source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0) -source_ent.set_attributes(type=u"ldapfeed", parser=u"ldapfeed") +source_ent.cw_set(type=u"ldapfeed", parser=u"ldapfeed") if raw_input('Commit ?') in 'yY': diff -r 7812093e21f7 -r ab57000bff7b predicates.py --- a/predicates.py Thu Jan 17 17:12:06 2013 +0100 +++ b/predicates.py Thu Jan 17 18:30:08 2013 +0100 @@ -737,12 +737,16 @@ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity class lookup / score rules according to the input context. - .. note:: when interface is an entity class, the score will reflect class - proximity so the most specific object will be selected. + .. note:: + + when interface is an entity class, the score will reflect class + proximity so the most specific object will be selected. - .. note:: deprecated in cubicweb >= 3.9, use either - :class:`~cubicweb.predicates.is_instance` or - :class:`~cubicweb.predicates.adaptable`. + .. note:: + + deprecated in cubicweb >= 3.9, use either + :class:`~cubicweb.predicates.is_instance` or + :class:`~cubicweb.predicates.adaptable`. """ def __init__(self, *expected_ifaces, **kwargs): diff -r 7812093e21f7 -r ab57000bff7b req.py --- a/req.py Thu Jan 17 17:12:06 2013 +0100 +++ b/req.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -62,6 +62,8 @@ :attribute vreg.schema: the instance's schema :attribute vreg.config: the instance's configuration """ + is_request = True # False for repository session + def __init__(self, vreg): self.vreg = vreg try: @@ -75,6 +77,17 @@ self.local_perm_cache = {} self._ = unicode + def set_language(self, lang): + """install i18n configuration for `lang` translation. + + Raises :exc:`KeyError` if translation doesn't exist. + """ + self.lang = lang + gettext, pgettext = self.vreg.config.translations[lang] + # use _cw.__ to translate a message without registering it to the catalog + self._ = self.__ = gettext + self.pgettext = pgettext + def get_option_value(self, option, foreid=None): raise NotImplementedError @@ -308,21 +321,12 @@ def user_data(self): """returns a dictionary with this user's information""" userinfo = {} - if self.is_internal_session: - userinfo['login'] = "cubicweb" - userinfo['name'] = "cubicweb" - userinfo['email'] = "" - return userinfo user = self.user userinfo['login'] = user.login userinfo['name'] = user.name() userinfo['email'] = user.cw_adapt_to('IEmailable').get_email() return userinfo - def is_internal_session(self): - """overrided on the server-side""" - return False - # formating methods ####################################################### def view(self, __vid, rset=None, __fallback_oid=None, __registry='views', @@ -426,7 +430,7 @@ """return the root url of the instance """ if secure: - raise NotImplementedError() + return self.vreg.config.get('https-url', self.vreg.config['base-url']) return self.vreg.config['base-url'] # abstract methods to override according to the web front-end ############# diff -r 7812093e21f7 -r ab57000bff7b schema.py --- a/schema.py Thu Jan 17 17:12:06 2013 +0100 +++ b/schema.py Thu Jan 17 18:30:08 2013 +0100 @@ -261,30 +261,34 @@ return self.has_local_role(action) or self.has_perm(req, action) PermissionMixIn.may_have_permission = may_have_permission -def has_perm(self, session, action, **kwargs): +def has_perm(self, _cw, action, **kwargs): """return true if the action is granted globaly or localy""" try: - self.check_perm(session, action, **kwargs) + self.check_perm(_cw, action, **kwargs) return True except Unauthorized: return False PermissionMixIn.has_perm = has_perm -def check_perm(self, session, action, **kwargs): - # NB: session may be a server session or a request object check user is - # in an allowed group, if so that's enough internal sessions should - # always stop there +def check_perm(self, _cw, action, **kwargs): + # NB: _cw may be a server transaction or a request object. + # + # check user is in an allowed group, if so that's enough internal + # transactions should always stop there groups = self.get_groups(action) - if session.user.matching_groups(groups): + if _cw.user.matching_groups(groups): return # if 'owners' in allowed groups, check if the user actually owns this # object, if so that's enough + # + # NB: give _cw to user.owns since user is not be bound to a transaction on + # the repository side if 'owners' in groups and ( kwargs.get('creating') - or ('eid' in kwargs and session.user.owns(kwargs['eid']))): + or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))): return # else if there is some rql expressions, check them - if any(rqlexpr.check(session, **kwargs) + if any(rqlexpr.check(_cw, **kwargs) for rqlexpr in self.get_rqlexprs(action)): return raise Unauthorized(action, str(self)) @@ -467,45 +471,45 @@ return True return False - def has_perm(self, session, action, **kwargs): + def has_perm(self, _cw, action, **kwargs): """return true if the action is granted globaly or localy""" if self.final: assert not ('fromeid' in kwargs or 'toeid' in kwargs), kwargs assert action in ('read', 'update') if 'eid' in kwargs: - subjtype = session.describe(kwargs['eid'])[0] + subjtype = _cw.describe(kwargs['eid'])[0] else: subjtype = objtype = None else: assert not 'eid' in kwargs, kwargs assert action in ('read', 'add', 'delete') if 'fromeid' in kwargs: - subjtype = session.describe(kwargs['fromeid'])[0] + subjtype = _cw.describe(kwargs['fromeid'])[0] elif 'frometype' in kwargs: subjtype = kwargs.pop('frometype') else: subjtype = None if 'toeid' in kwargs: - objtype = session.describe(kwargs['toeid'])[0] + objtype = _cw.describe(kwargs['toeid'])[0] elif 'toetype' in kwargs: objtype = kwargs.pop('toetype') else: objtype = None if objtype and subjtype: - return self.rdef(subjtype, objtype).has_perm(session, action, **kwargs) + return self.rdef(subjtype, objtype).has_perm(_cw, action, **kwargs) elif subjtype: for tschema in self.targets(subjtype, 'subject'): rdef = self.rdef(subjtype, tschema) - if not rdef.has_perm(session, action, **kwargs): + if not rdef.has_perm(_cw, action, **kwargs): return False elif objtype: for tschema in self.targets(objtype, 'object'): rdef = self.rdef(tschema, objtype) - if not rdef.has_perm(session, action, **kwargs): + if not rdef.has_perm(_cw, action, **kwargs): return False else: for rdef in self.rdefs.itervalues(): - if not rdef.has_perm(session, action, **kwargs): + if not rdef.has_perm(_cw, action, **kwargs): return False return True @@ -754,17 +758,17 @@ return rql, found, keyarg return rqlst.as_string(), None, None - def _check(self, session, **kwargs): + def _check(self, _cw, **kwargs): """return True if the rql expression is matching the given relation between fromeid and toeid - session may actually be a request as well + _cw may be a request or a server side transaction """ creating = kwargs.get('creating') if not creating and self.eid is not None: key = (self.eid, tuple(sorted(kwargs.iteritems()))) try: - return session.local_perm_cache[key] + return _cw.local_perm_cache[key] except KeyError: pass rql, has_perm_defs, keyarg = self.transform_has_permission() @@ -772,50 +776,50 @@ if creating and 'X' in self.rqlst.defined_vars: return True if keyarg is None: - kwargs.setdefault('u', session.user.eid) + kwargs.setdefault('u', _cw.user.eid) try: - rset = session.execute(rql, kwargs, build_descr=True) + rset = _cw.execute(rql, kwargs, build_descr=True) except NotImplementedError: self.critical('cant check rql expression, unsupported rql %s', rql) if self.eid is not None: - session.local_perm_cache[key] = False + _cw.local_perm_cache[key] = False return False except TypeResolverException, ex: # some expression may not be resolvable with current kwargs # (type conflict) self.warning('%s: %s', rql, str(ex)) if self.eid is not None: - session.local_perm_cache[key] = False + _cw.local_perm_cache[key] = False return False except Unauthorized, ex: self.debug('unauthorized %s: %s', rql, str(ex)) if self.eid is not None: - session.local_perm_cache[key] = False + _cw.local_perm_cache[key] = False return False else: - rset = session.eid_rset(kwargs[keyarg]) + rset = _cw.eid_rset(kwargs[keyarg]) # if no special has_*_permission relation in the rql expression, just # check the result set contains something if has_perm_defs is None: if rset: if self.eid is not None: - session.local_perm_cache[key] = True + _cw.local_perm_cache[key] = True return True elif rset: # check every special has_*_permission relation is satisfied - get_eschema = session.vreg.schema.eschema + get_eschema = _cw.vreg.schema.eschema try: for eaction, col in has_perm_defs: for i in xrange(len(rset)): eschema = get_eschema(rset.description[i][col]) - eschema.check_perm(session, eaction, eid=rset[i][col]) + eschema.check_perm(_cw, eaction, eid=rset[i][col]) if self.eid is not None: - session.local_perm_cache[key] = True + _cw.local_perm_cache[key] = True return True except Unauthorized: pass if self.eid is not None: - session.local_perm_cache[key] = False + _cw.local_perm_cache[key] = False return False @property @@ -843,15 +847,15 @@ rql += ', U eid %(u)s' return rql - def check(self, session, eid=None, creating=False, **kwargs): + def check(self, _cw, eid=None, creating=False, **kwargs): if 'X' in self.rqlst.defined_vars: if eid is None: if creating: - return self._check(session, creating=True, **kwargs) + return self._check(_cw, creating=True, **kwargs) return False assert creating == False - return self._check(session, x=eid, **kwargs) - return self._check(session, **kwargs) + return self._check(_cw, x=eid, **kwargs) + return self._check(_cw, **kwargs) def vargraph(rqlst): @@ -904,7 +908,7 @@ rql += ', U eid %(u)s' return rql - def check(self, session, fromeid=None, toeid=None): + def check(self, _cw, fromeid=None, toeid=None): kwargs = {} if 'S' in self.rqlst.defined_vars: if fromeid is None: @@ -914,7 +918,7 @@ if toeid is None: return False kwargs['o'] = toeid - return self._check(session, **kwargs) + return self._check(_cw, **kwargs) # in yams, default 'update' perm for attributes granted to managers and owners. @@ -1024,7 +1028,7 @@ 'expression': self.expression} raise ValidationError(maineid, {qname: msg}) - def exec_query(self, session, eidfrom, eidto): + def exec_query(self, _cw, eidfrom, eidto): if eidto is None: # checking constraint for an attribute relation expression = 'S eid %(s)s, ' + self.expression @@ -1034,11 +1038,11 @@ args = {'s': eidfrom, 'o': eidto} if 'U' in self.rqlst.defined_vars: expression = 'U eid %(u)s, ' + expression - args['u'] = session.user.eid + args['u'] = _cw.user.eid rql = 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), expression) if self.distinct_query: rql = 'DISTINCT ' + rql - return session.execute(rql, args, build_descr=False) + return _cw.execute(rql, args, build_descr=False) class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint): @@ -1061,7 +1065,7 @@ """ # XXX turns mainvars into a required argument in __init__ distinct_query = True - + def match_condition(self, session, eidfrom, eidto): return len(self.exec_query(session, eidfrom, eidto)) <= 1 diff -r 7812093e21f7 -r ab57000bff7b selectors.py diff -r 7812093e21f7 -r ab57000bff7b server/__init__.py --- a/server/__init__.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/__init__.py Thu Jan 17 18:30:08 2013 +0100 @@ -77,10 +77,14 @@ DBG_REPO = 4 #: multi-sources DBG_MS = 8 +#: hooks +DBG_HOOKS = 16 +#: operations +DBG_OPS = 32 #: more verbosity -DBG_MORE = 16 +DBG_MORE = 64 #: all level enabled -DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_MORE +DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_MORE #: current debug mode DEBUG = 0 @@ -165,6 +169,8 @@ # on connection config.creating = True config.consider_user_state = False + config.cubicweb_appobject_path = set(('hooks', 'entities')) + config.cube_appobject_path = set(('hooks', 'entities')) # only enable the system source at initialization time repo = Repository(config, vreg=vreg) schema = repo.schema @@ -224,10 +230,6 @@ config._cubes = None # avoid assertion error repo, cnx = in_memory_repo_cnx(config, login, password=pwd) repo.system_source.eid = ssource.eid # redo this manually - # trigger vreg initialisation of entity classes - config.cubicweb_appobject_path = set(('entities',)) - config.cube_appobject_path = set(('entities',)) - repo.vreg.set_schema(repo.schema) assert len(repo.sources) == 1, repo.sources handler = config.migration_handler(schema, interactive=False, cnx=cnx, repo=repo) @@ -246,20 +248,21 @@ # restore initial configuration config.creating = False config.consider_user_state = True + # (drop instance attribute to get back to class attribute) + del config.cubicweb_appobject_path + del config.cube_appobject_path print '-> database for instance %s initialized.' % config.appid def initialize_schema(config, schema, mhandler, event='create'): from cubicweb.server.schemaserial import serialize_schema - from cubicweb.server.session import hooks_control session = mhandler.session cubes = config.cubes() # deactivate every hooks but those responsible to set metadata # so, NO INTEGRITY CHECKS are done, to have quicker db creation. # Active integrity is kept else we may pb such as two default # workflows for one entity type. - with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata', - 'activeintegrity'): + with session.deny_all_hooks_but('metadata', 'activeintegrity'): # execute cubicweb's pre script mhandler.cmd_exec_event_script('pre%s' % event) # execute cubes pre script if any diff -r 7812093e21f7 -r ab57000bff7b server/checkintegrity.py --- a/server/checkintegrity.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/checkintegrity.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -32,7 +32,6 @@ from cubicweb.schema import PURE_VIRTUAL_RTYPES, VIRTUAL_RTYPES from cubicweb.server.sqlutils import SQL_PREFIX -from cubicweb.server.session import security_enabled def notify_fixed(fix): if fix: @@ -394,18 +393,18 @@ # yo, launch checks if checks: eids_cache = {} - with security_enabled(session, read=False, write=False): # ensure no read security + with session.security_enabled(read=False, write=False): # ensure no read security for check in checks: check_func = globals()['check_%s' % check] check_func(repo.schema, session, eids_cache, fix=fix) if fix: - cnx.commit() + session.commit() else: print if not fix: print 'WARNING: Diagnostic run, nothing has been corrected' if reindex: - cnx.rollback() + session.rollback() session.set_cnxset() reindex_entities(repo.schema, session, withpb=withpb) - cnx.commit() + session.commit() diff -r 7812093e21f7 -r ab57000bff7b server/edition.py --- a/server/edition.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/edition.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -103,6 +103,8 @@ assert not self.saved, 'too late to modify edited attributes' super(EditedEntity, self).__setitem__(attr, value) self.entity.cw_attr_cache[attr] = value + # mark attribute as needing purge by the client + self.entity._cw_dont_cache_attribute(attr) def oldnewvalue(self, attr): """returns the couple (old attr value, new attr value) @@ -141,8 +143,7 @@ for rtype in self] try: entity.e_schema.check(dict_protocol_catcher(entity), - creation=creation, _=entity._cw._, - relations=relations) + creation=creation, relations=relations) except ValidationError, ex: ex.entity = self.entity raise diff -r 7812093e21f7 -r ab57000bff7b server/hook.py --- a/server/hook.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/hook.py Thu Jan 17 18:30:08 2013 +0100 @@ -152,7 +152,7 @@ On those events, the entity has no `cw_edited` dictionary. -.. note:: `self.entity.set_attributes(age=42)` will set the `age` attribute to +.. note:: `self.entity.cw_set(age=42)` will set the `age` attribute to 42. But to do so, it will generate a rql query that will have to be processed, hence may trigger some hooks, etc. This could lead to infinitely looping hooks. @@ -197,14 +197,12 @@ ~~~~~~~~~~~~~ It is sometimes convenient to explicitly enable or disable some hooks. For -instance if you want to disable some integrity checking hook. This can be +instance if you want to disable some integrity checking hook. This can be controlled more finely through the `category` class attribute, which is a string giving a category name. One can then uses the -:class:`~cubicweb.server.session.hooks_control` context manager to explicitly -enable or disable some categories. - -.. autoclass:: cubicweb.server.session.hooks_control - +:meth:`~cubicweb.server.session.Session.deny_all_hooks_but` and +:meth:`~cubicweb.server.session.Session.allow_all_hooks_but` context managers to +explicitly enable or disable some categories. The existing categories are: @@ -230,10 +228,8 @@ * ``bookmark``, bookmark entities handling hooks -Nothing precludes one to invent new categories and use the -:class:`~cubicweb.server.session.hooks_control` context manager to -filter them in or out. Note that ending the transaction with commit() -or rollback() will restore the hooks. +Nothing precludes one to invent new categories and use existing mechanisms to +filter them in or out. Hooks specific predicates @@ -264,11 +260,10 @@ from logilab.common.registry import (Predicate, NotPredicate, OrPredicate, classid, objectify_predicate, yes) -from cubicweb import RegistryNotFound +from cubicweb import RegistryNotFound, server from cubicweb.cwvreg import CWRegistry, CWRegistryStore from cubicweb.predicates import ExpectedValuePredicate, is_instance from cubicweb.appobject import AppObject -from cubicweb.server.session import security_enabled ENTITIES_HOOKS = set(('before_add_entity', 'after_add_entity', 'before_update_entity', 'after_update_entity', @@ -326,12 +321,15 @@ pruned = self.get_pruned_hooks(session, event, entities, eids_from_to, kwargs) # by default, hooks are executed with security turned off - with security_enabled(session, read=False): + with session.security_enabled(read=False): for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs): hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs), key=lambda x: x.order) - with security_enabled(session, write=False): + debug = server.DEBUG & server.DBG_HOOKS + with session.security_enabled(write=False): for hook in hooks: + if debug: + print event, _kwargs, hook hook() def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs): diff -r 7812093e21f7 -r ab57000bff7b server/migractions.py --- a/server/migractions.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/migractions.py Thu Jan 17 18:30:08 2013 +0100 @@ -1321,7 +1321,7 @@ except Exception: self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value) else: - prop.set_attributes(value=value) + prop.cw_set(value=value) # other data migration commands ########################################### diff -r 7812093e21f7 -r ab57000bff7b server/pool.py --- a/server/pool.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/pool.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b server/querier.py --- a/server/querier.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/querier.py Thu Jan 17 18:30:08 2013 +0100 @@ -26,21 +26,26 @@ from itertools import repeat from logilab.common.compat import any -from rql import RQLSyntaxError +from rql import RQLSyntaxError, CoercionError from rql.stmts import Union, Select +from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj from rql.nodes import (Relation, VariableRef, Constant, SubQuery, Function, Exists, Not) +from yams import BASE_TYPES from cubicweb import ValidationError, Unauthorized, QueryError, UnknownEid -from cubicweb import server, typed_eid +from cubicweb import Binary, server, typed_eid from cubicweb.rset import ResultSet -from cubicweb.utils import QueryCache +from cubicweb.utils import QueryCache, RepeatList from cubicweb.server.utils import cleanup_solutions from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction from cubicweb.server.edition import EditedEntity -from cubicweb.server.session import security_enabled + + +ETYPE_PYOBJ_MAP[Binary] = 'Bytes' + def empty_rset(rql, args, rqlst=None): """build an empty result set object""" @@ -256,7 +261,7 @@ cached = True else: noinvariant = set() - with security_enabled(self.session, read=False): + with self.session.security_enabled(read=False): self._insert_security(union, noinvariant) if key is not None: self.session.transaction_data[key] = (union, self.args) @@ -751,14 +756,22 @@ if build_descr: if rqlst.TYPE == 'select': # sample selection - descr = session.build_description(orig_rqlst, args, results) + if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1: + # easy, all lines are identical + selected = rqlst.children[0].selection + solution = rqlst.children[0].solutions[0] + description = _make_description(selected, args, solution) + descr = RepeatList(len(results), tuple(description)) + else: + # hard, delegate the work :o) + descr = manual_build_descr(session, rqlst, args, results) elif rqlst.TYPE == 'insert': # on insert plan, some entities may have been auto-casted, # so compute description manually even if there is only # one solution basedescr = [None] * len(plan.selected) todetermine = zip(xrange(len(plan.selected)), repeat(False)) - descr = session._build_descr(results, basedescr, todetermine) + descr = _build_descr(session, results, basedescr, todetermine) # FIXME: get number of affected entities / relations on non # selection queries ? # return a result set object @@ -772,3 +785,77 @@ from cubicweb import set_log_methods LOGGER = getLogger('cubicweb.querier') set_log_methods(QuerierHelper, LOGGER) + + +def manual_build_descr(tx, rqlst, args, result): + """build a description for a given result by analysing each row + + XXX could probably be done more efficiently during execution of query + """ + # not so easy, looks for variable which changes from one solution + # to another + unstables = rqlst.get_variable_indices() + basedescr = [] + todetermine = [] + for i in xrange(len(rqlst.children[0].selection)): + ttype = _selection_idx_type(i, rqlst, args) + if ttype is None or ttype == 'Any': + ttype = None + isfinal = True + else: + isfinal = ttype in BASE_TYPES + if ttype is None or i in unstables: + basedescr.append(None) + todetermine.append( (i, isfinal) ) + else: + basedescr.append(ttype) + if not todetermine: + return RepeatList(len(result), tuple(basedescr)) + return _build_descr(tx, result, basedescr, todetermine) + +def _build_descr(tx, result, basedescription, todetermine): + description = [] + etype_from_eid = tx.describe + todel = [] + for i, row in enumerate(result): + row_descr = basedescription[:] + for index, isfinal in todetermine: + value = row[index] + if value is None: + # None value inserted by an outer join, no type + row_descr[index] = None + continue + if isfinal: + row_descr[index] = etype_from_pyobj(value) + else: + try: + row_descr[index] = etype_from_eid(value)[0] + except UnknownEid: + tx.error('wrong eid %s in repository, you should ' + 'db-check the database' % value) + todel.append(i) + break + else: + description.append(tuple(row_descr)) + for i in reversed(todel): + del result[i] + return description + +def _make_description(selected, args, solution): + """return a description for a result set""" + description = [] + for term in selected: + description.append(term.get_type(solution, args)) + return description + +def _selection_idx_type(i, rqlst, args): + """try to return type of term at index `i` of the rqlst's selection""" + for select in rqlst.children: + term = select.selection[i] + for solution in select.solutions: + try: + ttype = term.get_type(solution, args) + if ttype is not None: + return ttype + except CoercionError: + return None diff -r 7812093e21f7 -r ab57000bff7b server/repository.py --- a/server/repository.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/repository.py Thu Jan 17 18:30:08 2013 +0100 @@ -56,8 +56,7 @@ RepositoryError, UniqueTogetherError, typed_eid, onevent) from cubicweb import cwvreg, schema, server from cubicweb.server import ShuttingDown, utils, hook, pool, querier, sources -from cubicweb.server.session import Session, InternalSession, InternalManager, \ - security_enabled +from cubicweb.server.session import Session, InternalSession, InternalManager from cubicweb.server.ssplanner import EditedEntity NO_CACHE_RELATIONS = set( [('owned_by', 'object'), @@ -109,12 +108,12 @@ # * we don't want read permissions to be applied but we want delete # permission to be checked if card[0] in '1?': - with security_enabled(session, read=False): + with session.security_enabled(read=False): session.execute('DELETE X %s Y WHERE X eid %%(x)s, ' 'NOT Y eid %%(y)s' % rtype, {'x': eidfrom, 'y': eidto}) if card[1] in '1?': - with security_enabled(session, read=False): + with session.security_enabled(read=False): session.execute('DELETE X %s Y WHERE Y eid %%(y)s, ' 'NOT X eid %%(x)s' % rtype, {'x': eidfrom, 'y': eidto}) @@ -135,7 +134,7 @@ session.update_rel_cache_add(entity.eid, attr, value) rdef = session.rtype_eids_rdef(attr, entity.eid, value) if rdef.cardinality[1] in '1?' and activeintegrity: - with security_enabled(session, read=False): + with session.security_enabled(read=False): session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr, {'x': entity.eid, 'y': value}) return relations @@ -218,33 +217,21 @@ # information (eg dump/restore/...) config._cubes = () # only load hooks and entity classes in the registry - config.__class__.cube_appobject_path = set(('hooks', 'entities')) - config.__class__.cubicweb_appobject_path = set(('hooks', 'entities')) + config.cube_appobject_path = set(('hooks', 'entities')) + config.cubicweb_appobject_path = set(('hooks', 'entities')) self.set_schema(config.load_schema()) config['connections-pool-size'] = 1 # will be reinitialized later from cubes found in the database config._cubes = None - elif config.creating: - # repository creation + elif config.creating or not config.read_instance_schema: + if not config.creating: + # test start: use the file system schema (quicker) + self.warning("set fs instance'schema") config.bootstrap_cubes() - self.set_schema(config.load_schema(), resetvreg=False) - # need to load the Any and CWUser entity types - etdirectory = join(CW_SOFTWARE_ROOT, 'entities') - self.vreg.init_registration([etdirectory]) - for modname in ('__init__', 'authobjs', 'wfobjs'): - self.vreg.load_file(join(etdirectory, '%s.py' % modname), - 'cubicweb.entities.%s' % modname) - hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks') - self.vreg.load_file(join(hooksdirectory, 'metadata.py'), - 'cubicweb.hooks.metadata') - elif config.read_instance_schema: + self.set_schema(config.load_schema()) + else: # normal start: load the instance schema from the database self.fill_schema() - else: - # test start: use the file system schema (quicker) - self.warning("set fs instance'schema") - config.bootstrap_cubes() - self.set_schema(config.load_schema()) if not config.creating: self.init_sources_from_database() if 'CWProperty' in self.schema: @@ -767,10 +754,10 @@ raise `AuthenticationError` if the authentication failed raise `ConnectionError` if we can't open a connection """ + cnxprops = kwargs.pop('cnxprops', None) # use an internal connection with self.internal_session() as session: # try to get a user object - cnxprops = kwargs.pop('cnxprops', None) user = self.authenticate_user(session, login, **kwargs) session = Session(user, self, cnxprops) user._cw = user.cw_rset.req = session @@ -921,22 +908,9 @@ * update user information on each user's request (i.e. groups and custom properties) """ - session = self._get_session(sessionid, setcnxset=False) - if props is not None: - self.set_session_props(sessionid, props) - user = session.user + user = self._get_session(sessionid, setcnxset=False).user return user.eid, user.login, user.groups, user.properties - def set_session_props(self, sessionid, props): - """this method should be used by client to: - * check session id validity - * update user information on each user's request (i.e. groups and - custom properties) - """ - session = self._get_session(sessionid, setcnxset=False) - for prop, value in props.items(): - session.change_property(prop, value) - def undoable_transactions(self, sessionid, ueid=None, txid=None, **actionfilters): """See :class:`cubicweb.dbapi.Connection.undoable_transactions`""" @@ -1239,7 +1213,7 @@ source = self.sources_by_eid[scleanup] # delete remaining relations: if user can delete the entity, he can # delete all its relations without security checking - with security_enabled(session, read=False, write=False): + with session.security_enabled(read=False, write=False): eid = entity.eid for rschema, _, role in entity.e_schema.relation_definitions(): rtype = rschema.type @@ -1281,7 +1255,7 @@ source = self.sources_by_eid[scleanup] # delete remaining relations: if user can delete the entity, he can # delete all its relations without security checking - with security_enabled(session, read=False, write=False): + with session.security_enabled(read=False, write=False): in_eids = ','.join([str(_e.eid) for _e in entities]) for rschema, _, role in entities[0].e_schema.relation_definitions(): rtype = rschema.type @@ -1567,7 +1541,7 @@ rdef = session.rtype_eids_rdef(rtype, subjeid, objeid) card = rdef.cardinality if card[0] in '?1': - with security_enabled(session, read=False): + with session.security_enabled(read=False): session.execute('DELETE X %s Y WHERE X eid %%(x)s, ' 'NOT Y eid %%(y)s' % rtype, {'x': subjeid, 'y': objeid}) @@ -1578,7 +1552,7 @@ continue subjects[subjeid] = len(relations_by_rtype[rtype]) - 1 if card[1] in '?1': - with security_enabled(session, read=False): + with session.security_enabled(read=False): session.execute('DELETE X %s Y WHERE Y eid %%(y)s, ' 'NOT X eid %%(x)s' % rtype, {'x': subjeid, 'y': objeid}) diff -r 7812093e21f7 -r ab57000bff7b server/serverctl.py --- a/server/serverctl.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/serverctl.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b server/session.py --- a/server/session.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/session.py Thu Jan 17 18:30:08 2013 +0100 @@ -30,21 +30,16 @@ from logilab.common.deprecation import deprecated from logilab.common.textutils import unormalize from logilab.common.registry import objectify_predicate -from rql import CoercionError -from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj -from yams import BASE_TYPES -from cubicweb import Binary, UnknownEid, QueryError, schema +from cubicweb import UnknownEid, QueryError, schema, server from cubicweb.req import RequestSessionBase from cubicweb.dbapi import ConnectionProperties -from cubicweb.utils import make_uid, RepeatList +from cubicweb.utils import make_uid from cubicweb.rqlrewrite import RQLRewriter from cubicweb.server import ShuttingDown from cubicweb.server.edition import EditedEntity -ETYPE_PYOBJ_MAP[Binary] = 'Bytes' - NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy() NO_UNDO_TYPES.add('CWCache') # is / is_instance_of are usually added by sql hooks except when using @@ -55,25 +50,6 @@ NO_UNDO_TYPES.add('cw_source') # XXX rememberme,forgotpwd,apycot,vcsfile -def _make_description(selected, args, solution): - """return a description for a result set""" - description = [] - for term in selected: - description.append(term.get_type(solution, args)) - return description - -def selection_idx_type(i, rqlst, args): - """try to return type of term at index `i` of the rqlst's selection""" - for select in rqlst.children: - term = select.selection[i] - for solution in select.solutions: - try: - ttype = term.get_type(solution, args) - if ttype is not None: - return ttype - except CoercionError: - return None - @objectify_predicate def is_user_session(cls, req, **kwargs): """repository side only predicate returning 1 if the session is a regular @@ -132,6 +108,11 @@ with hooks_control(self.session, self.session.HOOKS_DENY_ALL, 'integrity'): # ... do stuff with none but 'integrity' hooks activated + + This is an internal api, you should rather use + :meth:`~cubicweb.server.session.Session.deny_all_hooks_but` or + :meth:`~cubicweb.server.session.Session.allow_all_hooks_but` session + methods. """ def __init__(self, session, mode, *categories): self.session = session @@ -241,7 +222,11 @@ :attr:`running_dbapi_query`, boolean flag telling if the executing query is coming from a dbapi connection or is a query from within the repository + + .. automethod:: cubicweb.server.session.deny_all_hooks_but + .. automethod:: cubicweb.server.session.all_all_hooks_but """ + is_request = False is_internal_session = False def __init__(self, user, repo, cnxprops=None, _id=None): @@ -264,7 +249,7 @@ # and the rql server self.data = {} # i18n initialization - self.set_language(cnxprops.lang) + self.set_language(user.prefered_language()) # internals self._tx_data = {} self.__threaddata = threading.local() @@ -464,28 +449,6 @@ self.cnxset.reconnect(source) return source.doexec(self, sql, args, rollback=rollback_on_failure) - def set_language(self, language): - """i18n configuration for translation""" - language = language or self.user.property_value('ui.language') - try: - gettext, pgettext = self.vreg.config.translations[language] - self._ = self.__ = gettext - self.pgettext = pgettext - except KeyError: - language = self.vreg.property_value('ui.language') - try: - gettext, pgettext = self.vreg.config.translations[language] - self._ = self.__ = gettext - self.pgettext = pgettext - except KeyError: - self._ = self.__ = unicode - self.pgettext = lambda x, y: y - self.lang = language - - def change_property(self, prop, value): - assert prop == 'lang' # this is the only one changeable property for now - self.set_language(value) - def deleted_in_transaction(self, eid): """return True if the entity of the given eid is being deleted in the current transaction @@ -509,7 +472,7 @@ DEFAULT_SECURITY = object() # evaluated to true by design - def security_enabled(self, read=False, write=False): + def security_enabled(self, read=None, write=None): return security_enabled(self, read=read, write=write) def init_security(self, read, write): @@ -976,16 +939,21 @@ # information: # - processed by the precommit/commit event or not # - if processed, is it the failed operation + debug = server.DEBUG & server.DBG_OPS try: # by default, operations are executed with security turned off with security_enabled(self, False, False): processed = [] self.commit_state = 'precommit' + if debug: + print self.commit_state, '*' * 20 try: while self.pending_operations: operation = self.pending_operations.pop(0) operation.processed = 'precommit' processed.append(operation) + if debug: + print operation operation.handle_event('precommit_event') self.pending_operations[:] = processed self.debug('precommit session %s done', self.id) @@ -1001,7 +969,11 @@ # instead of having to implements rollback, revertprecommit # and revertcommit, that will be enough in mont case. operation.failed = True + if debug: + print self.commit_state, '*' * 20 for operation in reversed(processed): + if debug: + print operation try: operation.handle_event('revertprecommit_event') except BaseException: @@ -1014,8 +986,12 @@ raise self.cnxset.commit() self.commit_state = 'postcommit' + if debug: + print self.commit_state, '*' * 20 while self.pending_operations: operation = self.pending_operations.pop(0) + if debug: + print operation operation.processed = 'postcommit' try: operation.handle_event('postcommit_event') @@ -1154,71 +1130,6 @@ self._threaddata._rewriter = RQLRewriter(self) return self._threaddata._rewriter - def build_description(self, rqlst, args, result): - """build a description for a given result""" - if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1: - # easy, all lines are identical - selected = rqlst.children[0].selection - solution = rqlst.children[0].solutions[0] - description = _make_description(selected, args, solution) - return RepeatList(len(result), tuple(description)) - # hard, delegate the work :o) - return self.manual_build_descr(rqlst, args, result) - - def manual_build_descr(self, rqlst, args, result): - """build a description for a given result by analysing each row - - XXX could probably be done more efficiently during execution of query - """ - # not so easy, looks for variable which changes from one solution - # to another - unstables = rqlst.get_variable_indices() - basedescr = [] - todetermine = [] - for i in xrange(len(rqlst.children[0].selection)): - ttype = selection_idx_type(i, rqlst, args) - if ttype is None or ttype == 'Any': - ttype = None - isfinal = True - else: - isfinal = ttype in BASE_TYPES - if ttype is None or i in unstables: - basedescr.append(None) - todetermine.append( (i, isfinal) ) - else: - basedescr.append(ttype) - if not todetermine: - return RepeatList(len(result), tuple(basedescr)) - return self._build_descr(result, basedescr, todetermine) - - def _build_descr(self, result, basedescription, todetermine): - description = [] - etype_from_eid = self.describe - todel = [] - for i, row in enumerate(result): - row_descr = basedescription[:] - for index, isfinal in todetermine: - value = row[index] - if value is None: - # None value inserted by an outer join, no type - row_descr[index] = None - continue - if isfinal: - row_descr[index] = etype_from_pyobj(value) - else: - try: - row_descr[index] = etype_from_eid(value)[0] - except UnknownEid: - self.error('wrong eid %s in repository, you should ' - 'db-check the database' % value) - todel.append(i) - break - else: - description.append(tuple(row_descr)) - for i in reversed(todel): - del result[i] - return description - # deprecated ############################################################### @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)') @@ -1317,6 +1228,24 @@ return 'en' return None + def prefered_language(self, language=None): + # mock CWUser.prefered_language, mainly for testing purpose + return self.property_value('ui.language') + + # CWUser compat for notification ########################################### + + def name(self): + return 'cubicweb' + + class _IEmailable: + @staticmethod + def get_email(): + return '' + + def cw_adapt_to(self, iface): + if iface == 'IEmailable': + return self._IEmailable + return None from logging import getLogger from cubicweb import set_log_methods diff -r 7812093e21f7 -r ab57000bff7b server/sources/__init__.py --- a/server/sources/__init__.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/sources/__init__.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b server/sources/datafeed.py --- a/server/sources/datafeed.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/sources/datafeed.py Thu Jan 17 18:30:08 2013 +0100 @@ -406,7 +406,7 @@ attrs = dict( (k, v) for k, v in attrs.iteritems() if v != getattr(entity, k)) if attrs: - entity.set_attributes(**attrs) + entity.cw_set(**attrs) self.notify_updated(entity) diff -r 7812093e21f7 -r ab57000bff7b server/sources/native.py --- a/server/sources/native.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/sources/native.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -961,6 +961,14 @@ cnx.commit() return eid + def _handle_is_relation_sql(self, session, sql, attrs): + """ Handler for specific is_relation sql that may be + overwritten in some stores""" + self.doexec(session, sql % attrs) + + _handle_insert_entity_sql = doexec + _handle_is_instance_of_sql = _handle_source_relation_sql = _handle_is_relation_sql + def add_info(self, session, entity, source, extid, complete): """add type and source info for an eid into the system table""" # begin by inserting eid/type/source/extid into the entities table @@ -970,21 +978,22 @@ uri = 'system' if source.copy_based_source else source.uri attrs = {'type': entity.__regid__, 'eid': entity.eid, 'extid': extid, 'source': uri, 'asource': source.uri, 'mtime': datetime.utcnow()} - self.doexec(session, self.sqlgen.insert('entities', attrs), attrs) + self._handle_insert_entity_sql(session, self.sqlgen.insert('entities', attrs), attrs) # insert core relations: is, is_instance_of and cw_source try: - self.doexec(session, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)' - % (entity.eid, eschema_eid(session, entity.e_schema))) + self._handle_is_relation_sql(session, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, eschema_eid(session, entity.e_schema))) except IndexError: # during schema serialization, skip pass else: for eschema in entity.e_schema.ancestors() + [entity.e_schema]: - self.doexec(session, 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)' - % (entity.eid, eschema_eid(session, eschema))) + self._handle_is_relation_sql(session, + 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, eschema_eid(session, eschema))) if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10 - self.doexec(session, 'INSERT INTO cw_source_relation(eid_from,eid_to) ' - 'VALUES (%s,%s)' % (entity.eid, source.eid)) + self._handle_is_relation_sql(session, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, source.eid)) # now we can update the full text index if self.do_fti and self.need_fti_indexation(entity.__regid__): if complete: diff -r 7812093e21f7 -r ab57000bff7b server/sources/storages.py --- a/server/sources/storages.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/sources/storages.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b server/ssplanner.py --- a/server/ssplanner.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/ssplanner.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -27,7 +27,6 @@ from cubicweb import QueryError, typed_eid from cubicweb.schema import VIRTUAL_RTYPES from cubicweb.rqlrewrite import add_types_restriction -from cubicweb.server.session import security_enabled from cubicweb.server.edition import EditedEntity READ_ONLY_RTYPES = set(('eid', 'has_text', 'is', 'is_instance_of', 'identity')) @@ -87,7 +86,7 @@ # the generated select substep if not emited (eg nothing # to be selected) if checkread and eid not in neweids: - with security_enabled(session, read=False): + with session.security_enabled(read=False): eschema(session.describe(eid)[0]).check_perm( session, 'read', eid=eid) eidconsts[lhs.variable] = eid diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_checkintegrity.py --- a/server/test/unittest_checkintegrity.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_checkintegrity.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -29,8 +29,9 @@ handler = get_test_db_handler(TestServerConfiguration(apphome=self.datadir)) handler.build_db_cache() self.repo, self.cnx = handler.get_repo_and_cnx() - self.execute = self.cnx.cursor().execute - self.session = self.repo._sessions[self.cnx.sessionid] + session = self.repo._get_session(self.cnx.sessionid, setcnxset=True) + self.session = session + self.execute = session.execute sys.stderr = sys.stdout = StringIO() def tearDown(self): diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_datafeed.py --- a/server/test/unittest_datafeed.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_datafeed.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2011-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_hook.py --- a/server/test/unittest_hook.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_hook.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_msplanner.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_multisources.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_querier.py --- a/server/test/unittest_querier.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_querier.py Thu Jan 17 18:30:08 2013 +0100 @@ -29,10 +29,10 @@ from cubicweb.server.sqlutils import SQL_PREFIX from cubicweb.server.utils import crypt_password from cubicweb.server.sources.native import make_schema +from cubicweb.server.querier import manual_build_descr, _make_description from cubicweb.devtools import get_test_db_handler, TestServerConfiguration from cubicweb.devtools.testlib import CubicWebTC from cubicweb.devtools.repotest import tuplify, BaseQuerierTC -from unittest_session import Variable class FixedOffset(tzinfo): def __init__(self, hours=0): @@ -87,6 +87,30 @@ del repo, cnx +class Variable: + def __init__(self, name): + self.name = name + self.children = [] + + def get_type(self, solution, args=None): + return solution[self.name] + def as_string(self): + return self.name + +class Function: + def __init__(self, name, varname): + self.name = name + self.children = [Variable(varname)] + def get_type(self, solution, args=None): + return 'Int' + +class MakeDescriptionTC(TestCase): + def test_known_values(self): + solution = {'A': 'Int', 'B': 'CWUser'} + self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution), + ['Int','CWUser']) + + class UtilsTC(BaseQuerierTC): setUpClass = classmethod(setUpClass) tearDownClass = classmethod(tearDownClass) @@ -242,6 +266,28 @@ rset = self.execute('Any %(x)s', {'x': u'str'}) self.assertEqual(rset.description[0][0], 'String') + def test_build_descr1(self): + rset = self.execute('(Any U,L WHERE U login L) UNION (Any G,N WHERE G name N, G is CWGroup)') + rset.req = self.session + orig_length = len(rset) + rset.rows[0][0] = 9999999 + description = manual_build_descr(rset.req, rset.syntax_tree(), None, rset.rows) + self.assertEqual(len(description), orig_length - 1) + self.assertEqual(len(rset.rows), orig_length - 1) + self.assertNotEqual(rset.rows[0][0], 9999999) + + def test_build_descr2(self): + rset = self.execute('Any X,Y WITH X,Y BEING ((Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G))') + for x, y in rset.description: + if y is not None: + self.assertEqual(y, 'CWGroup') + + def test_build_descr3(self): + rset = self.execute('(Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G)') + for x, y in rset.description: + if y is not None: + self.assertEqual(y, 'CWGroup') + class QuerierTC(BaseQuerierTC): setUpClass = classmethod(setUpClass) diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_repository.py --- a/server/test/unittest_repository.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_repository.py Thu Jan 17 18:30:08 2013 +0100 @@ -522,7 +522,7 @@ self.commit() self.assertEqual(len(c.reverse_fiche), 1) - def test_set_attributes_in_before_update(self): + def test_cw_set_in_before_update(self): # local hook class DummyBeforeHook(Hook): __regid__ = 'dummy-before-hook' @@ -534,31 +534,31 @@ pendings = self._cw.transaction_data.setdefault('pending', set()) if self.entity.eid not in pendings: pendings.add(self.entity.eid) - self.entity.set_attributes(alias=u'foo') + self.entity.cw_set(alias=u'foo') with self.temporary_appobjects(DummyBeforeHook): req = self.request() addr = req.create_entity('EmailAddress', address=u'a@b.fr') - addr.set_attributes(address=u'a@b.com') + addr.cw_set(address=u'a@b.com') rset = self.execute('Any A,AA WHERE X eid %(x)s, X address A, X alias AA', {'x': addr.eid}) self.assertEqual(rset.rows, [[u'a@b.com', u'foo']]) - def test_set_attributes_in_before_add(self): + def test_cw_set_in_before_add(self): # local hook class DummyBeforeHook(Hook): __regid__ = 'dummy-before-hook' __select__ = Hook.__select__ & is_instance('EmailAddress') events = ('before_add_entity',) def __call__(self): - # set_attributes is forbidden within before_add_entity() - self.entity.set_attributes(alias=u'foo') + # cw_set is forbidden within before_add_entity() + self.entity.cw_set(alias=u'foo') with self.temporary_appobjects(DummyBeforeHook): req = self.request() # XXX will fail with python -O self.assertRaises(AssertionError, req.create_entity, 'EmailAddress', address=u'a@b.fr') - def test_multiple_edit_set_attributes(self): + def test_multiple_edit_cw_set(self): """make sure cw_edited doesn't get cluttered by previous entities on multiple set """ @@ -664,7 +664,7 @@ self.commit() rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'}) self.assertEqual(rset.rows, []) - req.user.set_relations(use_email=toto) + req.user.cw_set(use_email=toto) self.commit() rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'}) self.assertEqual(rset.rows, [[req.user.eid]]) @@ -674,11 +674,11 @@ rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'}) self.assertEqual(rset.rows, []) tutu = req.create_entity('EmailAddress', address=u'tutu@logilab.fr') - req.user.set_relations(use_email=tutu) + req.user.cw_set(use_email=tutu) self.commit() rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'}) self.assertEqual(rset.rows, [[req.user.eid]]) - tutu.set_attributes(address=u'hip@logilab.fr') + tutu.cw_set(address=u'hip@logilab.fr') self.commit() rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'}) self.assertEqual(rset.rows, []) @@ -790,7 +790,7 @@ personnes.append(p) abraham = req.create_entity('Personne', nom=u'Abraham', prenom=u'John', sexe=u'M') for j in xrange(0, 2000, 100): - abraham.set_relations(personne_composite=personnes[j:j+100]) + abraham.cw_set(personne_composite=personnes[j:j+100]) t1 = time.time() self.info('creation: %.2gs', (t1 - t0)) req.cnx.commit() @@ -816,7 +816,7 @@ t1 = time.time() self.info('creation: %.2gs', (t1 - t0)) for j in xrange(100, 2000, 100): - abraham.set_relations(personne_composite=personnes[j:j+100]) + abraham.cw_set(personne_composite=personnes[j:j+100]) t2 = time.time() self.info('more relations: %.2gs', (t2-t1)) req.cnx.commit() @@ -836,7 +836,7 @@ t1 = time.time() self.info('creation: %.2gs', (t1 - t0)) for j in xrange(100, 2000, 100): - abraham.set_relations(personne_inlined=personnes[j:j+100]) + abraham.cw_set(personne_inlined=personnes[j:j+100]) t2 = time.time() self.info('more relations: %.2gs', (t2-t1)) req.cnx.commit() @@ -917,7 +917,7 @@ p1 = req.create_entity('Personne', nom=u'Vincent') p2 = req.create_entity('Personne', nom=u'Florent') w = req.create_entity('Affaire', ref=u'wc') - w.set_relations(todo_by=[p1,p2]) + w.cw_set(todo_by=[p1,p2]) w.cw_clear_all_caches() self.commit() self.assertEqual(len(w.todo_by), 1) @@ -928,9 +928,9 @@ p1 = req.create_entity('Personne', nom=u'Vincent') p2 = req.create_entity('Personne', nom=u'Florent') w = req.create_entity('Affaire', ref=u'wc') - w.set_relations(todo_by=p1) + w.cw_set(todo_by=p1) self.commit() - w.set_relations(todo_by=p2) + w.cw_set(todo_by=p2) w.cw_clear_all_caches() self.commit() self.assertEqual(len(w.todo_by), 1) diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_security.py --- a/server/test/unittest_security.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_security.py Thu Jan 17 18:30:08 2013 +0100 @@ -473,9 +473,9 @@ anon = cu.connection.user(self.session) # anonymous user can only read itself rset = cu.execute('Any L WHERE X owned_by U, U login L') - self.assertEqual(rset.rows, [['anon']]) + self.assertEqual([['anon']], rset.rows) rset = cu.execute('CWUser X') - self.assertEqual(rset.rows, [[anon.eid]]) + self.assertEqual([[anon.eid]], rset.rows) # anonymous user can read groups (necessary to check allowed transitions for instance) self.assert_(cu.execute('CWGroup X')) # should only be able to read the anonymous user, not another one @@ -488,7 +488,7 @@ # {'x': self.user.eid}) rset = cu.execute('CWUser X WHERE X eid %(x)s', {'x': anon.eid}) - self.assertEqual(rset.rows, [[anon.eid]]) + self.assertEqual([[anon.eid]], rset.rows) # but can't modify it cu.execute('SET X login "toto" WHERE X eid %(x)s', {'x': anon.eid}) self.assertRaises(Unauthorized, self.commit) diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_session.py --- a/server/test/unittest_session.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_session.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -17,33 +17,7 @@ # with CubicWeb. If not, see . from __future__ import with_statement -from logilab.common.testlib import TestCase, unittest_main, mock_object - from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.server.session import _make_description, hooks_control - -class Variable: - def __init__(self, name): - self.name = name - self.children = [] - - def get_type(self, solution, args=None): - return solution[self.name] - def as_string(self): - return self.name - -class Function: - def __init__(self, name, varname): - self.name = name - self.children = [Variable(varname)] - def get_type(self, solution, args=None): - return 'Int' - -class MakeDescriptionTC(TestCase): - def test_known_values(self): - solution = {'A': 'Int', 'B': 'CWUser'} - self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution), - ['Int','CWUser']) class InternalSessionTC(CubicWebTC): @@ -61,7 +35,7 @@ self.assertEqual(session.disabled_hook_categories, set()) self.assertEqual(session.enabled_hook_categories, set()) self.assertEqual(len(session._tx_data), 1) - with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata'): + with session.deny_all_hooks_but('metadata'): self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL) self.assertEqual(session.disabled_hook_categories, set()) self.assertEqual(session.enabled_hook_categories, set(('metadata',))) @@ -73,7 +47,7 @@ self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL) self.assertEqual(session.disabled_hook_categories, set()) self.assertEqual(session.enabled_hook_categories, set(('metadata',))) - with hooks_control(session, session.HOOKS_ALLOW_ALL, 'integrity'): + with session.allow_all_hooks_but('integrity'): self.assertEqual(session.hooks_mode, session.HOOKS_ALLOW_ALL) self.assertEqual(session.disabled_hook_categories, set(('integrity',))) self.assertEqual(session.enabled_hook_categories, set(('metadata',))) # not changed in such case @@ -88,27 +62,7 @@ self.assertEqual(session.disabled_hook_categories, set()) self.assertEqual(session.enabled_hook_categories, set()) - def test_build_descr1(self): - rset = self.execute('(Any U,L WHERE U login L) UNION (Any G,N WHERE G name N, G is CWGroup)') - orig_length = len(rset) - rset.rows[0][0] = 9999999 - description = self.session.build_description(rset.syntax_tree(), None, rset.rows) - self.assertEqual(len(description), orig_length - 1) - self.assertEqual(len(rset.rows), orig_length - 1) - self.assertFalse(rset.rows[0][0] == 9999999) - - def test_build_descr2(self): - rset = self.execute('Any X,Y WITH X,Y BEING ((Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G))') - for x, y in rset.description: - if y is not None: - self.assertEqual(y, 'CWGroup') - - def test_build_descr3(self): - rset = self.execute('(Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G)') - for x, y in rset.description: - if y is not None: - self.assertEqual(y, 'CWGroup') - if __name__ == '__main__': + from logilab.common.testlib import unittest_main unittest_main() diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_storage.py --- a/server/test/unittest_storage.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_storage.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -99,7 +99,7 @@ f1 = self.create_file() self.commit() self.assertEqual(file(expected_filepath).read(), 'the-data') - f1.set_attributes(data=Binary('the new data')) + f1.cw_set(data=Binary('the new data')) self.rollback() self.assertEqual(file(expected_filepath).read(), 'the-data') f1.cw_delete() @@ -118,7 +118,7 @@ def test_bfss_fs_importing_doesnt_touch_path(self): self.session.transaction_data['fs_importing'] = True filepath = osp.abspath(__file__) - f1 = self.session.create_entity('File', data=Binary(filepath), + f1 = self.request().create_entity('File', data=Binary(filepath), data_format=u'text/plain', data_name=u'foo') self.assertEqual(self.fspath(f1), filepath) @@ -196,15 +196,18 @@ filepath = osp.abspath(__file__) f1 = self.session.create_entity('File', data=Binary(filepath), data_format=u'text/plain', data_name=u'foo') - self.assertEqual(f1.data.getvalue(), file(filepath).read(), - 'files content differ') + cw_value = f1.data.getvalue() + fs_value = file(filepath).read() + if cw_value != fs_value: + self.fail('cw value %r is different from file content' % cw_value) + @tag('update') def test_bfss_update_with_existing_data(self): # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), data_format=u'text/plain', data_name=u'foo') - # NOTE: do not use set_attributes() which would automatically + # NOTE: do not use cw_set() which would automatically # update f1's local dict. We want the pure rql version to work self.execute('SET F data %(d)s WHERE F eid %(f)s', {'d': Binary('some other data'), 'f': f1.eid}) @@ -218,7 +221,7 @@ # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), data_format=u'text/plain', data_name=u'foo.txt') - # NOTE: do not use set_attributes() which would automatically + # NOTE: do not use cw_set() which would automatically # update f1's local dict. We want the pure rql version to work self.commit() old_path = self.fspath(f1) @@ -240,7 +243,7 @@ # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), data_format=u'text/plain', data_name=u'foo.txt') - # NOTE: do not use set_attributes() which would automatically + # NOTE: do not use cw_set() which would automatically # update f1's local dict. We want the pure rql version to work self.commit() old_path = self.fspath(f1) @@ -265,7 +268,7 @@ f = self.session.create_entity('Affaire', opt_attr=Binary('toto')) self.session.commit() self.session.set_cnxset() - f.set_attributes(opt_attr=None) + f.cw_set(opt_attr=None) self.session.commit() @tag('fs_importing', 'update') diff -r 7812093e21f7 -r ab57000bff7b server/test/unittest_undo.py --- a/server/test/unittest_undo.py Thu Jan 17 17:12:06 2013 +0100 +++ b/server/test/unittest_undo.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -203,7 +203,7 @@ c.cw_delete() txuuid = self.commit() c2 = session.create_entity('Card', title=u'hip', content=u'hip') - p.set_relations(fiche=c2) + p.cw_set(fiche=c2) self.commit() self.assertUndoTransaction(txuuid, [ "Can't restore object relation fiche to entity " @@ -217,7 +217,7 @@ session = self.session g = session.create_entity('CWGroup', name=u'staff') session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid}) - self.toto.set_relations(in_group=g) + self.toto.cw_set(in_group=g) self.commit() self.toto.cw_delete() txuuid = self.commit() @@ -228,6 +228,7 @@ "%s doesn't exist anymore." % g.eid]) with self.assertRaises(ValidationError) as cm: self.commit() + cm.exception.translate(unicode) self.assertEqual(cm.exception.entity, self.toto.eid) self.assertEqual(cm.exception.errors, {'in_group-subject': u'at least one relation in_group is ' @@ -265,7 +266,7 @@ email = self.request().create_entity('EmailAddress', address=u'tutu@cubicweb.org') prop = self.request().create_entity('CWProperty', pkey=u'ui.default-text-format', value=u'text/html') - tutu.set_relations(use_email=email, reverse_for_user=prop) + tutu.cw_set(use_email=email, reverse_for_user=prop) self.commit() with self.assertRaises(ValidationError) as cm: self.cnx.undo_transaction(txuuid) @@ -278,7 +279,7 @@ g = session.create_entity('CWGroup', name=u'staff') txuuid = self.commit() session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid}) - self.toto.set_relations(in_group=g) + self.toto.cw_set(in_group=g) self.commit() with self.assertRaises(ValidationError) as cm: self.cnx.undo_transaction(txuuid) @@ -304,7 +305,7 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis', fiche=c) self.commit() - p.set_relations(fiche=None) + p.cw_set(fiche=None) txuuid = self.commit() self.assertUndoTransaction(txuuid) self.commit() @@ -319,7 +320,7 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis', fiche=c) self.commit() - p.set_relations(fiche=None) + p.cw_set(fiche=None) txuuid = self.commit() c.cw_delete() self.commit() @@ -339,7 +340,7 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis') self.commit() - p.set_relations(fiche=c) + p.cw_set(fiche=c) txuuid = self.commit() self.assertUndoTransaction(txuuid) self.commit() @@ -354,7 +355,7 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis') self.commit() - p.set_relations(fiche=c) + p.cw_set(fiche=c) txuuid = self.commit() c.cw_delete() self.commit() @@ -369,7 +370,7 @@ c2 = session.create_entity('Card', title=u'hip', content=u'hip') p = session.create_entity('Personne', nom=u'louis', fiche=c1) self.commit() - p.set_relations(fiche=c2) + p.cw_set(fiche=c2) txuuid = self.commit() self.assertUndoTransaction(txuuid) self.commit() @@ -385,7 +386,7 @@ c2 = session.create_entity('Card', title=u'hip', content=u'hip') p = session.create_entity('Personne', nom=u'louis', fiche=c1) self.commit() - p.set_relations(fiche=c2) + p.cw_set(fiche=c2) txuuid = self.commit() c1.cw_delete() self.commit() @@ -401,7 +402,7 @@ p = session.create_entity('Personne', nom=u'toto') session.commit() self.session.set_cnxset() - p.set_attributes(nom=u'titi') + p.cw_set(nom=u'titi') txuuid = self.commit() self.assertUndoTransaction(txuuid) p.cw_clear_all_caches() @@ -412,7 +413,7 @@ p = session.create_entity('Personne', nom=u'toto') session.commit() self.session.set_cnxset() - p.set_attributes(nom=u'titi') + p.cw_set(nom=u'titi') txuuid = self.commit() p.cw_delete() self.commit() diff -r 7812093e21f7 -r ab57000bff7b sobjects/ldapparser.py --- a/sobjects/ldapparser.py Thu Jan 17 17:12:06 2013 +0100 +++ b/sobjects/ldapparser.py Thu Jan 17 18:30:08 2013 +0100 @@ -97,7 +97,7 @@ attrs = dict( (k, v) for k, v in attrs.iteritems() if v != getattr(entity, k)) if attrs: - entity.set_attributes(**attrs) + entity.cw_set(**attrs) self.notify_updated(entity) def ldap2cwattrs(self, sdict, tdict=None): @@ -131,7 +131,7 @@ if entity.__regid__ == 'EmailAddress': return groups = [self._get_group(n) for n in self.source.user_default_groups] - entity.set_relations(in_group=groups) + entity.cw_set(in_group=groups) self._process_email(entity, sourceparams) def is_deleted(self, extidplus, etype, eid): @@ -160,9 +160,9 @@ email = self.extid2entity(emailextid, 'EmailAddress', address=emailaddr) if entity.primary_email: - entity.set_relations(use_email=email) + entity.cw_set(use_email=email) else: - entity.set_relations(primary_email=email) + entity.cw_set(primary_email=email) elif self.sourceuris: # pop from sourceuris anyway, else email may be removed by the # source once import is finished diff -r 7812093e21f7 -r ab57000bff7b test/unittest_cwconfig.py --- a/test/unittest_cwconfig.py Thu Jan 17 17:12:06 2013 +0100 +++ b/test/unittest_cwconfig.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -101,10 +101,10 @@ self.assertEqual(self.config.expand_cubes(('email', 'comment')), ['email', 'comment', 'file']) - def test_vregistry_path(self): + def test_appobjects_path(self): self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR] self.config.adjust_sys_path() - self.assertEqual([unabsolutize(p) for p in self.config.vregistry_path()], + self.assertEqual([unabsolutize(p) for p in self.config.appobjects_path()], ['entities', 'web/views', 'sobjects', 'hooks', 'file/entities', 'file/views.py', 'file/hooks', 'email/entities.py', 'email/views', 'email/hooks.py', diff -r 7812093e21f7 -r ab57000bff7b test/unittest_dbapi.py --- a/test/unittest_dbapi.py Thu Jan 17 17:12:06 2013 +0100 +++ b/test/unittest_dbapi.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b test/unittest_entity.py --- a/test/unittest_entity.py Thu Jan 17 17:12:06 2013 +0100 +++ b/test/unittest_entity.py Thu Jan 17 18:30:08 2013 +0100 @@ -701,23 +701,23 @@ self.assertEqual(card4.rest_path(), unicode(card4.eid)) - def test_set_attributes(self): + def test_cw_set_attributes(self): req = self.request() person = req.create_entity('Personne', nom=u'di mascio', prenom=u'adrien') self.assertEqual(person.prenom, u'adrien') self.assertEqual(person.nom, u'di mascio') - person.set_attributes(prenom=u'sylvain', nom=u'thénault') + person.cw_set(prenom=u'sylvain', nom=u'thénault') person = self.execute('Personne P').get_entity(0, 0) # XXX retreival needed ? self.assertEqual(person.prenom, u'sylvain') self.assertEqual(person.nom, u'thénault') - def test_set_relations(self): + def test_cw_set_relations(self): req = self.request() person = req.create_entity('Personne', nom=u'chauvat', prenom=u'nicolas') note = req.create_entity('Note', type=u'x') - note.set_relations(ecrit_par=person) + note.cw_set(ecrit_par=person) note = req.create_entity('Note', type=u'y') - note.set_relations(ecrit_par=person.eid) + note.cw_set(ecrit_par=person.eid) self.assertEqual(len(person.reverse_ecrit_par), 2) def test_metainformation_and_external_absolute_url(self): diff -r 7812093e21f7 -r ab57000bff7b test/unittest_migration.py --- a/test/unittest_migration.py Thu Jan 17 17:12:06 2013 +0100 +++ b/test/unittest_migration.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -108,7 +108,13 @@ self.assertEqual(source['db-driver'], 'sqlite') handler = get_test_db_handler(config) handler.init_test_database() - + handler.build_db_cache() + repo, cnx = handler.get_repo_and_cnx() + cu = cnx.cursor() + self.assertEqual(cu.execute('Any SN WHERE X is CWUser, X login "admin", X in_state S, S name SN').rows, + [['activated']]) + cnx.close() + repo.shutdown() if __name__ == '__main__': unittest_main() diff -r 7812093e21f7 -r ab57000bff7b test/unittest_rset.py --- a/test/unittest_rset.py Thu Jan 17 17:12:06 2013 +0100 +++ b/test/unittest_rset.py Thu Jan 17 18:30:08 2013 +0100 @@ -112,6 +112,17 @@ # '%stask/title/go' % baseurl) # empty _restpath should not crash self.compare_urls(req.build_url('view', _restpath=''), baseurl) + self.assertNotIn('https', req.build_url('view', vid='foo', rql='yo', + __secure__=True)) + try: + self.config.global_set_option('https-url', 'https://testing.fr/') + self.assertTrue('https', req.build_url('view', vid='foo', rql='yo', + __secure__=True)) + self.compare_urls(req.build_url('view', vid='foo', rql='yo', + __secure__=True), + '%sview?vid=foo&rql=yo' % req.base_url(secure=True)) + finally: + self.config.global_set_option('https-url', None) def test_build(self): diff -r 7812093e21f7 -r ab57000bff7b test/unittest_schema.py --- a/test/unittest_schema.py Thu Jan 17 17:12:06 2013 +0100 +++ b/test/unittest_schema.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -350,8 +350,8 @@ class WorkflowShemaTC(CubicWebTC): def test_trinfo_default_format(self): - tr = self.session.user.cw_adapt_to('IWorkflowable').fire_transition('deactivate') - self.assertEqual(tr.comment_format, 'text/plain') + tr = self.request().user.cw_adapt_to('IWorkflowable').fire_transition('deactivate') + self.assertEqual(tr.comment_format, 'text/plain') if __name__ == '__main__': unittest_main() diff -r 7812093e21f7 -r ab57000bff7b web/application.py --- a/web/application.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/application.py Thu Jan 17 18:30:08 2013 +0100 @@ -462,7 +462,6 @@ result = self.notfound_content(req) req.status_out = ex.status except ValidationError, ex: - req.status_out = httplib.CONFLICT result = self.validation_error_handler(req, ex) except RemoteCallFailed, ex: result = self.ajax_error_handler(req, ex) @@ -486,7 +485,7 @@ except (AuthenticationError, LogOut): # the rollback is handled in the finally raise - ### Last defence line + ### Last defense line except BaseException, ex: result = self.error_handler(req, ex, tb=True) finally: @@ -517,7 +516,7 @@ return '' def validation_error_handler(self, req, ex): - ex.errors = dict((k, v) for k, v in ex.errors.items()) + ex.translate(req._) # translate messages using ui language if '__errorurl' in req.form: forminfo = {'error': ex, 'values': req.form, @@ -532,6 +531,7 @@ req.headers_out.setHeader('location', str(location)) req.status_out = httplib.SEE_OTHER return '' + req.status_out = httplib.CONFLICT return self.error_handler(req, ex, tb=False) def error_handler(self, req, ex, tb=False): diff -r 7812093e21f7 -r ab57000bff7b web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Thu Jan 17 17:12:06 2013 +0100 +++ b/web/data/cubicweb.ajax.js Thu Jan 17 18:30:08 2013 +0100 @@ -70,7 +70,7 @@ callback.apply(null, args); } } catch(error) { - this.error(this.xhr, null, error); + this.error(this._req, null, error); } }, @@ -704,7 +704,7 @@ var ajaxArgs = ['render', formparams, registry, compid]; ajaxArgs = ajaxArgs.concat(cw.utils.sliceList(arguments, 4)); var params = ajaxFuncArgs.apply(null, ajaxArgs); - return $('#'+domid).loadxhtml(AJAX_BASE_URL, params, null, 'swap'); + return $('#'+domid).loadxhtml(AJAX_BASE_URL, params, null, 'swap', true); } /* ajax tabs ******************************************************************/ diff -r 7812093e21f7 -r ab57000bff7b web/data/cubicweb.css --- a/web/data/cubicweb.css Thu Jan 17 17:12:06 2013 +0100 +++ b/web/data/cubicweb.css Thu Jan 17 18:30:08 2013 +0100 @@ -545,6 +545,16 @@ padding-left: 2em; } +/* actions around tables */ +.tableactions span { + padding: 0 18px; + height: 24px; + background: #F8F8F8; + border: 1px solid #DFDFDF; + border-bottom: none; + border-radius: 4px 4px 0 0; +} + /* custom boxes */ .search_box div.boxBody { diff -r 7812093e21f7 -r ab57000bff7b web/data/cubicweb.facets.js --- a/web/data/cubicweb.facets.js Thu Jan 17 17:12:06 2013 +0100 +++ b/web/data/cubicweb.facets.js Thu Jan 17 18:30:08 2013 +0100 @@ -68,6 +68,14 @@ var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + encodeURIComponent(bkPath); $bkLink.attr('href', bkUrl); } + var $focusLink = jQuery('#focusLink'); + if ($focusLink.length) { + var url = baseuri()+ 'view?rql=' + encodeURIComponent(rql); + if (vid) { + url += '&vid=' + encodeURIComponent(vid); + } + $focusLink.attr('href', url); + } var toupdate = result[1]; var extraparams = vidargs; if (paginate) { extraparams['paginate'] = '1'; } // XXX in vidargs diff -r 7812093e21f7 -r ab57000bff7b web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Thu Jan 17 17:12:06 2013 +0100 +++ b/web/data/cubicweb.old.css Thu Jan 17 18:30:08 2013 +0100 @@ -899,6 +899,16 @@ padding-left: 0.5em; } +/* actions around tables */ +.tableactions span { + padding: 0 18px; + height: 24px; + background: #F8F8F8; + border: 1px solid #DFDFDF; + border-bottom: none; + border-radius: 4px 4px 0 0; +} + /***************************************/ /* error view (views/management.py) */ /***************************************/ diff -r 7812093e21f7 -r ab57000bff7b web/httpcache.py --- a/web/httpcache.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/httpcache.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b web/request.py --- a/web/request.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/request.py Thu Jan 17 18:30:08 2013 +0100 @@ -170,7 +170,6 @@ @property def authmode(self): """Authentification mode of the instance - (see :ref:`WebServerConfig`)""" return self.vreg.config['auth-mode'] @@ -227,14 +226,6 @@ # 3. default language self.set_default_language(vreg) - def set_language(self, lang): - gettext, self.pgettext = self.translations[lang] - self._ = self.__ = gettext - self.lang = lang - self.debug('request language: %s', lang) - if self.cnx: - self.cnx.set_session_props(lang=lang) - # input form parameters management ######################################## # common form parameters which should be protected against html values @@ -366,7 +357,7 @@ def update_search_state(self): """update the current search state""" searchstate = self.form.get('__mode') - if not searchstate and self.cnx: + if not searchstate: searchstate = self.session.data.get('search_state', 'normal') self.set_search_state(searchstate) @@ -377,8 +368,7 @@ else: self.search_state = ('linksearch', searchstate.split(':')) assert len(self.search_state[-1]) == 4 - if self.cnx: - self.session.data['search_state'] = searchstate + self.session.data['search_state'] = searchstate def match_search_state(self, rset): """when searching an entity to create a relation, return True if entities in diff -r 7812093e21f7 -r ab57000bff7b web/test/data/views.py --- a/web/test/data/views.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/data/views.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,9 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -""" -""" from cubicweb.web import Redirect from cubicweb.web.application import CubicWebPublisher diff -r 7812093e21f7 -r ab57000bff7b web/test/unittest_application.py --- a/web/test/unittest_application.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/unittest_application.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b web/test/unittest_magicsearch.py --- a/web/test/unittest_magicsearch.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/unittest_magicsearch.py Thu Jan 17 18:30:08 2013 +0100 @@ -230,5 +230,118 @@ self.assertEqual(rset.rql, 'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s') self.assertEqual(rset.args, {'text': u'utilisateur Smith'}) + +class RQLSuggestionsBuilderTC(CubicWebTC): + def suggestions(self, rql): + req = self.request() + rbs = self.vreg['components'].select('rql.suggestions', req) + return rbs.build_suggestions(rql) + + def test_no_restrictions_rql(self): + self.assertListEqual([], self.suggestions('')) + self.assertListEqual([], self.suggestions('An')) + self.assertListEqual([], self.suggestions('Any X')) + self.assertListEqual([], self.suggestions('Any X, Y')) + + def test_invalid_rql(self): + self.assertListEqual([], self.suggestions('blabla')) + self.assertListEqual([], self.suggestions('Any X WHERE foo, bar')) + + def test_is_rql(self): + self.assertListEqual(['Any X WHERE X is %s' % eschema + for eschema in sorted(self.vreg.schema.entities()) + if not eschema.final], + self.suggestions('Any X WHERE X is')) + + self.assertListEqual(['Any X WHERE X is Personne', 'Any X WHERE X is Project'], + self.suggestions('Any X WHERE X is P')) + + self.assertListEqual(['Any X WHERE X is Personne, Y is Personne', + 'Any X WHERE X is Personne, Y is Project'], + self.suggestions('Any X WHERE X is Personne, Y is P')) + + + def test_relations_rql(self): + self.assertListEqual(['Any X WHERE X is Personne, X ass A', + 'Any X WHERE X is Personne, X datenaiss A', + 'Any X WHERE X is Personne, X description A', + 'Any X WHERE X is Personne, X fax A', + 'Any X WHERE X is Personne, X nom A', + 'Any X WHERE X is Personne, X prenom A', + 'Any X WHERE X is Personne, X promo A', + 'Any X WHERE X is Personne, X salary A', + 'Any X WHERE X is Personne, X sexe A', + 'Any X WHERE X is Personne, X tel A', + 'Any X WHERE X is Personne, X test A', + 'Any X WHERE X is Personne, X titre A', + 'Any X WHERE X is Personne, X travaille A', + 'Any X WHERE X is Personne, X web A', + ], + self.suggestions('Any X WHERE X is Personne, X ')) + self.assertListEqual(['Any X WHERE X is Personne, X tel A', + 'Any X WHERE X is Personne, X test A', + 'Any X WHERE X is Personne, X titre A', + 'Any X WHERE X is Personne, X travaille A', + ], + self.suggestions('Any X WHERE X is Personne, X t')) + # try completion on selected + self.assertListEqual(['Any X WHERE X is Personne, Y is Societe, X tel A', + 'Any X WHERE X is Personne, Y is Societe, X test A', + 'Any X WHERE X is Personne, Y is Societe, X titre A', + 'Any X WHERE X is Personne, Y is Societe, X travaille Y', + ], + self.suggestions('Any X WHERE X is Personne, Y is Societe, X t')) + # invalid relation should not break + self.assertListEqual([], + self.suggestions('Any X WHERE X is Personne, X asdasd')) + + def test_attribute_vocabulary_rql(self): + self.assertListEqual(['Any X WHERE X is Personne, X promo "bon"', + 'Any X WHERE X is Personne, X promo "pasbon"', + ], + self.suggestions('Any X WHERE X is Personne, X promo "')) + self.assertListEqual(['Any X WHERE X is Personne, X promo "pasbon"', + ], + self.suggestions('Any X WHERE X is Personne, X promo "p')) + # "bon" should be considered complete, hence no suggestion + self.assertListEqual([], + self.suggestions('Any X WHERE X is Personne, X promo "bon"')) + # no valid vocabulary starts with "po" + self.assertListEqual([], + self.suggestions('Any X WHERE X is Personne, X promo "po')) + + def test_attribute_value_rql(self): + # suggestions should contain any possible value for + # a given attribute (limited to 10) + req = self.request() + for i in xrange(15): + req.create_entity('Personne', nom=u'n%s' % i, prenom=u'p%s' % i) + self.assertListEqual(['Any X WHERE X is Personne, X nom "n0"', + 'Any X WHERE X is Personne, X nom "n1"', + 'Any X WHERE X is Personne, X nom "n10"', + 'Any X WHERE X is Personne, X nom "n11"', + 'Any X WHERE X is Personne, X nom "n12"', + 'Any X WHERE X is Personne, X nom "n13"', + 'Any X WHERE X is Personne, X nom "n14"', + 'Any X WHERE X is Personne, X nom "n2"', + 'Any X WHERE X is Personne, X nom "n3"', + 'Any X WHERE X is Personne, X nom "n4"', + 'Any X WHERE X is Personne, X nom "n5"', + 'Any X WHERE X is Personne, X nom "n6"', + 'Any X WHERE X is Personne, X nom "n7"', + 'Any X WHERE X is Personne, X nom "n8"', + 'Any X WHERE X is Personne, X nom "n9"', + ], + self.suggestions('Any X WHERE X is Personne, X nom "')) + self.assertListEqual(['Any X WHERE X is Personne, X nom "n1"', + 'Any X WHERE X is Personne, X nom "n10"', + 'Any X WHERE X is Personne, X nom "n11"', + 'Any X WHERE X is Personne, X nom "n12"', + 'Any X WHERE X is Personne, X nom "n13"', + 'Any X WHERE X is Personne, X nom "n14"', + ], + self.suggestions('Any X WHERE X is Personne, X nom "n1')) + + if __name__ == '__main__': unittest_main() diff -r 7812093e21f7 -r ab57000bff7b web/test/unittest_reledit.py --- a/web/test/unittest_reledit.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/unittest_reledit.py Thu Jan 17 18:30:08 2013 +0100 @@ -175,8 +175,8 @@ def setup_database(self): super(ClickAndEditFormUICFGTC, self).setup_database() - self.tick.set_relations(concerns=self.proj) - self.proj.set_relations(manager=self.toto) + self.tick.cw_set(concerns=self.proj) + self.proj.cw_set(manager=self.toto) def test_with_uicfg(self): old_rctl = reledit_ctrl._tagdefs.copy() diff -r 7812093e21f7 -r ab57000bff7b web/test/unittest_urlrewrite.py --- a/web/test/unittest_urlrewrite.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/unittest_urlrewrite.py Thu Jan 17 18:30:08 2013 +0100 @@ -60,7 +60,6 @@ ('/doc/images/(.+?)/?$', dict(fid='\\1', vid='wdocimages')), ('/doc/?$', dict(fid='main', vid='wdoc')), ('/doc/(.+?)/?$', dict(fid='\\1', vid='wdoc')), - ('/changelog/?$', dict(vid='changelog')), # now in SchemaBasedRewriter #('/search/(.+)$', dict(rql=r'Any X WHERE X has_text "\1"')), ]) @@ -105,9 +104,9 @@ def setup_database(self): req = self.request() self.p1 = self.create_user(req, u'user1') - self.p1.set_attributes(firstname=u'joe', surname=u'Dalton') + self.p1.cw_set(firstname=u'joe', surname=u'Dalton') self.p2 = self.create_user(req, u'user2') - self.p2.set_attributes(firstname=u'jack', surname=u'Dalton') + self.p2.cw_set(firstname=u'jack', surname=u'Dalton') def test_rgx_action_with_transforms(self): class TestSchemaBasedRewriter(SchemaBasedRewriter): diff -r 7812093e21f7 -r ab57000bff7b web/test/unittest_views_basecontrollers.py --- a/web/test/unittest_views_basecontrollers.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/unittest_views_basecontrollers.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -77,6 +77,7 @@ } with self.assertRaises(ValidationError) as cm: self.ctrl_publish(req) + cm.exception.translate(unicode) self.assertEqual(cm.exception.errors, {'login-subject': 'the value "admin" is already used, use another one'}) def test_user_editing_itself(self): @@ -249,6 +250,7 @@ } with self.assertRaises(ValidationError) as cm: self.ctrl_publish(req) + cm.exception.translate(unicode) self.assertEqual(cm.exception.errors, {'amount-subject': 'value -10 must be >= 0'}) req = self.request(rollbackfirst=True) req.form = {'eid': ['X'], @@ -259,6 +261,7 @@ } with self.assertRaises(ValidationError) as cm: self.ctrl_publish(req) + cm.exception.translate(unicode) self.assertEqual(cm.exception.errors, {'amount-subject': 'value 110 must be <= 100'}) req = self.request(rollbackfirst=True) req.form = {'eid': ['X'], diff -r 7812093e21f7 -r ab57000bff7b web/test/unittest_views_basetemplates.py --- a/web/test/unittest_views_basetemplates.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/unittest_views_basetemplates.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b web/test/unittest_views_searchrestriction.py --- a/web/test/unittest_views_searchrestriction.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/unittest_views_searchrestriction.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b web/test/unittest_viewselector.py --- a/web/test/unittest_viewselector.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/test/unittest_viewselector.py Thu Jan 17 18:30:08 2013 +0100 @@ -39,7 +39,6 @@ actions.LogoutAction] SITEACTIONS = [actions.ManageAction] FOOTERACTIONS = [wdoc.HelpAction, - wdoc.ChangeLogAction, wdoc.AboutAction, actions.PoweredByAction] MANAGEACTIONS = [actions.SiteConfigurationAction, @@ -93,8 +92,7 @@ def test_possible_views_none_rset(self): req = self.request() self.assertListEqual(self.pviews(req, None), - [('changelog', wdoc.ChangeLogView), - ('cw.sources-management', cwsources.CWSourcesManagementView), + [('cw.sources-management', cwsources.CWSourcesManagementView), ('cw.users-and-groups-management', cwuser.UsersAndGroupsManagementView), ('gc', debug.GCView), ('index', startup.IndexView), diff -r 7812093e21f7 -r ab57000bff7b web/uicfg.py --- a/web/uicfg.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/uicfg.py Thu Jan 17 18:30:08 2013 +0100 @@ -166,14 +166,6 @@ class AutoformSectionRelationTags(RelationTagsSet): """autoform relations'section""" - bw_tag_map = { - 'primary': {'main': 'attributes', 'muledit': 'attributes'}, - 'secondary': {'main': 'attributes', 'muledit': 'hidden'}, - 'metadata': {'main': 'metadata'}, - 'generic': {'main': 'relations'}, - 'generated': {'main': 'hidden'}, - } - _allowed_form_types = ('main', 'inlined', 'muledit') _allowed_values = {'main': ('attributes', 'inlined', 'relations', 'metadata', 'hidden'), diff -r 7812093e21f7 -r ab57000bff7b web/views/ajaxcontroller.py --- a/web/views/ajaxcontroller.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/ajaxcontroller.py Thu Jan 17 18:30:08 2013 +0100 @@ -28,7 +28,7 @@ functions that can be called from the javascript world. To register a new remote function, either decorate your function -with the :func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator: +with the :func:`~cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator: .. sourcecode:: python @@ -39,7 +39,7 @@ def list_users(self): return [u for (u,) in self._cw.execute('Any L WHERE U login L')] -or inherit from :class:`cubicwbe.web.views.ajaxcontroller.AjaxFunction` and +or inherit from :class:`~cubicweb.web.views.ajaxcontroller.AjaxFunction` and implement the ``__call__`` method: .. sourcecode:: python diff -r 7812093e21f7 -r ab57000bff7b web/views/authentication.py --- a/web/views/authentication.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/authentication.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b web/views/basecomponents.py --- a/web/views/basecomponents.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/basecomponents.py Thu Jan 17 18:30:08 2013 +0100 @@ -59,6 +59,14 @@ # display multilines query as one line rql = rset is not None and rset.printable_rql(encoded=False) or req.form.get('rql', '') rql = rql.replace(u"\n", u" ") + rql_suggestion_comp = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw) + if rql_suggestion_comp is not None: + # enable autocomplete feature only if the rql + # suggestions builder is available + self._cw.add_css('jquery.ui.css') + self._cw.add_js(('cubicweb.ajax.js', 'jquery.ui.js')) + self._cw.add_onload('$("#rql").autocomplete({source: "%s"});' + % (req.build_url('json', fname='rql_suggest'))) self.w(u'''
''' % (not self.cw_propval('visible') and 'hidden' or '', diff -r 7812093e21f7 -r ab57000bff7b web/views/basecontrollers.py --- a/web/views/basecontrollers.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/basecontrollers.py Thu Jan 17 18:30:08 2013 +0100 @@ -191,6 +191,7 @@ def _validation_error(req, ex): req.cnx.rollback() + ex.translate(req._) # translate messages using ui language # XXX necessary to remove existant validation error? # imo (syt), it's not necessary req.session.data.pop(req.form.get('__errorurl'), None) diff -r 7812093e21f7 -r ab57000bff7b web/views/editcontroller.py --- a/web/views/editcontroller.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/editcontroller.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b web/views/facets.py --- a/web/views/facets.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/facets.py Thu Jan 17 18:30:08 2013 +0100 @@ -26,6 +26,7 @@ from logilab.common.decorators import cachedproperty from logilab.common.registry import objectify_predicate, yes +from cubicweb import tags from cubicweb.predicates import (non_final_entity, multi_lines_rset, match_context_prop, relation_possible) from cubicweb.utils import json_dumps @@ -234,6 +235,7 @@ vid = req.form.get('vid') if self.bk_linkbox_template and req.vreg.schema['Bookmark'].has_perm(req, 'add'): w(self.bookmark_link(rset)) + w(self.focus_link(rset)) hiddens = {} for param in ('subvid', 'vtitle'): if param in req.form: @@ -269,6 +271,9 @@ req._('bookmark this search')) return self.bk_linkbox_template % bk_link + def focus_link(self, rset): + return self.bk_linkbox_template % tags.a(self._cw._('focus on this selection'), + href=self._cw.url(), id='focusLink') class FilterTable(FacetFilterMixIn, AnyRsetView): __regid__ = 'facet.filtertable' diff -r 7812093e21f7 -r ab57000bff7b web/views/magicsearch.py --- a/web/views/magicsearch.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/magicsearch.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,19 +15,23 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""a query processor to handle quick search shortcuts for cubicweb""" +"""a query processor to handle quick search shortcuts for cubicweb +""" __docformat__ = "restructuredtext en" import re from logging import getLogger -from warnings import warn + +from yams.interfaces import IVocabularyConstraint from rql import RQLSyntaxError, BadRQLQuery, parse +from rql.utils import rqlvar_maker from rql.nodes import Relation from cubicweb import Unauthorized, typed_eid from cubicweb.view import Component +from cubicweb.web.views.ajaxcontroller import ajaxfunc LOGGER = getLogger('cubicweb.magicsearch') @@ -408,3 +412,247 @@ # explicitly specified processor: don't try to catch the exception return proc.process_query(uquery) raise BadRQLQuery(self._cw._('sorry, the server is unable to handle this query')) + + + +## RQL suggestions builder #################################################### +class RQLSuggestionsBuilder(Component): + """main entry point is `build_suggestions()` which takes + an incomplete RQL query and returns a list of suggestions to complete + the query. + + This component is enabled by default and is used to provide autocompletion + in the RQL search bar. If you don't want this feature in your application, + just unregister it or make it unselectable. + + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.build_suggestions + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.etypes_suggestion_set + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_etypes + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_relations + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.vocabulary + """ + __regid__ = 'rql.suggestions' + + #: maximum number of results to fetch when suggesting attribute values + attr_value_limit = 20 + + def build_suggestions(self, user_rql): + """return a list of suggestions to complete `user_rql` + + :param user_rql: an incomplete RQL query + """ + req = self._cw + try: + if 'WHERE' not in user_rql: # don't try to complete if there's no restriction + return [] + variables, restrictions = [part.strip() for part in user_rql.split('WHERE', 1)] + if ',' in restrictions: + restrictions, incomplete_part = restrictions.rsplit(',', 1) + user_rql = '%s WHERE %s' % (variables, restrictions) + else: + restrictions, incomplete_part = '', restrictions + user_rql = variables + select = parse(user_rql, print_errors=False).children[0] + req.vreg.rqlhelper.annotate(select) + req.vreg.solutions(req, select, {}) + if restrictions: + return ['%s, %s' % (user_rql, suggestion) + for suggestion in self.rql_build_suggestions(select, incomplete_part)] + else: + return ['%s WHERE %s' % (user_rql, suggestion) + for suggestion in self.rql_build_suggestions(select, incomplete_part)] + except Exception, exc: # we never want to crash + self.debug('failed to build suggestions: %s', exc) + return [] + + ## actual completion entry points ######################################### + def rql_build_suggestions(self, select, incomplete_part): + """ + :param select: the annotated select node (rql syntax tree) + :param incomplete_part: the part of the rql query that needs + to be completed, (e.g. ``X is Pr``, ``X re``) + """ + chunks = incomplete_part.split(None, 2) + if not chunks: # nothing to complete + return [] + if len(chunks) == 1: # `incomplete` looks like "MYVAR" + return self._complete_rqlvar(select, *chunks) + elif len(chunks) == 2: # `incomplete` looks like "MYVAR some_rel" + return self._complete_rqlvar_and_rtype(select, *chunks) + elif len(chunks) == 3: # `incomplete` looks like "MYVAR some_rel something" + return self._complete_relation_object(select, *chunks) + else: # would be anything else, hard to decide what to do here + return [] + + # _complete_* methods are considered private, at least while the API + # isn't stabilized. + def _complete_rqlvar(self, select, rql_var): + """return suggestions for "variable only" incomplete_part + + as in : + + - Any X WHERE X + - Any X WHERE X is Project, Y + - etc. + """ + return ['%s %s %s' % (rql_var, rtype, dest_var) + for rtype, dest_var in self.possible_relations(select, rql_var)] + + def _complete_rqlvar_and_rtype(self, select, rql_var, user_rtype): + """return suggestions for "variable + rtype" incomplete_part + + as in : + + - Any X WHERE X is + - Any X WHERE X is Person, X firstn + - etc. + """ + # special case `user_type` == 'is', return every possible type. + if user_rtype == 'is': + return self._complete_is_relation(select, rql_var) + else: + return ['%s %s %s' % (rql_var, rtype, dest_var) + for rtype, dest_var in self.possible_relations(select, rql_var) + if rtype.startswith(user_rtype)] + + def _complete_relation_object(self, select, rql_var, user_rtype, user_value): + """return suggestions for "variable + rtype + some_incomplete_value" + + as in : + + - Any X WHERE X is Per + - Any X WHERE X is Person, X firstname " + - Any X WHERE X is Person, X firstname "Pa + - etc. + """ + # special case `user_type` == 'is', return every possible type. + if user_rtype == 'is': + return self._complete_is_relation(select, rql_var, user_value) + elif user_value: + if user_value[0] in ('"', "'"): + # if finished string, don't suggest anything + if len(user_value) > 1 and user_value[-1] == user_value[0]: + return [] + user_value = user_value[1:] + return ['%s %s "%s"' % (rql_var, user_rtype, value) + for value in self.vocabulary(select, rql_var, + user_rtype, user_value)] + return [] + + def _complete_is_relation(self, select, rql_var, prefix=''): + """return every possible types for rql_var + + :param prefix: if specified, will only return entity types starting + with the specified value. + """ + return ['%s is %s' % (rql_var, etype) + for etype in self.possible_etypes(select, rql_var, prefix)] + + def etypes_suggestion_set(self): + """returns the list of possible entity types to suggest + + The default is to return any non-final entity type available + in the schema. + + Can be overridden for instance if an application decides + to restrict this list to a meaningful set of business etypes. + """ + schema = self._cw.vreg.schema + return set(eschema.type for eschema in schema.entities() if not eschema.final) + + def possible_etypes(self, select, rql_var, prefix=''): + """return all possible etypes for `rql_var` + + The returned list will always be a subset of meth:`etypes_suggestion_set` + + :param select: the annotated select node (rql syntax tree) + :param rql_var: the variable name for which we want to know possible types + :param prefix: if specified, will only return etypes starting with it + """ + available_etypes = self.etypes_suggestion_set() + possible_etypes = set() + for sol in select.solutions: + if rql_var in sol and sol[rql_var] in available_etypes: + possible_etypes.add(sol[rql_var]) + if not possible_etypes: + # `Any X WHERE X is Person, Y is` + # -> won't have a solution, need to give all etypes + possible_etypes = available_etypes + return sorted(etype for etype in possible_etypes if etype.startswith(prefix)) + + def possible_relations(self, select, rql_var, include_meta=False): + """returns a list of couple (rtype, dest_var) for each possible + relations with `rql_var` as subject. + + ``dest_var`` will be picked among availabel variables if types match, + otherwise a new one will be created. + """ + schema = self._cw.vreg.schema + relations = set() + untyped_dest_var = rqlvar_maker(defined=select.defined_vars).next() + # for each solution + # 1. find each possible relation + # 2. for each relation: + # 2.1. if the relation is meta, skip it + # 2.2. for each possible destination type, pick up possible + # variables for this type or use a new one + for sol in select.solutions: + etype = sol[rql_var] + sol_by_types = {} + for varname, var_etype in sol.items(): + # don't push subject var to avoid "X relation X" suggestion + if varname != rql_var: + sol_by_types.setdefault(var_etype, []).append(varname) + for rschema in schema[etype].subject_relations(): + if include_meta or not rschema.meta: + for dest in rschema.objects(etype): + for varname in sol_by_types.get(dest.type, (untyped_dest_var,)): + suggestion = (rschema.type, varname) + if suggestion not in relations: + relations.add(suggestion) + return sorted(relations) + + def vocabulary(self, select, rql_var, user_rtype, rtype_incomplete_value): + """return acceptable vocabulary for `rql_var` + `user_rtype` in `select` + + Vocabulary is either found from schema (Yams) definition or + directly from database. + """ + schema = self._cw.vreg.schema + vocab = [] + for sol in select.solutions: + # for each solution : + # - If a vocabulary constraint exists on `rql_var+user_rtype`, use it + # to define possible values + # - Otherwise, query the database to fetch available values from + # database (limiting results to `self.attr_value_limit`) + try: + eschema = schema.eschema(sol[rql_var]) + rdef = eschema.rdef(user_rtype) + except KeyError: # unknown relation + continue + cstr = rdef.constraint_by_interface(IVocabularyConstraint) + if cstr is not None: + # a vocabulary is found, use it + vocab += [value for value in cstr.vocabulary() + if value.startswith(rtype_incomplete_value)] + elif rdef.final: + # no vocab, query database to find possible value + vocab_rql = 'DISTINCT Any V LIMIT %s WHERE X is %s, X %s V' % ( + self.attr_value_limit, eschema.type, user_rtype) + vocab_kwargs = {} + if rtype_incomplete_value: + vocab_rql += ', X %s LIKE %%(value)s' % user_rtype + vocab_kwargs['value'] = '%s%%' % rtype_incomplete_value + vocab += [value for value, in + self._cw.execute(vocab_rql, vocab_kwargs)] + return sorted(set(vocab)) + + + +@ajaxfunc(output_type='json') +def rql_suggest(self): + rql_builder = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw) + if rql_builder: + return rql_builder.build_suggestions(self._cw.form['term']) + return [] diff -r 7812093e21f7 -r ab57000bff7b web/views/sessions.py --- a/web/views/sessions.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/sessions.py Thu Jan 17 18:30:08 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. diff -r 7812093e21f7 -r ab57000bff7b web/views/startup.py --- a/web/views/startup.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/startup.py Thu Jan 17 18:30:08 2013 +0100 @@ -51,7 +51,7 @@ title = _('manage') http_cache_manager = httpcache.EtagHTTPCacheManager add_etype_links = () - skip_startup_views = set( ('index', 'manage', 'schema', 'owl', 'changelog', + skip_startup_views = set( ('index', 'manage', 'schema', 'owl', 'systempropertiesform', 'propertiesform', 'loggedout', 'login', 'cw.users-and-groups-management', 'cw.groups-management', diff -r 7812093e21f7 -r ab57000bff7b web/views/tableview.py --- a/web/views/tableview.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/tableview.py Thu Jan 17 18:30:08 2013 +0100 @@ -290,20 +290,17 @@ return attrs def render_actions(self, w, actions): - box = MenuWidget('', '', _class='tableActionsBox', islist=False) - label = tags.span(self._cw._('action menu')) - menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox', - ident='%sActions' % self.view.domid) - box.append(menu) + w(u'
') for action in actions: - menu.append(action) - box.render(w=w) - w(u'
') + w(u'') + action.render(w) + w(u'') + w(u'
') def show_hide_filter_actions(self, currentlydisplayed=False): divid = self.view.domid showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:] - for what in ('Form', 'Show', 'Hide', 'Actions')) + for what in ('Form', 'Actions')) showhide = 'javascript:' + showhide self._cw.add_onload(u'''\ $(document).ready(function() { @@ -313,10 +310,8 @@ $('#%(id)sShow').attr('class', 'hidden'); } });''' % {'id': divid}) - showlabel = self._cw._('show filter form') - hidelabel = self._cw._('hide filter form') - return [component.Link(showhide, showlabel, id='%sShow' % divid), - component.Link(showhide, hidelabel, id='%sHide' % divid)] + showlabel = self._cw._('toggle filter') + return [component.Link(showhide, showlabel, id='%sToggle' % divid)] class AbstractColumnRenderer(object): diff -r 7812093e21f7 -r ab57000bff7b web/views/urlrewrite.py --- a/web/views/urlrewrite.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/urlrewrite.py Thu Jan 17 18:30:08 2013 +0100 @@ -109,7 +109,6 @@ (rgx('/doc/images/(.+?)/?'), dict(vid='wdocimages', fid=r'\1')), (rgx('/doc/?'), dict(vid='wdoc', fid=r'main')), (rgx('/doc/(.+?)/?'), dict(vid='wdoc', fid=r'\1')), - (rgx('/changelog/?'), dict(vid='changelog')), ] def rewrite(self, req, uri): diff -r 7812093e21f7 -r ab57000bff7b web/views/wdoc.py --- a/web/views/wdoc.py Thu Jan 17 17:12:06 2013 +0100 +++ b/web/views/wdoc.py Thu Jan 17 18:30:08 2013 +0100 @@ -205,60 +205,6 @@ self.w(open(join(resourcedir, rid)).read()) -class ChangeLogView(StartupView): - __regid__ = 'changelog' - title = _('What\'s new?') - maxentries = 25 - - def call(self): - rid = 'ChangeLog_%s' % (self._cw.lang) - allentries = [] - title = self._cw._(self.title) - restdata = ['.. -*- coding: utf-8 -*-', '', title, '='*len(title), ''] - w = restdata.append - today = date.today() - for fpath in self._cw.vreg.config.locate_all_files(rid): - cl = ChangeLog(fpath) - encoding = 'utf-8' - # additional content may be found in title - for line in (cl.title + cl.additional_content).splitlines(): - m = CHARSET_DECL_RGX.search(line) - if m is not None: - encoding = m.group(1) - continue - elif line.startswith('.. '): - w(unicode(line, encoding)) - for entry in cl.entries: - if entry.date: - edate = todate(strptime(entry.date, '%Y-%m-%d')) - else: - edate = today - messages = [] - for msglines, submsgs in entry.messages: - msgstr = unicode(' '.join(l.strip() for l in msglines), encoding) - msgstr += u'\n\n' - for submsglines in submsgs: - msgstr += ' - ' + unicode(' '.join(l.strip() for l in submsglines), encoding) - msgstr += u'\n' - messages.append(msgstr) - entry = (edate, messages) - allentries.insert(bisect_right(allentries, entry), entry) - latestdate = None - i = 0 - for edate, messages in reversed(allentries): - if latestdate != edate: - fdate = self._cw.format_date(edate) - w(u'\n%s' % fdate) - w('~' * len(fdate)) - latestdate = edate - for msg in messages: - w(u'* %s' % msg) - i += 1 - if i > self.maxentries: - break - w('') # blank line - self.w(rest_publish(self, '\n'.join(restdata))) - class HelpAction(action.Action): __regid__ = 'help' @@ -271,17 +217,6 @@ def url(self): return self._cw.build_url('doc/main') -class ChangeLogAction(action.Action): - __regid__ = 'changelog' - __select__ = yes() - - category = 'footer' - order = 1 - title = ChangeLogView.title - - def url(self): - return self._cw.build_url('changelog') - class AboutAction(action.Action): __regid__ = 'about' diff -r 7812093e21f7 -r ab57000bff7b web/wdoc/ChangeLog_en --- a/web/wdoc/ChangeLog_en Thu Jan 17 17:12:06 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -.. -*- coding: utf-8 -*- -.. _`user preferences`: myprefs -.. _here: sparql -.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/ -.. _schema: schema -.. _OWL: http://www.w3.org/TR/owl-features/ -.. _CWSource: cwetype/CWSource - -2010-10-17 -- 3.10.0 - - * facets on optional relations now propose to search to entities - which miss the relation - - * box and content navigation components have been merged, so this - simplify the configuration and you should be able to move - boxes/components in various places of the interface - - * global / context dependant boxes should now be more - distinguishable to make the interface more intuitive - - * upgraded jQuery and jQuery UI respectively to version 1.4.2 and - 1.8. - - * data sources are now stored as CWSource_ entities in the database. - This allows on multi-sources instance to filter search results - according to the source entities are coming from. - - -2010-06-11 -- 3.8.4 - * support full text prefix search for instances using postgres > 8.4 as database: try it - by using search such as 'cubic*' - - -2010-04-20 -- 3.8.0 - * nicer schema_ and workflow views (clickable image!) - - * more power to undo, though not yet complete (missing entity updates, soon available...) - - * pdf export functionnality moved to its own cube. If it's no more - present on this site while you found it useful, ask you site - administrator to install the pdfexport_ cube. - - -2010-03-16 -- 3.7.0 - * experimental support for undoing of deletion. If you're not proposed to *undo* - deletion of one or several entities, ask you site administrator to activate - the feature. - - -2010-02-10 -- 3.6.0 - * nice 'demo widget' to edit bookmark's path, e.g. a relative url, splitted - as path and parameters and dealing nicely with url encodings. Try to - edit your bookmarks! - - * hell lot of refactorings, but you should hopefuly not see that from the outside - -2009-09-17 -- 3.5.0 - - * selectable workflows: authorized users may change the workflow used - by some workflowable entities - - -2009-08-07 -- 3.4.0 - - * support for SPARQL_. Click here_ to test it. - - * and another step toward the semantic web: new `ExternalUri` entity type - with its associated `same_as` relation. See - http://www.w3.org/TR/owl-ref/#sameAs-def for more information and check - this instance schema_ to see on which entity types it may be applied - - * new "view workflow" and "view history" items in the workflow - sub-menu of the actions box - - * you can now edit comments of workflow transition afterward (if authorized, - of course) - - * modification date of an entity is updated when its state is changed - - -2009-02-26 -- 3.1.0 - - * schema may be exported as OWL_ - - * new ajax interface for site configuration / `user preferences`_ - - -2008-10-24 -- 2.99.0 - * cubicweb is now open source ! diff -r 7812093e21f7 -r ab57000bff7b web/wdoc/ChangeLog_fr --- a/web/wdoc/ChangeLog_fr Thu Jan 17 17:12:06 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -.. -*- coding: utf-8 -*- -.. _`préférences utilisateurs`: myprefs#fieldset_ui -.. _ici: sparql -.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/ -.. _schema: schema -.. _OWL: http://www.w3.org/TR/owl-features/ -.. _pdfexport: http://www.cubicweb.org/project/cubicweb-pdfexport -.. _CWSource: cwetype/CWSource - -2010-10-07 -- 3.10.0 - - * les facettes sur les relations optionnelles proposent maintenant - de filter les entité qui n'ont *pas* la relation - - * les boîtes et composants contextuels ont été fusionnés, permettant - de simplifier la configuration et de placer ces nouveaux composants - comme vous le désirez - - * les boîtes globales ou dépendantes du contexte sont plus - facilement distinguable pour rendre l'interface plus intuitive - - * passage à jQuery 1.4.2, et jQuery UI 1.8 - - * les sources de données sont maintenant stockées dans la base de - données sous forme d'entités CWSource_. Cela permet sur les - instances multi-source de filter les résultats de recherche en - fonction de la source dont viennent les entités. - - -2010-06-11 -- 3.8.4 - * support pour la recherche de préfixe pour les instances utilisant postgres > 8.4 : - essayez en cherchant par ex. 'cubic*' - -2010-04-20 -- 3.8.0 - - * amélioration des vues de schema_ et des vues de workflows - (images clickable !) - - * meilleure support du "undo", mais il manque toujours le support - sur la modification d'entité (bientôt...) - - * la fonctionnalité d'export d'pdf a été déplacé dans son propre - cube. Si cette fonctionalité n'est plus disponible sur ce site et - que vous la trouviez utile, demander à l'administrateur - d'installer le cube pdfexport_. - - -2010-03-16 -- 3.7.0 - - * support experimental pour l'annulation ("undo") de la - suppression. Si, après une suppression d'une ou plusieurs - entités, on ne vous propose pas d'annuler l'opération, demander à - l'administrateur d'activé la fonctionnalité - - -2010-02-10 -- 3.6.0 - - * nouvelle widget (de démonstration :) pour éditer le chemin des - signets. Celui-ci, une url relative finalement, est décomposée de - chemin et paramètres que vous pouvez éditer individuellement et - surtout lisiblement car la gestion de l'échappement de l'url est - géré de manière transparente - - * beaucoup de refactoring, mais vous ne devriez rien remarquer :) - -2009-09-17 -- 3.5.0 - - * workflow sélectionnable: les utilisateurs autorisés peuvent - changer le workflow à utilister pour les entités le supportant - - -2009-08-07 -- 3.4.0 - - * support de SPARQL_. Cliquez ici_ pour le tester. - - * et encore un pas vers le web sémantique : un nouveau type d'entité - `ExternalUri` et la relation associée `same_as`. Voir - http://www.w3.org/TR/owl-ref/#sameAs-def pour plus d'information, ainsi - que le schema_ de cette instance pour voir à quels types d'entités cela - s'applique. - - * nouveau liens "voir les états possibles" et "voir l'historique" dans le sous-menu - workflow de la boite actions - - * vous pouvez dorénavant éditer les commentaires des passages de transition - depuis l'historique, pour peu que vous ayez les droits nécessaire bien sûr - - * la date de modification d'une entité est mise à jour lorsque son état est changé - - -2009-02-26 -- 3.1.0 - - * le schéma peut être exporté en OWL_ - - * nouvelle interface ajax pour la configuration du site et les `préférences utilisateurs`_ - - - diff -r 7812093e21f7 -r ab57000bff7b wsgi/handler.py --- a/wsgi/handler.py Thu Jan 17 17:12:06 2013 +0100 +++ b/wsgi/handler.py Thu Jan 17 18:30:08 2013 +0100 @@ -106,8 +106,8 @@ def __init__(self, config, vreg=None): self.appli = CubicWebPublisher(config, vreg=vreg) self.config = config - self.base_url = config['base-url'] - self.https_url = config['https-url'] + self.base_url = self.config['base-url'] + self.https_url = self.config['https-url'] self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter') def _render(self, req):