def find_version_master_and_previous_record(version_of): """Retrieve the PIDNodeVersioning and previous record of a record PID. :params version_of: record PID. """ from b2share.modules.records.providers import RecordUUIDProvider from b2share.modules.records.utils import is_publication try: child_pid = RecordUUIDProvider.get(version_of).pid if child_pid.status == PIDStatus.DELETED: raise RecordNotFoundVersioningError() except PIDDoesNotExistError as e: raise RecordNotFoundVersioningError() from e parent_pid = PIDNodeVersioning(pid=child_pid).parents.first() version_master = PIDNodeVersioning(pid=parent_pid) prev_pid = version_master.last_child assert prev_pid.pid_type == RecordUUIDProvider.pid_type prev_version = Record.get_record(prev_pid.object_uuid) # check that version_of references the last version of a record assert is_publication(prev_version.model) if prev_pid.pid_value != version_of: raise IncorrectRecordVersioningError(prev_pid.pid_value) return version_master, prev_version
def retrieve_version_master(child_pid): """Retrieve the PIDNodeVersioning from a child PID.""" if type(child_pid).__name__ == "FetchedPID": # when getting a pid-like object from elasticsearch child_pid = child_pid.provider.get(child_pid.pid_value).pid parent_pid = PIDNodeVersioning(child=child_pid).parent if not parent_pid: return None return PIDNodeVersioning(parent=parent_pid)
def test_versioning_insert_draft_child(db, version_pids, build_pid): """Test the insert_draft_child method of PIDNodeVersioning.""" parent_pid = build_pid(version_pids[0]['parent']) h1 = PIDNodeVersioning(parent_pid) # assert that there is a draft_child present assert h1.draft_child == version_pids[0]['children'][-1] draft2 = PersistentIdentifier.create('recid', 'foobar.draft2', object_type='rec', status=PIDStatus.RESERVED) with pytest.raises(PIDRelationConsistencyError): # try to add a second draft_child h1.insert_draft_child(draft2)
def record_minter(record_uuid, data): parent = data.get('conceptrecid') if not parent: parent_pid = RecordIdProvider.create(object_type='rec', object_uuid=None, status=PIDStatus.REGISTERED).pid data['conceptrecid'] = parent_pid.pid_value else: parent_pid = PersistentIdentifier.get( pid_type=RecordIdProvider.pid_type, pid_value=parent) provider = RecordIdProvider.create('rec', record_uuid) data['recid'] = provider.pid.pid_value versioning = PIDNodeVersioning(pid=parent_pid) versioning.insert_child(child_pid=provider.pid) return provider.pid
def check_records_migration(app): """Check that a set of records have been migrated.""" expected_records = _load_json('expected_records.json') for exp_record in expected_records: db_record = Record.get_record(exp_record['id'], with_deleted=True) assert str(db_record.created) == exp_record['created'] # If the record is deleted there is no metadata to check if db_record.model.json is None: continue # Check that the parent pid is minted properly parent_pid = b2share_parent_pid_fetcher(exp_record['id'], db_record) fetched_pid = b2share_record_uuid_fetcher(exp_record['id'], db_record) record_pid = PersistentIdentifier.get(fetched_pid.pid_type, fetched_pid.pid_value) assert PIDNodeVersioning( record_pid).parent.pid_value == parent_pid.pid_value # Remove the parent pid as it has been added by the migration db_record['_pid'].remove({ 'type': RecordUUIDProvider.parent_pid_type, 'value': parent_pid.pid_value, }) # The OAI-PMH identifier has been modified by the migration if db_record.get('_oai'): oai_prefix = app.config.get('OAISERVER_ID_PREFIX', 'oai:') record_id = exp_record['metadata']['_deposit']['id'] assert db_record['_oai']['id'] == str(oai_prefix) + record_id exp_record['metadata']['_oai']['id'] = db_record['_oai']['id'] assert db_record == exp_record['metadata']
def is_first_version(self, pid=None, pid_value=None): """Returns if the pid is the firts version of the record. If the pid is None, it assumes a pid_value. """ if not pid: pid = self.fetch_pid_db(pid_value) pv = PIDNodeVersioning(pid=pid) return pv.children.count() == 0
def test_index_relations(app, db): """Test the index_relations method.""" data_v1 = {'body': u'test_body', 'title': u'test_title'} data_v2 = {'body': u'test_body2', 'title': u'test_title2'} # add first child to the relation rec_v1 = Record.create(data_v1) parent_pid = RecordIdProvider.create(object_type='rec', object_uuid=None, status=PIDStatus.REGISTERED).pid data_v1['conceptrecid'] = parent_pid.pid_value provider = RecordIdProvider.create('rec', rec_v1.id) data_v1['recid'] = provider.pid.pid_value versioning = PIDNodeVersioning(pid=parent_pid) versioning.insert_child(child_pid=provider.pid) db.session.commit() output = index_relations(app, 'recid', record=rec_v1) expected_output = \ {'relations': { 'version': [{ u'children': [{u'pid_type': u'recid', u'pid_value': u'2'}], u'index': 0, u'is_child': True, u'is_last': True, u'is_parent': False, u'next': None, u'parent': {u'pid_type': u'recid', u'pid_value': u'1'}, u'previous': None, u'type': 'version'}]}} assert compare_dictionaries(output, expected_output) # add second child to the relation rec_v2 = Record.create(data_v2) data_v2['conceptrecid'] = parent_pid.pid_value provider_v2 = RecordIdProvider.create('rec', rec_v2.id) versioning.insert_child(child_pid=provider_v2.pid) db.session.commit() output = index_relations(app, 'recid', record=rec_v2) expected_output = \ {'relations': { 'version': [{ u'children': [{u'pid_type': u'recid', u'pid_value': u'2'}, {u'pid_type': u'recid', u'pid_value': u'3'}], u'index': 1, u'is_child': True, u'is_last': True, u'is_parent': False, u'next': None, u'parent': {u'pid_type': u'recid', u'pid_value': u'1'}, u'previous': {u'pid_type': u'recid', u'pid_value': u'2'}, u'type': 'version'}]}} assert compare_dictionaries(output, expected_output)
def delete(self, **kwargs): """Delete a record.""" from b2share.modules.deposit.api import Deposit from b2share.modules.deposit.providers import DepositUUIDProvider pid = self.pid # Fetch deposit id from record and resolve deposit record and pid. depid = PersistentIdentifier.get(DepositUUIDProvider.pid_type, pid.pid_value) if depid.status == PIDStatus.REGISTERED: depid, deposit = Resolver( pid_type=depid.pid_type, object_type='rec', # Retrieve the deposit with the Record class on purpose # as the current Deposit api prevents the deletion of # published deposits. getter=Deposit.get_record, ).resolve(depid.pid_value) deposit.delete() # Mark all record's PIDs as DELETED all_pids = PersistentIdentifier.query.filter( PersistentIdentifier.object_type == pid.object_type, PersistentIdentifier.object_uuid == pid.object_uuid, ).all() for rec_pid in all_pids: if not rec_pid.is_deleted(): rec_pid.delete() # Mark the bucket as deleted # delete all buckets linked to the deposit res = Bucket.query.join(RecordsBuckets).\ filter(RecordsBuckets.bucket_id == Bucket.id, RecordsBuckets.record_id == self.id).all() for bucket in res: bucket.deleted = True # Mark the record and deposit as deleted. The record is unindexed # via the trigger on record deletion. super(B2ShareRecord, self).delete() version_master = PIDNodeVersioning(pid=pid) # If the parent has no other children and no draft child # mark it as deleted if not version_master.children.all(): if not version_master.draft_child: version_master.parent.delete() else: # Reindex the "new" last published version in order to have # its "is_last_version" up to date. RecordIndexer().index_by_id(version_master.last_child.object_uuid)
def delete(self): """Delete a deposit.""" from b2share.modules.records.providers import RecordUUIDProvider deposit_pid = self.pid pid_value = deposit_pid.pid_value record_pid = RecordUUIDProvider.get(pid_value).pid version_master = PIDNodeVersioning(child=record_pid) # every deposit has a parent version after the 2.1.0 upgrade # except deleted ones. We check the parent version in case of a delete # revert. assert version_master is not None, 'Unexpected deposit without versioning.' # if the record is unpublished hard delete it if record_pid.status == PIDStatus.RESERVED: version_master.remove_draft_child() db.session.delete(record_pid) # if the parent doesn't have any published records hard delete it if version_master.parent.status == PIDStatus.RESERVED: db.session.delete(version_master.parent) deposit_pid.delete() # delete all buckets linked to the deposit res = Bucket.query.join(RecordsBuckets).\ filter(RecordsBuckets.bucket_id == Bucket.id, RecordsBuckets.record_id == self.id).all() # remove the deposit from ES self.indexer.delete(self) # we call the super of Invenio deposit instead of B2Share deposit as # Invenio deposit doesn't support the deletion of published deposits super(InvenioDeposit, self).delete(force=True) for bucket in res: bucket.locked = False bucket.remove()
def check_pids_migration(): """Check that the persistent identifiers have been migrated.""" expected_pids = _load_json('expected_pids.json') # Check unchanging properties for exp_pid in expected_pids: db_pid = PersistentIdentifier.get(exp_pid['pid_type'], exp_pid['pid_value']) for key, value in exp_pid.items(): if key != 'updated': assert str(getattr(db_pid, key)) == str(value) # check that deleted PID's records are (soft or hard) deleted if exp_pid['status'] == PIDStatus.DELETED.value: metadata = None try: record = Record.get_record(exp_pid['pid_value'], with_deleted=True) # Soft deleted record metadata = record.model.json except NoResultFound: # Hard deleted record pass assert metadata is None # Check versioning relations and PIDs if exp_pid['pid_type'] == 'b2dep': try: rec_pid = PersistentIdentifier.get('b2rec', exp_pid['pid_value']) # if the deposit is deleted, either the record PID was reserved # and has been deleted, or it still exists. if db_pid.status == PIDStatus.DELETED: assert rec_pid.status != PIDStatus.RESERVED except PIDDoesNotExistError: # The record PID was only reserved and has been deleted # with the deposit PID. assert db_pid.status == PIDStatus.DELETED continue # Check that a parent pid has been created versioning = PIDNodeVersioning(child=rec_pid) parent = versioning.parent assert rec_pid.status in [PIDStatus.RESERVED, PIDStatus.REGISTERED] if rec_pid.status == PIDStatus.RESERVED: assert parent.status == PIDStatus.RESERVED else: assert parent.status == PIDStatus.REDIRECTED assert parent.get_redirect() == rec_pid
def versioned_recid_minter_v2(record_uuid, data): """Reserve the Concept RECID and create the RECID.""" if 'conceptrecid' not in data: conceptrecid = conceptrecid_minter_v2(data=data) else: conceptrecid = PersistentIdentifier.get(pid_type='recid', pid_value=data["conceptrecid"]) # FIXME: Assuming its a version() call # Not nice here (use PIDRelations) data.pop('recid') recid = recid_minter_v2(record_uuid, data) PIDNodeVersioning(pid=conceptrecid).insert_draft_child(child_pid=recid) return recid
def indexer_receiver(sender, json=None, record=None, index=None, **dummy_kwargs): """Connect to before_record_index signal to transform record for ES.""" from b2share.modules.access.policies import allow_public_file_metadata from b2share.modules.records.fetchers import b2share_parent_pid_fetcher, b2share_record_uuid_fetcher if 'external_pids' in json['_deposit']: # Keep the 'external_pids' if the record is a draft (deposit) or # if the files are public. if (not is_deposit(record.model) and allow_public_file_metadata(json)): json['external_pids'] = json['_deposit']['external_pids'] del json['_deposit']['external_pids'] if not index.startswith('records'): return try: if '_files' in json: if not allow_public_file_metadata(json): for f in json['_files']: del f['key'] del json['_deposit'] json['_created'] = pytz.utc.localize(record.created).isoformat() json['_updated'] = pytz.utc.localize(record.updated).isoformat() json['owners'] = record['_deposit']['owners'] json['_internal'] = dict() # add the 'is_last_version' flag parent_pid = b2share_parent_pid_fetcher(None, record).pid_value pid = b2share_record_uuid_fetcher(None, record).pid_value last_version_pid = PIDNodeVersioning( pid=RecordUUIDProvider.get(parent_pid).pid ).last_child json['_internal']['is_last_version'] = \ (last_version_pid.pid_value == pid) # insert the bucket id for link generation in search results record_buckets = RecordsBuckets.query.filter( RecordsBuckets.record_id == record.id).all() if record_buckets: json['_internal']['files_bucket_id'] = \ str(record_buckets[0].bucket_id) except Exception: raise
def get(self, pid=None, **kwargs): """GET a list of record's versions.""" record_endpoint = 'b2share_records_rest.{0}_item'.format( RecordUUIDProvider.pid_type) pid_value = request.view_args['pid_value'] pid = RecordUUIDProvider.get(pid_value).pid pid_versioning = PIDNodeVersioning(child=pid) if pid_versioning.is_child: # This is a record PID. Retrieve the parent versioning PID. version_parent_pid_value = pid_versioning.parent.pid_value else: # This is a parent versioning PID version_parent_pid_value = pid_value records = [] child_pid_table = aliased(PersistentIdentifier) parent_pid_table = aliased(PersistentIdentifier) pids_and_meta = db.session.query(child_pid_table, RecordMetadata).join( PIDRelation, PIDRelation.child_id == child_pid_table.id, ).join(parent_pid_table, PIDRelation.parent_id == parent_pid_table.id).filter( parent_pid_table.pid_value == version_parent_pid_value, RecordMetadata.id == child_pid_table.object_uuid, ).order_by(RecordMetadata.created).all() for version_number, rec_pid_and_rec_meta in enumerate(pids_and_meta): rec_pid, rec_meta = rec_pid_and_rec_meta records.append({ 'version': version_number + 1, 'id': str(rec_pid.pid_value), 'url': url_for(record_endpoint, pid_value=str(rec_pid.pid_value), _external=True), 'created': rec_meta.created, 'updated': rec_meta.updated, }) return {'versions': records}
def test_versioning_remove_child(db, version_pids, build_pid): """Test the remove child method of PIDNodeVersioning.""" parent_pid = build_pid(version_pids[0]['parent']) h1 = PIDNodeVersioning(parent_pid) # try to remove the draft child using remove_child with pytest.raises(PIDRelationConsistencyError): h1.remove_child(version_pids[0]['children'][-1]) # assert that the parent redirects to the last child assert version_pids[0]['parent'].get_redirect() == \ version_pids[0]['children'][2] # remove the last child h1.remove_child(version_pids[0]['children'][2]) # assert that the pid is not a child assert version_pids[0]['children'][2] not in h1.children.all() # assert that the parent now redirects to the new last child assert version_pids[0]['parent'].get_redirect() == \ version_pids[0]['children'][1] # test removing the first child doesn't change the redirect h1.remove_child(version_pids[0]['children'][0]) assert version_pids[0]['parent'].get_redirect() == \ version_pids[0]['children'][1]
def test_index_siblings(app, db, version_pids): """Test the index_siblings method.""" # Create a pid relation with 3 children data_v1 = {'body': u'test_body', 'title': u'test_title'} data_v2 = {'body': u'test_body2', 'title': u'test_title2'} data_v3 = {'body': u'test_body3', 'title': u'test_title3'} rec_v1 = Record.create(data_v1) parent_pid = RecordIdProvider.create(object_type='rec', object_uuid=None, status=PIDStatus.REGISTERED).pid data_v1['conceptrecid'] = parent_pid.pid_value provider = RecordIdProvider.create('rec', rec_v1.id) data_v1['recid'] = provider.pid.pid_value versioning = PIDNodeVersioning(pid=parent_pid) versioning.insert_child(child_pid=provider.pid) rec_v2 = Record.create(data_v2) data_v2['conceptrecid'] = parent_pid.pid_value provider_v2 = RecordIdProvider.create('rec', rec_v2.id) data_v2['recid'] = provider_v2.pid.pid_value versioning.insert_child(child_pid=provider_v2.pid) rec_v3 = Record.create(data_v3) data_v3['conceptrecid'] = parent_pid.pid_value provider_v3 = RecordIdProvider.create('rec', rec_v3.id) data_v3['recid'] = provider.pid.pid_value versioning.insert_child(child_pid=provider_v3.pid) db.session.commit() with patch('invenio_indexer.api.RecordIndexer.index_by_id') as mock: index_siblings(provider.pid, include_pid=True, eager=True, with_deposits=False) mock.assert_any_call(str(provider.pid.object_uuid)) mock.assert_any_call(str(provider_v2.pid.object_uuid)) mock.assert_any_call(str(provider_v3.pid.object_uuid))
def test_record_delete_version(app, test_records, test_users): """Test deletion of a record version.""" with app.app_context(): resolver = Resolver( pid_type='b2rec', object_type='rec', getter=B2ShareRecord.get_record, ) v1 = test_records[0].data v1_pid, v1_id = pid_of(v1) _, v1_rec = resolver.resolve(v1_id) data = copy_data_from_previous(v1_rec.model.json) v2 = create_deposit(data, test_users['deposits_creator'], version_of=v1_id) ObjectVersion.create(v2.files.bucket, 'myfile1', stream=BytesIO(b'mycontent')) v2.submit() v2.publish() v2_pid, v2_id = pid_of(v2) data = copy_data_from_previous(v2.model.json) v3 = create_deposit(data, test_users['deposits_creator'], version_of=v2_id) v3.submit() v3.publish() v3_pid, v3_id = pid_of(v3) v3_pid, v3_rec = resolver.resolve(v3_pid.pid_value) # chain is now: [v1] -- [v2] -- [v3] version_child = PIDNodeVersioning(child=v2_pid) version_master = PIDNodeVersioning(parent=version_child.parent) assert len(version_master.children.all()) == 3 v3_rec.delete() assert len(version_master.children.all()) == 2 # chain is now [v1] -- [v2] # assert that we can create again a new version from v2 data = copy_data_from_previous(v2.model.json) v3 = create_deposit(data, test_users['deposits_creator'], version_of=v2_id) v3.submit() v3.publish() v3_pid, v3_id = pid_of(v3) v3_pid, v3_rec = resolver.resolve(v3_pid.pid_value) assert len(version_master.children.all()) == 3 v2_pid, v2_rec = resolver.resolve(v2_pid.pid_value) # Delete an intermediate version v2_rec.delete() assert len(version_master.children.all()) == 2 # chain is now [v1] -- [v3] # Add a new version data = copy_data_from_previous(v3.model.json) v4 = create_deposit(data, test_users['deposits_creator'], version_of=v3_id) v4.submit() v4.publish() assert len(version_master.children.all()) == 3 # final chain [v1] -- [v3] -- [v4] v4_pid, v4_id = pid_of(v4) v4_pid, v4_rec = resolver.resolve(v4_pid.pid_value) data = copy_data_from_previous(v4) draft_child = create_deposit(data, test_users['deposits_creator'], version_of=v4_id) draft_child.submit() # delete all children except the draft child assert len(version_master.children.all()) == 3 v4_rec.delete() assert len(version_master.children.all()) == 2 v3_rec.delete() assert len(version_master.children.all()) == 1 v1_rec.delete() assert len(version_master.children.all()) == 0 assert version_master.parent.status != PIDStatus.DELETED draft_child.publish() draft_child_pid, draft_child_id = pid_of(draft_child) draft_child_pid, draft_child_rec = \ resolver.resolve(draft_child_pid.pid_value) # assert that we can create again a new version assert len(version_master.children.all()) == 1 # no child remains and there is no draft_child draft_child_rec.delete() assert version_master.parent.status == PIDStatus.DELETED
def test_versioning_remove_draft_child(db, version_pids, build_pid): """Test the remove_draft_child method of PIDNodeVersioning.""" parent_pid = build_pid(version_pids[0]['parent']) h1 = PIDNodeVersioning(parent_pid) h1.remove_draft_child() assert h1.draft_child is None
def create(cls, data, id_=None, version_of=None): """Create a deposit with the optional id. :params version_of: PID of an existing record. If set, the new record will be marked as a new version of this referenced record. If no data is provided the new record will be a copy of this record. Note: this PID must reference the current last version of a record. """ # check that the status field is not set if 'publication_state' in data: raise InvalidDepositError( 'Field "publication_state" cannot be set.') data['publication_state'] = PublicationStates.draft.name # Set record's schema if '$schema' in data: raise InvalidDepositError('"$schema" field should not be set.') # Retrieve reserved record PID which should have already been created # by the deposit minter (The record PID value is the same # as the one of the deposit) rec_pid = RecordUUIDProvider.get(data['_deposit']['id']).pid version_master, prev_version = None, None # if this is a new version of an existing record, add the future # record pid in the chain of versions. if version_of: version_master, prev_version = \ find_version_master_and_previous_record(version_of) # The new version must be in the same community if data['community'] != prev_version['community']: raise ValidationError( 'The community field cannot change between versions.') try: version_master.insert_draft_child(rec_pid) except Exception as exc: # Only one draft is allowed per version chain. if 'Draft child already exists for this relation' in \ exc.args[0]: raise DraftExistsVersioningError( version_master.draft_child ) raise exc else: # create parent PID parent_pid = RecordUUIDProvider.create().pid version_master = PIDNodeVersioning(parent=parent_pid) version_master.insert_draft_child(child=rec_pid) # Mint the deposit with the parent PID data['_pid'] = [{ 'value': version_master.parent.pid_value, 'type': RecordUUIDProvider.parent_pid_type, }] if 'community' not in data or not data['community']: raise ValidationError( 'Record\s metadata has no community field.') try: community_id = uuid.UUID(data['community']) except ValueError as e: raise InvalidDepositError( 'Community ID is not a valid UUID.') from e try: schema = CommunitySchema.get_community_schema(community_id) except CommunitySchemaDoesNotExistError as e: raise InvalidDepositError( 'No schema for community {}.'.format(community_id)) from e if version_of: data['$schema'] = Deposit._build_deposit_schema(prev_version) else: from b2share.modules.schemas.serializers import \ community_schema_draft_json_schema_link data['$schema'] = community_schema_draft_json_schema_link( schema, _external=True ) # create file bucket if prev_version and prev_version.files: # Clone the bucket from the previous version. This doesn't # duplicate files. bucket = prev_version.files.bucket.snapshot(lock=False) bucket.locked = False else: bucket = Bucket.create(storage_class=current_app.config[ 'DEPOSIT_DEFAULT_STORAGE_CLASS' ]) if 'external_pids' in data: create_b2safe_file(data['external_pids'], bucket) del data['external_pids'] deposit = super(Deposit, cls).create(data, id_=id_) db.session.add(bucket) db.session.add(RecordsBuckets( record_id=deposit.id, bucket_id=bucket.id )) return deposit
def versioning(self): """Return the parent versionning PID.""" return PIDNodeVersioning(child=self.record_pid)
def test_versioning_draft_child_deposit(db, version_pids, build_pid): """Test the draft_child_deposit property of PIDNodeVersioning.""" parent_pid = build_pid(version_pids[0]['parent']) h1 = PIDNodeVersioning(parent_pid) assert h1.draft_child_deposit == version_pids[0]['deposit']
def test_update_redirect(db, version_pids, build_pid): """Test PIDNodeVersioning.update_redirect().""" # Test update_redirect on a PID without any child parent_pids = create_pids(1, prefix='parent', status=PIDStatus.RESERVED) draft_pids = create_pids(2, prefix='draft', status=PIDStatus.RESERVED) parent = PIDNodeVersioning(build_pid(parent_pids[0])) parent.update_redirect() assert parent_pids[0].status == PIDStatus.RESERVED # Test that update_redirect remains reserved once it has a draft child parent.insert_draft_child(draft_pids[0]) assert parent_pids[0].status == PIDStatus.RESERVED h1 = PIDNodeVersioning(build_pid(version_pids[0]['parent'])) def test_redirect(expected_length, expected_redirect): filtered = filter_pids(version_pids[0]['children'], status=PIDStatus.REGISTERED) assert len(filtered) == expected_length assert h1.children.ordered('asc').all() == filtered assert h1._resolved_pid.get_redirect() == expected_redirect # Test update_redirect when it already points to the last version last = h1.last_child draft = h1.draft_child h1.update_redirect() test_redirect(3, last) # Test update_redirect after publishing the draft h1.draft_child.register() h1.update_redirect() test_redirect(4, draft) # Test update_redirect after deleting the last version h1.last_child.delete() h1.update_redirect() test_redirect(3, last) # Test that if every version is deleted the HEAD pid is also deleted for pid in filter_pids(version_pids[0]['children'], status=PIDStatus.REGISTERED): pid.delete() h1.update_redirect() test_redirect(0, last) # Test that an exception is raised if unsupported PIDStatus are used. version_pids[0]['children'][0].status = PIDStatus.NEW with pytest.raises(PIDRelationConsistencyError): h1.update_redirect()
def commit(self): """Store changes on current instance in database. This method extends the default implementation by publishing the deposition when 'publication_state' is set to 'published'. """ from b2share.modules.records.providers import RecordUUIDProvider if 'external_pids' in self: deposit_id = self['_deposit']['id'] recid = PersistentIdentifier.query.filter_by( pid_value=deposit_id).first() assert recid.status == 'R' record_bucket = RecordsBuckets.query.filter_by( record_id=recid.pid_value).first() bucket = Bucket.query.filter_by(id=record_bucket.bucket_id).first() object_versions = ObjectVersion.query.filter_by( bucket_id=bucket.id).all() key_to_pid = { ext_pid.get('key'): ext_pid.get('ePIC_PID') for ext_pid in self['external_pids'] } # for the existing files for object_version in object_versions: if object_version.file is None or \ object_version.file.storage_class != 'B': continue # check that they are still in the file pids list or remove if object_version.key not in key_to_pid: ObjectVersion.delete(bucket, object_version.key) # check that the uri is still the same or update it elif object_version.file.uri != \ key_to_pid[object_version.key]: db.session.query(FileInstance).\ filter(FileInstance.id == object_version.file_id).\ update({"uri": key_to_pid[object_version.key]}) create_b2safe_file(self['external_pids'], bucket) del self['external_pids'] if self.model is None or self.model.json is None: raise MissingModelError() # automatically make embargoed records private if self.get('embargo_date') and self.get('open_access'): from b2share.modules.access.policies import is_under_embargo if is_under_embargo(self): self['open_access'] = False if 'community' in self: from b2share.modules.communities.api import Community from b2share.modules.communities.errors import CommunityDoesNotExistError from b2share.modules.communities.workflows import publication_workflows try: community = Community.get(self['community']) except CommunityDoesNotExistError as e: raise InvalidDepositError( 'Community {} does not exist.'.format( self['community'])) from e workflow = publication_workflows[community.publication_workflow] workflow(self.model, self) # publish the deposition if needed if (self['publication_state'] == PublicationStates.published.name # check invenio-deposit status so that we do not loop and self['_deposit']['status'] != PublicationStates.published.name): # Retrieve previous version in order to reindex it later. previous_version_pid = None parent_pid = self.versioning.parents.first() version_master = PIDNodeVersioning(pid=parent_pid) # Save the previous "last" version for later use if parent_pid.status == PIDStatus.REDIRECTED and \ version_master.has_children: previous_version_pid = version_master.last_child previous_version_uuid = str( RecordUUIDProvider.get( previous_version_pid.pid_value).pid.object_uuid) external_pids = generate_external_pids(self) if external_pids: self['_deposit']['external_pids'] = external_pids super(Deposit, self).publish() # publish() already calls commit() # Register parent PID if necessary and update redirect self.versioning.update_redirect() # Reindex previous version. This is needed in order to update # the is_last_version flag if previous_version_pid is not None: self.indexer.index_by_id(previous_version_uuid) # save the action for later indexing if g: g.deposit_action = 'publish' else: super(Deposit, self).commit() if g: g.deposit_action = 'update-metadata' return self
def test_versioning_children(db, version_pids, build_pid): """Test the children property of PIDNoneVersioning.""" parent_pid = build_pid(version_pids[0]['parent']) h1 = PIDNodeVersioning(parent_pid) assert h1.children.ordered('asc').all() == \ filter_pids(version_pids[0]['children'], PIDStatus.REGISTERED)
def test_versioning_insert_child(db, version_pids, build_pid): """Test PIDNodeVersioning.insert_child(...).""" new_pids = create_pids(3) parent_pid = build_pid(version_pids[0]['parent']) h1 = PIDNodeVersioning(parent_pid) # insert as first child h1.insert_child(new_pids[0], 0) version_pids[0]['children'].insert(0, new_pids[0]) assert h1.children.ordered('asc').all() == \ filter_pids(version_pids[0]['children'], PIDStatus.REGISTERED) # insert as last child. This should insert just before the draft version_pids[0]['children'].insert(h1.index(h1.draft_child), new_pids[1]) h1.insert_child(new_pids[1], -1) # Check that the parent redirects to the added PID assert (version_pids[0]['parent'].get_redirect() == new_pids[1]) # Register the draft so that it appears in the children h1.draft_child.register() h1.update_redirect() assert h1.children.ordered('asc').all() == \ filter_pids(version_pids[0]['children'], PIDStatus.REGISTERED) # insert again but without a draft child. It should be inserted at the end. version_pids[0]['children'].append(new_pids[2]) h1.insert_child(new_pids[2], -1) assert h1.children.ordered('asc').all() == \ filter_pids(version_pids[0]['children'], PIDStatus.REGISTERED) reserved_pid = create_pids(1, status=PIDStatus.RESERVED)[0] # Check the exception raised when trying to insert a RESERVED PID with pytest.raises(PIDRelationConsistencyError): h1.insert_child(reserved_pid)
def alembic_upgrade_database_data(alembic, verbose): """Migrate the database data from v2.0.0 to 2.1.0.""" ### Add versioning PIDs ### # Reserve the record PID and versioning PID for unpublished deposits # Hack: disable record indexing during record migration from invenio_indexer.api import RecordIndexer old_index_fn = RecordIndexer.index RecordIndexer.index = lambda s, record: None if verbose: click.secho('migrating deposits and records...') with db.session.begin_nested(): # Migrate published records records_pids = PersistentIdentifier.query.filter( PersistentIdentifier.pid_type == RecordUUIDProvider.pid_type, PersistentIdentifier.status == PIDStatus.REGISTERED, ).all() for rec_pid in records_pids: if verbose: click.secho(' record {}'.format(rec_pid.pid_value)) try: record = Record.get_record(rec_pid.object_uuid) except NoResultFound: # The record is deleted but not the PID. Fix it. rec_pid.status = PIDStatus.DELETED continue # Create parent version PID parent_pid = RecordUUIDProvider.create().pid version_master = PIDNodeVersioning(parent=parent_pid) version_master.insert_draft_child(child=rec_pid) version_master.update_redirect() migrate_record_metadata( Record.get_record(rec_pid.object_uuid), parent_pid ) # Migrate deposits deposit_pids = PersistentIdentifier.query.filter( PersistentIdentifier.pid_type == DepositUUIDProvider.pid_type, PersistentIdentifier.status == PIDStatus.REGISTERED, ).all() for dep_pid in deposit_pids: if verbose: click.secho(' deposit {}'.format(dep_pid.pid_value)) try: deposit = Deposit.get_record(dep_pid.object_uuid) if deposit['publication_state'] != \ PublicationStates.published.name: # The record is not published yet. Reserve the PID. rec_pid = RecordUUIDProvider.create( object_type='rec', pid_value=dep_pid.pid_value, ).pid # Create parent version PID parent_pid = RecordUUIDProvider.create().pid assert parent_pid version_master = PIDNodeVersioning(parent=parent_pid) version_master.insert_draft_child(child=rec_pid) else: # Retrieve previously created version PID rec_pid = RecordUUIDProvider.get(dep_pid.pid_value).pid version_master = PIDNodeVersioning(child=rec_pid) parent_pid = version_master.parent if not parent_pid: click.secho(' record {} was deleted, but the deposit has not been removed'.format(rec_pid.pid_value), fg='red') if parent_pid: migrate_record_metadata( Deposit.get_record(dep_pid.object_uuid), parent_pid ) except NoResultFound: # The deposit is deleted but not the PID. Fix it. dep_pid.status = PIDStatus.DELETED if verbose: click.secho('done migrating deposits.') RecordIndexer.index = old_index_fn
def test_deposit_create_versions(app, test_records_data, test_users, login_user): """Test the creation of new record version draft.""" # Use admin user in order to publish easily the records. login = lambda c: login_user(test_users['admin'], c) data = test_records_data # create and publish first record in a chain v1_draft = create_ok(app, login, data[0]) assert 'versions' in v1_draft['links'] check_links(app, v1_draft, []) v1_rec = publish(app, login, v1_draft) assert 'versions' in v1_rec['links'] check_links(app, v1_rec, [v1_rec]) # try to create a new version from an unknown pid res, json_data = create(app, login, data[1], version_of=uuid.uuid4().hex) assert res.status_code == 400 # try to create a new version from a parent pid with app.app_context(): v1_pid = PersistentIdentifier.get(pid_value=v1_rec['id'], pid_type='b2rec') parent_pid = PIDNodeVersioning(child=v1_pid).parent res, json_data = create(app, login, data[1], version_of=parent_pid.pid_value) assert res.status_code == 400 # create and publish second record in a chain v2_draft = create_ok(app, login, data[1], version_of=v1_rec['id']) check_links(app, v2_draft, [v1_rec]) v2_rec = publish(app, login, v2_draft) check_links(app, v2_rec, [v1_rec, v2_rec]) # test error if trying to create a non-linear version chain res, json_data = create(app, login, data[1], version_of=v1_rec['id']) assert res.status_code == 400 assert json_data['use_record'] == v2_rec['id'] # create third record draft in a chain v3_draft = create_ok(app, login, data[2], version_of=v2_rec['id']) check_links(app, v3_draft, [v1_rec, v2_rec]) # test error when a draft already exists in a version chain res, json_data = create(app, login, data[1], version_of=v2_rec['id']) assert res.status_code == 400 assert json_data['goto_draft'] == v3_draft['id'] # publish third record in a chain v3_rec = publish(app, login, v3_draft) check_links(app, v3_rec, [v1_rec, v2_rec, v3_rec]) # create a new version without data # assert that data is copied from the previous version v4_draft = create_ok(app, login, None, v3_rec['id']) with app.app_context(): record_resolver = Resolver( pid_type='b2rec', object_type='rec', getter=B2ShareRecord.get_record, ) deposit_resolver = Resolver( pid_type='b2dep', object_type='rec', getter=Deposit.get_record, ) v4_metadata = deposit_resolver.resolve(v4_draft['id'])[1].model.json v3_metadata = record_resolver.resolve(v3_rec['id'])[1].model.json assert copy_data_from_previous(v4_metadata) == \ copy_data_from_previous(v3_metadata)