From 876593f1d0eee7384bc2764bcd17e70db83c0995 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Mon, 23 Apr 2018 09:56:36 -0500 Subject: [PATCH 01/11] Bump version to a15. --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 911225b..ab2714b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0a14 +1.0.0a15 From e9d54a30996e4892c4021a5a17135d52a797a801 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Mon, 23 Apr 2018 17:26:40 -0500 Subject: [PATCH 02/11] fcr:versions improvements. * Replace the last SPARQL statement in class with Python functions * Remove `_parse_construct` method * Improve performance of `fcr:versions` method (4s > 30ms) --- .../store/ldp_rs/rsrc_centric_layout.py | 105 ++++++++++-------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/lakesuperior/store/ldp_rs/rsrc_centric_layout.py b/lakesuperior/store/ldp_rs/rsrc_centric_layout.py index 9833e70..52ad5b8 100644 --- a/lakesuperior/store/ldp_rs/rsrc_centric_layout.py +++ b/lakesuperior/store/ldp_rs/rsrc_centric_layout.py @@ -77,6 +77,7 @@ class and implementing all its abstract methods. nsc['ldp'].membershipResource, nsc['ldp'].hasMemberRelation, nsc['ldp'].insertedContentRelation, + nsc['iana'].describedBy, nsc['premis'].hasMessageDigest, nsc['premis'].hasSize, @@ -88,6 +89,7 @@ class and implementing all its abstract methods. nsc['fcrepo'].Container, nsc['fcrepo'].Pairtree, nsc['fcrepo'].Resource, + nsc['fcrepo'].Version, nsc['fcsystem'].Tombstone, nsc['ldp'].BasicContainer, nsc['ldp'].Container, @@ -106,13 +108,35 @@ class and implementing all its abstract methods. } }, } + """ + Human-manageable map of attribute routes. + + This serves as the source for :data:`attr_routes`. + """ - # RDF types of graphs by prefix. graph_ns_types = { nsc['fcadmin']: nsc['fcsystem'].AdminGraph, nsc['fcmain']: nsc['fcsystem'].UserProvidedGraph, nsc['fcstruct']: nsc['fcsystem'].StructureGraph, } + """ + RDF types of graphs by prefix. + """ + + ignore_vmeta_preds = { + nsc['foaf'].primaryTopic, + } + """ + Predicates of version metadata to be ignored in output. + """ + + ignore_vmeta_types = { + nsc['fcsystem'].AdminGraph, + nsc['fcsystem'].UserProvidedGraph, + } + """ + RDF types of version metadata to be ignored in output. + """ ## MAGIC METHODS ## @@ -293,6 +317,7 @@ def get_user_data(self, uid): Get all the user-provided data. :param string uid: Resource UID. + :rtype: rdflib.Graph """ # *TODO* This only works as long as there is only one user-provided # graph. If multiple user-provided graphs will be supported, this @@ -306,40 +331,39 @@ def get_user_data(self, uid): def get_version_info(self, uid, strict=True): """ Get all metadata about a resource's versions. + + :param string uid: Resource UID. + :rtype: rdflib.Graph """ - # **Note:** This pretty much bends the ontology—it replaces the graph URI - # with the subject URI. But the concepts of data and metadata in Fedora - # are quite fluid anyways... - - # WIP—Is it worth to replace SPARQL here? - #versions = self.ds.graph(nsc['fcadmin'][uid]).triples( - # (nsc['fcres'][uid], nsc['fcrepo'].hasVersion, None)) - #for version in versions: - # version_meta = self.ds.graph(HIST_GRAPH_URI).triples( - qry = """ - CONSTRUCT { - ?s fcrepo:hasVersion ?v . - ?v ?p ?o . - } { - GRAPH ?ag { - ?s fcrepo:hasVersion ?v . - } - GRAPH ?hg { - ?vm foaf:primaryTopic ?v . - ?vm ?p ?o . - FILTER (?o != ?v) - } - }""" - gr = self._parse_construct(qry, init_bindings={ - 'ag': nsc['fcadmin'][uid], - 'hg': HIST_GR_URI, - 's': nsc['fcres'][uid]}) - ver_info_gr = Graph(identifier=nsc['fcres'][uid]) - ver_info_gr += gr - if strict: - self._check_rsrc_status(ver_info_gr) + # **Note:** This pretty much bends the ontology—it replaces the graph + # URI with the subject URI. But the concepts of data and metadata in + # Fedora are quite fluid anyways... - return ver_info_gr + # Result graph. + vmeta_gr = Graph(identifier=nsc['fcres'][uid]) + + # Get version meta graphs. + v_triples = self.ds.graph(nsc['fcadmin'][uid]).triples( + (nsc['fcres'][uid], nsc['fcrepo'].hasVersion, None)) + + #import pdb; pdb.set_trace() + #Get version graphs proper. + for vtrp in v_triples: + # While at it, add the hasVersion triple to the result graph. + vmeta_gr.add(vtrp) + vmeta_uris = self.ds.graph(HIST_GR_URI).subjects( + nsc['foaf'].primaryTopic, vtrp[2]) + # Get triples in the meta graph filtering out undesired triples. + for vmuri in vmeta_uris: + for trp in self.ds.graph(HIST_GR_URI).triples( + (vmuri, None, None)): + if ( + (trp[1] != nsc['rdf'].type + or trp[2] not in self.ignore_vmeta_types) + and (trp[1] not in self.ignore_vmeta_preds)): + vmeta_gr.add((vtrp[2], trp[1], trp[2])) + + return vmeta_gr def get_inbound_rel(self, subj_uri, full_triple=True): @@ -594,21 +618,6 @@ def _check_rsrc_status(self, gr): gr.value(gr.identifier, nsc['fcrepo'].created)) - def _parse_construct(self, qry, init_bindings={}): - """ - Parse a CONSTRUCT query. - - :rtype: rdflib.Graph - """ - try: - qres = self.ds.query(qry, initBindings=init_bindings) - except ResultException: - # RDFlib bug: https://github.com/RDFLib/rdflib/issues/775 - return Graph() - else: - return qres.graph - - def _map_graph_uri(self, t, uid): """ Map a triple to a namespace prefix corresponding to a graph. From 62aed9ea3e22f8a7f060968dde71805886c9b7e2 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Mon, 23 Apr 2018 17:57:11 -0500 Subject: [PATCH 03/11] Remove redundant Ldpr.create_rsrc_snapshot. --- lakesuperior/model/ldpr.py | 93 ++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/lakesuperior/model/ldpr.py b/lakesuperior/model/ldpr.py index bed6fca..eedf8f8 100644 --- a/lakesuperior/model/ldpr.py +++ b/lakesuperior/model/ldpr.py @@ -399,7 +399,7 @@ def bury_rsrc(self, inbound, tstone_pointer=None): """ logger.info('Burying resource {}'.format(self.uid)) # Create a backup snapshot for resurrection purposes. - self.create_rsrc_snapshot(uuid4()) + self.create_version() remove_trp = { trp for trp in self.imr @@ -421,7 +421,7 @@ def bury_rsrc(self, inbound, tstone_pointer=None): remove_trp = {(ib_rsrc_uri, None, self.uri)} ib_rsrc = Ldpr(ib_rsrc_uri) # To preserve inbound links in history, create a snapshot - ib_rsrc.create_rsrc_snapshot(uuid4()) + ib_rsrc.create_version() ib_rsrc.modify(RES_UPDATED, remove_trp) return RES_DELETED @@ -440,52 +440,6 @@ def forget_rsrc(self, inbound=True): return RES_DELETED - def create_rsrc_snapshot(self, ver_uid): - """ - Perform version creation and return the version UID. - """ - # Create version resource from copying the current state. - logger.info( - 'Creating version snapshot {} for resource {}.'.format( - ver_uid, self.uid)) - ver_add_gr = set() - vers_uid = '{}/{}'.format(self.uid, VERS_CONT_LABEL) - ver_uid = '{}/{}'.format(vers_uid, ver_uid) - ver_uri = nsc['fcres'][ver_uid] - ver_add_gr.add((ver_uri, RDF.type, nsc['fcrepo'].Version)) - for t in self.imr: - if ( - t[1] == RDF.type and t[2] in { - nsc['fcrepo'].Binary, - nsc['fcrepo'].Container, - nsc['fcrepo'].Resource, - } - ) or ( - t[1] in { - nsc['fcrepo'].hasParent, - nsc['fcrepo'].hasVersions, - nsc['fcrepo'].hasVersion, - nsc['premis'].hasMessageDigest, - } - ): - pass - else: - ver_add_gr.add(( - self.tbox.replace_term_domain(t[0], self.uri, ver_uri), - t[1], t[2])) - - rdfly.modify_rsrc(ver_uid, add_trp=ver_add_gr) - - # Update resource admin data. - rsrc_add_gr = { - (self.uri, nsc['fcrepo'].hasVersion, ver_uri), - (self.uri, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uid]), - } - self.modify(RES_UPDATED, add_trp=rsrc_add_gr) - - return ver_uid - - def resurrect_rsrc(self): """ Resurrect a resource from a tombstone. @@ -538,7 +492,48 @@ def create_version(self, ver_uid=None): if not ver_uid or ver_uid in self.version_uids: ver_uid = str(uuid4()) - return self.create_rsrc_snapshot(ver_uid) + # Create version resource from copying the current state. + logger.info( + 'Creating version snapshot {} for resource {}.'.format( + ver_uid, self.uid)) + ver_add_gr = set() + vers_uid = '{}/{}'.format(self.uid, VERS_CONT_LABEL) + ver_uid = '{}/{}'.format(vers_uid, ver_uid) + ver_uri = nsc['fcres'][ver_uid] + ver_add_gr.add((ver_uri, RDF.type, nsc['fcrepo'].Version)) + for t in self.imr: + if ( + t[1] == RDF.type and t[2] in { + nsc['fcrepo'].Binary, + nsc['fcrepo'].Container, + nsc['fcrepo'].Resource, + } + ) or ( + t[1] in { + nsc['fcrepo'].hasParent, + nsc['fcrepo'].hasVersions, + nsc['fcrepo'].hasVersion, + nsc['premis'].hasMessageDigest, + } + ): + pass + else: + ver_add_gr.add(( + self.tbox.replace_term_domain(t[0], self.uri, ver_uri), + t[1], t[2])) + + rdfly.modify_rsrc(ver_uid, add_trp=ver_add_gr) + + # Update resource admin data. + rsrc_add_gr = { + (self.uri, nsc['fcrepo'].hasVersion, ver_uri), + (self.uri, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uid]), + } + self.modify(RES_UPDATED, add_trp=rsrc_add_gr) + + return ver_uid + + def revert_to_version(self, ver_uid, backup=True): From 2fda0a181c9698366d836b2f8213af1956bafbed Mon Sep 17 00:00:00 2001 From: Stefano Cossu <4306733+scossu@users.noreply.github.com> Date: Tue, 24 Apr 2018 21:00:13 -0500 Subject: [PATCH 04/11] Add PyPI package badge to README. --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8ef71eb..f995e74 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ LAKEsuperior ============ -|build status| |docs| +|build status| |docs| |pypi| LAKEsuperior is an alternative `Fedora Repository `__ implementation. @@ -65,3 +65,7 @@ including installation instructions. .. |docs| image:: https://readthedocs.org/projects/lakesuperior/badge/ :alt: Documentation Status :target: https://lakesuperior.readthedocs.io/en/latest/?badge=latest + +.. |pypi| image:: https://badge.fury.io/py/lakesuperior.svg + :alt: PyPI Package + :target: https://badge.fury.io/py/lakesuperior From d0a76c0569b7ecc6577d06967397f5f25a813d19 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Wed, 25 Apr 2018 16:21:10 -0500 Subject: [PATCH 05/11] Fix 500 error when POSTing a LDP-NR. --- lakesuperior/endpoints/ldp.py | 2 +- tests/endpoints/test_ldp.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lakesuperior/endpoints/ldp.py b/lakesuperior/endpoints/ldp.py index f22645c..1958576 100644 --- a/lakesuperior/endpoints/ldp.py +++ b/lakesuperior/endpoints/ldp.py @@ -251,7 +251,7 @@ def post_resource(parent_uid): uri = g.tbox.uid_to_uri(uid) hdr = {'Location' : uri} - if mimetype and not is_rdf: + if mimetype and rdf_fmt is None: hdr['Link'] = '<{0}/fcr:metadata>; rel="describedby"; anchor="{0}"'\ .format(uri) diff --git a/tests/endpoints/test_ldp.py b/tests/endpoints/test_ldp.py index e1d4ad0..d81ce6d 100644 --- a/tests/endpoints/test_ldp.py +++ b/tests/endpoints/test_ldp.py @@ -263,6 +263,25 @@ def test_post_resource(self, client): assert 'Location' in res.headers + def test_post_ldp_nr(self, rnd_img): + ''' + POST a resource with binary payload and verify checksums. + ''' + rnd_img['content'].seek(0) + resp = self.client.post('/ldp/', data=rnd_img['content'], + headers={ + 'slug': 'ldpnr03', + 'Content-Type': 'image/png', + 'Content-Disposition' : 'attachment; filename={}'.format( + rnd_img['filename'])}) + assert resp.status_code == 201 + + resp = self.client.get( + '/ldp/ldpnr03', headers={'accept' : 'image/png'}) + assert resp.status_code == 200 + assert sha1(resp.data).hexdigest() == rnd_img['hash'] + + def test_post_slug(self): ''' Verify that a POST with slug results in the expected URI only if the From 0bf760813729153fb8423c710fca0c4368a1be6c Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Wed, 25 Apr 2018 22:47:31 -0500 Subject: [PATCH 06/11] Fix SPARQL updates with hash URIs; clean up stale namespaces. --- lakesuperior/dictionaries/namespaces.py | 6 ----- lakesuperior/model/ldpr.py | 27 ++++++++++++++----- tests/api/test_resource_api.py | 36 +++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/lakesuperior/dictionaries/namespaces.py b/lakesuperior/dictionaries/namespaces.py index dc77d6e..b3660ad 100644 --- a/lakesuperior/dictionaries/namespaces.py +++ b/lakesuperior/dictionaries/namespaces.py @@ -12,7 +12,6 @@ 'dcterms' : rdflib.namespace.DCTERMS, 'ebucore' : Namespace( 'http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#'), - #'fcrconfig' : Namespace('http://fedora.info/definitions/v4/config#'), 'fcrepo' : Namespace('http://fedora.info/definitions/v4/repository#'), 'fcadmin' : Namespace('info:fcsystem/graph/admin'), 'fcres' : Namespace('info:fcres'), @@ -22,15 +21,12 @@ 'foaf': Namespace('http://xmlns.com/foaf/0.1/'), 'iana' : Namespace('http://www.iana.org/assignments/relation/'), 'ldp' : Namespace('http://www.w3.org/ns/ldp#'), - # This is used in the layout attribute router. 'pcdm': Namespace('http://pcdm.org/models#'), 'premis' : Namespace('http://www.loc.gov/premis/rdf/v1#'), 'rdf' : rdflib.namespace.RDF, 'rdfs' : rdflib.namespace.RDFS, 'webac' : Namespace('http://www.w3.org/ns/auth/acl#'), - 'xml' : Namespace('http://www.w3.org/XML/1998/namespace'), 'xsd' : rdflib.namespace.XSD, - 'xsi' : Namespace('http://www.w3.org/2001/XMLSchema-instance'), } ns_collection = core_namespaces.copy() @@ -38,9 +34,7 @@ ns_collection.update(custom_ns) ns_mgr = NamespaceManager(Graph()) -ns_pfx_sparql = {} # Collection of prefixes in a dict. for ns,uri in ns_collection.items(): ns_mgr.bind(ns, uri, override=False) - #ns_pfx_sparql[ns] = 'PREFIX {}: <{}>'.format(ns, uri) diff --git a/lakesuperior/model/ldpr.py b/lakesuperior/model/ldpr.py index eedf8f8..97130bb 100644 --- a/lakesuperior/model/ldpr.py +++ b/lakesuperior/model/ldpr.py @@ -414,10 +414,11 @@ def bury_rsrc(self, inbound, tstone_pointer=None): (self.uri, nsc['fcrepo'].created, thread_env.timestamp_term), } + ib_rsrc_uris = self.imr.subjects(None, self.uri) self.modify(RES_DELETED, remove_trp, add_trp) if inbound: - for ib_rsrc_uri in self.imr.subjects(None, self.uri): + for ib_rsrc_uri in ib_rsrc_uris: remove_trp = {(ib_rsrc_uri, None, self.uri)} ib_rsrc = Ldpr(ib_rsrc_uri) # To preserve inbound links in history, create a snapshot @@ -611,7 +612,7 @@ def check_mgd_terms(self, trp): return trp - def sparql_delta(self, q): + def sparql_delta(self, qry_str): """ Calculate the delta obtained by a SPARQL Update operation. @@ -635,11 +636,18 @@ def sparql_delta(self, q): with ``BaseStoreLayout.update_resource`` and/or recorded as separate events in a provenance tracking system. """ - logger.debug('Provided SPARQL query: {}'.format(q)) - pre_gr = self.imr + logger.debug('Provided SPARQL query: {}'.format(qry_str)) + # Workaround for RDFLib bug. See + # https://github.com/RDFLib/rdflib/issues/824 + qry_str = ( + re.sub('<#([^>]+)>', '<{}#\\1>'.format(self.uri), qry_str) + .replace('<>', '<{}>'.format(self.uri))) + pre_gr = Graph(identifier=self.uri) + pre_gr += self.imr + post_gr = Graph(identifier=self.uri) + post_gr += self.imr - post_gr = pre_gr | Graph() - post_gr.update(q) + post_gr.update(qry_str) remove_gr, add_gr = self._dedup_deltas(pre_gr, post_gr) @@ -684,6 +692,13 @@ def modify( :param set add_trp: Triples to be added. """ rdfly.modify_rsrc(self.uid, remove_trp, add_trp) + # Clear IMR buffer. + if hasattr(self, 'imr'): + delattr(self, '_imr') + try: + self.imr + except (ResourceNotExistsError, TombstoneError): + pass if ( ev_type is not None and diff --git a/tests/api/test_resource_api.py b/tests/api/test_resource_api.py index 681f8e5..165325a 100644 --- a/tests/api/test_resource_api.py +++ b/tests/api/test_resource_api.py @@ -250,6 +250,42 @@ def test_delta_update_wildcard(self): rsrc.uri : nsc['foaf'].name : Literal('Joe 12oz Bob')] + def test_sparql_update(self): + """ + Update a resource using a SPARQL Update string. + + Use a mix of relative and absolute URIs. + """ + uid = '/test_sparql' + rdf_data = b'<> "Original title." .' + update_str = '''DELETE { + <> "Original title." . + } INSERT { + <> "Title #2." . + + "Title #3." . + <#h1> "This is a hash." . + } WHERE { + }''' + rsrc_api.create_or_replace(uid, rdf_data=rdf_data, rdf_fmt='turtle') + ver_uid = rsrc_api.create_version(uid, 'v1').split('fcr:versions/')[-1] + + rsrc = rsrc_api.update(uid, update_str) + assert ( + (rsrc.uri, nsc['dcterms'].title, Literal('Original title.')) + not in set(rsrc.imr)) + assert ( + (rsrc.uri, nsc['dcterms'].title, Literal('Title #2.')) + in set(rsrc.imr)) + assert ( + (rsrc.uri, nsc['dcterms'].title, Literal('Title #3.')) + in set(rsrc.imr)) + assert (( + URIRef(str(rsrc.uri) + '#h1'), + nsc['dcterms'].title, Literal('This is a hash.')) + in set(rsrc.imr)) + + def test_create_ldp_dc_post(self, dc_rdf): """ Create an LDP Direct Container via POST. From 1dfad98d38c546c6dcd56b8765577fc92db8d961 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Wed, 25 Apr 2018 23:05:25 -0500 Subject: [PATCH 07/11] Fix and improve version handling; revert to version. --- lakesuperior/api/resource.py | 13 +++++++- lakesuperior/endpoints/ldp.py | 2 +- lakesuperior/model/ldp_factory.py | 8 ++--- lakesuperior/model/ldpr.py | 40 +++++++++++++++++-------- tests/api/test_resource_api.py | 49 ++++++++++++++++++++++++++++++- 5 files changed, 91 insertions(+), 21 deletions(-) diff --git a/lakesuperior/api/resource.py b/lakesuperior/api/resource.py index 6beff8d..10f5553 100644 --- a/lakesuperior/api/resource.py +++ b/lakesuperior/api/resource.py @@ -164,7 +164,7 @@ def get(uid, repr_options={}): - incl_children: include children URIs. Default: True. - embed_children: Embed full graph of all child resources. Default: False """ - rsrc = LdpFactory.from_stored(uid, repr_options) + rsrc = LdpFactory.from_stored(uid, repr_opts=repr_options) # Load graph before leaving the transaction. rsrc.imr @@ -329,6 +329,17 @@ def delete(uid, soft=True): return ret +@transaction(True) +def revert_to_version(uid, ver_uid): + """ + Restore a resource to a previous version state. + + :param str uid: Resource UID. + :param str ver_uid: Version UID. + """ + return LdpFactory.from_stored(uid).revert_to_version(ver_uid) + + @transaction(True) def resurrect(uid): """ diff --git a/lakesuperior/endpoints/ldp.py b/lakesuperior/endpoints/ldp.py index 1958576..2d162fd 100644 --- a/lakesuperior/endpoints/ldp.py +++ b/lakesuperior/endpoints/ldp.py @@ -446,7 +446,7 @@ def patch_version(uid, ver_uid): :param str ver_uid: Version UID. """ try: - LdpFactory.from_stored(uid).revert_to_version(ver_uid) + rsrc_api.revert_to_version(uid, rsrc_uid) except ResourceNotExistsError as e: return str(e), 404 except InvalidResourceError as e: diff --git a/lakesuperior/model/ldp_factory.py b/lakesuperior/model/ldp_factory.py index 70c0f4d..106cf7b 100644 --- a/lakesuperior/model/ldp_factory.py +++ b/lakesuperior/model/ldp_factory.py @@ -42,7 +42,7 @@ def new_container(uid): @staticmethod - def from_stored(uid, repr_opts={}, **kwargs): + def from_stored(uid, ver_label=None, repr_opts={}, strict=True, **kwargs): """ Create an instance for retrieval purposes. @@ -55,12 +55,10 @@ def from_stored(uid, repr_opts={}, **kwargs): :param uid: UID of the instance. """ #logger.info('Retrieving stored resource: {}'.format(uid)) - imr_urn = nsc['fcres'][uid] - - rsrc_meta = rdfly.get_metadata(uid) + rsrc_meta = rdfly.get_metadata(uid, strict=strict) #logger.debug('Extracted metadata: {}'.format( # pformat(set(rsrc_meta)))) - rdf_types = set(rsrc_meta[imr_urn : RDF.type]) + rdf_types = set(rsrc_meta[nsc['fcres'][uid] : RDF.type]) if LDP_NR_TYPE in rdf_types: logger.info('Resource is a LDP-NR.') diff --git a/lakesuperior/model/ldpr.py b/lakesuperior/model/ldpr.py index 97130bb..637e03a 100644 --- a/lakesuperior/model/ldpr.py +++ b/lakesuperior/model/ldpr.py @@ -1,4 +1,5 @@ import logging +import re from abc import ABCMeta from collections import defaultdict @@ -98,6 +99,30 @@ class Ldpr(metaclass=ABCMeta): } """Predicates to remove when a resource is replaced.""" + _ignore_version_preds = { + nsc['fcrepo'].hasParent, + nsc['fcrepo'].hasVersions, + nsc['fcrepo'].hasVersion, + nsc['premis'].hasMessageDigest, + nsc['ldp'].contains, + } + """Predicates that don't get versioned.""" + + _ignore_version_types = { + nsc['fcrepo'].Binary, + nsc['fcrepo'].Container, + nsc['fcrepo'].Pairtree, + nsc['fcrepo'].Resource, + nsc['fcrepo'].Version, + nsc['ldp'].BasicContainer, + nsc['ldp'].Container, + nsc['ldp'].DirectContainer, + nsc['ldp'].Resource, + nsc['ldp'].RDFSource, + nsc['ldp'].NonRDFSource, + } + """RDF types that don't get versioned.""" + ## MAGIC METHODS ## @@ -504,19 +529,8 @@ def create_version(self, ver_uid=None): ver_add_gr.add((ver_uri, RDF.type, nsc['fcrepo'].Version)) for t in self.imr: if ( - t[1] == RDF.type and t[2] in { - nsc['fcrepo'].Binary, - nsc['fcrepo'].Container, - nsc['fcrepo'].Resource, - } - ) or ( - t[1] in { - nsc['fcrepo'].hasParent, - nsc['fcrepo'].hasVersions, - nsc['fcrepo'].hasVersion, - nsc['premis'].hasMessageDigest, - } - ): + t[1] == RDF.type and t[2] in self._ignore_version_types + ) or t[1] in self._ignore_version_preds: pass else: ver_add_gr.add(( diff --git a/tests/api/test_resource_api.py b/tests/api/test_resource_api.py index 165325a..910d81a 100644 --- a/tests/api/test_resource_api.py +++ b/tests/api/test_resource_api.py @@ -46,7 +46,7 @@ def ic_rdf(): @pytest.mark.usefixtures('db') -class TestResourceApi: +class TestResourceCRUD: ''' Test interaction with the Resource API. ''' @@ -361,3 +361,50 @@ def test_indirect_container(self, ic_rdf): top_cont_rsrc.uri: nsc['dcterms'].relation: nsc['fcres'][target_uid]] + + +@pytest.mark.usefixtures('db') +class TestResourceVersioning: + ''' + Test resource version lifecycle. + ''' + def test_create_version(self): + """ + Create a version snapshot. + """ + uid = '/test_version1' + rdf_data = b'<> "Original title." .' + update_str = '''DELETE { + <> "Original title." . + } INSERT { + <> "Title #2." . + } WHERE { + }''' + rsrc_api.create_or_replace(uid, rdf_data=rdf_data, rdf_fmt='turtle') + ver_uid = rsrc_api.create_version(uid, 'v1').split('fcr:versions/')[-1] + + rsrc_api.update(uid, update_str) + current = rsrc_api.get(uid) + assert ( + (current.uri, nsc['dcterms'].title, Literal('Title #2.')) + in current.imr) + + v1 = rsrc_api.get_version(uid, ver_uid) + assert ( + (v1.identifier, nsc['dcterms'].title, Literal('Original title.')) + in set(v1)) + + + def test_revert_to_version(self): + """ + Test reverting to a previous version. + + Uses assets from previous test. + """ + uid = '/test_version1' + ver_uid = 'v1' + rsrc_api.revert_to_version(uid, ver_uid) + rev = rsrc_api.get(uid) + assert ( + (rev.uri, nsc['dcterms'].title, Literal('Original title.')) + in rev.imr) From 573467e27a94e2aee8224608e870ae29626de5c1 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Thu, 26 Apr 2018 09:06:24 -0500 Subject: [PATCH 08/11] Test children of versioned resource. --- lakesuperior/api/resource.py | 2 +- tests/api/test_resource_api.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lakesuperior/api/resource.py b/lakesuperior/api/resource.py index 10f5553..acdff6a 100644 --- a/lakesuperior/api/resource.py +++ b/lakesuperior/api/resource.py @@ -347,4 +347,4 @@ def resurrect(uid): :param str uid: Resource UID. """ - return LdpFactory.from_stored(uid).resurrect_rsrc() + return LdpFactory.from_stored(uid, strict=False).resurrect_rsrc() diff --git a/tests/api/test_resource_api.py b/tests/api/test_resource_api.py index 910d81a..d2e6857 100644 --- a/tests/api/test_resource_api.py +++ b/tests/api/test_resource_api.py @@ -388,11 +388,17 @@ def test_create_version(self): assert ( (current.uri, nsc['dcterms'].title, Literal('Title #2.')) in current.imr) + assert ( + (current.uri, nsc['dcterms'].title, Literal('Original title.')) + not in current.imr) v1 = rsrc_api.get_version(uid, ver_uid) assert ( (v1.identifier, nsc['dcterms'].title, Literal('Original title.')) in set(v1)) + assert ( + (v1.identifier, nsc['dcterms'].title, Literal('Title #2.')) + not in set(v1)) def test_revert_to_version(self): @@ -408,3 +414,41 @@ def test_revert_to_version(self): assert ( (rev.uri, nsc['dcterms'].title, Literal('Original title.')) in rev.imr) + + + def test_versioning_children(self): + """ + Test that children are not affected by version restoring. + + This test does the following: + + 1. create parent resource + 2. Create child 1 + 3. Version parent + 4. Create child 2 + 5. Restore parent to previous version + 6. Verify that restored version still has 2 children + """ + uid = '/test_version_children' + ver_uid = 'v1' + ch1_uid = '{}/kid_a'.format(uid) + ch2_uid = '{}/kid_b'.format(uid) + rsrc_api.create_or_replace(uid) + rsrc_api.create_or_replace(ch1_uid) + ver_uid = rsrc_api.create_version(uid, ver_uid).split('fcr:versions/')[-1] + rsrc = rsrc_api.get(uid) + assert nsc['fcres'][ch1_uid] in rsrc.imr.objects( + rsrc.uri, nsc['ldp'].contains) + + rsrc_api.create_or_replace(ch2_uid) + rsrc = rsrc_api.get(uid) + assert nsc['fcres'][ch2_uid] in rsrc.imr.objects( + rsrc.uri, nsc['ldp'].contains) + + rsrc_api.revert_to_version(uid, ver_uid) + rsrc = rsrc_api.get(uid) + assert nsc['fcres'][ch1_uid] in rsrc.imr.objects( + rsrc.uri, nsc['ldp'].contains) + assert nsc['fcres'][ch2_uid] in rsrc.imr.objects( + rsrc.uri, nsc['ldp'].contains) + From d3efab525ebc162405feece462e6dc378c5ba7b9 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Thu, 26 Apr 2018 22:42:53 -0500 Subject: [PATCH 09/11] Bury + resurrect. --- lakesuperior/api/resource.py | 14 ++++- lakesuperior/model/ldp_factory.py | 6 +- lakesuperior/model/ldpr.py | 58 ++++++------------- .../store/ldp_rs/rsrc_centric_layout.py | 2 +- tests/api/test_resource_api.py | 23 ++++++++ 5 files changed, 55 insertions(+), 48 deletions(-) diff --git a/lakesuperior/api/resource.py b/lakesuperior/api/resource.py index acdff6a..21ce8c4 100644 --- a/lakesuperior/api/resource.py +++ b/lakesuperior/api/resource.py @@ -310,7 +310,7 @@ def delete(uid, soft=True): if soft: rsrc = LdpFactory.from_stored(uid, repr_opts) - ret = rsrc.bury_rsrc(inbound) + ret = rsrc.bury(inbound) for child_uri in children: try: @@ -319,7 +319,7 @@ def delete(uid, soft=True): repr_opts={'incl_children' : False}) except (TombstoneError, ResourceNotExistsError): continue - child_rsrc.bury_rsrc(inbound, tstone_pointer=rsrc.uri) + child_rsrc.bury(inbound, tstone_pointer=rsrc.uri) else: ret = env.app_globals.rdfly.forget_rsrc(uid, inbound) for child_uri in children: @@ -347,4 +347,12 @@ def resurrect(uid): :param str uid: Resource UID. """ - return LdpFactory.from_stored(uid, strict=False).resurrect_rsrc() + try: + rsrc = LdpFactory.from_stored(uid) + except TombstoneError as e: + if e.uid != uid: + raise + else: + return LdpFactory.from_stored(uid, strict=False).resurrect() + else: + raise InvalidResourceError('Resource {} is not dead.'.format(uid)) diff --git a/lakesuperior/model/ldp_factory.py b/lakesuperior/model/ldp_factory.py index 106cf7b..a877d31 100644 --- a/lakesuperior/model/ldp_factory.py +++ b/lakesuperior/model/ldp_factory.py @@ -52,12 +52,10 @@ def from_stored(uid, ver_label=None, repr_opts={}, strict=True, **kwargs): N.B. The resource must exist. - :param uid: UID of the instance. + :param str uid: UID of the instance. """ - #logger.info('Retrieving stored resource: {}'.format(uid)) + # This will blow up if strict is True and the resource is a tombstone. rsrc_meta = rdfly.get_metadata(uid, strict=strict) - #logger.debug('Extracted metadata: {}'.format( - # pformat(set(rsrc_meta)))) rdf_types = set(rsrc_meta[nsc['fcres'][uid] : RDF.type]) if LDP_NR_TYPE in rdf_types: diff --git a/lakesuperior/model/ldpr.py b/lakesuperior/model/ldpr.py index 637e03a..87159a2 100644 --- a/lakesuperior/model/ldpr.py +++ b/lakesuperior/model/ldpr.py @@ -412,23 +412,21 @@ def create_or_replace(self, create_only=False): return ev_type - def bury_rsrc(self, inbound, tstone_pointer=None): + def bury(self, inbound, tstone_pointer=None): """ Delete a single resource and create a tombstone. :param boolean inbound: Whether to delete the inbound relationships. - :param rdflib.URIRef tstone_pointer: If set to a URN, this creates a + :param rdflib.URIRef tstone_pointer: If set to a URI, this creates a pointer to the tombstone of the resource that used to contain the deleted resource. Otherwise the deleted resource becomes a tombstone. """ logger.info('Burying resource {}'.format(self.uid)) - # Create a backup snapshot for resurrection purposes. - self.create_version() - + # ldp:Resource is also used in rdfly.ask_rsrc_exists. remove_trp = { - trp for trp in self.imr - if trp[1] != nsc['fcrepo'].hasVersion} + (nsc['fcrepo'].uid, nsc['rdf'].type, nsc['ldp'].Resource) + } if tstone_pointer: add_trp = { @@ -436,7 +434,7 @@ def bury_rsrc(self, inbound, tstone_pointer=None): else: add_trp = { (self.uri, RDF.type, nsc['fcsystem'].Tombstone), - (self.uri, nsc['fcrepo'].created, thread_env.timestamp_term), + (self.uri, nsc['fcsystem'].buried, thread_env.timestamp_term), } ib_rsrc_uris = self.imr.subjects(None, self.uri) @@ -453,7 +451,7 @@ def bury_rsrc(self, inbound, tstone_pointer=None): return RES_DELETED - def forget_rsrc(self, inbound=True): + def forget(self, inbound=True): """ Remove all traces of a resource and versions. """ @@ -466,42 +464,22 @@ def forget_rsrc(self, inbound=True): return RES_DELETED - def resurrect_rsrc(self): + def resurrect(self): """ Resurrect a resource from a tombstone. - - @EXPERIMENTAL """ - tstone_trp = set(rdfly.get_imr(self.uid, strict=False)) - - ver_rsp = self.version_info.query(''' - SELECT ?uid { - ?latest fcrepo:hasVersionLabel ?uid ; - fcrepo:created ?ts . + remove_trp = { + (self.uri, nsc['rdf'].type, nsc['fcsystem'].Tombstone), + (self.uri, nsc['fcsystem'].tombstone, None), + (self.uri, nsc['fcsystem'].buried, None), + } + add_trp = { + (self.uri, nsc['rdf'].type, nsc['ldp'].Resource), } - ORDER BY DESC(?ts) - LIMIT 1 - ''') - ver_uid = str(ver_rsp.bindings[0]['uid']) - ver_trp = set(rdfly.get_metadata(self.uid, 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.uri, t[1], t[2])) - laz_gr.add((self.uri, RDF.type, nsc['fcrepo'].Resource)) - if nsc['ldp'].NonRdfSource in laz_gr[:RDF.type:]: - laz_gr.add((self.uri, RDF.type, nsc['fcrepo'].Binary)) - elif nsc['ldp'].Container in laz_gr[:RDF.type:]: - laz_gr.add((self.uri, RDF.type, nsc['fcrepo'].Container)) - - laz_set = set(laz_gr) | self._containment_rel() - self.modify(RES_CREATED, tstone_trp, laz_set) - return self.uri + self.modify(RES_CREATED, remove_trp, add_trp) + return self.uri def create_version(self, ver_uid=None): @@ -707,7 +685,7 @@ def modify( """ rdfly.modify_rsrc(self.uid, remove_trp, add_trp) # Clear IMR buffer. - if hasattr(self, 'imr'): + if hasattr(self, '_imr'): delattr(self, '_imr') try: self.imr diff --git a/lakesuperior/store/ldp_rs/rsrc_centric_layout.py b/lakesuperior/store/ldp_rs/rsrc_centric_layout.py index 52ad5b8..ab27ff5 100644 --- a/lakesuperior/store/ldp_rs/rsrc_centric_layout.py +++ b/lakesuperior/store/ldp_rs/rsrc_centric_layout.py @@ -328,7 +328,7 @@ def get_user_data(self, uid): return userdata_gr - def get_version_info(self, uid, strict=True): + def get_version_info(self, uid): """ Get all metadata about a resource's versions. diff --git a/tests/api/test_resource_api.py b/tests/api/test_resource_api.py index d2e6857..14664e2 100644 --- a/tests/api/test_resource_api.py +++ b/tests/api/test_resource_api.py @@ -362,6 +362,29 @@ def test_indirect_container(self, ic_rdf): nsc['fcres'][target_uid]] + def test_soft_delete(self): + """ + Soft-delete a resource. + """ + uid = '/test_soft_delete01' + rsrc_api.create_or_replace(uid) + rsrc_api.delete(uid) + with pytest.raises(TombstoneError): + rsrc_api.get(uid) + + + def test_resurrect(self): + """ + Resurrect a soft-deleted resource. + """ + uid = '/test_soft_delete02' + rsrc_api.create_or_replace(uid) + rsrc_api.delete(uid) + rsrc_api.resurrect(uid) + + rsrc = rsrc_api.get(uid) + assert nsc['ldp'].Resource in rsrc.ldp_types + @pytest.mark.usefixtures('db') class TestResourceVersioning: From 1f4f357a2463b6ef503e40138261f555ec8233a9 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Fri, 27 Apr 2018 12:22:57 -0500 Subject: [PATCH 10/11] Bury and resurrect inbound and descendants. --- lakesuperior/api/resource.py | 24 ++------- lakesuperior/model/ldpr.py | 37 ++++++++++--- .../store/ldp_rs/rsrc_centric_layout.py | 15 ++++++ tests/api/test_resource_api.py | 52 ++++++++++++++++++- 4 files changed, 100 insertions(+), 28 deletions(-) diff --git a/lakesuperior/api/resource.py b/lakesuperior/api/resource.py index 21ce8c4..5cc0a18 100644 --- a/lakesuperior/api/resource.py +++ b/lakesuperior/api/resource.py @@ -292,7 +292,7 @@ def create_version(uid, ver_uid): @transaction(True) -def delete(uid, soft=True): +def delete(uid, soft=True, inbound=True): """ Delete a resource. @@ -306,27 +306,11 @@ def delete(uid, soft=True): inbound = True if refint else inbound repr_opts = {'incl_inbound' : True} if refint else {} - children = env.app_globals.rdfly.get_descendants(uid) - + rsrc = LdpFactory.from_stored(uid, repr_opts) if soft: - rsrc = LdpFactory.from_stored(uid, repr_opts) - ret = rsrc.bury(inbound) - - for child_uri in children: - try: - child_rsrc = LdpFactory.from_stored( - env.app_globals.rdfly.uri_to_uid(child_uri), - repr_opts={'incl_children' : False}) - except (TombstoneError, ResourceNotExistsError): - continue - child_rsrc.bury(inbound, tstone_pointer=rsrc.uri) + return rsrc.bury(inbound) else: - ret = env.app_globals.rdfly.forget_rsrc(uid, inbound) - for child_uri in children: - child_uid = env.app_globals.rdfly.uri_to_uid(child_uri) - ret = env.app_globals.rdfly.forget_rsrc(child_uid, inbound) - - return ret + return rsrc.forget(inbound) @transaction(True) diff --git a/lakesuperior/model/ldpr.py b/lakesuperior/model/ldpr.py index 87159a2..057b3c9 100644 --- a/lakesuperior/model/ldpr.py +++ b/lakesuperior/model/ldpr.py @@ -416,7 +416,11 @@ def bury(self, inbound, tstone_pointer=None): """ Delete a single resource and create a tombstone. - :param boolean inbound: Whether to delete the inbound relationships. + :param bool inbound: Whether inbound relationships are + removed. If ``False``, resources will keep referring + to the deleted resource; their link will point to a tombstone + (which will raise a ``TombstoneError`` in the Python API or a + ``410 Gone`` in the LDP API). :param rdflib.URIRef tstone_pointer: If set to a URI, this creates a pointer to the tombstone of the resource that used to contain the deleted resource. Otherwise the deleted resource becomes a @@ -437,17 +441,28 @@ def bury(self, inbound, tstone_pointer=None): (self.uri, nsc['fcsystem'].buried, thread_env.timestamp_term), } - ib_rsrc_uris = self.imr.subjects(None, self.uri) - self.modify(RES_DELETED, remove_trp, add_trp) - + # Bury descendants. + from lakesuperior.model.ldp_factory import LdpFactory + for desc_uri in rdfly.get_descendants(self.uid): + try: + desc_rsrc = LdpFactory.from_stored( + env.app_globals.rdfly.uri_to_uid(desc_uri), + repr_opts={'incl_children' : False}) + except (TombstoneError, ResourceNotExistsError): + continue + desc_rsrc.bury(inbound, tstone_pointer=self.uri) + + # Cut inbound relationships if inbound: - for ib_rsrc_uri in ib_rsrc_uris: + for ib_rsrc_uri in self.imr.subjects(None, self.uri): remove_trp = {(ib_rsrc_uri, None, self.uri)} ib_rsrc = Ldpr(ib_rsrc_uri) # To preserve inbound links in history, create a snapshot ib_rsrc.create_version() ib_rsrc.modify(RES_UPDATED, remove_trp) + self.modify(RES_DELETED, remove_trp, add_trp) + return RES_DELETED @@ -458,9 +473,12 @@ def forget(self, inbound=True): logger.info('Purging resource {}'.format(self.uid)) refint = rdfly.config['referential_integrity'] inbound = True if refint else inbound + + for desc_uri in rdfly.get_descendants(self.uid): + rdfly.forget_rsrc(rdfly.uri_to_uuid(desc_uri), inbound) + rdfly.forget_rsrc(self.uid, inbound) - # @TODO This could be a different event type. return RES_DELETED @@ -479,6 +497,13 @@ def resurrect(self): self.modify(RES_CREATED, remove_trp, add_trp) + # Resurrect descendants. + from lakesuperior.model.ldp_factory import LdpFactory + descendants = env.app_globals.rdfly.get_descendants(self.uid) + for desc_uri in descendants: + LdpFactory.from_stored( + rdfly.uri_to_uid(desc_uri), strict=False).resurrect() + return self.uri diff --git a/lakesuperior/store/ldp_rs/rsrc_centric_layout.py b/lakesuperior/store/ldp_rs/rsrc_centric_layout.py index ab27ff5..e4f1492 100644 --- a/lakesuperior/store/ldp_rs/rsrc_centric_layout.py +++ b/lakesuperior/store/ldp_rs/rsrc_centric_layout.py @@ -418,6 +418,21 @@ def _recurse(dset, s, p, c): else ds.graph(ctx_uri)[subj_uri : nsc['ldp'].contains : ]) + def get_last_version_uid(self, uid): + """ + Get the UID of the last version of a resource. + + This can be used for tombstones too. + """ + ver_info = self.get_version_info(uid) + last_version_uri = sorted( + [trp for trp in ver_info if trp[1] == nsc['fcrepo'].created], + key=lambda trp:trp[2] + )[-1][0] + + return str(last_version_uri).split(VERS_CONT_LABEL + '/')[-1] + + def patch_rsrc(self, uid, qry): """ Patch a resource with SPARQL-Update statements. diff --git a/tests/api/test_resource_api.py b/tests/api/test_resource_api.py index 14664e2..d628a7d 100644 --- a/tests/api/test_resource_api.py +++ b/tests/api/test_resource_api.py @@ -364,7 +364,7 @@ def test_indirect_container(self, ic_rdf): def test_soft_delete(self): """ - Soft-delete a resource. + Soft-delete (bury) a resource. """ uid = '/test_soft_delete01' rsrc_api.create_or_replace(uid) @@ -375,7 +375,7 @@ def test_soft_delete(self): def test_resurrect(self): """ - Resurrect a soft-deleted resource. + Restore (resurrect) a soft-deleted resource. """ uid = '/test_soft_delete02' rsrc_api.create_or_replace(uid) @@ -386,6 +386,54 @@ def test_resurrect(self): assert nsc['ldp'].Resource in rsrc.ldp_types + def test_hard_delete(self): + """ + Hard-delete (forget) a resource. + """ + uid = '/test_hard_delete01' + rsrc_api.create_or_replace(uid) + rsrc_api.delete(uid, False) + with pytest.raises(ResourceNotExistsError): + rsrc_api.get(uid) + with pytest.raises(ResourceNotExistsError): + rsrc_api.resurrect(uid) + + + def test_delete_children(self): + """ + Soft-delete a resource with children. + """ + uid = '/test_soft_delete_children01' + rsrc_api.create_or_replace(uid) + for i in range(3): + rsrc_api.create_or_replace('{}/child{}'.format(uid, i)) + rsrc_api.delete(uid) + with pytest.raises(TombstoneError): + rsrc_api.get(uid) + for i in range(3): + with pytest.raises(TombstoneError): + rsrc_api.get('{}/child{}'.format(uid, i)) + # Cannot resurrect children of a tombstone. + with pytest.raises(TombstoneError): + rsrc_api.resurrect('{}/child{}'.format(uid, i)) + + + def test_resurrect_children(self): + """ + Resurrect a resource with its children. + + This uses fixtures from the previous test. + """ + uid = '/test_soft_delete_children01' + rsrc_api.resurrect(uid) + parent_rsrc = rsrc_api.get(uid) + assert nsc['ldp'].Resource in parent_rsrc.ldp_types + for i in range(3): + child_rsrc = rsrc_api.get('{}/child{}'.format(uid, i)) + assert nsc['ldp'].Resource in child_rsrc.ldp_types + + + @pytest.mark.usefixtures('db') class TestResourceVersioning: ''' From e21f107d9c9396030dc9e7b356f8d6f030cec256 Mon Sep 17 00:00:00 2001 From: Stefano Cossu Date: Fri, 27 Apr 2018 12:41:13 -0500 Subject: [PATCH 11/11] Forget resource with descendants. --- lakesuperior/api/resource.py | 5 +++-- lakesuperior/model/ldpr.py | 2 +- tests/api/test_resource_api.py | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lakesuperior/api/resource.py b/lakesuperior/api/resource.py index 5cc0a18..ae2616c 100644 --- a/lakesuperior/api/resource.py +++ b/lakesuperior/api/resource.py @@ -306,7 +306,7 @@ def delete(uid, soft=True, inbound=True): inbound = True if refint else inbound repr_opts = {'incl_inbound' : True} if refint else {} - rsrc = LdpFactory.from_stored(uid, repr_opts) + rsrc = LdpFactory.from_stored(uid, repr_opts, strict=soft) if soft: return rsrc.bury(inbound) else: @@ -339,4 +339,5 @@ def resurrect(uid): else: return LdpFactory.from_stored(uid, strict=False).resurrect() else: - raise InvalidResourceError('Resource {} is not dead.'.format(uid)) + raise InvalidResourceError( + uid, msg='Resource {} is not dead.'.format(uid)) diff --git a/lakesuperior/model/ldpr.py b/lakesuperior/model/ldpr.py index 057b3c9..2bec926 100644 --- a/lakesuperior/model/ldpr.py +++ b/lakesuperior/model/ldpr.py @@ -475,7 +475,7 @@ def forget(self, inbound=True): inbound = True if refint else inbound for desc_uri in rdfly.get_descendants(self.uid): - rdfly.forget_rsrc(rdfly.uri_to_uuid(desc_uri), inbound) + rdfly.forget_rsrc(rdfly.uri_to_uid(desc_uri), inbound) rdfly.forget_rsrc(self.uid, inbound) diff --git a/tests/api/test_resource_api.py b/tests/api/test_resource_api.py index d628a7d..7af8405 100644 --- a/tests/api/test_resource_api.py +++ b/tests/api/test_resource_api.py @@ -433,6 +433,29 @@ def test_resurrect_children(self): assert nsc['ldp'].Resource in child_rsrc.ldp_types + def test_hard_delete_children(self): + """ + Hard-delete (forget) a resource with its children. + + This uses fixtures from the previous test. + """ + uid = '/test_hard_delete_children01' + rsrc_api.create_or_replace(uid) + for i in range(3): + rsrc_api.create_or_replace('{}/child{}'.format(uid, i)) + rsrc_api.delete(uid, False) + with pytest.raises(ResourceNotExistsError): + rsrc_api.get(uid) + with pytest.raises(ResourceNotExistsError): + rsrc_api.resurrect(uid) + + for i in range(3): + with pytest.raises(ResourceNotExistsError): + rsrc_api.get('{}/child{}'.format(uid, i)) + with pytest.raises(ResourceNotExistsError): + rsrc_api.resurrect('{}/child{}'.format(uid, i)) + + @pytest.mark.usefixtures('db') class TestResourceVersioning: