Beispiel #1
0
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
Beispiel #2
0
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)
Beispiel #3
0
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)
Beispiel #4
0
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
Beispiel #5
0
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']
Beispiel #6
0
    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
Beispiel #7
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)
Beispiel #8
0
    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)
Beispiel #9
0
    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()
Beispiel #10
0
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
Beispiel #11
0
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
Beispiel #12
0
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
Beispiel #13
0
    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]
Beispiel #15
0
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))
Beispiel #16
0
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
Beispiel #18
0
    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
Beispiel #19
0
 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()
Beispiel #22
0
    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)
Beispiel #25
0
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
Beispiel #26
0
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)