Ejemplo n.º 1
0
class PersistentIdentifier(db.Model, Timestamp):
    """Store and register persistent identifiers.

    Assumptions:
      * Persistent identifiers can be represented as a string of max 255 chars.
      * An object has many persistent identifiers.
      * A persistent identifier has one and only one object.
    """

    __tablename__ = 'pidstore_pid'
    __table_args__ = (
        db.Index('uidx_type_pid', 'pid_type', 'pid_value', unique=True),
        db.Index('idx_status', 'status'),
        db.Index('idx_object', 'object_type', 'object_uuid'),
    )

    id = db.Column(db.Integer, primary_key=True)
    """Id of persistent identifier entry."""

    pid_type = db.Column(db.String(6), nullable=False)
    """Persistent Identifier Schema."""

    pid_value = db.Column(db.String(255), nullable=False)
    """Persistent Identifier."""

    pid_provider = db.Column(db.String(8), nullable=True)
    """Persistent Identifier Provider"""

    status = db.Column(ChoiceType(PIDStatus, impl=db.CHAR(1)), nullable=False)
    """Status of persistent identifier, e.g. registered, reserved, deleted."""

    object_type = db.Column(db.String(3), nullable=True)
    """Object Type - e.g. rec for record."""

    object_uuid = db.Column(UUIDType, nullable=True)
    """Object ID - e.g. a record id."""

    #
    # Class methods
    #
    @classmethod
    def create(
        cls,
        pid_type,
        pid_value,
        pid_provider=None,
        status=PIDStatus.NEW,
        object_type=None,
        object_uuid=None,
    ):
        """Create a new persistent identifier with specific type and value.

        :param pid_type: Persistent identifier type.
        :param pid_value: Persistent identifier value.
        :param pid_provider: Persistent identifier provider. (default: None).
        :param status: Current PID status.
            (Default: :attr:`invenio_pidstore.models.PIDStatus.NEW`)
        :param object_type: The object type is a string that identify its type.
            (default: None).
        :param object_uuid: The object UUID. (default: None).
        :returns: A :class:`invenio_pidstore.models.PersistentIdentifier`
            instance.
        """
        try:
            with db.session.begin_nested():
                obj = cls(pid_type=pid_type,
                          pid_value=pid_value,
                          pid_provider=pid_provider,
                          status=status)
                if object_type and object_uuid:
                    obj.assign(object_type, object_uuid)
                db.session.add(obj)
            logger.info("Created PID {0}:{1}".format(pid_type, pid_value),
                        extra={'pid': obj})
        except IntegrityError:
            logger.exception("PID already exists: {0}:{1}".format(
                pid_type, pid_value),
                             extra=dict(
                                 pid_type=pid_type,
                                 pid_value=pid_value,
                                 pid_provider=pid_provider,
                                 status=status,
                                 object_type=object_type,
                                 object_uuid=object_uuid,
                             ))
            raise PIDAlreadyExists(pid_type=pid_type, pid_value=pid_value)
        except SQLAlchemyError:
            logger.exception("Failed to create PID: {0}:{1}".format(
                pid_type, pid_value),
                             extra=dict(
                                 pid_type=pid_type,
                                 pid_value=pid_value,
                                 pid_provider=pid_provider,
                                 status=status,
                                 object_type=object_type,
                                 object_uuid=object_uuid,
                             ))
            raise
        return obj

    @classmethod
    def get(cls, pid_type, pid_value, pid_provider=None):
        """Get persistent identifier.

        :param pid_type: Persistent identifier type.
        :param pid_value: Persistent identifier value.
        :param pid_provider: Persistent identifier provider. (default: None).
        :raises: :exc:`invenio_pidstore.errors.PIDDoesNotExistError` if no
            PID is found.
        :returns: A :class:`invenio_pidstore.models.PersistentIdentifier`
            instance.
        """
        try:
            args = dict(pid_type=pid_type, pid_value=six.text_type(pid_value))
            if pid_provider:
                args['pid_provider'] = pid_provider
            return cls.query.filter_by(**args).one()
        except NoResultFound:
            raise PIDDoesNotExistError(pid_type, pid_value)

    @classmethod
    def get_by_object(cls, pid_type, object_type, object_uuid):
        """Get a persistent identifier for a given object.

        :param pid_type: Persistent identifier type.
        :param object_type: The object type is a string that identify its type.
        :param object_uuid: The object UUID.
        :raises invenio_pidstore.errors.PIDDoesNotExistError: If no PID is
            found.
        :returns: A :class:`invenio_pidstore.models.PersistentIdentifier`
            instance.
        """
        try:
            return cls.query.filter_by(pid_type=pid_type,
                                       object_type=object_type,
                                       object_uuid=object_uuid).one()
        except NoResultFound:
            raise PIDDoesNotExistError(pid_type, None)

    #
    # Assigned object methods
    #
    def has_object(self):
        """Determine if this PID has an assigned object.

        :returns: `True` if the PID has a object assigned.
        """
        return bool(self.object_type and self.object_uuid)

    def get_assigned_object(self, object_type=None):
        """Return the current assigned object UUID.

        :param object_type: If it's specified, returns only if the PID
            object_type is the same, otherwise returns None. (default: None).
        :returns: The object UUID.
        """
        if object_type is not None:
            if self.object_type == object_type:
                return self.object_uuid
            else:
                return None
        return self.object_uuid

    def assign(self, object_type, object_uuid, overwrite=False):
        """Assign this persistent identifier to a given object.

        Note, the persistent identifier must first have been reserved. Also,
        if an existing object is already assigned to the pid, it will raise an
        exception unless overwrite=True.

        :param object_type: The object type is a string that identify its type.
        :param object_uuid: The object UUID.
        :param overwrite: Force PID overwrites in case was previously assigned.
        :raises invenio_pidstore.errors.PIDInvalidAction: If the PID was
            previously deleted.
        :raises invenio_pidstore.errors.PIDObjectAlreadyAssigned: If the PID
            was previously assigned with a different type/uuid.
        :returns: `True` if the PID is successfully assigned.
        """
        if self.is_deleted():
            raise PIDInvalidAction(
                "You cannot assign objects to a deleted/redirected persistent"
                " identifier.")

        if not isinstance(object_uuid, uuid.UUID):
            object_uuid = uuid.UUID(object_uuid)

        if self.object_type or self.object_uuid:
            # The object is already assigned to this pid.
            if object_type == self.object_type and \
               object_uuid == self.object_uuid:
                return True
            if not overwrite:
                raise PIDObjectAlreadyAssigned(object_type, object_uuid)
            self.unassign()

        try:
            with db.session.begin_nested():
                self.object_type = object_type
                self.object_uuid = object_uuid
                db.session.add(self)
        except SQLAlchemyError:
            logger.exception("Failed to assign {0}:{1}".format(
                object_type, object_uuid),
                             extra=dict(pid=self))
            raise
        logger.info("Assigned object {0}:{1}".format(object_type, object_uuid),
                    extra=dict(pid=self))
        return True

    def unassign(self):
        """Unassign the registered object.

        Note:
        Only registered PIDs can be redirected so we set it back to registered.

        :returns: `True` if the PID is successfully unassigned.
        """
        if self.object_uuid is None and self.object_type is None:
            return True

        try:
            with db.session.begin_nested():
                if self.is_redirected():
                    db.session.delete(Redirect.query.get(self.object_uuid))
                    # Only registered PIDs can be redirected so we set it back
                    # to registered
                    self.status = PIDStatus.REGISTERED
                self.object_type = None
                self.object_uuid = None
                db.session.add(self)
        except SQLAlchemyError:
            logger.exception("Failed to unassign object.".format(self),
                             extra=dict(pid=self))
            raise
        logger.info("Unassigned object from {0}.".format(self),
                    extra=dict(pid=self))
        return True

    def get_redirect(self):
        """Get redirected persistent identifier.

        :returns: The :class:`invenio_pidstore.models.PersistentIdentifier`
            instance.
        """
        return Redirect.query.get(self.object_uuid).pid

    #
    # Status methods.
    #
    def redirect(self, pid):
        """Redirect persistent identifier to another persistent identifier.

        :param pid: The :class:`invenio_pidstore.models.PersistentIdentifier`
            where redirect the PID.
        :raises invenio_pidstore.errors.PIDInvalidAction: If the PID is not
            registered or is not already redirecting to another PID.
        :raises invenio_pidstore.errors.PIDDoesNotExistError: If PID is not
            found.
        :returns: `True` if the PID is successfully redirect.
        """
        if not (self.is_registered() or self.is_redirected()):
            raise PIDInvalidAction("Persistent identifier is not registered.")

        try:
            with db.session.begin_nested():
                if self.is_redirected():
                    r = Redirect.query.get(self.object_uuid)
                    r.pid = pid
                else:
                    with db.session.begin_nested():
                        r = Redirect(pid=pid)
                        db.session.add(r)

                self.status = PIDStatus.REDIRECTED
                self.object_type = None
                self.object_uuid = r.id
                db.session.add(self)
        except IntegrityError:
            raise PIDDoesNotExistError(pid.pid_type, pid.pid_value)
        except SQLAlchemyError:
            logger.exception("Failed to redirect to {0}".format(pid),
                             extra=dict(pid=self))
            raise
        logger.info("Redirected PID to {0}".format(pid), extra=dict(pid=self))
        return True

    def reserve(self):
        """Reserve the persistent identifier.

        Note, the reserve method may be called multiple times, even if it was
        already reserved.

        :raises: :exc:`invenio_pidstore.errors.PIDInvalidAction` if the PID is
            not new or is not already reserved a PID.
        :returns: `True` if the PID is successfully reserved.
        """
        if not (self.is_new() or self.is_reserved()):
            raise PIDInvalidAction(
                "Persistent identifier is not new or reserved.")

        try:
            with db.session.begin_nested():
                self.status = PIDStatus.RESERVED
                db.session.add(self)
        except SQLAlchemyError:
            logger.exception("Failed to reserve PID.", extra=dict(pid=self))
            raise
        logger.info("Reserved PID.", extra=dict(pid=self))
        return True

    def register(self):
        """Register the persistent identifier with the provider.

        :raises invenio_pidstore.errors.PIDInvalidAction: If the PID is not
            already registered or is deleted or is a redirection to another
            PID.
        :returns: `True` if the PID is successfully register.
        """
        if self.is_registered() or self.is_deleted() or self.is_redirected():
            raise PIDInvalidAction(
                "Persistent identifier has already been registered"
                " or is deleted.")

        try:
            with db.session.begin_nested():
                self.status = PIDStatus.REGISTERED
                db.session.add(self)
        except SQLAlchemyError:
            logger.exception("Failed to register PID.", extra=dict(pid=self))
            raise
        logger.info("Registered PID.", extra=dict(pid=self))
        return True

    def delete(self):
        """Delete the persistent identifier.

        If the persistent identifier haven't been registered yet, it is
        removed from the database.
        Otherwise, it's marked as
        :data:`invenio_pidstore.models.PIDStatus.DELETED`.
        :returns: `True` if the PID is successfully removed.
        """
        removed = False
        try:
            with db.session.begin_nested():
                if self.is_new():
                    # New persistent identifier which haven't been registered
                    # yet.
                    db.session.delete(self)
                    removed = True
                else:
                    self.status = PIDStatus.DELETED
                    db.session.add(self)
        except SQLAlchemyError:
            logger.exception("Failed to delete PID.", extra=dict(pid=self))
            raise

        if removed:
            logger.info("Deleted PID (removed).", extra=dict(pid=self))
        else:
            logger.info("Deleted PID.", extra=dict(pid=self))
        return True

    def sync_status(self, status):
        """Synchronize persistent identifier status.

        Used when the provider uses an external service, which might have been
        modified outside of our system.

        :param status: The new status to set.
        :returns: `True` if the PID is successfully sync.
        """
        if self.status == status:
            return True

        try:
            with db.session.begin_nested():
                self.status = status
                db.session.add(self)
        except SQLAlchemyError:
            logger.exception("Failed to sync status {0}.".format(status),
                             extra=dict(pid=self))
            raise
        logger.info("Synced PID status to {0}.".format(status),
                    extra=dict(pid=self))
        return True

    def is_redirected(self):
        """Return true if the persistent identifier has been registered."""
        return self.status == PIDStatus.REDIRECTED

    def is_registered(self):
        """Return true if the persistent identifier has been registered.

        :returns: A :class:`invenio_pidstore.models.PIDStatus` status.
        """
        return self.status == PIDStatus.REGISTERED

    def is_deleted(self):
        """Return true if the persistent identifier has been deleted.

        :returns: A boolean value.
        """
        return self.status == PIDStatus.DELETED

    def is_new(self):
        """Return true if the PIDhas not yet been registered or reserved.

        :returns: A boolean value.
        """
        return self.status == PIDStatus.NEW

    def is_reserved(self):
        """Return true if the PID has not yet been reserved.

        :returns: A boolean value.
        """
        return self.status == PIDStatus.RESERVED

    def __repr__(self):
        """Get representation of object."""
        return "<PersistentIdentifier {0}:{1}{3} ({2})>".format(
            self.pid_type, self.pid_value, self.status,
            " / {0}:{1}".format(self.object_type, self.object_uuid)
            if self.object_type else "")
Ejemplo n.º 2
0
class AccessRequest(db.Model):
    """Represent an request for access to restricted files in a record."""

    __tablename__ = 'accessrequests_request'

    STATUS_CODES = {
        RequestStatus.EMAIL_VALIDATION: _(u'Email validation'),
        RequestStatus.PENDING: _(u'Pending'),
        RequestStatus.ACCEPTED: _(u'Accepted'),
        RequestStatus.REJECTED: _(u'Rejected'),
    }

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    """Access request ID."""

    status = db.Column(ChoiceType(STATUS_CODES.items(), impl=db.CHAR(1)),
                       nullable=False,
                       index=True)
    """Status of request."""

    receiver_user_id = db.Column(db.Integer,
                                 db.ForeignKey(User.id),
                                 nullable=False,
                                 default=None)
    """Receiver's user id."""

    receiver = db.relationship(User, foreign_keys=[receiver_user_id])
    """Relationship to user"""

    sender_user_id = db.Column(db.Integer,
                               db.ForeignKey(User.id),
                               nullable=True,
                               default=None)
    """Sender's user id (for authenticated users)."""

    sender = db.relationship(User, foreign_keys=[sender_user_id])
    """Relationship to user for a sender"""

    sender_full_name = db.Column(db.String(length=255),
                                 nullable=False,
                                 default='')
    """Sender's full name."""

    sender_email = db.Column(db.String(length=255), nullable=False, default='')
    """Sender's email address."""

    recid = db.Column(db.Integer, nullable=False, index=True)
    """Record concerned for the request."""

    created = db.Column(db.DateTime,
                        nullable=False,
                        default=datetime.utcnow,
                        index=True)
    """Creation timestamp."""

    modified = db.Column(db.DateTime,
                         nullable=False,
                         default=datetime.utcnow,
                         onupdate=datetime.utcnow)
    """Last modification timestamp."""

    justification = db.Column(db.Text, default='', nullable=False)
    """Sender's justification for how they fulfill conditions."""

    message = db.Column(db.Text, default='', nullable=False)
    """Receivers message to the sender."""

    link_id = db.Column(db.Integer,
                        db.ForeignKey(SecretLink.id),
                        nullable=True,
                        default=None)
    """Relation to secret link if request was accepted."""

    link = db.relationship(SecretLink, foreign_keys=[link_id])
    """Relationship to secret link."""
    @classmethod
    def create(cls,
               recid=None,
               receiver=None,
               sender_full_name=None,
               sender_email=None,
               justification=None,
               sender=None):
        """Create a new access request.

        :param recid: Record id (required).
        :param receiver: User object of receiver (required).
        :param sender_full_name: Full name of sender (required).
        :param sender_email: Email address of sender (required).
        :param justification: Justification message (required).
        :param sender: User object of sender (optional).
        """
        sender_user_id = None if sender is None else sender.id

        assert recid
        assert receiver
        assert sender_full_name
        assert sender_email
        assert justification

        # Determine status
        status = RequestStatus.EMAIL_VALIDATION
        if sender and sender.confirmed_at:
            status = RequestStatus.PENDING

        with db.session.begin_nested():
            # Create object
            obj = cls(status=status,
                      recid=recid,
                      receiver_user_id=receiver.id,
                      sender_user_id=sender_user_id,
                      sender_full_name=sender_full_name,
                      sender_email=sender_email,
                      justification=justification)

            db.session.add(obj)

        # Send signal
        if obj.status == RequestStatus.EMAIL_VALIDATION:
            request_created.send(obj)
        else:
            request_confirmed.send(obj)
        return obj

    @classmethod
    def query_by_receiver(cls, user):
        """Get access requests for a specific receiver."""
        return cls.query.filter_by(receiver_user_id=user.id)

    @classmethod
    def get_by_receiver(cls, request_id, user):
        """Get access request for a specific receiver."""
        return cls.query.filter_by(id=request_id,
                                   receiver_user_id=user.id).first()

    def confirm_email(self):
        """Confirm that senders email is valid."""
        with db.session.begin_nested():
            if self.status != RequestStatus.EMAIL_VALIDATION:
                raise InvalidRequestStateError(RequestStatus.EMAIL_VALIDATION)

            self.status = RequestStatus.PENDING
        request_confirmed.send(self)

    def accept(self, message=None, expires_at=None):
        """Accept request."""
        with db.session.begin_nested():
            if self.status != RequestStatus.PENDING:
                raise InvalidRequestStateError(RequestStatus.PENDING)
            self.status = RequestStatus.ACCEPTED
        request_accepted.send(self, message=message, expires_at=expires_at)

    def reject(self, message=None):
        """Reject request."""
        with db.session.begin_nested():
            if self.status != RequestStatus.PENDING:
                raise InvalidRequestStateError(RequestStatus.PENDING)
            self.status = RequestStatus.REJECTED
        request_rejected.send(self, message=message)

    def create_secret_link(self, title, description=None, expires_at=None):
        """Create a secret link from request."""
        self.link = SecretLink.create(
            title,
            self.receiver,
            extra_data=dict(recid=self.recid),
            description=description,
            expires_at=expires_at,
        )
        return self.link
Ejemplo n.º 3
0
class CommunityMember(db.Model):
    """Represent a community member role."""

    __tablename__ = 'communities_members'

    "Community PID ID"
    comm_id = db.Column(
        UUIDType,
        db.ForeignKey(CommunityMetadata.id),
        primary_key=True,
        nullable=False,
    )

    "USER ID"
    user_id = db.Column(
        db.Integer,
        db.ForeignKey(User.id),
        primary_key=True,
        nullable=False,
    )

    role = db.Column(ChoiceType(CommunityRoles, impl=db.CHAR(1)),
                     nullable=False)

    user = db.relationship(User, backref='communities')

    community = db.relationship(CommunityMetadata, backref='members')

    @property
    def email(self):
        """Get user email."""
        return self.user.email

    @classmethod
    def create_from_object(cls, membership_request):
        """Create Community Membership Role."""
        with db.session.begin_nested():
            obj = cls.create(comm_id=membership_request.comm_id,
                             user_id=membership_request.user_id,
                             role=membership_request.role)
            db.session.delete(membership_request)
        return obj

    @classmethod
    def create(cls, comm_id, user_id, role):
        """Create Community Membership Role."""
        try:
            with db.session.begin_nested():
                obj = cls(comm_id=comm_id, user_id=user_id, role=role)
                db.session.add(obj)
        except IntegrityError:
            raise CommunityMemberAlreadyExists(comm_id=comm_id,
                                               user_id=user_id,
                                               role=role)
        return obj

    @classmethod
    def delete(cls, comm_id, user_id):
        """Delete a community membership."""
        try:
            with db.session.begin_nested():
                membership = cls.query.filter_by(comm_id=comm_id,
                                                 user_id=user_id).one()
                db.session.delete(membership)
        except IntegrityError:
            raise CommunityMemberDoesNotExist(comm_id=comm_id, user_id=user_id)

    @classmethod
    def get_admins(cls, comm_id):
        with db.session.begin_nested():
            return cls.query.filter_by(comm_id=comm_id, role='A').all()

    @classmethod
    def get_members(cls, comm_id):
        with db.session.begin_nested():
            return cls.query.filter_by(comm_id=comm_id)
Ejemplo n.º 4
0
class MembershipRequest(db.Model):
    """Represent a community member role."""

    __tablename__ = 'communities_membership_request'

    id = db.Column(
        UUIDType,
        primary_key=True,
        default=uuid.uuid4,
    )

    user_id = db.Column(
        db.Integer,
        db.ForeignKey(User.id),
        nullable=True,
    )

    comm_id = db.Column(
        UUIDType,
        db.ForeignKey(CommunityMetadata.id),
        nullable=False,
    )

    email = db.Column(
        db.String(255),
        nullable=True,
    )

    role = db.Column(ChoiceType(CommunityRoles, impl=db.CHAR(1)),
                     nullable=True)

    is_invite = db.Column(db.Boolean(name='is_invite'),
                          nullable=False,
                          default=True)

    @classmethod
    def create(cls, comm_id, is_invite, role=None, user_id=None, email=None):
        """Create Community Membership request."""
        try:
            with db.session.begin_nested():
                obj = cls(comm_id=comm_id,
                          user_id=user_id,
                          role=role,
                          is_invite=is_invite,
                          email=email)
                db.session.add(obj)
        except IntegrityError:
            raise CommunityMemberAlreadyExists(comm_id=comm_id,
                                               user_id=user_id,
                                               role=role)
        return obj

    @classmethod
    def delete(cls, id):
        """Delete a community membership request."""
        try:
            with db.session.begin_nested():
                membership = cls.query.get(id)
                db.session.delete(membership)
        except IntegrityError:
            raise CommunityMemberDoesNotExist(id)
Ejemplo n.º 5
0
class Release(db.Model, Timestamp):
    """Information about a GitHub release."""

    __tablename__ = 'github_releases'

    id = db.Column(
        UUIDType,
        primary_key=True,
        default=uuid.uuid4,
    )
    """Release identifier."""

    release_id = db.Column(db.Integer, unique=True, nullable=True)
    """Unique GitHub release identifier."""

    tag = db.Column(db.String(255))
    """Release tag."""

    errors = db.Column(
        JSONType().with_variant(
            postgresql.JSON(none_as_null=True),
            'postgresql',
        ),
        nullable=True,
    )
    """Release processing errors."""

    repository_id = db.Column(UUIDType, db.ForeignKey(Repository.id))
    """Repository identifier."""

    event_id = db.Column(UUIDType, db.ForeignKey(Event.id), nullable=True)
    """Incoming webhook event identifier."""

    record_id = db.Column(
        UUIDType,
        db.ForeignKey(RecordMetadata.id),
        nullable=True,
    )
    """Record identifier."""

    status = db.Column(
        ChoiceType(ReleaseStatus, impl=db.CHAR(1)),
        nullable=False,
    )
    """Status of the release, e.g. 'processing', 'published', 'failed', etc."""

    repository = db.relationship(Repository,
                                 backref=db.backref('releases',
                                                    lazy='dynamic'))

    recordmetadata = db.relationship(RecordMetadata, backref='github_releases')
    event = db.relationship(Event)

    @classmethod
    def create(cls, event):
        """Create a new Release model."""
        # Check if the release has already been received
        release_id = event.payload['release']['id']
        existing_release = Release.query.filter_by(
            release_id=release_id, ).first()
        if existing_release:
            raise ReleaseAlreadyReceivedError(release=existing_release)

        # Create the Release
        repo_id = event.payload['repository']['id']
        repo = Repository.get(user_id=event.user_id, github_id=repo_id)
        if repo.enabled:
            with db.session.begin_nested():
                release = cls(
                    release_id=release_id,
                    tag=event.payload['release']['tag_name'],
                    repository=repo,
                    event=event,
                    status=ReleaseStatus.RECEIVED,
                )
                db.session.add(release)
            return release
        else:
            raise RepositoryDisabledError(repo=repo)

    @property
    def record(self):
        """Get Record object."""
        if self.recordmetadata:
            return Record(self.recordmetadata.json, model=self.recordmetadata)
        else:
            return None

    @property
    def deposit_id(self):
        """Get deposit identifier."""
        if self.record and '_deposit' in self.record:
            return self.record['_deposit']['id']
        else:
            return None

    def __repr__(self):
        """Get release representation."""
        return (u'<Release {self.tag}:{self.release_id} ({self.status.title})>'
                .format(self=self))
Ejemplo n.º 6
0
class CommunityRecord(db.Model, RecordMetadataBase):
    """Comunity-record relationship model."""

    __tablename__ = 'communities_community_record'
    __table_args__ = (
        db.Index(
            'uidx_community_pid_record_pid',
            'community_pid_id', 'record_pid_id',
            unique=True,
        ),
        {'extend_existing': True},
    )
    __versioned__ = {'versioning': False}

    community_pid_id = db.Column(
        db.Integer,
        db.ForeignKey(PersistentIdentifier.id),
        nullable=False,
    )

    record_pid_id = db.Column(
        db.Integer,
        db.ForeignKey(PersistentIdentifier.id),
        nullable=False,
    )

    request_id = db.Column(
        UUIDType,
        db.ForeignKey(Request.id),
        # TODO: should we also allow CommunityRecords without a request?
        nullable=False,
    )

    status = db.Column(
        ChoiceType(CommunityRecordStatus, impl=db.CHAR(1)),
        nullable=False,
        default=CommunityRecordStatus.PENDING,
    )

    community_pid = db.relationship(
        PersistentIdentifier,
        foreign_keys=[community_pid_id],
    )
    record_pid = db.relationship(
        PersistentIdentifier,
        foreign_keys=[record_pid_id],
    )

    request = db.relationship(Request)

    @property
    def commmunity(self):
        """Return community model."""
        # TODO: make a JOIN instead?
        return CommunityMetadata.query.get(self.community_pid.object_uuid)

    @classmethod
    def create(cls, community_pid_id, record_pid_id, request_id,
               status=None, json=None):
        try:
            with db.session.begin_nested():
                # TODO: check if status None works with default
                obj = cls(
                    community_pid_id=community_pid_id,
                    record_pid_id=record_pid_id,
                    request_id=request_id,
                    status=status,
                    json=json,
                )
                db.session.add(obj)
        # TODO: Check if actually this constraint check happens on the DB side
        #       when db.session.add() is called.
        except IntegrityError:
            raise CommunityRecordAlreadyExists(
                community_pid_id=community_pid_id,
                record_pid_id=record_pid_id,
            )
        return obj

    @classmethod
    def delete(cls, community_record):
        """Delete community record relationship."""
        with db.session.begin_nested():
            db.session.delete(community_record)