Example #1
0
class Comment(Roleable, Relatable, Described, Notifiable, base.ContextRBAC,
              Base, Indexed, db.Model):
    """Basic comment model."""
    __tablename__ = "comments"

    assignee_type = db.Column(db.String, nullable=False, default=u"")
    revision_id = deferred(
        db.Column(
            db.Integer,
            db.ForeignKey('revisions.id', ondelete='SET NULL'),
            nullable=True,
        ), 'Comment')
    revision = db.relationship(
        'Revision',
        uselist=False,
    )
    custom_attribute_definition_id = deferred(
        db.Column(
            db.Integer,
            db.ForeignKey('custom_attribute_definitions.id',
                          ondelete='SET NULL'),
            nullable=True,
        ), 'Comment')
    custom_attribute_definition = db.relationship(
        'CustomAttributeDefinition',
        uselist=False,
    )

    initiator_instance_id = db.Column(db.Integer, nullable=True)
    initiator_instance_type = db.Column(db.String, nullable=True)
    INITIATOR_INSTANCE_TMPL = "{}_comment_initiated_by"

    initiator_instance = utils.PolymorphicRelationship(
        "initiator_instance_id", "initiator_instance_type",
        INITIATOR_INSTANCE_TMPL)

    # REST properties
    _api_attrs = reflection.ApiAttributes(
        "assignee_type",
        reflection.Attribute("custom_attribute_revision",
                             create=False,
                             update=False),
        reflection.Attribute("custom_attribute_revision_upd", read=False),
        reflection.Attribute("header_url_link", create=False, update=False),
    )

    _sanitize_html = [
        "description",
    ]

    AUTO_REINDEX_RULES = [
        ReindexRule("Comment", get_objects_to_reindex),
        ReindexRule("Relationship", reindex_by_relationship),
    ]

    _aliases = {
        "custom_attribute_definition": "custom_attribute_definition",
    }

    @builder.simple_property
    def header_url_link(self):
        """Return header url link to comment if that comment related to proposal
    and that proposal is only proposed."""
        if self.initiator_instance_type != "Proposal":
            return ""
        proposed_status = self.initiator_instance.STATES.PROPOSED
        if self.initiator_instance.status == proposed_status:
            return "proposal_link"
        return ""

    @classmethod
    def eager_query(cls, **kwargs):
        query = super(Comment, cls).eager_query(**kwargs)
        return query.options(
            orm.joinedload('revision'),
            orm.joinedload('custom_attribute_definition').undefer_group(
                'CustomAttributeDefinition_complete'),
        )

    def log_json(self):
        """Log custom attribute revisions."""
        res = super(Comment, self).log_json()
        res["custom_attribute_revision"] = self.custom_attribute_revision
        return res

    @builder.simple_property
    def custom_attribute_revision(self):
        """Get the historical value of the relevant CA value."""
        if not self.revision:
            return None
        revision = self.revision.content
        cav_stored_value = revision['attribute_value']
        cad = self.custom_attribute_definition
        return {
            'custom_attribute': {
                'id': cad.id if cad else None,
                'title': cad.title if cad else 'DELETED DEFINITION',
            },
            'custom_attribute_stored_value': cav_stored_value,
        }

    def custom_attribute_revision_upd(self, value):
        """Create a Comment-CA mapping with current CA value stored."""
        ca_revision_dict = value.get('custom_attribute_revision_upd')
        if not ca_revision_dict:
            return
        ca_val_dict = self._get_ca_value(ca_revision_dict)

        ca_val_id = ca_val_dict['id']
        ca_val_revision = Revision.query.filter_by(
            resource_type='CustomAttributeValue',
            resource_id=ca_val_id,
        ).order_by(Revision.created_at.desc(), ).limit(1).first()
        if not ca_val_revision:
            raise BadRequest(
                "No Revision found for CA value with id provided under "
                "'custom_attribute_value': {}".format(ca_val_dict))

        self.revision_id = ca_val_revision.id
        self.revision = ca_val_revision

        # Here *attribute*_id is assigned to *definition*_id, strange but,
        # as you can see in src/ggrc/models/custom_attribute_value.py
        # custom_attribute_id is link to custom_attribute_definitions.id
        # possible best way is use definition id from request:
        # ca_revision_dict["custom_attribute_definition"]["id"]
        # but needs to be checked that is always exist in request
        self.custom_attribute_definition_id = ca_val_revision.content.get(
            'custom_attribute_id', )

        self.custom_attribute_definition = CustomAttributeDefinition.query.get(
            self.custom_attribute_definition_id, )

    @staticmethod
    def _get_ca_value(ca_revision_dict):
        """Get CA value dict from json and do a basic validation."""
        ca_val_dict = ca_revision_dict.get('custom_attribute_value')
        if not ca_val_dict:
            raise ValueError(
                "CA value expected under "
                "'custom_attribute_value': {}".format(ca_revision_dict))
        if not ca_val_dict.get('id'):
            raise ValueError(
                "CA value id expected under 'id': {}".format(ca_val_dict))
        return ca_val_dict
Example #2
0
class Assessment(statusable.Statusable, AuditRelationship,
                 AutoStatusChangeable, Assignable, HasObjectState, TestPlanned,
                 CustomAttributable, EvidenceURL, Commentable, Personable,
                 reminderable.Reminderable, Timeboxed, Relatable,
                 WithSimilarityScore, FinishedDate, VerifiedDate,
                 ValidateOnComplete, Notifiable, BusinessObject, Indexed,
                 db.Model):
    """Class representing Assessment.

  Assessment is an object representing an individual assessment performed on
  a specific object during an audit to ascertain whether or not
  certain conditions were met for that object.
  """

    __tablename__ = 'assessments'
    _title_uniqueness = False

    ASSIGNEE_TYPES = (u"Creator", u"Assessor", u"Verifier")

    REMINDERABLE_HANDLERS = {
        "statusToPerson": {
            "handler":
            reminderable.Reminderable.handle_state_to_person_reminder,
            "data": {
                statusable.Statusable.START_STATE: "Assessor",
                "In Progress": "Assessor"
            },
            "reminders": {
                "assessment_assessor_reminder",
            }
        }
    }

    design = deferred(db.Column(db.String), "Assessment")
    operationally = deferred(db.Column(db.String), "Assessment")
    audit_id = deferred(
        db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False),
        'Assessment')

    @declared_attr
    def object_level_definitions(self):
        """Set up a backref so that we can create an object level custom
       attribute definition without the need to do a flush to get the
       assessment id.

      This is used in the relate_ca method in hooks/assessment.py.
    """
        return db.relationship('CustomAttributeDefinition',
                               primaryjoin=lambda: and_(
                                   remote(CustomAttributeDefinition.
                                          definition_id) == Assessment.id,
                                   remote(CustomAttributeDefinition.
                                          definition_type) == "assessment"),
                               foreign_keys=[
                                   CustomAttributeDefinition.definition_id,
                                   CustomAttributeDefinition.definition_type
                               ],
                               backref='assessment_definition',
                               cascade='all, delete-orphan')

    object = {}  # we add this for the sake of client side error checking

    VALID_CONCLUSIONS = frozenset(
        ["Effective", "Ineffective", "Needs improvement", "Not Applicable"])

    # REST properties
    _publish_attrs = [
        'design', 'operationally', 'audit',
        PublishOnly('object')
    ]

    _fulltext_attrs = [
        'design',
        'operationally',
        MultipleSubpropertyFullTextAttr('related_assessors', 'assessors',
                                        ['email', 'name']),
        MultipleSubpropertyFullTextAttr('related_creators', 'creators',
                                        ['email', 'name']),
        MultipleSubpropertyFullTextAttr('related_verifiers', 'verifiers',
                                        ['email', 'name']),
    ]

    _tracked_attrs = {
        'contact_id', 'description', 'design', 'notes', 'operationally',
        'reference_url', 'secondary_contact_id', 'test_plan', 'title', 'url',
        'start_date', 'end_date'
    }

    _aliases = {
        "owners": None,
        "assessment_template": {
            "display_name": "Template",
            "ignore_on_update": True,
            "filter_by": "_ignore_filter",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "url": "Assessment URL",
        "design": "Conclusion: Design",
        "operationally": "Conclusion: Operation",
        "related_creators": {
            "display_name": "Creators",
            "mandatory": True,
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_assessors": {
            "display_name": "Assignees",
            "mandatory": True,
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_verifiers": {
            "display_name": "Verifiers",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
    }

    AUTO_REINDEX_RULES = [
        ReindexRule("RelationshipAttr", reindex_by_relationship_attr)
    ]

    similarity_options = similarity_options_module.ASSESSMENT

    @property
    def assessors(self):
        """Get the list of assessor assignees"""
        return self.assignees_by_type.get("Assessor", [])

    @property
    def creators(self):
        """Get the list of creator assignees"""
        return self.assignees_by_type.get("Creator", [])

    @property
    def verifiers(self):
        """Get the list of verifier assignees"""
        return self.assignees_by_type.get("Verifier", [])

    def validate_conclusion(self, value):
        return value if value in self.VALID_CONCLUSIONS else None

    @validates("operationally")
    def validate_opperationally(self, key, value):
        # pylint: disable=unused-argument
        return self.validate_conclusion(value)

    @validates("design")
    def validate_design(self, key, value):
        # pylint: disable=unused-argument
        return self.validate_conclusion(value)

    @classmethod
    def _ignore_filter(cls, _):
        return None
Example #3
0
class Comment(Roleable, Relatable, Described, Notifiable, Base, Indexed,
              db.Model):
    """Basic comment model."""
    __tablename__ = "comments"

    assignee_type = db.Column(db.String)
    revision_id = deferred(
        db.Column(
            db.Integer,
            db.ForeignKey('revisions.id', ondelete='SET NULL'),
            nullable=True,
        ), 'Comment')
    revision = db.relationship(
        'Revision',
        uselist=False,
    )
    custom_attribute_definition_id = deferred(
        db.Column(
            db.Integer,
            db.ForeignKey('custom_attribute_definitions.id',
                          ondelete='SET NULL'),
            nullable=True,
        ), 'Comment')
    custom_attribute_definition = db.relationship(
        'CustomAttributeDefinition',
        uselist=False,
    )

    # REST properties
    _api_attrs = reflection.ApiAttributes(
        "assignee_type",
        reflection.Attribute("custom_attribute_revision",
                             create=False,
                             update=False),
        reflection.Attribute("custom_attribute_revision_upd", read=False),
    )

    _sanitize_html = [
        "description",
    ]

    def get_objects_to_reindex(self):
        """Return list required objects for reindex if comment C.U.D."""
        source_qs = db.session.query(
            Relationship.destination_type, Relationship.destination_id).filter(
                Relationship.source_type == self.__class__.__name__,
                Relationship.source_id == self.id)
        destination_qs = db.session.query(
            Relationship.source_type, Relationship.source_id).filter(
                Relationship.destination_type == self.__class__.__name__,
                Relationship.destination_id == self.id)
        result_qs = source_qs.union(destination_qs)
        klass_dict = defaultdict(set)
        for klass, object_id in result_qs:
            klass_dict[klass].add(object_id)

        queries = []
        for klass, object_ids in klass_dict.iteritems():
            model = inflector.get_model(klass)
            if not model:
                continue
            if issubclass(model, (Indexed, Commentable)):
                queries.append(
                    model.query.filter(model.id.in_(list(object_ids))))
        return list(itertools.chain(*queries))

    AUTO_REINDEX_RULES = [
        ReindexRule("Comment", lambda x: x.get_objects_to_reindex()),
        ReindexRule("Relationship", reindex_by_relationship),
    ]

    @classmethod
    def eager_query(cls):
        query = super(Comment, cls).eager_query()
        return query.options(
            orm.joinedload('revision'),
            orm.joinedload('custom_attribute_definition').undefer_group(
                'CustomAttributeDefinition_complete'),
        )

    @builder.simple_property
    def custom_attribute_revision(self):
        """Get the historical value of the relevant CA value."""
        if not self.revision:
            return None
        revision = self.revision.content
        cav_stored_value = revision['attribute_value']
        cad = self.custom_attribute_definition
        return {
            'custom_attribute': {
                'id': cad.id if cad else None,
                'title': cad.title if cad else 'DELETED DEFINITION',
            },
            'custom_attribute_stored_value': cav_stored_value,
        }

    def custom_attribute_revision_upd(self, value):
        """Create a Comment-CA mapping with current CA value stored."""
        ca_revision_dict = value.get('custom_attribute_revision_upd')
        if not ca_revision_dict:
            return
        ca_val_dict = self._get_ca_value(ca_revision_dict)

        ca_val_id = ca_val_dict['id']
        ca_val_revision = Revision.query.filter_by(
            resource_type='CustomAttributeValue',
            resource_id=ca_val_id,
        ).order_by(Revision.created_at.desc(), ).limit(1).first()
        if not ca_val_revision:
            raise BadRequest(
                "No Revision found for CA value with id provided under "
                "'custom_attribute_value': {}".format(ca_val_dict))

        self.revision_id = ca_val_revision.id
        self.custom_attribute_definition_id = ca_val_revision.content.get(
            'custom_attribute_id', )

    @staticmethod
    def _get_ca_value(ca_revision_dict):
        """Get CA value dict from json and do a basic validation."""
        ca_val_dict = ca_revision_dict.get('custom_attribute_value')
        if not ca_val_dict:
            raise ValueError(
                "CA value expected under "
                "'custom_attribute_value': {}".format(ca_revision_dict))
        if not ca_val_dict.get('id'):
            raise ValueError(
                "CA value id expected under 'id': {}".format(ca_val_dict))
        return ca_val_dict
Example #4
0
class CycleTaskGroup(WithContact, Stateful, Slugged, Timeboxed, Described,
                     Titled, Indexed, Base, db.Model):
    """Cycle Task Group model.
  """
    __tablename__ = 'cycle_task_groups'
    _title_uniqueness = False

    @classmethod
    def generate_slug_prefix_for(cls, obj):
        return "CYCLEGROUP"

    VALID_STATES = (u'Assigned', u'InProgress', u'Finished', u'Verified',
                    u'Declined')

    cycle_id = db.Column(
        db.Integer,
        db.ForeignKey('cycles.id', ondelete="CASCADE"),
        nullable=False,
    )
    task_group_id = db.Column(db.Integer,
                              db.ForeignKey('task_groups.id'),
                              nullable=True)
    cycle_task_group_tasks = db.relationship('CycleTaskGroupObjectTask',
                                             backref='cycle_task_group',
                                             cascade='all, delete-orphan')
    sort_index = db.Column(db.String(length=250), default="", nullable=False)
    next_due_date = db.Column(db.Date)

    _publish_attrs = [
        'cycle', 'task_group', 'cycle_task_group_tasks', 'sort_index',
        'next_due_date'
    ]

    _aliases = {
        "cycle": {
            "display_name": "Cycle",
            "filter_by": "_filter_by_cycle",
        },
    }

    PROPERTY_TEMPLATE = u"group {}"

    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr("task title", 'cycle_task_group_tasks',
                                        ["title"], False),
        MultipleSubpropertyFullTextAttr(
            "task assignee", lambda instance:
            [t.contact for t in instance.cycle_task_group_tasks],
            ["name", "email"], False),
        DateMultipleSubpropertyFullTextAttr("task due date",
                                            "cycle_task_group_tasks",
                                            ["end_date"], False),
        DateFullTextAttr(
            "due date",
            'next_due_date',
        ),
        FullTextAttr("assignee", "contact", ['name', 'email']),
        FullTextAttr("cycle title", 'cycle', ['title'], False),
        FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                     ['email', 'name'], False),
        DateFullTextAttr("cycle due date",
                         lambda x: x.cycle.next_due_date,
                         with_template=False),
    ]

    AUTO_REINDEX_RULES = [
        ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group),
        ReindexRule(
            "Person", lambda x: CycleTaskGroup.query.filter(
                CycleTaskGroup.contact_id == x.id)),
        ReindexRule(
            "Person", lambda x: [
                i.cycle for i in CycleTaskGroup.query.filter(
                    CycleTaskGroup.contact_id == x.id)
            ]),
    ]

    @classmethod
    def _filter_by_cycle(cls, predicate):
        """Get query that filters cycle task groups.

    Args:
      predicate: lambda function that excepts a single parameter and returns
      true of false.

    Returns:
      An sqlalchemy query that evaluates to true or false and can be used in
      filtering cycle task groups by related cycle.
    """
        return Cycle.query.filter((Cycle.id == cls.cycle_id)
                                  & (predicate(Cycle.slug)
                                     | predicate(Cycle.title))).exists()

    @classmethod
    def eager_query(cls):
        """Add cycle tasks and objects to cycle task group eager query.

    Make sure we load all cycle task group relevant data in a single query.

    Returns:
      a query object with cycle_task_group_tasks added to joined load options.
    """
        query = super(CycleTaskGroup, cls).eager_query()
        return query.options(orm.joinedload('cycle_task_group_tasks'))