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
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)
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')
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)
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
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)
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])
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)