Exemplo n.º 1
0
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()
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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()
Exemplo n.º 4
0
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"))
Exemplo n.º 5
0
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)