Browse Source

Resurrect resource; set extract_imr strict by default.

* One test still failing
* Need to test recursiveness
Stefano Cossu 7 years ago
parent
commit
06608acbab

+ 8 - 4
doc/notes/TODO

@@ -32,11 +32,15 @@
   - [D] Create version
   - [D] Create version
   - [D] Retrieve version info
   - [D] Retrieve version info
   - [D] Retrieve version
   - [D] Retrieve version
-  - [W] Revert to version
-  - [W] Slug behavior
+  - [D] Revert to version
+  - [D] Slug behavior
   - [W] Tests
   - [W] Tests
-  - [W] Avoid duplicate versions
-  - [ ] Delete version
+  - [D] Avoid duplicate versions
+  - [D] Delete version
+- [W] Delete vs. purge
+  - [W] Resurrect
+  - [W] Resurrect recursive
+
 
 
 # Alpha 2 TODO
 # Alpha 2 TODO
 
 

+ 9 - 5
doc/notes/fcrepo4_deltas.md

@@ -83,7 +83,7 @@ in LAKEsuperior in a 404.
 In both above cases, PUTting into `rest/a` yields a 409, POSTing to it results
 In both above cases, PUTting into `rest/a` yields a 409, POSTing to it results
 in a 201.
 in a 201.
 
 
-### Non-mandatory slug in version POST
+### Non-mandatory, non-authoritative slug in version POST
 
 
 FCREPO requires a `Slug` header to POST to `fcr:versions` to create a new
 FCREPO requires a `Slug` header to POST to `fcr:versions` to create a new
 version.
 version.
@@ -91,10 +91,14 @@ version.
 LAKEsuperior adheres to the more general FCREPO POST rule and if no slug is
 LAKEsuperior adheres to the more general FCREPO POST rule and if no slug is
 provided, an automatic ID is generated instead. The ID is a UUID4.
 provided, an automatic ID is generated instead. The ID is a UUID4.
 
 
-Also, note that internally this ID is not called "label" but "uid" since it is
-treated as a fully qualified identifier. The `fcrepo:hasVersionLabel` predicate
-has been kept as not worth changing until the adoption of Memento, which will
-change the retrieval mechanisms.
+Note that internally this ID is not called "label" but "uid" since it
+is treated as a fully qualified identifier. The `fcrepo:hasVersionLabel`
+predicate, however ambiguous in this context, will be kept until the adoption
+of Memento, which will change the retrieval mechanisms.
+
+Also, if a POST is issued on the same resource `fcr:versions` location using
+a version ID that already exists, LAKEsuperior will just mint a random
+identifier rather than returning an error.
 
 
 
 
 ## Deprecation track
 ## Deprecation track

+ 0 - 3
lakesuperior/endpoints/ldp.py

@@ -231,9 +231,6 @@ def post_version(uuid):
     Create a new resource version.
     Create a new resource version.
     '''
     '''
     ver_uid = request.headers.get('slug', None)
     ver_uid = request.headers.get('slug', None)
-    if not ver_uid:
-        ver_uid = str(uuid4())
-
     try:
     try:
         ver_uri = Ldpr.outbound_inst(uuid).create_version(ver_uid)
         ver_uri = Ldpr.outbound_inst(uuid).create_version(ver_uid)
     except ResourceNotExistsError as e:
     except ResourceNotExistsError as e:

+ 3 - 3
lakesuperior/exceptions.py

@@ -147,7 +147,7 @@ class TombstoneError(RuntimeError):
 
 
     def __str__(self):
     def __str__(self):
         return (
         return (
-            'Discovered tombstone resource at /{}, departed: {}\n'.format(
-                self.uuid, self.ts),
-            'To resurrect that resource, send a POST request to it.'
+            'Discovered tombstone resource at /{}, departed: {}\n'
+            'To resurrect this resource, send a POST request to its tombstone.'
+            .format(self.uuid, self.ts)
         )
         )

+ 0 - 8
lakesuperior/model/ldp_rs.py

@@ -6,14 +6,6 @@ class LdpRs(Ldpr):
 
 
     Definition: https://www.w3.org/TR/ldp/#ldprs
     Definition: https://www.w3.org/TR/ldp/#ldprs
     '''
     '''
-
-    base_types = {
-        nsc['fcrepo'].Resource,
-        nsc['ldp'].Resource,
-        nsc['ldp'].RDFSource,
-    }
-
-
     def __init__(self, uuid, repr_opts={}, handling='lenient', **kwargs):
     def __init__(self, uuid, repr_opts={}, handling='lenient', **kwargs):
         '''
         '''
         Extends Ldpr.__init__ by adding LDP-RS specific parameters.
         Extends Ldpr.__init__ by adding LDP-RS specific parameters.

+ 73 - 16
lakesuperior/model/ldpr.py

@@ -104,6 +104,12 @@ class Ldpr(metaclass=ABCMeta):
 
 
     RES_VER_CONT_LABEL = 'fcr:versions'
     RES_VER_CONT_LABEL = 'fcr:versions'
 
 
+    base_types = {
+        nsc['fcrepo'].Resource,
+        nsc['ldp'].Resource,
+        nsc['ldp'].RDFSource,
+    }
+
     protected_pred = (
     protected_pred = (
         nsc['fcrepo'].created,
         nsc['fcrepo'].created,
         nsc['fcrepo'].createdBy,
         nsc['fcrepo'].createdBy,
@@ -397,7 +403,10 @@ class Ldpr(metaclass=ABCMeta):
         Return version metadata (`fcr:versions`).
         Return version metadata (`fcr:versions`).
         '''
         '''
         if not hasattr(self, '_version_info'):
         if not hasattr(self, '_version_info'):
-            self._version_info = self.rdfly.get_version_info(self.urn)
+            try:
+                self._version_info = self.rdfly.get_version_info(self.urn)
+            except ResourceNotExistsError as e:
+                self._version_info = Graph()
 
 
         return self._version_info
         return self._version_info
 
 
@@ -415,10 +424,12 @@ class Ldpr(metaclass=ABCMeta):
         '''
         '''
         Return a generator of version UIDs (relative to their parent resource).
         Return a generator of version UIDs (relative to their parent resource).
         '''
         '''
-        return set(self.version_info[
+        gen = self.version_info[
             self.urn
             self.urn
             : nsc['fcrepo'].hasVersion / nsc['fcrepo'].hasVersionLabel
             : nsc['fcrepo'].hasVersion / nsc['fcrepo'].hasVersionLabel
-            :])
+            :]
+
+        return { str(uid) for uid in gen }
 
 
 
 
     @property
     @property
@@ -536,18 +547,57 @@ class Ldpr(metaclass=ABCMeta):
         else:
         else:
             ret = self._purge_rsrc(inbound)
             ret = self._purge_rsrc(inbound)
 
 
-            for child_uri in children:
-                child_rsrc = Ldpr.outbound_inst(
-                    g.tbox.uri_to_uuid(child_uri.identifier),
-                    repr_opts={'incl_children' : False})
-                if leave_tstone:
-                    child_rsrc._bury_rsrc(inbound, tstone_pointer=self.urn)
-                else:
-                    child_rsrc._purge_rsrc(inbound)
+        for child_uri in children:
+            child_rsrc = Ldpr.outbound_inst(
+                g.tbox.uri_to_uuid(child_uri.identifier),
+                repr_opts={'incl_children' : False})
+            if leave_tstone:
+                child_rsrc._bury_rsrc(inbound, tstone_pointer=self.urn)
+            else:
+                child_rsrc._purge_rsrc(inbound)
 
 
         return ret
         return ret
 
 
 
 
+    @atomic
+    def resurrect(self):
+        '''
+        Resurrect a resource from a tombstone.
+
+        @EXPERIMENTAL
+        '''
+        tstone_trp = set(self.rdfly.extract_imr(self.urn, strict=False).graph)
+
+        ver_rsp = self.version_info.query('''
+        SELECT ?uid {
+          ?latest fcrepo:hasVersionLabel ?uid ;
+            fcrepo:created ?ts .
+        }
+        ORDER BY ?ts
+        LIMIT 1
+        ''')
+        ver_uid = str(ver_rsp.bindings[0]['uid'])
+        ver_trp = set(self.rdfly.get_version(self.urn, ver_uid))
+
+        laz_gr = Graph()
+        for t in ver_trp:
+            if t[1] != RDF.type or t[2] not in {
+                nsc['fcrepo'].Version,
+            }:
+                laz_gr.add((self.urn, t[1], t[2]))
+        laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Resource))
+        if nsc['ldp'].NonRdfSource in laz_gr[: RDF.type :]:
+            laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Binary))
+        elif nsc['ldp'].Container in laz_gr[: RDF.type :]:
+            laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Container))
+
+        self._modify_rsrc(self.RES_CREATED, tstone_trp, set(laz_gr))
+        self._set_containment_rel()
+
+        return self.uri
+
+
+
     @atomic
     @atomic
     def purge(self, inbound=True):
     def purge(self, inbound=True):
         '''
         '''
@@ -589,20 +639,26 @@ class Ldpr(metaclass=ABCMeta):
         @param ver_uid Version ver_uid. If already existing, an exception is
         @param ver_uid Version ver_uid. If already existing, an exception is
         raised.
         raised.
         '''
         '''
+        if not ver_uid or ver_uid in self.version_uids:
+            ver_uid = str(uuid4())
+
         return g.tbox.globalize_term(self._create_rsrc_version(ver_uid))
         return g.tbox.globalize_term(self._create_rsrc_version(ver_uid))
 
 
 
 
     @atomic
     @atomic
-    def revert_to_version(self, ver_uid):
+    def revert_to_version(self, ver_uid, backup=True):
         '''
         '''
         Revert to a previous version.
         Revert to a previous version.
 
 
         NOTE: this will create a new version.
         NOTE: this will create a new version.
 
 
         @param ver_uid (string) Version UID.
         @param ver_uid (string) Version UID.
+        @param backup (boolean) Whether to create a backup copy. Default is
+        true.
         '''
         '''
         # Create a backup snapshot.
         # Create a backup snapshot.
-        self.create_version(uuid4())
+        if backup:
+            self.create_version(uuid4())
 
 
         ver_gr = self.rdfly.get_version(self.urn, ver_uid)
         ver_gr = self.rdfly.get_version(self.urn, ver_uid)
         revert_gr = Graph()
         revert_gr = Graph()
@@ -685,7 +741,7 @@ class Ldpr(metaclass=ABCMeta):
         '''
         '''
         self._logger.info('Removing resource {}'.format(self.urn))
         self._logger.info('Removing resource {}'.format(self.urn))
         # Create a backup snapshot for resurrection purposes.
         # Create a backup snapshot for resurrection purposes.
-        self.create_version(uuid4())
+        self._create_rsrc_version(uuid4())
 
 
         remove_trp = self.imr.graph
         remove_trp = self.imr.graph
         add_trp = Graph()
         add_trp = Graph()
@@ -713,7 +769,6 @@ class Ldpr(metaclass=ABCMeta):
         '''
         '''
         self._logger.info('Purging resource {}'.format(self.urn))
         self._logger.info('Purging resource {}'.format(self.urn))
 
 
-        import pdb; pdb.set_trace()
         # Remove resource itself.
         # Remove resource itself.
         self.rdfly.modify_dataset({(self.urn, None, None)}, types=None)
         self.rdfly.modify_dataset({(self.urn, None, None)}, types=None)
 
 
@@ -727,7 +782,9 @@ class Ldpr(metaclass=ABCMeta):
 
 
         # Remove inbound references.
         # Remove inbound references.
         if inbound:
         if inbound:
-            for ib_rsrc_uri in self.imr.graph.subjects(None, self.urn):
+            imr = self.rdfly.extract_imr(
+                    self.urn, incl_inbound=True, strict=False)
+            for ib_rsrc_uri in imr.graph.subjects(None, self.urn):
                 remove_trp = {(ib_rsrc_uri, None, self.urn)}
                 remove_trp = {(ib_rsrc_uri, None, self.urn)}
                 Ldpr(ib_rsrc_uri)._modify_rsrc(self.RES_UPDATED, remove_trp)
                 Ldpr(ib_rsrc_uri)._modify_rsrc(self.RES_UPDATED, remove_trp)
 
 

+ 3 - 2
lakesuperior/store_layouts/ldp_rs/base_rdf_layout.py

@@ -80,7 +80,7 @@ class BaseRdfLayout(metaclass=ABCMeta):
     # implement.
     # implement.
 
 
     @abstractmethod
     @abstractmethod
-    def extract_imr(self, uri, strict=False, incl_inbound=False,
+    def extract_imr(self, uri, strict=True, incl_inbound=False,
                 incl_children=True, embed_children=False, incl_srv_mgd=True):
                 incl_children=True, embed_children=False, incl_srv_mgd=True):
         '''
         '''
         Extract an in-memory resource from the dataset restricted to a subject.
         Extract an in-memory resource from the dataset restricted to a subject.
@@ -93,7 +93,8 @@ class BaseRdfLayout(metaclass=ABCMeta):
 
 
         @param uri (URIRef) Resource URI.
         @param uri (URIRef) Resource URI.
         @param strict (boolean) If set to True, an empty result graph will
         @param strict (boolean) If set to True, an empty result graph will
-        raise a `ResourceNotExistsError`.
+        raise a `ResourceNotExistsError`; if a tombstone is found, a
+        `TombstoneError` is raised. Otherwise, the raw graph is returned.
         @param incl_inbound (boolean) Whether to pull triples that have the
         @param incl_inbound (boolean) Whether to pull triples that have the
         resource URI as their object.
         resource URI as their object.
         @param incl_children (boolean) Whether to include all children
         @param incl_children (boolean) Whether to include all children

+ 10 - 9
lakesuperior/store_layouts/ldp_rs/default_layout.py

@@ -30,7 +30,7 @@ class DefaultLayout(BaseRdfLayout):
     META_GRAPH_URI = nsc['fcg'].metadata
     META_GRAPH_URI = nsc['fcg'].metadata
 
 
 
 
-    def extract_imr(self, uri, strict=False, incl_inbound=False,
+    def extract_imr(self, uri, strict=True, incl_inbound=False,
                 incl_children=True, embed_children=False, incl_srv_mgd=True):
                 incl_children=True, embed_children=False, incl_srv_mgd=True):
         '''
         '''
         See base_rdf_layout.extract_imr.
         See base_rdf_layout.extract_imr.
@@ -90,15 +90,16 @@ class DefaultLayout(BaseRdfLayout):
         rsrc = Resource(gr, uri)
         rsrc = Resource(gr, uri)
 
 
         # Check if resource is a tombstone.
         # Check if resource is a tombstone.
-        if rsrc[RDF.type : nsc['fcsystem'].Tombstone]:
-            raise TombstoneError(
-                    g.tbox.uri_to_uuid(rsrc.identifier),
-                    rsrc.value(nsc['fcrepo'].created))
-        elif rsrc.value(nsc['fcsystem'].tombstone):
-            raise TombstoneError(
-                    g.tbox.uri_to_uuid(
+        if strict:
+            if rsrc[RDF.type : nsc['fcsystem'].Tombstone]:
+                raise TombstoneError(
+                        g.tbox.uri_to_uuid(rsrc.identifier),
+                        rsrc.value(nsc['fcrepo'].created))
+            elif rsrc.value(nsc['fcsystem'].tombstone):
+                raise TombstoneError(
+                        g.tbox.uri_to_uuid(
                             rsrc.value(nsc['fcsystem'].tombstone).identifier),
                             rsrc.value(nsc['fcsystem'].tombstone).identifier),
-                    rsrc.value(nsc['fcrepo'].created))
+                        rsrc.value(nsc['fcrepo'].created))
 
 
         return rsrc
         return rsrc
 
 

+ 54 - 2
tests/endpoints/test_ldp.py

@@ -320,6 +320,8 @@ class TestLdp:
     def test_tombstone(self):
     def test_tombstone(self):
         '''
         '''
         Test tombstone behaviors.
         Test tombstone behaviors.
+
+        For POST on a tombstone, check `test_resurrection`.
         '''
         '''
         tstone_resp = self.client.get('/ldp/test_delete01')
         tstone_resp = self.client.get('/ldp/test_delete01')
         assert tstone_resp.status_code == 410
         assert tstone_resp.status_code == 410
@@ -330,7 +332,6 @@ class TestLdp:
         tstone_path = '/ldp/test_delete01/fcr:tombstone'
         tstone_path = '/ldp/test_delete01/fcr:tombstone'
         assert self.client.get(tstone_path).status_code == 405
         assert self.client.get(tstone_path).status_code == 405
         assert self.client.put(tstone_path).status_code == 405
         assert self.client.put(tstone_path).status_code == 405
-        assert self.client.post(tstone_path).status_code == 405
         assert self.client.delete(tstone_path).status_code == 204
         assert self.client.delete(tstone_path).status_code == 204
 
 
         assert self.client.get('/ldp/test_delete01').status_code == 404
         assert self.client.get('/ldp/test_delete01').status_code == 404
@@ -621,6 +622,24 @@ class TestVersion:
             Literal('v1')]
             Literal('v1')]
 
 
 
 
+    def test_dupl_version(self):
+        '''
+        Make sure that two POSTs with the same slug result in two different
+        versions.
+        '''
+        path = '/ldp/test_duplicate_slug'
+        self.client.put(path)
+        v1_rsp = self.client.post(path + '/fcr:versions',
+            headers={'slug' : 'v1'})
+        v1_uri = v1_rsp.headers['Location']
+
+        dup_rsp = self.client.post(path + '/fcr:versions',
+            headers={'slug' : 'v1'})
+        dup_uri = dup_rsp.headers['Location']
+
+        assert v1_uri != dup_uri
+
+
     def test_revert_version(self):
     def test_revert_version(self):
         '''
         '''
         Take a version snapshot, update a resource, and then revert to the
         Take a version snapshot, update a resource, and then revert to the
@@ -658,9 +677,42 @@ class TestVersion:
 
 
         revert_rsp = self.client.get(rsrc_path)
         revert_rsp = self.client.get(rsrc_path)
         revert_gr = Graph().parse(data=revert_rsp.data, format='turtle')
         revert_gr = Graph().parse(data=revert_rsp.data, format='turtle')
-        #import pdb; pdb.set_trace()
         assert revert_gr[
         assert revert_gr[
             URIRef(g.webroot + '/test_revert_version')
             URIRef(g.webroot + '/test_revert_version')
             : URIRef('urn:demo:p1')
             : URIRef('urn:demo:p1')
             : URIRef('urn:demo:o1')
             : URIRef('urn:demo:o1')
         ]
         ]
+
+
+    def test_resurrection(self):
+        '''
+        Delete and then resurrect a resource.
+
+        Make sure that the resource is resurrected to the latest version.
+        '''
+        path = '/ldp/test_lazarus'
+        self.client.put(path)
+
+        self.client.post(path + '/fcr:versions')
+        self.client.put(
+            path, headers={'content-type': 'text/turtle'},
+            data=b'<> <urn:demo:p1> <urn:demo:o1> .')
+        self.client.post(path + '/fcr:versions')
+        self.client.put(
+            path, headers={'content-type': 'text/turtle'},
+            data=b'<> <urn:demo:p1> <urn:demo:o2> .')
+
+        self.client.delete(path)
+
+        assert self.client.get(path).status_code == 410
+
+        self.client.post(path + '/fcr:tombstone')
+
+        laz_data = self.client.get(path).data
+        laz_gr = Graph().parse(data=laz_data, format='turtle')
+        import pdb; pdb.set_trace()
+        assert laz_gr[
+            URIRef(g.webroot + '/test_lazarus')
+            : URIRef('urn:demo:p1')
+            : URIRef('urn:demo:o2')
+        ]