def post(self, **kwargs): """Create a record. :returns: The created record. """ # import deposit dependencies here in order to avoid recursive imports from b2share.modules.deposit.links import deposit_links_factory if request.content_type not in self.loaders: abort(415) version_of = request.args.get('version_of') previous_record = None data = None if version_of: try: _, previous_record = Resolver( pid_type='b2rec', object_type='rec', getter=B2ShareRecord.get_record, ).resolve(version_of) # if the pid doesn't exist except PIDDoesNotExistError as e: raise RecordNotFoundVersioningError() # if it is the parent pid except PIDRedirectedError as e: raise IncorrectRecordVersioningError(version_of) # Copy the metadata from a previous version if this version is # specified and no data was provided. if request.content_length == 0: data = copy_data_from_previous(previous_record.model.json) if data is None: data = self.loaders[request.content_type]() if data is None: abort(400) # Check permissions permission_factory = self.create_permission_factory if permission_factory: verify_record_permission(permission_factory, data, previous_record=previous_record) # Create uuid for record record_uuid = uuid.uuid4() # Create persistent identifier pid = self.minter(record_uuid, data=data) # Create record record = self.record_class.create(data, id_=record_uuid, version_of=version_of) db.session.commit() response = self.make_response( pid, record, 201, links_factory=deposit_links_factory) # Add location headers endpoint = 'b2share_deposit_rest.{0}_item'.format(pid.pid_type) location = url_for(endpoint, pid_value=pid.pid_value, _external=True) response.headers.extend(dict(location=location)) return response
def test_deposit_copy_data_from_previous(app, test_records, test_users): """Test copying of metadata from previous version.""" with app.app_context(): prev_record = test_records[0] _prev_record = B2ShareRecord.get_record(prev_record.record_id) copied_data = copy_data_from_previous(_prev_record.model.json) for field in copy_data_from_previous.extra_removed_fields: del prev_record.data[field] data = {k: v for k, v in prev_record.data.items() if not k.startswith('_')} assert copied_data == data
def test_new_deposit_versions_preserve_schema(app, test_records, test_users): """Creating new versions of existing records.""" with app.app_context(): # Retrieve a record which will be the first version, and its deposit v1_rec = B2ShareRecord.get_record(test_records[0].record_id) _, v1_id = pid_of(test_records[0].data) # update the community's schema community_id = v1_rec.model.json['community'] old_schema = CommunitySchema.get_community_schema(community_id) json_schema = json.loads(old_schema.model.community_schema) new_schema = CommunitySchema.create_version(community_id, json_schema) assert new_schema.version > old_schema.version # create new, unversioned, draft and records data = copy_data_from_previous(v1_rec.model.json) unver_dep = create_deposit(data, test_users['deposits_creator']) unver_dep.submit() unver_dep.publish() _, unver_rec = unver_dep.fetch_published() # the unversioned draft or record in a version chain have the updated schema assert unver_dep['$schema'] != Deposit._build_deposit_schema( v1_rec.model.json) assert unver_rec.model.json['$schema'] != v1_rec.model.json['$schema'] # create new draft and record in the version chain of v1 data = copy_data_from_previous(v1_rec.model.json) v2_dep = create_deposit(data, test_users['deposits_creator'], version_of=v1_id) v2_dep.submit() v2_dep.publish() _, v2_rec = v2_dep.fetch_published() # the new draft and record in a version chain preserve the version of the schema assert v2_dep['$schema'] == Deposit._build_deposit_schema( v1_rec.model.json) assert v2_rec.model.json['$schema'] == v1_rec.model.json['$schema']
def test_deposit_copy_data_from_previous(app, test_records, test_users): """Test copying of metadata from previous version.""" with app.app_context(): prev_record = test_records[0] _prev_record = B2ShareRecord.get_record(prev_record.record_id) copied_data = copy_data_from_previous(_prev_record.model.json) for field in copy_data_from_previous.extra_removed_fields: del prev_record.data[field] data = { k: v for k, v in prev_record.data.items() if not k.startswith('_') } assert copied_data == data
def test_deposit_delete(app, draft_deposits, test_records, test_users, test_communities): """Test deposit deletion.""" with app.app_context(): # create a deposit with a parent pid which has no other children first_deposit = create_deposit( {'community': str(test_communities['MyTestCommunity1'])}, test_users['deposits_creator']) parent_pid = first_deposit['_pid'][0]['value'] deposit_pid_value = first_deposit['_deposit']['id'] # delete the deposit first_deposit.delete() deposit = PersistentIdentifier.query.filter_by( pid_value=deposit_pid_value).first() parent = PersistentIdentifier.query.filter_by( pid_value=parent_pid).one_or_none() # check that deleting it deletes parent from the db because there are # no remaining published versions and draft. assert not parent assert deposit.status == PIDStatus.DELETED # check that the buckets are removed assert not db.session.query( Bucket.query.join(RecordsBuckets).filter( RecordsBuckets.bucket_id == Bucket.id, RecordsBuckets.record_id == first_deposit.id).exists()).scalar() # create a deposit with a parent which has other children v1 = test_records[0].data v1_rec = B2ShareRecord.get_record(test_records[0].record_id) v1_pid, v1_id = pid_of(v1) data = copy_data_from_previous(v1_rec.model.json) v2 = create_deposit(data, test_users['deposits_creator'], version_of=v1_id) deposit_pid_value = v2['_deposit']['id'] parent_pid = v2['_pid'][0]['value'] v2.delete() deposit = PersistentIdentifier.query.filter_by( pid_value=deposit_pid_value).first() parent = PersistentIdentifier.query.filter_by( pid_value=parent_pid).first() # check that the parent status is not changed after deleting assert parent.status != PIDStatus.DELETED assert parent.get_redirect() == v1_pid assert deposit.status == PIDStatus.DELETED
def test_deposit_delete(app, draft_deposits, test_records, test_users, test_communities): """Test deposit deletion.""" with app.app_context(): # create a deposit with a parent pid which has no other children first_deposit = create_deposit( {'community': str(test_communities['MyTestCommunity1'])}, test_users['deposits_creator']) parent_pid = first_deposit['_pid'][0]['value'] deposit_pid_value = first_deposit['_deposit']['id'] # delete the deposit first_deposit.delete() deposit = PersistentIdentifier.query.filter_by(pid_value=deposit_pid_value).first() parent = PersistentIdentifier.query.filter_by(pid_value=parent_pid).one_or_none() # check that deleting it deletes parent from the db because there are # no remaining published versions and draft. assert not parent assert deposit.status == PIDStatus.DELETED # check that the buckets are removed assert not db.session.query( Bucket.query.join(RecordsBuckets). filter(RecordsBuckets.bucket_id == Bucket.id, RecordsBuckets.record_id == first_deposit.id).exists() ).scalar() # create a deposit with a parent which has other children v1 = test_records[0].data v1_rec = B2ShareRecord.get_record(test_records[0].record_id) v1_pid, v1_id = pid_of(v1) data = copy_data_from_previous(v1_rec.model.json) v2 = create_deposit(data, test_users['deposits_creator'], version_of=v1_id) deposit_pid_value = v2['_deposit']['id'] parent_pid = v2['_pid'][0]['value'] v2.delete() deposit = PersistentIdentifier.query.filter_by(pid_value=deposit_pid_value).first() parent = PersistentIdentifier.query.filter_by(pid_value=parent_pid).first() # check that the parent status is not changed after deleting assert parent.status != PIDStatus.DELETED assert parent.get_redirect() == v1_pid assert deposit.status == PIDStatus.DELETED
def test_deposit_versions_create(app, test_records, test_users): """Creating new versions of existing records.""" with app.app_context(): # Retrieve a record which will be the first version v1 = test_records[0].data v1_rec = B2ShareRecord.get_record(test_records[0].record_id) v1_pid, v1_id = pid_of(v1) assert list_published_pids(v1_pid) == [v1_pid] # create draft in version chain: # version chain becomes: [v1] -- [v2 draft] # v2 = create_deposit({}, version_of=v1_id) data = copy_data_from_previous(v1_rec.model.json) v2 = create_deposit(data, test_users['deposits_creator'], version_of=v1_id) assert filenames(v2) == [] ObjectVersion.create(v2.files.bucket, 'myfile1', stream=BytesIO(b'mycontent')) assert filenames(v2) == ['myfile1'] assert list_published_pids(v1_pid) == [v1_pid] # cannot create another draft if one exists # not possible: [v1] -- [v2 draft] # `- [new draft] with pytest.raises(DraftExistsVersioningError): data = copy_data_from_previous(v1_rec.model.json) create_deposit(data, test_users['deposits_creator'], version_of=v1_id) # cannot create a version from a draft pid # not possible: [v1] -- [v2 draft] -- [new draft] with pytest.raises(IncorrectRecordVersioningError): # record pid not created yet data = copy_data_from_previous(v1_rec.model.json) create_deposit(data, test_users['deposits_creator'], version_of=v2['_deposit']['id']) # publish previous draft # version chain becomes: [v1] -- [v2] v2.submit() v2.publish() v2_pid, v2_id = pid_of(v2) assert list_published_pids(v1_pid) == [v1_pid, v2_pid] # cannot create draft based on the first version in a chain # not possible: [v1] -- [v2] # `- [new draft] with pytest.raises(IncorrectRecordVersioningError): data = copy_data_from_previous(v1_rec.model.json) create_deposit(data, test_users['deposits_creator'], version_of=v1_id) # create and publish other versions: # version chain becomes: [v1] -- [v2] -- [v3] data = copy_data_from_previous(v1_rec.model.json) v3 = create_deposit(data, test_users['deposits_creator'], version_of=v2_id) # assert files are imported from v2 assert filenames(v3) == ['myfile1'] ObjectVersion.create(v3.files.bucket, 'myfile2', stream=BytesIO(b'mycontent')) assert filenames(v3) == ['myfile1', 'myfile2'] assert list_published_pids(v1_pid) == [v1_pid, v2_pid] v3.submit() v3.publish() v3_pid, v3_id = pid_of(v3) v3_rec = Record.get_record(v3_id) assert filenames(v3_rec) == ['myfile1', 'myfile2'] assert list_published_pids(v1_pid) == [v1_pid, v2_pid, v3_pid] # cannot create draft based on an intermediate version in a chain # not possible: [v1] -- [v2] -- [v3] # `- [new draft] with pytest.raises(IncorrectRecordVersioningError): create_deposit({}, test_users['deposits_creator'], version_of=v2_id) # Create yet another version # Version chain becomes: [v1] -- [v2] -- [v3] -- [v4] data = copy_data_from_previous(v1_rec.model.json) v4 = create_deposit(data, test_users['deposits_creator'], version_of=v3_id) v4.submit() v4.publish() assert filenames(v4) == ['myfile1', 'myfile2'] v4_pid, v4_id = pid_of(v4) assert list_published_pids(v1_pid) == [ v1_pid, v2_pid, v3_pid, v4_pid] # assert that creating a new version from a deleted pid is not allowed resolver = Resolver(pid_type=v4_pid.pid_type, object_type='rec', getter=partial(B2ShareRecord.get_record, with_deleted=True)) v4_pid, v4_rec = LazyPIDValue(resolver, v4_pid.pid_value).data # delete [v4] v4_rec.delete() with pytest.raises(RecordNotFoundVersioningError): v5 = create_deposit(data, test_users['deposits_creator'], version_of=v4_id)
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 = PIDVersioning(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)
def test_deposit_versions_create(app, test_records, test_users): """Creating new versions of existing records.""" with app.app_context(): # Retrieve a record which will be the first version v1 = test_records[0].data v1_rec = B2ShareRecord.get_record(test_records[0].record_id) v1_pid, v1_id = pid_of(v1) assert list_published_pids(v1_pid) == [v1_pid] # create draft in version chain: # version chain becomes: [v1] -- [v2 draft] # v2 = create_deposit({}, version_of=v1_id) data = copy_data_from_previous(v1_rec.model.json) v2 = create_deposit(data, test_users['deposits_creator'], version_of=v1_id) assert filenames(v2) == [] ObjectVersion.create(v2.files.bucket, 'myfile1', stream=BytesIO(b'mycontent')) assert filenames(v2) == ['myfile1'] assert list_published_pids(v1_pid) == [v1_pid] # cannot create another draft if one exists # not possible: [v1] -- [v2 draft] # `- [new draft] with pytest.raises(DraftExistsVersioningError): data = copy_data_from_previous(v1_rec.model.json) create_deposit(data, test_users['deposits_creator'], version_of=v1_id) # cannot create a version from a draft pid # not possible: [v1] -- [v2 draft] -- [new draft] with pytest.raises( IncorrectRecordVersioningError): # record pid not created yet data = copy_data_from_previous(v1_rec.model.json) create_deposit(data, test_users['deposits_creator'], version_of=v2['_deposit']['id']) # publish previous draft # version chain becomes: [v1] -- [v2] v2.submit() v2.publish() v2_pid, v2_id = pid_of(v2) assert list_published_pids(v1_pid) == [v1_pid, v2_pid] # cannot create draft based on the first version in a chain # not possible: [v1] -- [v2] # `- [new draft] with pytest.raises(IncorrectRecordVersioningError): data = copy_data_from_previous(v1_rec.model.json) create_deposit(data, test_users['deposits_creator'], version_of=v1_id) # create and publish other versions: # version chain becomes: [v1] -- [v2] -- [v3] data = copy_data_from_previous(v1_rec.model.json) v3 = create_deposit(data, test_users['deposits_creator'], version_of=v2_id) # assert files are imported from v2 assert filenames(v3) == ['myfile1'] ObjectVersion.create(v3.files.bucket, 'myfile2', stream=BytesIO(b'mycontent')) assert filenames(v3) == ['myfile1', 'myfile2'] assert list_published_pids(v1_pid) == [v1_pid, v2_pid] v3.submit() v3.publish() v3_pid, v3_id = pid_of(v3) v3_rec = Record.get_record(v3_id) assert filenames(v3_rec) == ['myfile1', 'myfile2'] assert list_published_pids(v1_pid) == [v1_pid, v2_pid, v3_pid] # cannot create draft based on an intermediate version in a chain # not possible: [v1] -- [v2] -- [v3] # `- [new draft] with pytest.raises(IncorrectRecordVersioningError): create_deposit({}, test_users['deposits_creator'], version_of=v2_id) # Create yet another version # Version chain becomes: [v1] -- [v2] -- [v3] -- [v4] data = copy_data_from_previous(v1_rec.model.json) v4 = create_deposit(data, test_users['deposits_creator'], version_of=v3_id) v4.submit() v4.publish() assert filenames(v4) == ['myfile1', 'myfile2'] v4_pid, v4_id = pid_of(v4) assert list_published_pids(v1_pid) == [v1_pid, v2_pid, v3_pid, v4_pid] # assert that creating a new version from a deleted pid is not allowed resolver = Resolver(pid_type=v4_pid.pid_type, object_type='rec', getter=partial(B2ShareRecord.get_record, with_deleted=True)) v4_pid, v4_rec = LazyPIDValue(resolver, v4_pid.pid_value).data # delete [v4] v4_rec.delete() with pytest.raises(RecordNotFoundVersioningError): v5 = create_deposit(data, test_users['deposits_creator'], version_of=v4_id)
def post(self, **kwargs): """Create a record. :returns: The created record. """ # import deposit dependencies here in order to avoid recursive imports from b2share.modules.deposit.links import deposit_links_factory from b2share.modules.records.api import B2ShareRecord if request.content_type not in self.loaders: abort(415) version_of = request.args.get('version_of') previous_record = None data = None if version_of: try: _, previous_record = Resolver( pid_type='b2rec', object_type='rec', getter=B2ShareRecord.get_record, ).resolve(version_of) # if the pid doesn't exist except PIDDoesNotExistError as e: raise RecordNotFoundVersioningError() # if it is the parent pid except PIDRedirectedError as e: raise IncorrectRecordVersioningError(version_of) # Copy the metadata from a previous version if this version is # specified and no data was provided. if request.content_length == 0: data = copy_data_from_previous(previous_record.model.json) if data is None: data = self.loaders[request.content_type]() if data is None: abort(400) # Check permissions permission_factory = self.create_permission_factory if permission_factory: verify_record_permission(permission_factory, data, previous_record=previous_record) # Create uuid for record record_uuid = uuid.uuid4() # Create persistent identifier pid = self.minter(record_uuid, data=data) # Create record record = self.record_class.create(data, id_=record_uuid, version_of=version_of) db.session.commit() response = self.make_response(pid, record, 201, links_factory=deposit_links_factory) # Add location headers endpoint = 'b2share_deposit_rest.{0}_item'.format(pid.pid_type) location = url_for(endpoint, pid_value=pid.pid_value, _external=True) response.headers.extend(dict(location=location)) return response
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 = PIDVersioning(child=v2_pid) version_master = PIDVersioning(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