merge public heads
authorFabien Amarger <fabien.amarger@logilab.fr>
Wed, 11 Mar 2020 11:18:40 +0100
changeset 12913 ebf4806e4ab7
parent 12900 2cc3f481ecd0 (current diff)
parent 12912 3966f09d5f5c (diff)
child 12917 db0f56b19583
merge public heads
--- a/cubicweb/entities/adapters.py	Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/entities/adapters.py	Wed Mar 11 11:18:40 2020 +0100
@@ -23,56 +23,117 @@
 from hashlib import sha1
 from itertools import chain
 
-from rdflib import URIRef, Literal
-
+from rdflib import URIRef, Literal, namespace as rdflib_namespace
 from logilab.mtconverter import TransformError
 from logilab.common.decorators import cached, cachedproperty
 
-from cubicweb.entity import EntityAdapter
+from cubicweb import rdf
 from cubicweb import (Unauthorized, ValidationError, ViolatedConstraint,
                       UniqueTogetherError)
-from cubicweb.schema import constraint_name_for
+from cubicweb.entity import EntityAdapter
+from cubicweb.schema import constraint_name_for, VIRTUAL_RTYPES
 from cubicweb.predicates import is_instance, relation_possible, match_exception
 
-from cubicweb.rdf import NAMESPACES
-
 
 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
+    __regid__ = "rdf"
+
+    SKIP_RTYPES = VIRTUAL_RTYPES | set(['cwuri', 'is', 'is_instance_of'])
 
     def __init__(self, _cw, **kwargs):
         super().__init__(_cw, **kwargs)
         self.entity.complete()
+        self.used_namespaces = {}
+
+    def _use_namespace(self, prefix, base_url=None):
+        if prefix not in self.used_namespaces:
+            if base_url is not None:
+                if prefix in rdf.NAMESPACES:
+                    raise KeyError('prefix redefinition not allowed: '
+                                   f'"{prefix}" already exists as "{rdf.NAMESPACES[prefix]}"')
+                ns = rdflib_namespace.Namespace(base_url)
+            else:
+                ns = rdf.NAMESPACES[prefix]
+            self.used_namespaces[prefix] = ns
+        elif base_url is not None:
+            if self.used_namespaces[prefix] != rdflib_namespace.Namespace(base_url):
+                raise ValueError('prefix redefinition not allowed: '
+                                 f'"{prefix}" already exists as "{self.used_namespaces[prefix]}"')
+        return self.used_namespaces[prefix]
 
     @cachedproperty
     def uri(self):
-        return self.entity.cwuri
+        return URIRef(self.entity.cwuri)
 
     def triples(self):
         """return sequence of 3-tuple of rdflib identifiers"""
-        raise NotImplementedError()
+        yield from self.cw_triples()
+        yield from self.dc_triples()
+
+    def cw_triples(self):
+        RDF = self._use_namespace('rdf')
+        CW = self._use_namespace('cubicweb')
+
+        yield (self.uri, RDF.type, CW[self.entity.e_schema.type])
+
+        for rschema, targettypes, role in self.entity.e_schema.relation_definitions('relation'):
+            rtype = rschema.type
+            if rtype in self.SKIP_RTYPES or rtype.endswith('_permission'):
+                continue
+            for targetype in targettypes:
+                # if rschema is an attribute
+                if targetype.final:
+                    try:
+                        value = self.entity.cw_attr_cache[rtype]
+                    except KeyError:
+                        continue
+                    if value is not None:
+                        yield (self.uri, CW[rtype], Literal(value))
+                # else if rschema is a relation
+                else:
+                    for related in self.entity.related(rtype, role, entities=True, safe=True):
+                        if role == 'subject':
+                            yield (self.uri, CW[rtype], URIRef(related.cwuri))
+                        else:
+                            yield (URIRef(related.cwuri), CW[rtype], self.uri)
+
+    def dc_triples(self):
+        dc_entity = self.entity.cw_adapt_to('IDublinCore')
+        DC = self._use_namespace('dc')
+        yield (self.uri, DC.title, Literal(dc_entity.long_title()))  # or title() ?
+        desc = dc_entity.description()
+        if desc:
+            yield (self.uri, DC.description, Literal(desc))
+        creator = dc_entity.creator()  # use URI instead of Literal ?
+        if creator:
+            yield (self.uri, DC.creator, Literal(creator))
+        yield (self.uri, DC.date, Literal(dc_entity.date()))
+        yield (self.uri, DC.type, Literal(dc_entity.type()))
+        yield (self.uri, DC.language, Literal(dc_entity.language()))
 
 
-class CWUserFoafAdapter(EntityRDFAdapter):
-    __regid__ = "rdf.foaf"
+class CWUserRDFAdapter(EntityRDFAdapter):
     __select__ = is_instance("CWUser")
 
     def triples(self):
-        RDF = NAMESPACES["rdf"]
-        FOAF = NAMESPACES["foaf"]
-        uri = URIRef(self.uri)
-        yield (uri, RDF.type, FOAF.Person)
+        yield from super().triples()
+        yield from self.foaf_triples()
+
+    def foaf_triples(self):
+        RDF = self._use_namespace('rdf')
+        FOAF = self._use_namespace('foaf')
+        yield (self.uri, RDF.type, FOAF.Person)
         if self.entity.surname:
-            yield (uri, FOAF.familyName, Literal(self.entity.surname))
+            yield (self.uri, FOAF.familyName, Literal(self.entity.surname))
         if self.entity.firstname:
-            yield (uri, FOAF.givenName, Literal(self.entity.firstname))
+            yield (self.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))
+            yield (self.uri, FOAF.mbox_sha1sum, Literal(email_digest))
 
 
 class IDublinCoreAdapter(EntityAdapter):
@@ -104,7 +165,7 @@
             return self.entity.printable_value('description', format=format)
         return u''
 
-    def authors(self):
+    def authors(self):  # XXX is this part of DC ?
         """Return a suitable description for the author(s) of the entity"""
         try:
             return u', '.join(u.name() for u in self.entity.owned_by)
@@ -132,7 +193,7 @@
         # check if entities has internationalizable attributes
         # XXX one is enough or check if all String attributes are internationalizable?
         for rschema, attrschema in eschema.attribute_definitions():
-            if rschema.rdef(eschema, attrschema).internationalizable:
+            if getattr(rschema.rdef(eschema, attrschema), 'internationalizable', False):
                 return self._cw._(self._cw.user.property_value('ui.language'))
         return self._cw._(self._cw.vreg.property_value('ui.language'))
 
--- a/cubicweb/pyramid/__init__.py	Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/pyramid/__init__.py	Wed Mar 11 11:18:40 2020 +0100
@@ -231,6 +231,8 @@
     cwconfig = config.registry.get('cubicweb.config')
     repo = config.registry.get('cubicweb.repository')
 
+    config.include('cubicweb.pyramid.rest_api')
+
     if repo is not None:
         if cwconfig is None:
             config.registry['cubicweb.config'] = cwconfig = repo.config
--- a/cubicweb/pyramid/predicates.py	Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/pyramid/predicates.py	Wed Mar 11 11:18:40 2020 +0100
@@ -24,22 +24,6 @@
 from cubicweb._exceptions import UnknownEid
 
 
-class MatchIsETypePredicate(object):
-    """A predicate that match if a given etype exist in schema.
-    """
-    def __init__(self, matchname, config):
-        self.matchname = matchname
-
-    def text(self):
-        return 'match_is_etype = %s' % self.matchname
-
-    phash = text
-
-    def __call__(self, info, request):
-        return info['match'][self.matchname].lower() in \
-            request.registry['cubicweb.registry'].case_insensitive_etypes
-
-
 class MatchIsEIDPredicate(object):
     """A predicate that match if a given eid exist in the database.
     """
@@ -64,6 +48,33 @@
         return True
 
 
+class MatchIsETypeAndEIDPredicate(object):
+    """A predicate that match if a given eid exist in the database and if the
+    etype of the entity same as the one given in the URL
+    """
+    def __init__(self, matchnames, config):
+        self.match_etype, self.match_eid = matchnames
+
+    def text(self):
+        return f"match_is_etype_and_eid = {self.match_etype}/{self.match_eid}"
+
+    phash = text
+
+    def __call__(self, info, request):
+        try:
+            eid = int(info['match'][self.match_eid])
+        except ValueError:
+            return False
+
+        try:
+            entity = request.cw_cnx.entity_from_eid(eid)
+        except UnknownEid:
+            return False
+
+        etype = info['match'][self.match_etype]
+        return entity.__regid__.lower() == etype.lower()
+
+
 def includeme(config):
-    config.add_route_predicate('match_is_etype', MatchIsETypePredicate)
     config.add_route_predicate('match_is_eid', MatchIsEIDPredicate)
+    config.add_route_predicate('match_is_etype_and_eid', MatchIsETypeAndEIDPredicate)
--- a/cubicweb/pyramid/resources.py	Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/pyramid/resources.py	Wed Mar 11 11:18:40 2020 +0100
@@ -18,84 +18,33 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
-"""Pyramid resource definitions for CubicWeb."""
+from pyramid.httpexceptions import HTTPNotFound
 
-from rql import TypeResolverException
-
-from pyramid.decorator import reify
-from pyramid.httpexceptions import HTTPNotFound
+from cubicweb import rdf
 
 
-class EntityResource(object):
-
-    """A resource class for an entity. It provide method to retrieve an entity
-    by eid.
-    """
-
-    @classmethod
-    def from_eid(cls):
-        def factory(request):
-            return cls(request, None, None, request.matchdict['eid'])
-        return factory
-
-    def __init__(self, request, cls, attrname, value):
-        self.request = request
-        self.cls = cls
-        self.attrname = attrname
-        self.value = value
-
-    @reify
-    def rset(self):
-        req = self.request.cw_request
-        if self.cls is None:
-            return req.execute('Any X WHERE X eid %(x)s',
-                               {'x': int(self.value)})
-        st = self.cls.fetch_rqlst(self.request.cw_cnx.user, ordermethod=None)
-        st.add_constant_restriction(st.get_variable('X'), self.attrname,
-                                    'x', 'Substitute')
-        if self.attrname == 'eid':
-            try:
-                rset = req.execute(st.as_string(), {'x': int(self.value)})
-            except (ValueError, TypeResolverException):
-                # conflicting eid/type
-                raise HTTPNotFound()
-        else:
-            rset = req.execute(st.as_string(), {'x': self.value})
-        return rset
+def negociate_mime_type(request, possible_mimetypes):
+    accepted_headers_by_weight = sorted(
+        request.accept.parsed or [], key=lambda h: h[1], reverse=True
+    )
+    mime_type_negociated = None
+    for parsed_header in accepted_headers_by_weight:
+        accepted_mime_type = parsed_header[0]
+        if accepted_mime_type in possible_mimetypes:
+            mime_type_negociated = accepted_mime_type
+            break
+    return mime_type_negociated
 
 
-class ETypeResource(object):
-
-    """A resource for etype.
-    """
-    @classmethod
-    def from_match(cls, matchname):
-        def factory(request):
-            return cls(request, request.matchdict[matchname])
-        return factory
-
-    def __init__(self, request, etype):
-        vreg = request.registry['cubicweb.registry']
-
-        self.request = request
-        self.etype = vreg.case_insensitive_etypes[etype.lower()]
-        self.cls = vreg['etypes'].etype_class(self.etype)
+def rdf_context_from_eid(request):
+    mime_type = negociate_mime_type(request, rdf.RDF_MIMETYPE_TO_FORMAT)
+    if mime_type is None:
+        raise HTTPNotFound()
+    entity = request.cw_request.entity_from_eid(request.matchdict['eid'])
+    return RDFResource(entity, mime_type)
 
-    def __getitem__(self, value):
-        # Try eid first, then rest attribute as for URL path evaluation
-        # mecanism in cubicweb.web.views.urlpublishing.
-        for attrname in ('eid', self.cls.cw_rest_attr_info()[0]):
-            resource = EntityResource(self.request, self.cls, attrname, value)
-            try:
-                rset = resource.rset
-            except HTTPNotFound:
-                continue
-            if rset.rowcount:
-                return resource
-        raise KeyError(value)
 
-    @reify
-    def rset(self):
-        rql = self.cls.fetch_rql(self.request.cw_cnx.user)
-        rset = self.request.cw_request.execute(rql)
-        return rset
+class RDFResource:
+    def __init__(self, entity, mime_type):
+        self.entity = entity
+        self.mime_type = mime_type
--- a/cubicweb/pyramid/rest_api.py	Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/pyramid/rest_api.py	Wed Mar 11 11:18:40 2020 +0100
@@ -20,26 +20,44 @@
 
 """Experimental REST API for CubicWeb using Pyramid."""
 
-from __future__ import absolute_import
-
+import rdflib
 
 from pyramid.view import view_config
-from cubicweb.pyramid.resources import EntityResource, ETypeResource
+from pyramid.response import Response
+
+from cubicweb import rdf
+from cubicweb.pyramid.resources import rdf_context_from_eid, RDFResource
 
 
 @view_config(
-    route_name='cwentities',
-    context=EntityResource,
-    request_method='DELETE')
-def delete_entity(context, request):
-    context.rset.one().cw_delete()
-    request.response.status_int = 204
-    return request.response
+    route_name='one_entity',
+    context=RDFResource,
+)
+@view_config(
+    route_name='one_entity_eid',
+    context=RDFResource,
+)
+def view_entity_as_rdf(context, request):
+    graph = rdflib.ConjunctiveGraph()
+    rdf.add_entity_to_graph(graph, context.entity)
+    rdf_format = rdf.RDF_MIMETYPE_TO_FORMAT[context.mime_type]
+    response = Response(graph.serialize(format=rdf_format))
+    response.content_type = context.mime_type
+    return response
 
 
 def includeme(config):
     config.include('.predicates')
     config.add_route(
-        'cwentities', '/{etype}/*traverse',
-        factory=ETypeResource.from_match('etype'), match_is_etype='etype')
+        'one_entity',
+        '/{etype}/{eid}',
+        factory=rdf_context_from_eid,
+        match_is_etype_and_eid=('etype', 'eid'),
+    )
+    config.add_route(
+        'one_entity_eid',
+        '/{eid}',
+        factory=rdf_context_from_eid,
+        match_is_eid='eid'
+    )
     config.scan(__name__)
--- a/cubicweb/rdf.py	Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/rdf.py	Wed Mar 11 11:18:40 2020 +0100
@@ -15,8 +15,7 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
-from rdflib import ConjunctiveGraph, plugin
-from rdflib.namespace import Namespace, RDF, FOAF
+from rdflib import plugin, namespace
 import rdflib_jsonld  # noqa
 
 plugin.register("jsonld", plugin.Serializer, "rdflib_jsonld.serializer", "JsonLDSerializer")
@@ -32,35 +31,25 @@
 }
 
 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",),
+    "rdf": namespace.RDF,
+    "rdfs": namespace.RDFS,
+    "owl": namespace.OWL,
+    "xsd": namespace.XSD,
+    "skos": namespace.SKOS,
+    "void": namespace.VOID,
+    "dc": namespace.DC,
+    "dcterms": namespace.DCTERMS,
+    "foaf": namespace.FOAF,
+    "doap": namespace.DOAP,
+    "schema": namespace.Namespace("http://schema.org/"),
+    "cubicweb": namespace.Namespace("http://ns.cubicweb.org/cubicweb/0.0/")
 }
 
 
-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):
+    adapter = entity.cw_adapt_to("rdf")
+    if adapter:
         for triple in adapter.triples():
             graph.add(triple)
+        for prefix, rdfns in adapter.used_namespaces.items():
+            graph.bind(prefix, rdfns)
--- a/cubicweb/web/views/basecontrollers.py	Thu Mar 05 10:15:38 2020 +0100
+++ b/cubicweb/web/views/basecontrollers.py	Wed Mar 11 11:18:40 2020 +0100
@@ -23,7 +23,7 @@
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, UndoTransactionException,
-                      Forbidden)
+                      Forbidden, rdf)
 from cubicweb.utils import json_dumps
 from cubicweb.predicates import (authenticated_user, anonymous_user,
                                 match_form_params)
@@ -101,6 +101,10 @@
             else:
                 rset = None
         vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
+        if rset and len(rset) == 1:
+            for mimetype in rdf.RDF_MIMETYPE_TO_FORMAT:
+                req.headers_out.addRawHeader(
+                    'Link', "<%s>;rel=alternate;type=%s" % (rset.one().cwuri, mimetype))
         try:
             view = self._cw.vreg['views'].select(vid, req, rset=rset)
         except ObjectNotFound: