# HG changeset patch # User Philippe Pepiot # Date 1583399738 -3600 # Node ID 2cc3f481ecd02a092162ef2c34d1dd205fb6bb82 # Parent 0df0db725f07cea86caad147c16f91863429af93# Parent af59ca8b23d0ab1c53ca0116987ccd4e89c34410 Merge 3.27 diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/__pkginfo__.py --- a/cubicweb/__pkginfo__.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/__pkginfo__.py Thu Mar 05 10:15:38 2020 +0100 @@ -22,8 +22,8 @@ modname = distname = "cubicweb" -numversion = (3, 27, 2) -version = '.'.join(str(num) for num in numversion) +numversion = (3, 28, 0) +version = '.'.join(str(num) for num in numversion) + '.dev0' description = "a repository of entities / relations for knowledge management" author = "Logilab" diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/cwvreg.py --- a/cubicweb/cwvreg.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/cwvreg.py Thu Mar 05 10:15:38 2020 +0100 @@ -550,7 +550,7 @@ return RQLHelper(self.schema, special_relations={'eid': 'uid', 'has_text': 'fti'}) - def solutions(self, req, rqlst, args): + def compute_var_types(self, req, rqlst, args): def type_from_eid(eid, req=req): return req.entity_type(eid) return self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args) @@ -558,7 +558,7 @@ def parse(self, req, rql, args=None): rqlst = self.rqlhelper.parse(rql) try: - self.solutions(req, rqlst, args) + self.compute_var_types(req, rqlst, args) except UnknownEid: for select in rqlst.children: select.solutions = [] diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/devtools/__init__.py --- a/cubicweb/devtools/__init__.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/devtools/__init__.py Thu Mar 05 10:15:38 2020 +0100 @@ -624,14 +624,14 @@ def init_test_database(self): """initialize a fresh postgresql database used for testing purpose""" from cubicweb.server import init_repository - from cubicweb.server.serverctl import system_source_cnx, createdb + from cubicweb.server.serverctl import source_cnx, createdb # connect on the dbms system base to create our base try: self._drop(self.system_source['db-name']) createdb(self.helper, self.system_source, self.dbcnx, self.cursor) self.dbcnx.commit() - cnx = system_source_cnx(self.system_source, special_privs='LANGUAGE C', - interactive=False) + cnx = source_cnx(self.system_source, special_privs='LANGUAGE C', + interactive=False) templcursor = cnx.cursor() try: # XXX factorize with db-create code diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/devtools/repotest.py --- a/cubicweb/devtools/repotest.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/devtools/repotest.py Thu Mar 05 10:15:38 2020 +0100 @@ -209,7 +209,7 @@ def _prepare_plan(self, cnx, rql, kwargs=None): rqlst = self.repo.vreg.rqlhelper.parse(rql, annotate=True) - self.repo.vreg.solutions(cnx, rqlst, kwargs) + self.repo.vreg.compute_var_types(cnx, rqlst, kwargs) if rqlst.TYPE == 'select': self.repo.vreg.rqlhelper.annotate(rqlst) for select in rqlst.children: diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/entities/adapters.py --- a/cubicweb/entities/adapters.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/entities/adapters.py Thu Mar 05 10:15:38 2020 +0100 @@ -20,18 +20,62 @@ """ from cubicweb import _ +from hashlib import sha1 from itertools import chain +from rdflib import URIRef, Literal + from logilab.mtconverter import TransformError -from logilab.common.decorators import cached +from logilab.common.decorators import cached, cachedproperty -from cubicweb import (Unauthorized, ValidationError, view, ViolatedConstraint, +from cubicweb.entity import EntityAdapter +from cubicweb import (Unauthorized, ValidationError, ViolatedConstraint, UniqueTogetherError) from cubicweb.schema import constraint_name_for from cubicweb.predicates import is_instance, relation_possible, match_exception +from cubicweb.rdf import NAMESPACES -class IDublinCoreAdapter(view.EntityAdapter): + +class EntityRDFAdapter(EntityAdapter): + """EntityRDFAdapter is to be specialized for each entity that wants to + be converted to RDF using the mechanism from cubicweb.rdf + """ + __abstract__ = True + + def __init__(self, _cw, **kwargs): + super().__init__(_cw, **kwargs) + self.entity.complete() + + @cachedproperty + def uri(self): + return self.entity.cwuri + + def triples(self): + """return sequence of 3-tuple of rdflib identifiers""" + raise NotImplementedError() + + +class CWUserFoafAdapter(EntityRDFAdapter): + __regid__ = "rdf.foaf" + __select__ = is_instance("CWUser") + + def triples(self): + RDF = NAMESPACES["rdf"] + FOAF = NAMESPACES["foaf"] + uri = URIRef(self.uri) + yield (uri, RDF.type, FOAF.Person) + if self.entity.surname: + yield (uri, FOAF.familyName, Literal(self.entity.surname)) + if self.entity.firstname: + yield (uri, FOAF.givenName, Literal(self.entity.firstname)) + emailaddr = self.entity.cw_adapt_to("IEmailable").get_email() + if emailaddr: + email_digest = sha1(emailaddr.encode("utf-8")).hexdigest() + yield (uri, FOAF.mbox_sha1sum, Literal(email_digest)) + + +class IDublinCoreAdapter(EntityAdapter): __regid__ = 'IDublinCore' __select__ = is_instance('Any') @@ -93,7 +137,7 @@ return self._cw._(self._cw.vreg.property_value('ui.language')) -class IEmailableAdapter(view.EntityAdapter): +class IEmailableAdapter(EntityAdapter): __regid__ = 'IEmailable' __select__ = relation_possible('primary_email') | relation_possible('use_email') @@ -126,7 +170,7 @@ for attr in self.allowed_massmail_keys()) -class INotifiableAdapter(view.EntityAdapter): +class INotifiableAdapter(EntityAdapter): __regid__ = 'INotifiable' __select__ = is_instance('Any') @@ -145,7 +189,7 @@ return () -class IFTIndexableAdapter(view.EntityAdapter): +class IFTIndexableAdapter(EntityAdapter): """standard adapter to handle fulltext indexing .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers @@ -226,7 +270,7 @@ maindict.setdefault(weight, []).extend(words) -class IDownloadableAdapter(view.EntityAdapter): +class IDownloadableAdapter(EntityAdapter): """interface for downloadable entities""" __regid__ = 'IDownloadable' __abstract__ = True @@ -256,7 +300,7 @@ # XXX should propose to use two different relations for children/parent -class ITreeAdapter(view.EntityAdapter): +class ITreeAdapter(EntityAdapter): """This adapter provides a tree interface. It has to be overriden to be configured using the tree_relation, @@ -412,7 +456,7 @@ return path -class ISerializableAdapter(view.EntityAdapter): +class ISerializableAdapter(EntityAdapter): """Adapter to serialize an entity to a bare python structure that may be directly serialized to e.g. JSON. """ @@ -441,7 +485,7 @@ # error handling adapters ###################################################### -class IUserFriendlyError(view.EntityAdapter): +class IUserFriendlyError(EntityAdapter): __regid__ = 'IUserFriendlyError' __abstract__ = True diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/entities/wfobjs.py --- a/cubicweb/entities/wfobjs.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/entities/wfobjs.py Thu Mar 05 10:15:38 2020 +0100 @@ -24,7 +24,7 @@ from logilab.common.decorators import cached, clear_cache from cubicweb.entities import AnyEntity, fetch_config -from cubicweb.view import EntityAdapter +from cubicweb.entity import EntityAdapter from cubicweb.predicates import relation_possible diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/entity.py --- a/cubicweb/entity.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/entity.py Thu Mar 05 10:15:38 2020 +0100 @@ -1118,7 +1118,7 @@ etypecls.fetch_rqlst(self._cw.user, select, searchedvar, ordermethod=ordermethod) # from now on, we need variable type resolving - self._cw.vreg.solutions(self._cw, select, args) + self._cw.vreg.compute_var_types(self._cw, select, args) # insert RQL expressions for schema constraints into the rql syntax tree if vocabconstraints: cstrcls = (RQLVocabularyConstraint, RQLConstraint) @@ -1314,6 +1314,28 @@ def __set__(self, eobj, value): raise NotImplementedError +# entity adapters ############################################################# + +class Adapter(AppObject): + """base class for adapters""" + __registry__ = 'adapters' + + +class EntityAdapter(Adapter): + """base class for entity adapters (eg adapt an entity to an interface) + + An example would be: + + >>> some_entity.cw_adapt_to('IDownloadable') + """ + def __init__(self, _cw, **kwargs): + try: + self.entity = kwargs.pop('entity') + except KeyError: + self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0, + kwargs.get('col') or 0) + Adapter.__init__(self, _cw, **kwargs) + from logging import getLogger from cubicweb import set_log_methods diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/pyramid/predicates.py --- a/cubicweb/pyramid/predicates.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/pyramid/predicates.py Thu Mar 05 10:15:38 2020 +0100 @@ -21,6 +21,8 @@ """Contains predicates used in Pyramid views. """ +from cubicweb._exceptions import UnknownEid + class MatchIsETypePredicate(object): """A predicate that match if a given etype exist in schema. @@ -38,5 +40,30 @@ request.registry['cubicweb.registry'].case_insensitive_etypes +class MatchIsEIDPredicate(object): + """A predicate that match if a given eid exist in the database. + """ + def __init__(self, matchname, config): + self.matchname = matchname + + def text(self): + return 'match_is_eid = %s' % self.matchname + + phash = text + + def __call__(self, info, request): + try: + eid = int(info['match'][self.matchname]) + except ValueError: + return False + + try: + request.cw_cnx.entity_from_eid(eid) + except UnknownEid: + return False + return True + + def includeme(config): config.add_route_predicate('match_is_etype', MatchIsETypePredicate) + config.add_route_predicate('match_is_eid', MatchIsEIDPredicate) diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/rdf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/rdf.py Thu Mar 05 10:15:38 2020 +0100 @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# copyright 2019 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +from rdflib import ConjunctiveGraph, plugin +from rdflib.namespace import Namespace, RDF, FOAF +import rdflib_jsonld # noqa + +plugin.register("jsonld", plugin.Serializer, "rdflib_jsonld.serializer", "JsonLDSerializer") + +RDF_MIMETYPE_TO_FORMAT = { + 'application/rdf+xml': 'xml', + 'text/turtle': 'turtle', + 'text/n3': 'n3', + 'application/n-quads': 'nquads', + 'application/n-triples': 'nt', + 'application/trig': 'trig', + 'application/ld+json': 'json-ld', +} + +NAMESPACES = { + "rdf": RDF, + "schema": Namespace("http://schema.org/"), + "foaf": FOAF, +} + + +# dict: name of CWEType -> list of regid of adapters derived from EntityRDFAdapter +ETYPES_ADAPTERS = { + "CWUser": ("rdf.foaf",), +} + + +def conjunctive_graph(): + """factory to build a ``ConjunctiveGraph`` and bind all namespaces + """ + graph = ConjunctiveGraph() + for prefix, rdfns in NAMESPACES.items(): + graph.bind(prefix, rdfns) + return graph + + +def iter_rdf_adapters(entity): + for adapter_id in ETYPES_ADAPTERS.get(entity.__regid__, ()): + adapter = entity.cw_adapt_to(adapter_id) + if adapter: + yield adapter + + +def add_entity_to_graph(graph, entity): + for adapter in iter_rdf_adapters(entity): + for triple in adapter.triples(): + graph.add(triple) diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/rqlrewrite.py --- a/cubicweb/rqlrewrite.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/rqlrewrite.py Thu Mar 05 10:15:38 2020 +0100 @@ -245,7 +245,7 @@ vreg = session.vreg self.schema = vreg.schema self.annotate = vreg.rqlhelper.annotate - self._compute_solutions = vreg.solutions + self._compute_solutions = vreg.compute_var_types def compute_solutions(self): self.annotate(self.select) diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/server/hook.py --- a/cubicweb/server/hook.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/server/hook.py Thu Mar 05 10:15:38 2020 +0100 @@ -248,6 +248,7 @@ from logging import getLogger from itertools import chain +from typing import Union, Tuple from logilab.common.decorators import classproperty, cached from logilab.common.logging_ext import set_log_methods @@ -521,8 +522,8 @@ """ __select__ = enabled_category() # set this in derivated classes - events = None - category = None + events: Union[None, Tuple[str], Tuple[str, str]] = None + category: Union[None, str] = None order = 0 # stop pylint from complaining about missing attributes in Hooks classes eidfrom = eidto = entity = rtype = repo = None diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/server/querier.py --- a/cubicweb/server/querier.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/server/querier.py Thu Mar 05 10:15:38 2020 +0100 @@ -36,9 +36,9 @@ from cubicweb.utils import QueryCache, RepeatList from cubicweb.misc.source_highlight import highlight_terminal -from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata +from cubicweb.server.rqlannotation import RQLAnnotator, set_qdata from cubicweb.server.ssplanner import (READ_ONLY_RTYPES, add_types_restriction, - SSPlanner) + prepare_plan) from cubicweb.server.edition import EditedEntity from cubicweb.statsd_logger import statsd_timeit, statsd_c @@ -154,20 +154,15 @@ class ExecutionPlan(object): """the execution model of a rql query, composed of querier steps""" - def __init__(self, querier, rqlst, args, cnx): + def __init__(self, schema, rqlst, args, cnx): + self.schema = schema # original rql syntax tree self.rqlst = rqlst self.args = args or {} # cnx executing the query self.cnx = cnx - # quick reference to the system source - self.syssource = cnx.repo.system_source # execution steps self.steps = [] - # various resource accesors - self.querier = querier - self.schema = querier.schema - self.rqlhelper = cnx.vreg.rqlhelper # tracing token for debugging self.rql_query_tracing_token = None @@ -176,7 +171,7 @@ self.steps.append(step) def sqlexec(self, sql, args=None): - return self.syssource.sqlexec(self.cnx, sql, args) + return self.cnx.repo.system_source.sqlexec(self.cnx, sql, args) def execute(self): """execute a plan and return resulting rows""" @@ -215,8 +210,8 @@ else: noinvariant = () if cached is None: - self.rqlhelper.simplify(union) - self.querier.sqlgen_annotate(union) + self.cnx.vreg.rqlhelper.simplify(union) + RQLAnnotator(self.schema).annotate(union) set_qdata(self.schema.rschema, union, noinvariant) if union.has_text_query: self.cache_key = None @@ -314,7 +309,7 @@ sol[newvarname] = nvartype select.clean_solutions(solutions) add_types_restriction(self.schema, select) - self.rqlhelper.annotate(rqlst) + self.cnx.vreg.rqlhelper.annotate(rqlst) self.preprocess(rqlst, security=False) return rqlst @@ -323,8 +318,8 @@ """an execution model specific to the INSERT rql query """ - def __init__(self, querier, rqlst, args, cnx): - ExecutionPlan.__init__(self, querier, rqlst, args, cnx) + def __init__(self, schema, rqlst, args, cnx): + ExecutionPlan.__init__(self, schema, rqlst, args, cnx) # save originally selected variable, we may modify this # dictionary for substitution (query parameters) self.selected = rqlst.selection @@ -478,10 +473,6 @@ def set_schema(self, schema): self.schema = schema self.clear_caches() - # rql planner - self._planner = SSPlanner(schema, self._repo.vreg.rqlhelper) - # sql generation annotator - self.sqlgen_annotate = SQLGenAnnotator(schema).annotate def clear_caches(self, eids=None, etypes=None): if eids is None: @@ -496,8 +487,8 @@ def plan_factory(self, rqlst, args, cnx): """create an execution plan for an INSERT RQL query""" if rqlst.TYPE == 'insert': - return InsertPlan(self, rqlst, args, cnx) - return ExecutionPlan(self, rqlst, args, cnx) + return InsertPlan(self.schema, rqlst, args, cnx) + return ExecutionPlan(self.schema, rqlst, args, cnx) @statsd_timeit def execute(self, cnx, rql, args=None, build_descr=True): @@ -555,7 +546,7 @@ plan = self.plan_factory(rqlst, args, cnx) plan.cache_key = cachekey plan.rql_query_tracing_token = str(uuid.uuid4()) - self._planner.build_plan(plan) + prepare_plan(plan, self.schema, self._repo.vreg.rqlhelper) query_debug_informations = { "rql": rql, @@ -567,7 +558,6 @@ } start = time.time() - # execute the plan try: results = plan.execute() @@ -634,7 +624,7 @@ # some cache usage stats self.cache_hit, self.cache_miss = 0, 0 # rql parsing / analysing helper - self.solutions = repo.vreg.solutions + self.compute_var_types = repo.vreg.compute_var_types rqlhelper = repo.vreg.rqlhelper # set backend on the rql helper, will be used for function checking rqlhelper.backend = repo.config.system_source_config['db-driver'] @@ -677,7 +667,7 @@ # which are eids. Notice that if you may not need `eidkeys`, we # have to compute solutions anyway (kept as annotation on the # tree) - eidkeys = self.solutions(cnx, rqlst, args) + eidkeys = self.compute_var_types(cnx, rqlst, args) if args and rql not in self._ck_cache: self._ck_cache[rql] = eidkeys if eidkeys: diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/server/rqlannotation.py --- a/cubicweb/server/rqlannotation.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/server/rqlannotation.py Thu Mar 05 10:15:38 2020 +0100 @@ -117,7 +117,8 @@ var._q_invariant = False -class SQLGenAnnotator(object): +class RQLAnnotator(object): + """Annotate the RQL abstract syntax tree to inform the SQL generation""" def __init__(self, schema): self.schema = schema diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/server/serverctl.py --- a/cubicweb/server/serverctl.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/server/serverctl.py Thu Mar 05 10:15:38 2020 +0100 @@ -26,6 +26,7 @@ from logilab.common.configuration import Configuration, merge_options from logilab.common.shellutils import ASK, generate_password +from logilab.common.deprecation import deprecated from logilab.database import get_db_helper, get_connection @@ -96,6 +97,9 @@ return cnx +@deprecated('[3.28] instead of system_source_cnx(source, True, **args) use ' + 'source_cnx(source, get_db_helper(source[\'db-driver\']).system_database(), ' + '**args)') def system_source_cnx(source, dbms_system_base=False, special_privs='CREATE/DROP DATABASE', interactive=True): """shortcut to get a connextion to the instance system database @@ -118,8 +122,9 @@ import logilab.common as lgp lgp.USE_MX_DATETIME = False # connect on the dbms system base to create our base - cnx = system_source_cnx(source, True, special_privs=special_privs, - interactive=interactive) + system_db = get_db_helper(source['db-driver']).system_database() + cnx = source_cnx(source, system_db, special_privs=special_privs, + interactive=interactive) # disable autocommit (isolation_level(1)) because DROP and # CREATE DATABASE can't be executed in a transaction set_isolation_level = getattr(cnx, 'set_isolation_level', None) @@ -201,7 +206,7 @@ @contextmanager def db_transaction(source, privilege): """Open a transaction to the instance database""" - cnx = system_source_cnx(source, special_privs=privilege) + cnx = source_cnx(source, special_privs=privilege) cursor = cnx.cursor() try: yield cursor @@ -379,8 +384,8 @@ except BaseException: dbcnx.rollback() raise - cnx = system_source_cnx(source, special_privs='CREATE LANGUAGE/SCHEMA', - interactive=not automatic) + cnx = source_cnx(source, special_privs='CREATE LANGUAGE/SCHEMA', + interactive=not automatic) cursor = cnx.cursor() helper.init_fti_extensions(cursor) namespace = source.get('db-namespace') @@ -572,7 +577,7 @@ config = ServerConfiguration.config_for(appid) source = config.system_source_config set_owner = self.config.set_owner - cnx = system_source_cnx(source, special_privs='GRANT') + cnx = source_cnx(source, special_privs='GRANT') cursor = cnx.cursor() schema = config.load_schema() try: diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/server/sources/native.py --- a/cubicweb/server/sources/native.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/server/sources/native.py Thu Mar 05 10:15:38 2020 +0100 @@ -48,11 +48,10 @@ from cubicweb.server import schema2sql as y2sql from cubicweb.server.utils import crypt_password, verify_and_update from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn -from cubicweb.server.rqlannotation import set_qdata +from cubicweb.server.rqlannotation import RQLAnnotator, set_qdata from cubicweb.server.hook import CleanupDeletedEidsCacheOp from cubicweb.server.edition import EditedEntity -from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results -from cubicweb.server.sources.rql2sql import SQLGenerator +from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results, rql2sql from cubicweb.misc.source_highlight import highlight_terminal from cubicweb.statsd_logger import statsd_timeit @@ -260,7 +259,7 @@ class NativeSQLSource(SQLAdapterMixIn, AbstractSource): """adapter for source using the native cubicweb schema (see below) """ - sqlgen_class = SQLGenerator + options = ( ('db-driver', {'type': 'string', @@ -333,8 +332,8 @@ self.authentifiers.insert(0, EmailPasswordAuthentifier(self)) AbstractSource.__init__(self, repo, source_config, *args, **kwargs) # sql generator - self._rql_sqlgen = self.sqlgen_class(self.schema, self.dbhelper, - ATTR_MAP.copy()) + self._rql_sqlgen = rql2sql.SQLGenerator(self.schema, self.dbhelper, + ATTR_MAP.copy()) # full text index helper self.do_fti = not repo.config['delay-full-text-indexation'] # sql queries cache @@ -473,7 +472,7 @@ rqlst = self.repo.vreg.rqlhelper.parse(rql) rqlst.restricted_vars = () rqlst.children[0].solutions = sols - self.repo.querier.sqlgen_annotate(rqlst) + RQLAnnotator(self.repo.querier.schema).annotate(rqlst) set_qdata(self.schema.rschema, rqlst, ()) return rqlst diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/server/ssplanner.py --- a/cubicweb/server/ssplanner.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/server/ssplanner.py Thu Mar 05 10:15:38 2020 +0100 @@ -307,6 +307,11 @@ return self.build_select_plan(plan, union) +def prepare_plan(plan, schema, rqlhelper): + """Add steps to a plan to prepare it for execution""" + return SSPlanner(schema, rqlhelper).build_plan(plan) + + # execution steps and helper functions ######################################## class Step(object): diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/server/test/unittest_rqlannotation.py --- a/cubicweb/server/test/unittest_rqlannotation.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/server/test/unittest_rqlannotation.py Thu Mar 05 10:15:38 2020 +0100 @@ -22,14 +22,14 @@ from cubicweb.devtools.repotest import BaseQuerierTC -class SQLGenAnnotatorTC(BaseQuerierTC): +class RQLAnnotatorTC(BaseQuerierTC): def setUp(self): handler = devtools.get_test_db_handler(devtools.TestServerConfiguration('data', __file__)) handler.build_db_cache() repo, _cnx = handler.get_repo_and_cnx() self.__class__.repo = repo - super(SQLGenAnnotatorTC, self).setUp() + super(RQLAnnotatorTC, self).setUp() def test_0_1(self): with self.admin_access.cnx() as cnx: diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/server/test/unittest_server_security.py --- a/cubicweb/server/test/unittest_server_security.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/server/test/unittest_server_security.py Thu Mar 05 10:15:38 2020 +0100 @@ -44,7 +44,7 @@ nom = self.repo.schema['Personne'].rdef('nom') with self.temporary_permissions((nom, {'read': ('users', 'managers')})): with self.admin_access.repo_cnx() as cnx: - self.repo.vreg.solutions(cnx, rqlst, None) + self.repo.vreg.compute_var_types(cnx, rqlst, None) check_relations_read_access(cnx, rqlst, {}) with self.new_access(u'anon').repo_cnx() as cnx: self.assertRaises(Unauthorized, @@ -57,7 +57,7 @@ rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0] with self.temporary_permissions(Personne={'read': ('users', 'managers')}): with self.admin_access.repo_cnx() as cnx: - self.repo.vreg.solutions(cnx, rqlst, None) + self.repo.vreg.compute_var_types(cnx, rqlst, None) solution = rqlst.solutions[0] localchecks = get_local_checks(cnx, rqlst, solution) self.assertEqual({}, localchecks) @@ -520,7 +520,7 @@ ERQLExpression('X owned_by U'))}): with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx: rqlst = self.repo.vreg.rqlhelper.parse('Any X WHERE X is_instance_of Societe') - self.repo.vreg.solutions(cnx, rqlst, {}) + self.repo.vreg.compute_var_types(cnx, rqlst, {}) self.repo.vreg.rqlhelper.annotate(rqlst) plan = cnx.repo.querier.plan_factory(rqlst, {}, cnx) plan.preprocess(rqlst) diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/test/unittest_predicates.py --- a/cubicweb/test/unittest_predicates.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/test/unittest_predicates.py Thu Mar 05 10:15:38 2020 +0100 @@ -29,7 +29,7 @@ multi_lines_rset, score_entity, is_in_state, rql_condition, relation_possible, match_form_params, paginated_rset) -from cubicweb.view import EntityAdapter +from cubicweb.entity import EntityAdapter from cubicweb.web import action diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/test/unittest_rqlrewrite.py --- a/cubicweb/test/unittest_rqlrewrite.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/test/unittest_rqlrewrite.py Thu Mar 05 10:15:38 2020 +0100 @@ -58,7 +58,7 @@ schema = schema @staticmethod - def solutions(sqlcursor, rqlst, kwargs): + def compute_var_types(sqlcursor, rqlst, kwargs): rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs) class rqlhelper: @@ -602,7 +602,7 @@ args = {} union = parse(rql) # self.vreg.parse(rql, annotate=True) with self.admin_access.repo_cnx() as cnx: - self.vreg.solutions(cnx, union, args) + self.vreg.compute_var_types(cnx, union, args) self.vreg.rqlhelper.annotate(union) plan = self.repo.querier.plan_factory(union, args, cnx) plan.preprocess(union) diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/test/unittest_vregistry.py --- a/cubicweb/test/unittest_vregistry.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/test/unittest_vregistry.py Thu Mar 05 10:15:38 2020 +0100 @@ -23,7 +23,7 @@ from cubicweb import CW_SOFTWARE_ROOT as BASE, devtools from cubicweb.cwvreg import CWRegistryStore, UnknownProperty from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.view import EntityAdapter +from cubicweb.entity import EntityAdapter class YesSchema: diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/view.py --- a/cubicweb/view.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/view.py Thu Mar 05 10:15:38 2020 +0100 @@ -554,18 +554,7 @@ def domid(self): return '%sComponent' % domid(self.__regid__) - -class Adapter(AppObject): - """base class for adapters""" - __registry__ = 'adapters' - - -class EntityAdapter(Adapter): - """base class for entity adapters (eg adapt an entity to an interface)""" - def __init__(self, _cw, **kwargs): - try: - self.entity = kwargs.pop('entity') - except KeyError: - self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0, - kwargs.get('col') or 0) - Adapter.__init__(self, _cw, **kwargs) +# EntityAdapter moved to cubicweb.entity ###################################### +from logilab.common.deprecation import class_moved +from cubicweb import entity +EntityAdapter = class_moved(entity.EntityAdapter) # cubicweb 3.28 diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/web/action.py --- a/cubicweb/web/action.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/web/action.py Thu Mar 05 10:15:38 2020 +0100 @@ -32,6 +32,7 @@ Many examples are available in :mod:`cubicweb.web.views.actions`. """ +from typing import Optional from cubicweb import _ @@ -48,7 +49,7 @@ __registry__ = 'actions' __select__ = match_search_state('normal') order = 99 - category = 'moreactions' + category: Optional[str] = 'moreactions' # actions in category 'moreactions' can specify a sub-menu in which they should be filed submenu = None diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/web/test/unittest_idownloadable.py --- a/cubicweb/web/test/unittest_idownloadable.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/web/test/unittest_idownloadable.py Thu Mar 05 10:15:38 2020 +0100 @@ -23,12 +23,12 @@ from pytz import utc from cubicweb.devtools.testlib import CubicWebTC, real_error_handling -from cubicweb import view +from cubicweb.entity import EntityAdapter from cubicweb.predicates import is_instance from cubicweb.web import http_headers -class IDownloadableUser(view.EntityAdapter): +class IDownloadableUser(EntityAdapter): __regid__ = 'IDownloadable' __select__ = is_instance('CWUser') diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/web/views/calendar.py --- a/cubicweb/web/views/calendar.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/web/views/calendar.py Thu Mar 05 10:15:38 2020 +0100 @@ -28,7 +28,8 @@ from cubicweb.utils import json_dumps, make_uid from cubicweb.predicates import adaptable -from cubicweb.view import EntityView, EntityAdapter +from cubicweb.view import EntityView +from cubicweb.entity import EntityAdapter # useful constants & functions ################################################ diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/web/views/editcontroller.py --- a/cubicweb/web/views/editcontroller.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/web/views/editcontroller.py Thu Mar 05 10:15:38 2020 +0100 @@ -27,7 +27,7 @@ from rql.utils import rqlvar_maker from cubicweb import _, ValidationError, UnknownEid -from cubicweb.view import EntityAdapter +from cubicweb.entity import EntityAdapter from cubicweb.predicates import is_instance from cubicweb.web import RequestError, NothingToEdit, ProcessFormError from cubicweb.web.views import basecontrollers, autoform diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/web/views/ibreadcrumbs.py --- a/cubicweb/web/views/ibreadcrumbs.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/web/views/ibreadcrumbs.py Thu Mar 05 10:15:38 2020 +0100 @@ -25,11 +25,11 @@ from logilab.mtconverter import xml_escape from cubicweb import tags, uilib -from cubicweb.entity import Entity +from cubicweb.entity import Entity, EntityAdapter from cubicweb.predicates import (is_instance, one_line_rset, adaptable, one_etype_rset, multi_lines_rset, any_rset, match_form_params) -from cubicweb.view import EntityView, EntityAdapter +from cubicweb.view import EntityView from cubicweb.web.views import basecomponents # don't use AnyEntity since this may cause bug with isinstance() due to reloading diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/web/views/magicsearch.py --- a/cubicweb/web/views/magicsearch.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/web/views/magicsearch.py Thu Mar 05 10:15:38 2020 +0100 @@ -454,7 +454,7 @@ user_rql = variables select = parse(user_rql, print_errors=False).children[0] req.vreg.rqlhelper.annotate(select) - req.vreg.solutions(req, select, {}) + req.vreg.compute_var_types(req, select, {}) if restrictions: return ['%s, %s' % (user_rql, suggestion) for suggestion in self.rql_build_suggestions(select, incomplete_part)] diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/web/views/navigation.py --- a/cubicweb/web/views/navigation.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/web/views/navigation.py Thu Mar 05 10:15:38 2020 +0100 @@ -56,7 +56,7 @@ from cubicweb.predicates import paginated_rset, sorted_rset, adaptable from cubicweb.uilib import cut -from cubicweb.view import EntityAdapter +from cubicweb.entity import EntityAdapter from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent diff -r af59ca8b23d0 -r 2cc3f481ecd0 cubicweb/web/views/xmlrss.py --- a/cubicweb/web/views/xmlrss.py Thu Mar 05 09:54:49 2020 +0100 +++ b/cubicweb/web/views/xmlrss.py Thu Mar 05 10:15:38 2020 +0100 @@ -26,7 +26,8 @@ from cubicweb.predicates import (is_instance, non_final_entity, one_line_rset, appobject_selectable, adaptable) -from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component +from cubicweb.view import EntityView, AnyRsetView, Component +from cubicweb.entity import EntityAdapter from cubicweb.uilib import simple_sgml_tag from cubicweb.web import httpcache, component diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/book/devrepo/datamodel/definition.rst --- a/doc/book/devrepo/datamodel/definition.rst Thu Mar 05 09:54:49 2020 +0100 +++ b/doc/book/devrepo/datamodel/definition.rst Thu Mar 05 10:15:38 2020 +0100 @@ -466,7 +466,7 @@ __permissions__ = {'read': ('managers', 'users'), 'add': ('managers', RRQLExpression('U has_update_permission S')), 'delete': ('managers', RRQLExpression('U has_update_permission S')) - } + } In the above example, user will be allowed to add/delete `my_relation` if he has the `update` permission on the subject of the relation. diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/book/devrepo/repo/hooks.rst --- a/doc/book/devrepo/repo/hooks.rst Thu Mar 05 09:54:49 2020 +0100 +++ b/doc/book/devrepo/repo/hooks.rst Thu Mar 05 10:15:38 2020 +0100 @@ -35,11 +35,11 @@ events = ('before_add_entity', 'before_update_entity') def __call__(self): - if 'age' in self.entity.cw_edited: + if 'age' in self.entity.cw_edited: if 0 <= self.entity.age <= 120: return - msg = self._cw._('age must be between 0 and 120') - raise ValidationError(self.entity.eid, {'age': msg}) + msg = self._cw._('age must be between 0 and 120') + raise ValidationError(self.entity.eid, {'age': msg}) In our example the base `__select__` is augmented with an `is_instance` selector matching the desired entity type. diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/book/devrepo/vreg.rst --- a/doc/book/devrepo/vreg.rst Thu Mar 05 09:54:49 2020 +0100 +++ b/doc/book/devrepo/vreg.rst Thu Mar 05 10:15:38 2020 +0100 @@ -300,18 +300,18 @@ .. sourcecode:: python class UserLink(component.Component): - '''if the user is the anonymous user, build a link to login else a link - to the connected user object with a logout link - ''' - __regid__ = 'loggeduserlink' + '''if the user is the anonymous user, build a link to login else a link + to the connected user object with a logout link + ''' + __regid__ = 'loggeduserlink' - def call(self): - if self._cw.session.anonymous_session: - # display login link - ... - else: - # display a link to the connected user object with a loggout link - ... + def call(self): + if self._cw.session.anonymous_session: + # display login link + ... + else: + # display a link to the connected user object with a loggout link + ... The proper way to implement this with |cubicweb| is two have two different classes sharing the same identifier but with different selectors so you'll get @@ -320,21 +320,21 @@ .. sourcecode:: python class UserLink(component.Component): - '''display a link to the connected user object with a loggout link''' - __regid__ = 'loggeduserlink' - __select__ = component.Component.__select__ & authenticated_user() + '''display a link to the connected user object with a loggout link''' + __regid__ = 'loggeduserlink' + __select__ = component.Component.__select__ & authenticated_user() - def call(self): + def call(self): # display useractions and siteactions - ... + ... class AnonUserLink(component.Component): - '''build a link to login''' - __regid__ = 'loggeduserlink' - __select__ = component.Component.__select__ & anonymous_user() + '''build a link to login''' + __regid__ = 'loggeduserlink' + __select__ = component.Component.__select__ & anonymous_user() - def call(self): - # display login link + def call(self): + # display login link ... The big advantage, aside readability once you're familiar with the diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/book/devweb/edition/examples.rst --- a/doc/book/devweb/edition/examples.rst Thu Mar 05 09:54:49 2020 +0100 +++ b/doc/book/devweb/edition/examples.rst Thu Mar 05 10:15:38 2020 +0100 @@ -56,42 +56,42 @@ .. sourcecode:: python def sender_value(form, field): - return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email()) + return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email()) def recipient_choices(form, field): - return [(e.get_email(), e.eid) + return [(e.get_email(), e.eid) for e in form.cw_rset.entities() - if e.get_email()] + if e.get_email()] def recipient_value(form, field): - return [e.eid for e in form.cw_rset.entities() + return [e.eid for e in form.cw_rset.entities() if e.get_email()] class MassMailingForm(forms.FieldsForm): - __regid__ = 'massmailing' + __regid__ = 'massmailing' - needs_js = ('cubicweb.widgets.js',) - domid = 'sendmail' - action = 'sendmail' + needs_js = ('cubicweb.widgets.js',) + domid = 'sendmail' + action = 'sendmail' - sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}), - label=_('From:'), - value=sender_value) + sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}), + label=_('From:'), + value=sender_value) - recipient = ff.StringField(widget=CheckBox(), - label=_('Recipients:'), - choices=recipient_choices, - value=recipients_value) + recipient = ff.StringField(widget=CheckBox(), + label=_('Recipients:'), + choices=recipient_choices, + value=recipients_value) - subject = ff.StringField(label=_('Subject:'), max_length=256) + subject = ff.StringField(label=_('Subject:'), max_length=256) - mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField', - inputid='mailbody')) + mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField', + inputid='mailbody')) - form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()", - _('send email'), 'SEND_EMAIL_ICON'), - ImgButton('cancelbutton', "javascript: history.back()", - stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')] + form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()", + _('send email'), 'SEND_EMAIL_ICON'), + ImgButton('cancelbutton', "javascript: history.back()", + stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')] Let's detail what's going on up there. Our form will hold four fields: @@ -125,13 +125,13 @@ .. sourcecode:: python class MassMailingFormView(form.FormViewMixIn, EntityView): - __regid__ = 'massmailing' - __select__ = is_instance(IEmailable) & authenticated_user() + __regid__ = 'massmailing' + __select__ = is_instance(IEmailable) & authenticated_user() - def call(self): - form = self._cw.vreg['forms'].select('massmailing', self._cw, - rset=self.cw_rset) - form.render(w=self.w) + def call(self): + form = self._cw.vreg['forms'].select('massmailing', self._cw, + rset=self.cw_rset) + form.render(w=self.w) As you see, we simply define a view with proper selector so it only apply to a result set containing :class:`IEmailable` entities, and so that only users in the diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/book/devweb/js.rst --- a/doc/book/devweb/js.rst Thu Mar 05 09:54:49 2020 +0100 +++ b/doc/book/devweb/js.rst Thu Mar 05 10:15:38 2020 +0100 @@ -142,7 +142,7 @@ function removeBookmark(beid) { d = asyncRemoteExec('delete_bookmark', beid); d.addCallback(function(boxcontent) { - reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box'); + reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box'); document.location.hash = '#header'; updateMessage(_("bookmark has been removed")); }); diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/book/devweb/views/reledit.rst --- a/doc/book/devweb/views/reledit.rst Thu Mar 05 09:54:49 2020 +0100 +++ b/doc/book/devweb/views/reledit.rst Thu Mar 05 10:15:38 2020 +0100 @@ -136,14 +136,14 @@ from cubicweb.web.views import reledit class DeactivatedAutoClickAndEditFormView(reledit.AutoClickAndEditFormView): - def _should_edit_attribute(self, rschema): - return False + def _should_edit_attribute(self, rschema): + return False - def _should_edit_attribute(self, rschema, role): - return False + def _should_edit_attribute(self, rschema, role): + return False def registration_callback(vreg): - vreg.register_and_replace(DeactivatedAutoClickAndEditFormView, - reledit.AutoClickAndEditFormView) + vreg.register_and_replace(DeactivatedAutoClickAndEditFormView, + reledit.AutoClickAndEditFormView) diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/changes/3.28.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/changes/3.28.rst Thu Mar 05 10:15:38 2020 +0100 @@ -0,0 +1,8 @@ +3.28 +==== + +Changes +------- + +- the class cubicweb.view.EntityAdapter was moved to cubicweb.entity.EntityAdapter + a deprecation warning is in place, but please update your source code accordingly. diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/changes/changelog.rst --- a/doc/changes/changelog.rst Thu Mar 05 09:54:49 2020 +0100 +++ b/doc/changes/changelog.rst Thu Mar 05 10:15:38 2020 +0100 @@ -2,6 +2,7 @@ Changelog history =================== +.. include:: 3.28.rst .. include:: 3.27.rst .. include:: 3.26.rst .. include:: 3.25.rst diff -r af59ca8b23d0 -r 2cc3f481ecd0 doc/tutorials/advanced/part02_security.rst --- a/doc/tutorials/advanced/part02_security.rst Thu Mar 05 09:54:49 2020 +0100 +++ b/doc/tutorials/advanced/part02_security.rst Thu Mar 05 10:15:38 2020 +0100 @@ -117,25 +117,25 @@ from cubicweb.schema import ERQLExpression VISIBILITY_PERMISSIONS = { - 'read': ('managers', - ERQLExpression('X visibility "public"'), - ERQLExpression('X may_be_read_by U')), - 'add': ('managers',), - 'update': ('managers', 'owners',), - 'delete': ('managers', 'owners'), - } + 'read': ('managers', + ERQLExpression('X visibility "public"'), + ERQLExpression('X may_be_read_by U')), + 'add': ('managers',), + 'update': ('managers', 'owners',), + 'delete': ('managers', 'owners'), + } AUTH_ONLY_PERMISSIONS = { - 'read': ('managers', 'users'), - 'add': ('managers',), - 'update': ('managers', 'owners',), - 'delete': ('managers', 'owners'), - } + 'read': ('managers', 'users'), + 'add': ('managers',), + 'update': ('managers', 'owners',), + 'delete': ('managers', 'owners'), + } CLASSIFIERS_PERMISSIONS = { - 'read': ('managers', 'users', 'guests'), - 'add': ('managers',), - 'update': ('managers', 'owners',), - 'delete': ('managers', 'owners'), - } + 'read': ('managers', 'users', 'guests'), + 'add': ('managers',), + 'update': ('managers', 'owners',), + 'delete': ('managers', 'owners'), + } from cubicweb_folder.schema import Folder from cubicweb_file.schema import File diff -r af59ca8b23d0 -r 2cc3f481ecd0 setup.py --- a/setup.py Thu Mar 05 09:54:49 2020 +0100 +++ b/setup.py Thu Mar 05 10:15:38 2020 +0100 @@ -73,6 +73,8 @@ 'pytz', 'Markdown', 'filelock', + 'rdflib', + 'rdflib-jsonld', ], entry_points={ 'console_scripts': [ @@ -102,9 +104,6 @@ 'pyramid_multiauth', 'repoze.lru', ], - 'rdf': [ - 'rdflib', - ], 'sparql': [ 'fyzz >= 0.1.0', ],