Esempio n. 1
0
class PaperRevision(ProposalRevisionMixin, RenderModeMixin, db.Model):
    __tablename__ = 'revisions'
    __table_args__ = (
        db.Index(None,
                 'contribution_id',
                 unique=True,
                 postgresql_where=db.text('state = {}'.format(
                     PaperRevisionState.accepted))),
        db.UniqueConstraint('contribution_id', 'submitted_dt'),
        db.CheckConstraint(
            '(state IN ({}, {}, {})) = (judge_id IS NOT NULL)'.format(
                PaperRevisionState.accepted, PaperRevisionState.rejected,
                PaperRevisionState.to_be_corrected),
            name='judge_if_judged'),
        db.CheckConstraint(
            '(state IN ({}, {}, {})) = (judgment_dt IS NOT NULL)'.format(
                PaperRevisionState.accepted, PaperRevisionState.rejected,
                PaperRevisionState.to_be_corrected),
            name='judgment_dt_if_judged'), {
                'schema': 'event_paper_reviewing'
            })

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    proposal_attr = 'paper'

    id = db.Column(db.Integer, primary_key=True)
    state = db.Column(PyIntEnum(PaperRevisionState),
                      nullable=False,
                      default=PaperRevisionState.submitted)
    _contribution_id = db.Column('contribution_id',
                                 db.Integer,
                                 db.ForeignKey('events.contributions.id'),
                                 index=True,
                                 nullable=False)
    submitter_id = db.Column(db.Integer,
                             db.ForeignKey('users.users.id'),
                             index=True,
                             nullable=False)
    submitted_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    judge_id = db.Column(db.Integer,
                         db.ForeignKey('users.users.id'),
                         index=True,
                         nullable=True)
    judgment_dt = db.Column(UTCDateTime, nullable=True)
    _judgment_comment = db.Column('judgment_comment',
                                  db.Text,
                                  nullable=False,
                                  default='')

    _contribution = db.relationship('Contribution',
                                    lazy=True,
                                    backref=db.backref(
                                        '_paper_revisions',
                                        lazy=True,
                                        order_by=submitted_dt.asc()))
    submitter = db.relationship('User',
                                lazy=True,
                                foreign_keys=submitter_id,
                                backref=db.backref('paper_revisions',
                                                   lazy='dynamic'))
    judge = db.relationship('User',
                            lazy=True,
                            foreign_keys=judge_id,
                            backref=db.backref('judged_papers',
                                               lazy='dynamic'))

    judgment_comment = RenderModeMixin.create_hybrid_property(
        '_judgment_comment')

    # relationship backrefs:
    # - comments (PaperReviewComment.paper_revision)
    # - files (PaperFile.paper_revision)
    # - reviews (PaperReview.revision)

    def __init__(self, *args, **kwargs):
        paper = kwargs.pop('paper', None)
        if paper:
            kwargs.setdefault('_contribution', paper.contribution)
        super(PaperRevision, self).__init__(*args, **kwargs)

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', '_contribution_id', state=None)

    @locator_property
    def locator(self):
        return dict(self.paper.locator, revision_id=self.id)

    @property
    def paper(self):
        return self._contribution.paper

    @property
    def is_last_revision(self):
        return self == self.paper.last_revision

    @property
    def number(self):
        return self.paper.revisions.index(self) + 1

    @property
    def spotlight_file(self):
        return self.get_spotlight_file()

    @property
    def timeline(self):
        return self.get_timeline()

    @paper.setter
    def paper(self, paper):
        self._contribution = paper.contribution

    def get_timeline(self, user=None):
        comments = [x for x in self.comments
                    if x.can_view(user)] if user else self.comments
        reviews = [x for x in self.reviews
                   if x.can_view(user)] if user else self.reviews
        judgment = [
            PaperJudgmentProxy(self)
        ] if self.state == PaperRevisionState.to_be_corrected else []
        return sorted(chain(comments, reviews, judgment),
                      key=attrgetter('created_dt'))

    def get_reviews(self, group=None, user=None):
        reviews = []
        if user and group:
            reviews = [
                x for x in self.reviews
                if x.group.instance == group and x.user == user
            ]
        elif user:
            reviews = [x for x in self.reviews if x.user == user]
        elif group:
            reviews = [x for x in self.reviews if x.group.instance == group]
        return reviews

    def get_reviewed_for_groups(self, user, include_reviewed=False):
        from indico.modules.events.papers.models.reviews import PaperTypeProxy
        from indico.modules.events.papers.util import is_type_reviewing_possible

        cfp = self.paper.cfp
        reviewed_for = set()
        if include_reviewed:
            reviewed_for = {
                x.type
                for x in self.reviews
                if x.user == user and is_type_reviewing_possible(cfp, x.type)
            }
        if is_type_reviewing_possible(
                cfp, PaperReviewType.content
        ) and user in self.paper.cfp.content_reviewers:
            reviewed_for.add(PaperReviewType.content)
        if is_type_reviewing_possible(
                cfp, PaperReviewType.layout
        ) and user in self.paper.cfp.layout_reviewers:
            reviewed_for.add(PaperReviewType.layout)
        return set(map(PaperTypeProxy, reviewed_for))

    def has_user_reviewed(self, user, review_type=None):
        from indico.modules.events.papers.models.reviews import PaperReviewType
        if review_type:
            if isinstance(review_type, basestring):
                review_type = PaperReviewType[review_type]
            return any(review.user == user and review.type == review_type
                       for review in self.reviews)
        else:
            layout_review = next(
                (review for review in self.reviews if review.user == user
                 and review.type == PaperReviewType.layout), None)
            content_review = next(
                (review for review in self.reviews if review.user == user
                 and review.type == PaperReviewType.content), None)
            if user in self._contribution.paper_layout_reviewers and user in self._contribution.paper_content_reviewers:
                return bool(layout_review and content_review)
            elif user in self._contribution.paper_layout_reviewers:
                return bool(layout_review)
            elif user in self._contribution.paper_content_reviewers:
                return bool(content_review)

    def get_spotlight_file(self):
        pdf_files = [
            paper_file for paper_file in self.files
            if paper_file.content_type == 'application/pdf'
        ]
        return pdf_files[0] if len(pdf_files) == 1 else None
Esempio n. 2
0
class PaperReview(ProposalReviewMixin, RenderModeMixin, db.Model):
    """A paper review, emitted by a layout or content reviewer."""

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    revision_attr = 'revision'
    group_attr = 'type'
    group_proxy_cls = PaperTypeProxy

    __tablename__ = 'reviews'
    __table_args__ = (db.UniqueConstraint('revision_id', 'user_id', 'type'), {
        'schema': 'event_paper_reviewing'
    })
    TIMELINE_TYPE = 'review'

    id = db.Column(db.Integer, primary_key=True)
    revision_id = db.Column(
        db.Integer,
        db.ForeignKey('event_paper_reviewing.revisions.id'),
        index=True,
        nullable=False)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=False)
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc,
    )
    modified_dt = db.Column(UTCDateTime, nullable=True)
    _comment = db.Column('comment', db.Text, nullable=False, default='')
    type = db.Column(PyIntEnum(PaperReviewType), nullable=False)
    proposed_action = db.Column(PyIntEnum(PaperAction), nullable=False)

    revision = db.relationship('PaperRevision',
                               lazy=True,
                               backref=db.backref('reviews',
                                                  lazy=True,
                                                  order_by=created_dt.desc()))
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('paper_reviews', lazy='dynamic'))

    # relationship backrefs:
    # - ratings (PaperReviewRating.review)

    comment = RenderModeMixin.create_hybrid_property('_comment')

    @locator_property
    def locator(self):
        return dict(self.revision.locator, review_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'type',
                           'revision_id',
                           'user_id',
                           proposed_action=None)

    def can_edit(self, user, check_state=False):
        from indico.modules.events.papers.models.revisions import PaperRevisionState
        if user is None:
            return False
        if check_state and self.revision.state != PaperRevisionState.submitted:
            return False
        return self.user == user

    def can_view(self, user):
        if user is None:
            return False
        elif user == self.user:
            return True
        elif self.revision.paper.can_judge(user):
            return True
        return False

    @property
    def visibility(self):
        return PaperCommentVisibility.reviewers

    @property
    def score(self):
        ratings = [
            r for r in self.ratings if not r.question.is_deleted
            and r.question.field_type == 'rating' and r.value is not None
        ]
        if not ratings:
            return None
        return sum(x.value for x in ratings) / len(ratings)
Esempio n. 3
0
class ReviewCommentMixin(RenderModeMixin):
    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    user_backref_name = None
    user_modified_backref_name = None
    TIMELINE_TYPE = 'comment'

    @declared_attr
    def id(cls):
        return db.Column(db.Integer, primary_key=True)

    @declared_attr
    def user_id(cls):
        return db.Column(db.Integer,
                         db.ForeignKey('users.users.id'),
                         index=True,
                         nullable=False)

    @declared_attr
    def _text(cls):
        return db.Column('text', db.Text, nullable=False)

    @declared_attr
    def modified_by_id(cls):
        return db.Column(db.Integer,
                         db.ForeignKey('users.users.id'),
                         index=True,
                         nullable=True)

    @declared_attr
    def created_dt(cls):
        return db.Column(UTCDateTime, nullable=False, default=now_utc)

    @declared_attr
    def modified_dt(cls):
        return db.Column(UTCDateTime, nullable=True)

    @declared_attr
    def is_deleted(cls):
        return db.Column(db.Boolean, nullable=False, default=False)

    @declared_attr
    def user(cls):
        return db.relationship(
            'User',
            lazy=True,
            foreign_keys=cls.user_id,
            backref=db.backref(
                cls.user_backref_name,
                primaryjoin='({0}.user_id == User.id) & ~{0}.is_deleted'.
                format(cls.__name__),
                lazy='dynamic'))

    @declared_attr
    def modified_by(cls):
        return db.relationship(
            'User',
            lazy=True,
            foreign_keys=cls.modified_by_id,
            backref=db.backref(
                cls.user_modified_backref_name,
                primaryjoin='({0}.modified_by_id == User.id) & ~{0}.is_deleted'
                .format(cls.__name__),
                lazy='dynamic'))

    text = RenderModeMixin.create_hybrid_property('_text')
Esempio n. 4
0
class AbstractReview(ProposalReviewMixin, RenderModeMixin, db.Model):
    """An abstract review, emitted by a reviewer."""

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    revision_attr = 'abstract'
    group_attr = 'track'

    marshmallow_aliases = {'_comment': 'comment'}

    __tablename__ = 'abstract_reviews'
    __table_args__ = (
        db.UniqueConstraint('abstract_id', 'user_id', 'track_id'),
        db.CheckConstraint(
            "proposed_action = {} OR (proposed_contribution_type_id IS NULL)".
            format(AbstractAction.accept),
            name='prop_contrib_id_only_accepted'),
        db.CheckConstraint(
            "(proposed_action IN ({}, {})) = (proposed_related_abstract_id IS NOT NULL)"
            .format(AbstractAction.mark_as_duplicate, AbstractAction.merge),
            name='prop_abstract_id_only_duplicate_merge'), {
                'schema': 'event_abstracts'
            })

    id = db.Column(db.Integer, primary_key=True)
    abstract_id = db.Column(db.Integer,
                            db.ForeignKey('event_abstracts.abstracts.id'),
                            index=True,
                            nullable=False)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=False)
    track_id = db.Column(db.Integer,
                         db.ForeignKey('events.tracks.id'),
                         index=True,
                         nullable=True)
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc,
    )
    modified_dt = db.Column(UTCDateTime, nullable=True)
    _comment = db.Column('comment', db.Text, nullable=False, default='')
    proposed_action = db.Column(PyIntEnum(AbstractAction), nullable=False)
    proposed_related_abstract_id = db.Column(
        db.Integer,
        db.ForeignKey('event_abstracts.abstracts.id'),
        index=True,
        nullable=True)
    proposed_contribution_type_id = db.Column(
        db.Integer,
        db.ForeignKey('events.contribution_types.id'),
        nullable=True,
        index=True)
    abstract = db.relationship('Abstract',
                               lazy=True,
                               foreign_keys=abstract_id,
                               backref=db.backref('reviews',
                                                  cascade='all, delete-orphan',
                                                  lazy=True))
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('abstract_reviews',
                                              lazy='dynamic'))
    track = db.relationship('Track',
                            lazy=True,
                            foreign_keys=track_id,
                            backref=db.backref('abstract_reviews',
                                               lazy='dynamic'))
    proposed_related_abstract = db.relationship(
        'Abstract',
        lazy=True,
        foreign_keys=proposed_related_abstract_id,
        backref=db.backref('proposed_related_abstract_reviews',
                           lazy='dynamic'))
    proposed_tracks = db.relationship(
        'Track',
        secondary='event_abstracts.proposed_for_tracks',
        lazy=True,
        collection_class=set,
        backref=db.backref('proposed_abstract_reviews',
                           lazy='dynamic',
                           passive_deletes=True))
    proposed_contribution_type = db.relationship('ContributionType',
                                                 lazy=True,
                                                 backref=db.backref(
                                                     'abstract_reviews',
                                                     lazy='dynamic'))

    # relationship backrefs:
    # - ratings (AbstractReviewRating.review)

    comment = RenderModeMixin.create_hybrid_property('_comment')

    @locator_property
    def locator(self):
        return dict(self.abstract.locator, review_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'abstract_id',
                           'user_id',
                           proposed_action=None)

    @property
    def visibility(self):
        return AbstractCommentVisibility.reviewers

    @property
    def score(self):
        ratings = [
            r for r in self.ratings if not r.question.no_score
            and not r.question.is_deleted and r.value is not None
        ]
        if not ratings:
            return None
        return sum(x.value for x in ratings) / len(ratings)

    def can_edit(self, user, check_state=False):
        if user is None:
            return False
        if check_state and self.abstract.public_state.name != 'under_review':
            return False
        return self.user == user

    def can_view(self, user):
        if user is None:
            return False
        elif user == self.user:
            return True
        if self.abstract.can_judge(user):
            return True
        else:
            return self.track.can_convene(user)
Esempio n. 5
0
class EditingRevisionComment(RenderModeMixin, db.Model):
    __tablename__ = 'comments'
    __table_args__ = (db.CheckConstraint('(user_id IS NULL) = system',
                                         name='system_comment_no_user'), {
                                             'schema': 'event_editing'
                                         })

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(db.Integer, primary_key=True)
    revision_id = db.Column(db.ForeignKey('event_editing.revisions.id',
                                          ondelete='CASCADE'),
                            index=True,
                            nullable=False)
    user_id = db.Column(db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=True)
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    modified_dt = db.Column(
        UTCDateTime,
        nullable=True,
    )
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: Whether the comment is only visible to editors
    internal = db.Column(db.Boolean, nullable=False, default=False)
    #: Whether the comment is system-generated and cannot be deleted/modified.
    system = db.Column(db.Boolean, nullable=False, default=False)
    _text = db.Column('text', db.Text, nullable=False, default='')
    text = RenderModeMixin.create_hybrid_property('_text')

    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('editing_comments',
                                              lazy='dynamic'))
    revision = db.relationship(
        'EditingRevision',
        lazy=True,
        backref=db.backref(
            'comments',
            primaryjoin=(
                '(EditingRevisionComment.revision_id == EditingRevision.id) & '
                '~EditingRevisionComment.is_deleted'),
            order_by=created_dt,
            cascade='all, delete-orphan',
            passive_deletes=True,
            lazy=True,
        ))

    def __repr__(self):
        return format_repr(self,
                           'id',
                           'revision_id',
                           'user_id',
                           internal=False,
                           _text=text_to_repr(self.text))

    @locator_property
    def locator(self):
        return dict(self.revision.locator, comment_id=self.id)

    def can_modify(self, user):
        authorized_submitter = self.revision.editable.can_perform_submitter_actions(
            user)
        authorized_editor = self.revision.editable.can_perform_editor_actions(
            user)

        if self.user != user:
            return False
        elif self.system:
            return False
        elif self.internal and not authorized_editor:
            return False
        return authorized_editor or authorized_submitter
Esempio n. 6
0
class EditingRevision(RenderModeMixin, db.Model):
    __tablename__ = 'revisions'
    __table_args__ = (db.CheckConstraint(_make_state_check(), name='valid_state_combination'),
                      {'schema': 'event_editing'})

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    editable_id = db.Column(
        db.ForeignKey('event_editing.editables.id'),
        index=True,
        nullable=False
    )
    submitter_id = db.Column(
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=False
    )
    editor_id = db.Column(
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=True
    )
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc
    )
    initial_state = db.Column(
        PyIntEnum(InitialRevisionState),
        nullable=False,
        default=InitialRevisionState.new
    )
    final_state = db.Column(
        PyIntEnum(FinalRevisionState),
        nullable=False,
        default=FinalRevisionState.none
    )
    _comment = db.Column(
        'comment',
        db.Text,
        nullable=False,
        default=''
    )

    editable = db.relationship(
        'Editable',
        foreign_keys=editable_id,
        lazy=True,
        backref=db.backref(
            'revisions',
            lazy=True,
            order_by=created_dt,
            cascade='all, delete-orphan'
        )
    )
    submitter = db.relationship(
        'User',
        lazy=True,
        foreign_keys=submitter_id,
        backref=db.backref(
            'editing_revisions',
            lazy='dynamic'
        )
    )
    editor = db.relationship(
        'User',
        lazy=True,
        foreign_keys=editor_id,
        backref=db.backref(
            'editor_for_revisions',
            lazy='dynamic'
        )
    )
    tags = db.relationship(
        'EditingTag',
        secondary='event_editing.revision_tags',
        collection_class=set,
        lazy=True,
        backref=db.backref(
            'revisions',
            collection_class=set,
            lazy=True
        )
    )

    #: A comment provided by whoever assigned the final state of the revision.
    comment = RenderModeMixin.create_hybrid_property('_comment')

    # relationship backrefs:
    # - comments (EditingRevisionComment.revision)
    # - files (EditingRevisionFile.revision)

    def __repr__(self):
        return format_repr(self, 'id', 'editable_id', 'initial_state', final_state=FinalRevisionState.none)

    @locator_property
    def locator(self):
        return dict(self.editable.locator, revision_id=self.id)

    def get_spotlight_file(self):
        files = [file for file in self.files if file.file_type.publishable]
        return files[0] if len(files) == 1 else None

    def get_published_files(self):
        """Get the published files, grouped by file type."""
        files = defaultdict(list)
        for file in self.files:
            if file.file_type.publishable:
                files[file.file_type].append(file)
        return dict(files)
Esempio n. 7
0
class AbstractComment(ProposalCommentMixin, RenderModeMixin, db.Model):
    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    __tablename__ = 'abstract_comments'
    __table_args__ = {'schema': 'event_abstracts'}

    id = db.Column(db.Integer, primary_key=True)
    abstract_id = db.Column(db.Integer,
                            db.ForeignKey('event_abstracts.abstracts.id'),
                            index=True,
                            nullable=False)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=False)
    _text = db.Column('text', db.Text, nullable=False)
    #: ID of the user who last modified the comment
    modified_by_id = db.Column(db.Integer,
                               db.ForeignKey('users.users.id'),
                               index=True,
                               nullable=True)
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    modified_dt = db.Column(UTCDateTime, nullable=True)
    visibility = db.Column(PyIntEnum(ProposalCommentVisibility),
                           nullable=False,
                           default=ProposalCommentVisibility.contributors)
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    abstract = db.relationship(
        'Abstract',
        lazy=True,
        backref=db.backref(
            'comments',
            primaryjoin=
            '(AbstractComment.abstract_id == Abstract.id) & ~AbstractComment.is_deleted',
            order_by=created_dt,
            cascade='all, delete-orphan',
            lazy=True,
        ))
    user = db.relationship(
        'User',
        lazy=True,
        foreign_keys=user_id,
        backref=db.backref(
            'abstract_comments',
            primaryjoin=
            '(AbstractComment.user_id == User.id) & ~AbstractComment.is_deleted',
            lazy='dynamic'))
    modified_by = db.relationship(
        'User',
        lazy=True,
        foreign_keys=modified_by_id,
        backref=db.backref(
            'modified_abstract_comments',
            primaryjoin=
            '(AbstractComment.modified_by_id == User.id) & ~AbstractComment.is_deleted',
            lazy='dynamic'))

    text = RenderModeMixin.create_hybrid_property('_text')

    @locator_property
    def locator(self):
        return dict(self.abstract.locator, comment_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'abstract_id',
                           is_deleted=False,
                           _text=text_to_repr(self.text))

    def can_edit(self, user):
        if user is None:
            return False
        return self.user == user or self.abstract.event_new.can_manage(user)

    def can_view(self, user):
        if user is None:
            return False
        elif user == self.user:
            return True
        elif self.visibility == ProposalCommentVisibility.users:
            return True
        visibility_checks = {
            ProposalCommentVisibility.judges: [self.abstract.can_judge],
            ProposalCommentVisibility.conveners:
            [self.abstract.can_judge, self.abstract.can_convene],
            ProposalCommentVisibility.reviewers: [
                self.abstract.can_judge, self.abstract.can_convene,
                self.abstract.can_review
            ],
            ProposalCommentVisibility.contributors: [
                self.abstract.can_judge, self.abstract.can_convene,
                self.abstract.can_review, self.abstract.user_owns
            ]
        }
        return any(fn(user) for fn in visibility_checks[self.visibility])
Esempio n. 8
0
class EditingRevision(RenderModeMixin, db.Model):
    __tablename__ = 'revisions'
    __table_args__ = (db.CheckConstraint(_make_state_check(),
                                         name='valid_state_combination'), {
                                             'schema': 'event_editing'
                                         })

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(db.Integer, primary_key=True)
    editable_id = db.Column(db.ForeignKey('event_editing.editables.id'),
                            index=True,
                            nullable=False)
    submitter_id = db.Column(db.ForeignKey('users.users.id'),
                             index=True,
                             nullable=False)
    editor_id = db.Column(db.ForeignKey('users.users.id'),
                          index=True,
                          nullable=True)
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    initial_state = db.Column(PyIntEnum(InitialRevisionState),
                              nullable=False,
                              default=InitialRevisionState.new)
    final_state = db.Column(PyIntEnum(FinalRevisionState),
                            nullable=False,
                            default=FinalRevisionState.none)
    _comment = db.Column('comment', db.Text, nullable=False, default='')

    editable = db.relationship('Editable',
                               foreign_keys=editable_id,
                               lazy=True,
                               backref=db.backref(
                                   'revisions',
                                   lazy=True,
                                   order_by=created_dt,
                                   cascade='all, delete-orphan'))
    submitter = db.relationship('User',
                                lazy=True,
                                foreign_keys=submitter_id,
                                backref=db.backref('editing_revisions',
                                                   lazy='dynamic'))
    editor = db.relationship('User',
                             lazy=True,
                             foreign_keys=editor_id,
                             backref=db.backref('editor_for_revisions',
                                                lazy='dynamic'))
    tags = db.relationship('EditingTag',
                           secondary='event_editing.revision_tags',
                           collection_class=set,
                           lazy=True,
                           backref=db.backref('revisions',
                                              collection_class=set,
                                              lazy=True))

    #: A comment provided by whoever assigned the final state of the revision.
    comment = RenderModeMixin.create_hybrid_property('_comment')

    # relationship backrefs:
    # - comments (EditingRevisionComment.revision)
    # - files (EditingRevisionFile.revision)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'editable_id',
                           'initial_state',
                           final_state=FinalRevisionState.none)