class Review(mixins.person_relation_factory("last_reviewed_by"), mixins.person_relation_factory("created_by"), mixins.datetime_mixin_factory("last_reviewed_at"), mixins.Stateful, rest_handable.WithPostHandable, rest_handable.WithPutHandable, roleable.Roleable, issue_tracker.IssueTracked, Relatable, mixins.base.ContextRBAC, mixins.Base, db.Model): """Review object""" # pylint: disable=too-few-public-methods __tablename__ = "reviews" REVIEWER_ROLE_NAME = "Reviewer" class STATES(object): """Review states container """ REVIEWED = "Reviewed" UNREVIEWED = "Unreviewed" VALID_STATES = [STATES.UNREVIEWED, STATES.REVIEWED] class NotificationTypes(object): """Notification types container """ EMAIL_TYPE = "email" ISSUE_TRACKER = "issue_tracker" class NotificationObjectTypes(object): """Review Notification Object types container """ STATUS_UNREVIEWED = "review_status_unreviewed" REVIEW_CREATED = "review_request_created" reviewable_id = db.Column(db.Integer, nullable=False) reviewable_type = db.Column(db.String, nullable=False) REVIEWABLE_TMPL = "{}_reviewable" reviewable = model_utils.json_polymorphic_relationship_factory(Reviewable)( "reviewable_id", "reviewable_type", REVIEWABLE_TMPL) notification_type = db.Column( sa.types.Enum(NotificationTypes.EMAIL_TYPE, NotificationTypes.ISSUE_TRACKER), nullable=False, ) email_message = db.Column(db.Text, nullable=False, default=u"") _api_attrs = reflection.ApiAttributes( "notification_type", "email_message", reflection.Attribute("reviewable", update=False), reflection.Attribute("last_reviewed_by", create=False, update=False), reflection.Attribute("last_reviewed_at", create=False, update=False), "issuetracker_issue", "status", ) def validate_acl(self): """Reviewer is mandatory Role""" super(Review, self).validate_acl() review_global_roles = role.get_ac_roles_data_for("Review").values() mandatory_role_ids = {acr[0] for acr in review_global_roles if acr[3]} passed_acr_ids = { acl.ac_role_id for _, acl in self.access_control_list } missed_mandatory_roles = mandatory_role_ids - passed_acr_ids if missed_mandatory_roles: raise exceptions.ValidationError("{} roles are mandatory".format( ",".join(missed_mandatory_roles))) def handle_post(self): self._create_relationship() self._update_new_reviewed_by() if (self.notification_type == Review.NotificationTypes.EMAIL_TYPE and self.status == Review.STATES.UNREVIEWED): add_notification(self, Review.NotificationObjectTypes.REVIEW_CREATED) def handle_put(self): self._update_reviewed_by() def _create_relationship(self): """Create relationship for newly created review (used for ACL)""" from ggrc.models import all_models if self in db.session.new: db.session.add( all_models.Relationship(source=self.reviewable, destination=self)) def _update_new_reviewed_by(self): """When create new review with state REVIEWED set last_reviewed_by""" # pylint: disable=attribute-defined-outside-init from ggrc.models import all_models if self.status == all_models.Review.STATES.REVIEWED: self.last_reviewed_by = get_current_user() self.last_reviewed_at = datetime.datetime.utcnow() def _update_reviewed_by(self): """Update last_reviewed_by, last_reviewed_at""" # pylint: disable=attribute-defined-outside-init from ggrc.models import all_models if not db.inspect(self).attrs["status"].history.has_changes(): return self.reviewable.updated_at = datetime.datetime.utcnow() if self.status == all_models.Review.STATES.REVIEWED: self.last_reviewed_by = self.modified_by self.last_reviewed_at = datetime.datetime.utcnow()
class Review(mixins.person_relation_factory("last_reviewed_by"), mixins.person_relation_factory("created_by"), mixins.datetime_mixin_factory("last_reviewed_at"), mixins.Stateful, rest_handable.WithPostHandable, rest_handable.WithPutHandable, rest_handable.WithPostAfterCommitHandable, rest_handable.WithPutAfterCommitHandable, with_comment_created.WithCommentCreated, comment.CommentInitiator, roleable.Roleable, issue_tracker.IssueTracked, relationship.Relatable, mixins.base.ContextRBAC, mixins.Base, db.Model): """Review object""" # pylint: disable=too-few-public-methods __tablename__ = "reviews" REVIEWER_ROLE_NAME = "Reviewer" class STATES(object): """Review states container """ REVIEWED = "Reviewed" UNREVIEWED = "Unreviewed" VALID_STATES = [STATES.UNREVIEWED, STATES.REVIEWED] class NotificationTypes(object): """Notification types container """ EMAIL_TYPE = "email" ISSUE_TRACKER = "issue_tracker" class NotificationObjectTypes(object): """Review Notification Object types container """ STATUS_UNREVIEWED = "review_status_unreviewed" REVIEW_CREATED = "review_request_created" reviewable_id = db.Column(db.Integer, nullable=False) reviewable_type = db.Column(db.String, nullable=False) REVIEWABLE_TMPL = "{}_reviewable" reviewable = model_utils.json_polymorphic_relationship_factory( Reviewable )( "reviewable_id", "reviewable_type", REVIEWABLE_TMPL ) notification_type = db.Column( sa.types.Enum(NotificationTypes.EMAIL_TYPE, NotificationTypes.ISSUE_TRACKER), nullable=False, ) email_message = db.Column(db.Text, nullable=False, default=u"") _api_attrs = reflection.ApiAttributes( "notification_type", "email_message", reflection.Attribute("reviewable", update=False), reflection.Attribute("last_reviewed_by", create=False, update=False), reflection.Attribute("last_reviewed_at", create=False, update=False), "issuetracker_issue", "status", ) def validate_acl(self): """Reviewer is mandatory Role""" super(Review, self).validate_acl() review_global_roles = role.get_ac_roles_data_for("Review").values() mandatory_role_ids = {acr[0] for acr in review_global_roles if acr[3]} passed_acr_ids = {acl.ac_role_id for _, acl in self.access_control_list} missed_mandatory_roles = mandatory_role_ids - passed_acr_ids if missed_mandatory_roles: raise exceptions.ValidationError("{} roles are mandatory".format( ",".join(missed_mandatory_roles)) ) def _add_comment_about(self, text): """Create comment about proposal for reason with required text.""" if not isinstance(self.reviewable, comment.Commentable): return text = self.clear_text(text) # pylint: disable=not-an-iterable existing_people = set(acp.person.email for acl in self._access_control_list for acp in acl.access_control_people) comment_text = ( u"<p>Review requested from</p><p>{people}</p>" u"<p>with a comment: {text}</p>" ).format( people=', '.join(existing_people), text=text, ) self.add_comment( comment_text, source=self.reviewable, initiator_object=self ) def handle_post(self): """Handle POST request.""" if self.email_message: self._add_comment_about(self.email_message) self._create_relationship() self._update_new_reviewed_by() if (self.notification_type == Review.NotificationTypes.EMAIL_TYPE and self.status == Review.STATES.UNREVIEWED and not isinstance(self.reviewable, synchronizable.Synchronizable)): add_notification(self, Review.NotificationObjectTypes.REVIEW_CREATED) def is_status_changed(self): """Checks whether the status has changed.""" return inspect(self).attrs.status.history.has_changes() def handle_put(self): """Handle PUT request.""" if not self.is_status_changed() and self.email_message: self._add_comment_about(self.email_message) self._update_reviewed_by() def handle_posted_after_commit(self, event): """Handle POST after commit.""" self.apply_mentions_comment(obj=self.reviewable, event=event) def handle_put_after_commit(self, event): """Handle PUT after commit.""" self.apply_mentions_comment(obj=self.reviewable, event=event) def _create_relationship(self): """Create relationship for newly created review (used for ACL)""" if self in db.session.new: db.session.add( relationship.Relationship(source=self.reviewable, destination=self) ) def _update_new_reviewed_by(self): """When create new review with state REVIEWED set last_reviewed_by""" # pylint: disable=attribute-defined-outside-init if self.status == Review.STATES.REVIEWED: self.last_reviewed_by = get_current_user() self.last_reviewed_at = datetime.datetime.utcnow() def _update_reviewed_by(self): """Update last_reviewed_by, last_reviewed_at""" # pylint: disable=attribute-defined-outside-init if not db.inspect(self).attrs["status"].history.has_changes(): return self.reviewable.updated_at = datetime.datetime.utcnow() if self.status == Review.STATES.REVIEWED: self.last_reviewed_by = self.modified_by self.last_reviewed_at = datetime.datetime.utcnow() # pylint: disable=no-self-use @validates("reviewable_type") def validate_reviewable_type(self, _, reviewable_type): """Validate reviewable_type attribute. We preventing creation of reviews for external models. """ reviewable_class = inflector.get_model(reviewable_type) if issubclass(reviewable_class, synchronizable.Synchronizable): raise ValueError("Trying to create review for external model.") return reviewable_type
class Review(mixins.person_relation_factory("last_reviewed_by"), mixins.person_relation_factory("created_by"), mixins.datetime_mixin_factory("last_reviewed_at"), mixins.Stateful, rest_handable.WithPostHandable, rest_handable.WithPutHandable, roleable.Roleable, issue_tracker.IssueTracked, Relatable, mixins.base.ContextRBAC, mixins.Base, ft_mixin.Indexed, db.Model): """Review object""" # pylint: disable=too-few-public-methods __tablename__ = "reviews" def __init__(self, *args, **kwargs): super(Review, self).__init__(*args, **kwargs) self.last_reviewed_by = None self.last_reviewed_at = None class STATES(object): """Review states container """ REVIEWED = "Reviewed" UNREVIEWED = "Unreviewed" VALID_STATES = [STATES.UNREVIEWED, STATES.REVIEWED] class ACRoles(object): """ACR roles container """ REVIEWER = "Reviewer" REVIEWABLE_READER = "Reviewable Reader" REVIEW_EDITOR = "Review Editor" class NotificationTypes(object): """Notification types container """ EMAIL_TYPE = "email" ISSUE_TRACKER = "issue_tracker" reviewable_id = db.Column(db.Integer, nullable=False) reviewable_type = db.Column(db.String, nullable=False) REVIEWABLE_TMPL = "{}_reviewable" reviewable = model_utils.json_polymorphic_relationship_factory(Reviewable)( "reviewable_id", "reviewable_type", REVIEWABLE_TMPL) notification_type = db.Column( sa.types.Enum(NotificationTypes.EMAIL_TYPE, NotificationTypes.ISSUE_TRACKER), nullable=False, ) email_message = db.Column(db.Text, nullable=False, default=u"") _api_attrs = reflection.ApiAttributes( "notification_type", "email_message", reflection.Attribute("reviewable", update=False), reflection.Attribute("last_reviewed_by", create=False, update=False), reflection.Attribute("last_reviewed_at", create=False, update=False), "issuetracker_issue", "status", ) _fulltext_attrs = [ "reviewable_id", "reviewable_type", ] def handle_post(self): self._create_relationship() self._update_new_reviewed_by() def handle_put(self): self._update_reviewed_by() def _create_relationship(self): """Create relationship for newly created review (used for ACL)""" from ggrc.models import all_models if self in db.session.new: db.session.add( all_models.Relationship(source=self.reviewable, destination=self)) def _update_new_reviewed_by(self): """When create new review with state REVIEWED set last_reviewed_by""" from ggrc.models import all_models if self.status == all_models.Review.STATES.REVIEWED: self.last_reviewed_by = get_current_user() self.last_reviewed_at = datetime.datetime.utcnow() def _update_reviewed_by(self): """Update last_reviewed_by, last_reviewed_at""" # pylint: disable=attribute-defined-outside-init from ggrc.models import all_models if not db.inspect(self).attrs["status"].history.has_changes(): return self.reviewable.updated_at = datetime.datetime.utcnow() if self.status == all_models.Review.STATES.REVIEWED: self.last_reviewed_by = self.modified_by self.last_reviewed_at = datetime.datetime.utcnow()
class Proposal(mixins.person_relation_factory("applied_by"), mixins.person_relation_factory("declined_by"), mixins.person_relation_factory("proposed_by"), comment.CommentInitiator, mixins.Stateful, roleable.Roleable, mixins.Base, ft_mixin.Indexed, db.Model): """Proposal model. Collect all information about propose change to Proposable instances.""" __tablename__ = 'proposals' # pylint: disable=too-few-public-methods class NotificationContext(object): DIGEST_TITLE = "Proposal Digest" DIGEST_TMPL = settings.JINJA2.get_template( "notifications/proposal_digest.html") class ACRoles(object): READER = "ProposalReader" EDITOR = "ProposalEditor" class STATES(object): """All states for proposals.""" PROPOSED = "proposed" APPLIED = "applied" DECLINED = "declined" class CommentTemplatesTextBuilder(object): """Temapltes for comments for proposals.""" PROPOSED_WITH_AGENDA = ("<p>Proposal has been created with comment: " "{text}</p>") APPLIED_WITH_COMMENT = ( "<p>Proposal created by {user} has been applied " "with a comment: {text}</p>") DECLINED_WITH_COMMENT = ( "<p>Proposal created by {user} has been declined " "with a comment: {text}</p>") PROPOSED_WITHOUT_AGENDA = "<p>Proposal has been created.</p>" APPLIED_WITHOUT_COMMENT = ("<p>Proposal created by {user} " "has been applied.</p>") DECLINED_WITHOUT_COMMENT = ("<p>Proposal created by {user} " "has been declined.</p>") # pylint: enable=too-few-public-methods def build_comment_text(self, reason, text, proposed_by): """Build proposal comment dependable from proposal state.""" if reason == self.STATES.PROPOSED: with_tmpl = self.CommentTemplatesTextBuilder.PROPOSED_WITH_AGENDA without_tmpl = self.CommentTemplatesTextBuilder.PROPOSED_WITHOUT_AGENDA elif reason == self.STATES.APPLIED: with_tmpl = self.CommentTemplatesTextBuilder.APPLIED_WITH_COMMENT without_tmpl = self.CommentTemplatesTextBuilder.APPLIED_WITHOUT_COMMENT elif reason == self.STATES.DECLINED: with_tmpl = self.CommentTemplatesTextBuilder.DECLINED_WITH_COMMENT without_tmpl = self.CommentTemplatesTextBuilder.DECLINED_WITHOUT_COMMENT tmpl = with_tmpl if text else without_tmpl return tmpl.format(user=proposed_by.email, text=text) VALID_STATES = [STATES.PROPOSED, STATES.APPLIED, STATES.DECLINED] instance_id = db.Column(db.Integer, nullable=False) instance_type = db.Column(db.String, nullable=False) content = db.Column('content', types.LongJsonType, nullable=False) agenda = db.Column(db.Text, nullable=False, default=u"") decline_reason = db.Column(db.Text, nullable=False, default=u"") decline_datetime = db.Column(db.DateTime, nullable=True) apply_reason = db.Column(db.Text, nullable=False, default=u"") apply_datetime = db.Column(db.DateTime, nullable=True) proposed_notified_datetime = db.Column(db.DateTime, nullable=True) INSTANCE_TMPL = "{}_proposalable" instance = JsonPolymorphicRelationship("instance_id", "instance_type", INSTANCE_TMPL) _fulltext_attrs = [ "instance_id", "instance_type", "agenda", "decline_reason", "decline_datetime", "apply_reason", "apply_datetime", ] _api_attrs = reflection.ApiAttributes( reflection.Attribute("instance", update=False), reflection.Attribute("content", create=False, update=False), reflection.Attribute("agenda", update=False), # ignore create proposal in specific state to be shure # new proposal will be only in proposed state reflection.Attribute('status', create=False), reflection.Attribute('decline_reason', create=False), reflection.Attribute('decline_datetime', create=False, update=False), reflection.Attribute('declined_by', create=False, update=False), reflection.Attribute('apply_reason', create=False), reflection.Attribute('apply_datetime', create=False, update=False), reflection.Attribute('applied_by', create=False, update=False), reflection.Attribute('full_instance_content', create=True, update=False, read=False), reflection.Attribute('proposed_by', create=False, update=False), ) full_instance_content = FullInstanceContentFased() @staticmethod def _extra_table_args(_): return (db.Index("fk_instance", "instance_id", "instance_type"), db.Index("ix_decline_datetime", "decline_datetime"), db.Index("ix_apply_datetime", "apply_datetime"), db.Index("ix_proposed_notified_datetime", "proposed_notified_datetime"))
class Proposal(mixins.person_relation_factory("applied_by"), mixins.person_relation_factory("declined_by"), mixins.person_relation_factory("proposed_by"), rest_handable.WithPostHandable, rest_handable.WithPutHandable, rest_handable.WithPutAfterCommitHandable, rest_handable.WithPostAfterCommitHandable, with_comment_created.WithCommentCreated, comment.CommentInitiator, mixins.Stateful, roleable.Roleable, relationship.Relatable, base.ContextRBAC, mixins.Base, ft_mixin.Indexed, db.Model): """Proposal model. Collect all information about propose change to Proposable instances.""" __tablename__ = 'proposals' class STATES(object): """All states for proposals.""" PROPOSED = "proposed" APPLIED = "applied" DECLINED = "declined" class CommentTemplatesTextBuilder(object): """Temapltes for comments for proposals.""" PROPOSED_WITH_AGENDA = ("<p>Proposal has been created with comment: " "{text}</p>") APPLIED_WITH_COMMENT = ( "<p>Proposal created by {user} has been applied " "with a comment: {text}</p>") DECLINED_WITH_COMMENT = ( "<p>Proposal created by {user} has been declined " "with a comment: {text}</p>") PROPOSED_WITHOUT_AGENDA = "<p>Proposal has been created.</p>" APPLIED_WITHOUT_COMMENT = ("<p>Proposal created by {user} " "has been applied.</p>") DECLINED_WITHOUT_COMMENT = ("<p>Proposal created by {user} " "has been declined.</p>") # pylint: enable=too-few-public-methods def build_comment_text(self, reason, text, proposed_by): """Build proposal comment dependable from proposal state.""" if reason == self.STATES.PROPOSED: with_tmpl = self.CommentTemplatesTextBuilder.PROPOSED_WITH_AGENDA without_tmpl = self.CommentTemplatesTextBuilder.PROPOSED_WITHOUT_AGENDA elif reason == self.STATES.APPLIED: with_tmpl = self.CommentTemplatesTextBuilder.APPLIED_WITH_COMMENT without_tmpl = self.CommentTemplatesTextBuilder.APPLIED_WITHOUT_COMMENT elif reason == self.STATES.DECLINED: with_tmpl = self.CommentTemplatesTextBuilder.DECLINED_WITH_COMMENT without_tmpl = self.CommentTemplatesTextBuilder.DECLINED_WITHOUT_COMMENT tmpl = with_tmpl if text else without_tmpl return tmpl.format(user=proposed_by.email, text=text) VALID_STATES = [STATES.PROPOSED, STATES.APPLIED, STATES.DECLINED] instance_id = db.Column(db.Integer, nullable=False) instance_type = db.Column(db.String, nullable=False) content = db.Column('content', types.LongJsonType, nullable=False) agenda = db.Column(db.Text, nullable=False, default=u"") decline_reason = db.Column(db.Text, nullable=False, default=u"") decline_datetime = db.Column(db.DateTime, nullable=True) apply_reason = db.Column(db.Text, nullable=False, default=u"") apply_datetime = db.Column(db.DateTime, nullable=True) proposed_notified_datetime = db.Column(db.DateTime, nullable=True) INSTANCE_TMPL = "{}_proposalable" instance = ProposalablePolymorphicRelationship("instance_id", "instance_type", INSTANCE_TMPL) _fulltext_attrs = [ "instance_id", "instance_type", "agenda", "decline_reason", "decline_datetime", "apply_reason", "apply_datetime", ] _api_attrs = reflection.ApiAttributes( reflection.Attribute("instance", update=False), reflection.Attribute("content", create=False, update=False), reflection.Attribute("agenda", update=False), # ignore create proposal in specific state to be shure # new proposal will be only in proposed state reflection.Attribute('status', create=False), reflection.Attribute('decline_reason', create=False), reflection.Attribute('decline_datetime', create=False, update=False), reflection.Attribute('declined_by', create=False, update=False), reflection.Attribute('apply_reason', create=False), reflection.Attribute('apply_datetime', create=False, update=False), reflection.Attribute('applied_by', create=False, update=False), reflection.Attribute('full_instance_content', create=True, update=False, read=False), reflection.Attribute('proposed_by', create=False, update=False), ) full_instance_content = FullInstanceContentFased() @staticmethod def _extra_table_args(_): return (db.Index("fk_instance", "instance_id", "instance_type"), db.Index("ix_decline_datetime", "decline_datetime"), db.Index("ix_apply_datetime", "apply_datetime"), db.Index("ix_proposed_notified_datetime", "proposed_notified_datetime")) # pylint: disable=no-self-use @validates("instance_type") def validate_instance_type(self, _, instance_type): """Validate instance_type attribute. We preventing creation of proposals for external models. """ instance_class = inflector.get_model(instance_type) if issubclass(instance_class, synchronizable.Synchronizable): raise ValueError("Trying to create proposal for external model.") return instance_type def _add_comment_about(self, reason, txt): """Create comment about proposal for reason with required text.""" if not isinstance(self.instance, comment.Commentable): return txt = self.clear_text(txt) self.add_comment(self.build_comment_text(reason, txt, self.proposed_by), source=self.instance, initiator_object=self) def is_status_changed_to(self, required_status): """Checks whether the status of proposal has changed.""" return (inspect(self).attrs.status.history.has_changes() and self.status == required_status) def handle_post(self): """POST handler.""" # pylint: disable=attribute-defined-outside-init self.proposed_by = login.get_current_user() # pylint: enable=attribute-defined-outside-init self._add_comment_about(self.STATES.PROPOSED, self.agenda) relationship.Relationship(source=self.instance, destination=self) def _apply_proposal(self): """Apply proposal procedure hook.""" from ggrc.utils.revisions_diff import applier current_user = login.get_current_user() now = datetime.datetime.utcnow() # pylint: disable=attribute-defined-outside-init self.applied_by = current_user # pylint: enable=attribute-defined-outside-init self.apply_datetime = now if applier.apply_action(self.instance, self.content): self.instance.modified_by = current_user self.instance.updated_at = now self._add_comment_about(self.STATES.APPLIED, self.apply_reason) # notify proposalable instance that proposal applied signals.Proposal.proposal_applied.send(self.instance.__class__, instance=self.instance) def _decline_proposal(self): """Decline proposal procedure hook.""" # pylint: disable=attribute-defined-outside-init self.declined_by = login.get_current_user() # pylint: enable=attribute-defined-outside-init self.decline_datetime = datetime.datetime.utcnow() self._add_comment_about(self.STATES.DECLINED, self.decline_reason) def handle_put(self): """PUT handler.""" if self.is_status_changed_to(self.STATES.APPLIED): self._apply_proposal() elif self.is_status_changed_to(self.STATES.DECLINED): self._decline_proposal() def handle_posted_after_commit(self, event): """Handle POST after commit.""" self.apply_mentions_comment(obj=self.instance, event=event) def handle_put_after_commit(self, event): """Handle PUT after commit.""" self.apply_mentions_comment(obj=self.instance, event=event)