Beispiel #1
0
class Assessment(Assignable, statusable.Statusable, AuditRelationship,
                 AutoStatusChangeable, TestPlanned, CustomAttributable,
                 WithEvidence, Commentable, Personable,
                 reminderable.Reminderable, Relatable, LastDeprecatedTimeboxed,
                 WithSimilarityScore, FinishedDate, VerifiedDate, Notifiable,
                 WithAction, labeled.Labeled,
                 with_last_comment.WithLastComment,
                 issue_tracker_mixins.IssueTrackedWithUrl, base.ContextRBAC,
                 BusinessObject, with_sox_302.WithSOX302FlowReadOnly,
                 WithCustomRestrictions, 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

    REWORK_NEEDED = u"Rework Needed"
    NOT_DONE_STATES = statusable.Statusable.NOT_DONE_STATES | {
        REWORK_NEEDED,
    }
    VALID_STATES = tuple(NOT_DONE_STATES | statusable.Statusable.DONE_STATES
                         | statusable.Statusable.INACTIVE_STATES)

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

    design = deferred(db.Column(db.String, nullable=False, default=""),
                      "Assessment")
    operationally = deferred(db.Column(db.String, nullable=False, default=""),
                             "Assessment")
    audit_id = deferred(
        db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False),
        'Assessment')
    assessment_type = deferred(
        db.Column(db.String, nullable=False, server_default="Control"),
        "Assessment")
    # whether to use the object test plan on snapshot mapping
    test_plan_procedure = db.Column(db.Boolean, nullable=False, default=True)

    @declared_attr
    def object_level_definitions(cls):  # pylint: disable=no-self-argument
        """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.
    """
        cad = custom_attribute_definition.CustomAttributeDefinition
        current_type = cls.__name__

        def join_expr():
            return sa.and_(
                orm.foreign(orm.remote(cad.definition_id)) == cls.id,
                cad.definition_type == utils.underscore_from_camelcase(
                    current_type),
            )

        # Since there is some kind of generic relationship on CAD side, correct
        # join expression for backref should be provided. If default, every call of
        # "{}_definition".format(definition_type) on CAD will produce a lot of
        # unnecessary DB queries returning nothing.
        def backref_join_expr():
            return orm.remote(cls.id) == orm.foreign(cad.definition_id)

        return db.relationship(
            "CustomAttributeDefinition",
            primaryjoin=join_expr,
            backref=db.backref(
                "{}_definition".format(
                    utils.underscore_from_camelcase(current_type)),
                lazy="joined",
                primaryjoin=backref_join_expr,
            ),
            cascade="all, delete-orphan",
        )

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

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

    # REST properties
    _api_attrs = reflection.ApiAttributes(
        'design',
        'operationally',
        'audit',
        'assessment_type',
        'test_plan_procedure',
        reflection.Attribute('archived', create=False, update=False),
        reflection.Attribute('folder', create=False, update=False),
        reflection.Attribute('object', create=False, update=False),
    )

    _fulltext_attrs = [
        'archived',
        'design',
        'operationally',
        'folder',
    ]

    AUTO_REINDEX_RULES = [
        mixin.ReindexRule("Audit", lambda x: x.assessments, ["archived"]),
    ]

    _custom_publish = {
        'audit': audit.build_audit_stub,
    }

    _in_progress_restrictions = (
        "access_control_list",
        "description",
        "title",
        "labels",
        "test_plan",
        "assessment_type",
        "slug",
        "notes",
        "start_date",
        "design",
        "operationally",
        "reminderType",
        "issue_tracker",
        "global_custom_attributes_values",
        "map: Snapshot",
        "map: Issue",
    )

    _done_state_restrictions = _in_progress_restrictions + (
        "custom_attributes_values",
        "map: Evidence",
        "import: status",
    )

    _restriction_condition = {
        "status": {
            (statusable.Statusable.START_STATE, statusable.Statusable.PROGRESS_STATE, REWORK_NEEDED, statusable.Statusable.DONE_STATE):
            _in_progress_restrictions,
            (statusable.Statusable.VERIFIED_STATE, statusable.Statusable.FINAL_STATE, statusable.Statusable.DEPRECATED):
            _done_state_restrictions
        }
    }

    @classmethod
    def _populate_query(cls, query):
        return query.options(
            orm.Load(cls).undefer_group("Assessment_complete"),
            orm.Load(cls).joinedload("audit").undefer_group("Audit_complete"),
            orm.Load(cls).joinedload("audit").joinedload(
                audit.Audit.issuetracker_issue))

    @classmethod
    def eager_query(cls, **kwargs):
        return cls._populate_query(
            super(Assessment, cls).eager_query(**kwargs))

    @classmethod
    def indexed_query(cls):
        return super(Assessment, cls).indexed_query().options(
            orm.Load(cls).load_only(
                "id",
                "design",
                "operationally",
                "audit_id",
            ),
            orm.Load(cls).joinedload("audit").load_only("archived", "folder"),
        )

    def log_json(self):
        out_json = super(Assessment, self).log_json()
        out_json["folder"] = self.folder
        return out_json

    ASSESSMENT_TYPE_OPTIONS = (
        "Access Groups",
        "Account Balances",
        "Data Assets",
        "Facilities",
        "Key Reports",
        "Markets",
        "Org Groups",
        "Processes",
        "Product Groups",
        "Products",
        "Systems",
        "Technology Environments",
        "Vendors",
        "Contracts",
        "Controls",
        "Objectives",
        "Policies",
        "Regulations",
        "Requirements",
        "Risks",
        "Standards",
        "Threats",
    )

    _aliases = {
        "owners": None,
        "assessment_template": {
            "display_name": "Template",
            "ignore_on_update": True,
            "filter_by": "_ignore_filter",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "assessment_type": {
            "display_name":
            "Assessment Type",
            "mandatory":
            False,
            "description":
            "Allowed values are:\n{}".format(
                '\n'.join(ASSESSMENT_TYPE_OPTIONS)),
        },
        "design": {
            "display_name":
            "Conclusion: Design",
            "description":
            "Allowed values are:\n{}".format('\n'.join(VALID_CONCLUSIONS)),
        },
        "operationally": {
            "display_name":
            "Conclusion: Operation",
            "description":
            "Allowed values are:\n{}".format('\n'.join(VALID_CONCLUSIONS)),
        },
        "archived": {
            "display_name": "Archived",
            "mandatory": False,
            "ignore_on_update": True,
            "view_only": True,
            "description": "Allowed values are:\nyes\nno"
        },
        "test_plan": "Assessment Procedure",
        # Currently we decided to have 'Due Date' alias for start_date,
        # but it can be changed in future
        "start_date": "Due Date",
        "status": {
            "display_name":
            "State",
            "mandatory":
            False,
            "description":
            "Allowed values are:\n{}".format('\n'.join(VALID_STATES))
        },
        "issue_tracker": {
            "display_name": "Ticket Tracker",
            "mandatory": False,
            "view_only": True
        },
        "issue_priority": {
            "display_name":
            "Priority",
            "mandatory":
            False,
            "description":
            "Allowed values are:\n{}".format('\n'.join(
                constants.AVAILABLE_PRIORITIES))
        },
    }

    @simple_property
    def archived(self):
        """Returns a boolean whether assessment is archived or not."""
        return self.audit.archived if self.audit else False

    @simple_property
    def folder(self):
        return self.audit.folder if self.audit else ""

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

    @validates("status")
    def validate_status(self, key, value):
        value = super(Assessment, self).validate_status(key, value)
        # pylint: disable=unused-argument
        if self.status == value:
            return value
        if self.status == self.REWORK_NEEDED:
            valid_states = [self.DONE_STATE, self.FINAL_STATE, self.DEPRECATED]
            if value not in valid_states:
                if not getattr(self, "skip_rework_validation", False):
                    raise ValueError("Assessment in `Rework Needed` "
                                     "state can be only moved to: [{}]".format(
                                         ",".join(valid_states)))
        return value

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

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

    @validates("assessment_type")
    def validate_assessment_type(self, key, value):
        """Validate assessment type to be the same as existing model name"""
        # pylint: disable=unused-argument
        # pylint: disable=no-self-use
        from ggrc.snapshotter.rules import Types
        if value and value not in Types.all:
            raise ValueError(
                "Assessment type '{}' is not snapshotable".format(value))
        return value

    @classmethod
    def _ignore_filter(cls, _):
        return None
class CycleTaskGroupObjectTask(
        roleable.Roleable, wf_mixins.CycleTaskStatusValidatedMixin,
        wf_mixins.WorkflowCommentable, mixins.WithLastDeprecatedDate,
        mixins.Timeboxed, relationship.Relatable, mixins.Notifiable,
        mixins.Described, mixins.Titled, mixins.Slugged, mixins.Base,
        base.ContextRBAC, ft_mixin.Indexed, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'

    readable_name_alias = 'cycle task'

    _title_uniqueness = False

    IMPORTABLE_FIELDS = (
        'slug',
        'title',
        'description',
        'start_date',
        'end_date',
        'finished_date',
        'verified_date',
        'status',
        '__acl__:Task Assignees',
        '__acl__:Task Secondary Assignees',
    )

    @classmethod
    def generate_slug_prefix(cls):
        return "CYCLETASK"

    # Note: this statuses are used in utils/query_helpers to filter out the tasks
    # that should be visible on My Tasks pages.

    PROPERTY_TEMPLATE = u"task {}"

    _fulltext_attrs = [
        ft_attributes.DateFullTextAttr(
            "end_date",
            'end_date',
        ),
        ft_attributes.FullTextAttr("group title", 'cycle_task_group',
                                   ['title'], False),
        ft_attributes.FullTextAttr("object_approval",
                                   'object_approval',
                                   with_template=False),
        ft_attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
        ft_attributes.FullTextAttr("group assignee",
                                   lambda x: x.cycle_task_group.contact,
                                   ['email', 'name'], False),
        ft_attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                                   ['email', 'name'], False),
        ft_attributes.DateFullTextAttr(
            "group due date",
            lambda x: x.cycle_task_group.next_due_date,
            with_template=False),
        ft_attributes.DateFullTextAttr("cycle due date",
                                       lambda x: x.cycle.next_due_date,
                                       with_template=False),
        ft_attributes.MultipleSubpropertyFullTextAttr("comments",
                                                      "cycle_task_entries",
                                                      ["description"]),
        ft_attributes.BooleanFullTextAttr("needs verification",
                                          "is_verification_needed",
                                          with_template=False,
                                          true_value="Yes",
                                          false_value="No"),
        "folder",
    ]

    # The app should not pass to the json representation of
    # relationships to the internal models
    IGNORED_RELATED_TYPES = ["CalendarEvent"]

    _custom_publish = {
        "related_sources":
        lambda obj: [
            rel.log_json() for rel in obj.related_sources
            if rel.source_type not in obj.IGNORED_RELATED_TYPES
        ],
        "related_destinations":
        lambda obj: [
            rel.log_json() for rel in obj.related_destinations
            if rel.destination_type not in obj.IGNORED_RELATED_TYPES
        ]
    }

    AUTO_REINDEX_RULES = [
        ft_mixin.ReindexRule("CycleTaskEntry",
                             lambda x: x.cycle_task_group_object_task),
    ]

    cycle_id = db.Column(
        db.Integer,
        db.ForeignKey('cycles.id', ondelete="CASCADE"),
        nullable=False,
    )
    cycle_task_group_id = db.Column(
        db.Integer,
        db.ForeignKey('cycle_task_groups.id', ondelete="CASCADE"),
        nullable=False,
    )
    task_group_task_id = db.Column(db.Integer,
                                   db.ForeignKey('task_group_tasks.id'),
                                   nullable=True)
    task_group_task = db.relationship(
        "TaskGroupTask",
        foreign_keys="CycleTaskGroupObjectTask.task_group_task_id")
    task_type = db.Column(db.String(length=250), nullable=False)
    response_options = db.Column(types.JsonType(), nullable=False, default=[])
    selected_response_options = db.Column(types.JsonType(),
                                          nullable=False,
                                          default=[])

    sort_index = db.Column(db.String(length=250), default="", nullable=False)

    finished_date = db.Column(db.DateTime)
    verified_date = db.Column(db.DateTime)

    # This parameter is overridden by cycle task group backref, but is here to
    # ensure pylint does not complain
    _cycle_task_group = None

    @hybrid.hybrid_property
    def cycle_task_group(self):
        """Getter for cycle task group foreign key."""
        return self._cycle_task_group

    @cycle_task_group.setter
    def cycle_task_group(self, cycle_task_group):
        """Setter for cycle task group foreign key."""
        if not self._cycle_task_group and cycle_task_group:
            relationship.Relationship(source=cycle_task_group,
                                      destination=self)
        self._cycle_task_group = cycle_task_group

    @hybrid.hybrid_property
    def object_approval(self):
        return self.cycle.workflow.object_approval

    @object_approval.expression
    def object_approval(cls):  # pylint: disable=no-self-argument
        return sa.select([
            Workflow.object_approval,
        ]).where(
            sa.and_(
                (Cycle.id == cls.cycle_id),
                (Cycle.workflow_id == Workflow.id))).label('object_approval')

    @builder.simple_property
    def folder(self):
        """Simple property for cycle folder."""
        if self.cycle:
            return self.cycle.folder
        return ""

    @builder.simple_property
    def is_in_history(self):
        """Used on UI to disable editing finished CycleTask which is in history"""
        return not self.cycle.is_current

    @property
    def cycle_task_objects_for_cache(self):
        """Get all related objects for this CycleTaskGroupObjectTask

    Returns:
      List of tuples with (related_object_type, related_object_id)
    """
        return [(object_.__class__.__name__, object_.id)
                for object_ in self.related_objects()]

    _api_attrs = reflection.ApiAttributes(
        'cycle',
        'cycle_task_group',
        'task_group_task',
        'cycle_task_entries',
        'sort_index',
        'task_type',
        'response_options',
        'selected_response_options',
        reflection.Attribute('related_sources', create=False, update=False),
        reflection.Attribute('related_destinations',
                             create=False,
                             update=False),
        reflection.Attribute('object_approval', create=False, update=False),
        reflection.Attribute('finished_date', create=False, update=False),
        reflection.Attribute('verified_date', create=False, update=False),
        reflection.Attribute('allow_change_state', create=False, update=False),
        reflection.Attribute('folder', create=False, update=False),
        reflection.Attribute('workflow', create=False, update=False),
        reflection.Attribute('workflow_title', create=False, update=False),
        reflection.Attribute('cycle_task_group_title',
                             create=False,
                             update=False),
        reflection.Attribute('is_in_history', create=False, update=False),
    )

    default_description = "<ol>"\
                          + "<li>Expand the object review task.</li>"\
                          + "<li>Click on the Object to be reviewed.</li>"\
                          + "<li>Review the object in the Info tab.</li>"\
                          + "<li>Click \"Approve\" to approve the object.</li>"\
                          + "<li>Click \"Decline\" to decline the object.</li>"\
                          + "</ol>"

    _aliases = {
        "title": "Summary",
        "description": "Task Details",
        "finished_date": {
            "display_name":
            "Actual Finish Date",
            "description": ("Make sure that 'Actual Finish Date' isn't set, "
                            "if cycle task state is <'Assigned' / "
                            "'In Progress' / 'Declined' / 'Deprecated'>. "
                            "Type double dash '--' into "
                            "'Actual Finish Date' cell to remove it.")
        },
        "verified_date": {
            "display_name":
            "Actual Verified Date",
            "description": ("Make sure that 'Actual Verified Date' isn't set, "
                            "if cycle task state is <'Assigned' / "
                            "'In Progress' / 'Declined' / 'Deprecated' / "
                            "'Finished'>. Type double dash '--' into "
                            "'Actual Verified Date' cell to remove it.")
        },
        "cycle": {
            "display_name": "Cycle",
            "filter_by": "_filter_by_cycle",
        },
        "cycle_task_group": {
            "display_name": "Task Group",
            "mandatory": True,
            "filter_by": "_filter_by_cycle_task_group",
        },
        "task_type": {
            "display_name": "Task Type",
            "mandatory": True,
        },
        "end_date": "Due Date",
        "start_date": "Start Date",
    }

    @builder.simple_property
    def cycle_task_group_title(self):
        """Property. Returns parent CycleTaskGroup title."""
        return self.cycle_task_group.title

    @builder.simple_property
    def workflow_title(self):
        """Property. Returns parent Workflow's title."""
        return self.workflow.title

    @builder.simple_property
    def workflow(self):
        """Property which returns parent workflow object."""
        return self.cycle.workflow

    @builder.simple_property
    def allow_change_state(self):
        return self.cycle.is_current and self.current_user_wfa_or_assignee()

    def current_user_wfa_or_assignee(self):
        """Current user is WF Admin, Assignee or Secondary Assignee for self."""
        wfa_ids = self.workflow.get_person_ids_for_rolename("Admin")
        ta_ids = self.get_person_ids_for_rolename("Task Assignees")
        tsa_ids = self.get_person_ids_for_rolename("Task Secondary Assignees")
        return login.get_current_user_id() in set().union(
            wfa_ids, ta_ids, tsa_ids)

    @classmethod
    def _filter_by_cycle(cls, predicate):
        """Get query that filters cycle tasks by related cycles.

    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 tasks by related cycles.
    """
        return Cycle.query.filter((Cycle.id == cls.cycle_id)
                                  & (predicate(Cycle.slug)
                                     | predicate(Cycle.title))).exists()

    @classmethod
    def _filter_by_cycle_task_group(cls, predicate):
        """Get query that filters cycle tasks by related 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 tasks by related cycle task groups.
    """
        return CycleTaskGroup.query.filter(
            (CycleTaskGroup.id == cls.cycle_id)
            & (predicate(CycleTaskGroup.slug)
               | predicate(CycleTaskGroup.title))).exists()

    @classmethod
    def eager_query(cls):
        """Add cycle task entries to cycle task eager query

    This function adds cycle_task_entries as a join option when fetching cycles
    tasks, and makes sure that with one query we fetch all cycle task related
    data needed for generating cycle taks json for a response.

    Returns:
      a query object with cycle_task_entries added to joined load options.
    """
        query = super(CycleTaskGroupObjectTask, cls).eager_query()
        return query.options(
            orm.subqueryload('cycle_task_entries'),
            orm.joinedload('cycle').undefer_group('Cycle_complete'),
            orm.joinedload('cycle').joinedload('workflow').undefer_group(
                'Workflow_complete'),
            orm.joinedload('cycle').joinedload('workflow').joinedload(
                '_access_control_list'),
            orm.joinedload('cycle_task_group').undefer_group(
                'CycleTaskGroup_complete'),
        )

    @classmethod
    def indexed_query(cls):
        return super(CycleTaskGroupObjectTask, cls).indexed_query().options(
            orm.Load(cls).load_only("end_date", "start_date", "created_at",
                                    "updated_at"),
            orm.Load(cls).joinedload("cycle_task_group").load_only(
                "id",
                "title",
                "end_date",
                "next_due_date",
            ),
            orm.Load(cls).joinedload("cycle").load_only(
                "id",
                "title",
                "next_due_date",
                "is_verification_needed",
            ),
            orm.Load(cls).joinedload("cycle_task_group").joinedload(
                "contact").load_only("email", "name", "id"),
            orm.Load(cls).joinedload("cycle").joinedload("contact").load_only(
                "email", "name", "id"),
            orm.Load(cls).subqueryload("cycle_task_entries").load_only(
                "description", "id"),
            orm.Load(cls).joinedload("cycle").joinedload(
                "workflow").undefer_group("Workflow_complete"),
        )

    def log_json(self):
        out_json = super(CycleTaskGroupObjectTask, self).log_json()
        out_json["folder"] = self.folder
        return out_json

    @classmethod
    def bulk_update(cls, src):
        """Update statuses for bunch of tasks in a bulk.

    Args:
        src: input json with next structure:
          [{"status": "Assigned", "id": 1}, {"status": "In Progress", "id": 2}]

    Returns:
        list of updated_instances
    """
        new_prv_state_map = {
            cls.DEPRECATED: (cls.ASSIGNED, cls.IN_PROGRESS, cls.FINISHED,
                             cls.VERIFIED, cls.DECLINED),
            cls.IN_PROGRESS: (cls.ASSIGNED, ),
            cls.FINISHED: (cls.IN_PROGRESS, cls.DECLINED),
            cls.VERIFIED: (cls.FINISHED, ),
            cls.DECLINED: (cls.FINISHED, ),
            cls.ASSIGNED: (),
        }
        uniq_states = set([item['state'] for item in src])
        if len(list(uniq_states)) != 1:
            raise BadRequest("Request's JSON contains multiple statuses for "
                             "CycleTasks")
        new_state = uniq_states.pop()
        LOGGER.info("Do bulk update CycleTasks with '%s' status", new_state)
        if new_state not in cls.VALID_STATES:
            raise BadRequest("Request's JSON contains invalid statuses for "
                             "CycleTasks")
        prv_states = new_prv_state_map[new_state]
        all_ids = {item['id'] for item in src}
        # Eagerly loading is needed to get user permissions for CycleTask faster
        updatable_objects = cls.eager_query().filter(
            cls.id.in_(list(all_ids)), cls.status.in_(prv_states))
        if new_state in (cls.VERIFIED, cls.DECLINED):
            updatable_objects = [
                obj for obj in updatable_objects
                if obj.cycle.is_verification_needed
            ]
        # Bulk update works only on MyTasks page. Don't need to check for
        # WorkflowMembers' permissions here. User should update only his own tasks.
        updatable_objects = [
            obj for obj in updatable_objects
            if obj.current_user_wfa_or_assignee()
        ]
        # Queries count is constant because we are using eager query for objects.
        for obj in updatable_objects:
            obj.status = new_state
            obj.modified_by_id = login.get_current_user_id()
        return updatable_objects
class CycleTaskGroupObjectTask(roleable.Roleable,
                               wf_mixins.CycleTaskStatusValidatedMixin,
                               mixins.Stateful, mixins.Timeboxed,
                               relationship.Relatable, mixins.Notifiable,
                               mixins.Described, mixins.Titled, mixins.Slugged,
                               mixins.Base, ft_mixin.Indexed, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'

    readable_name_alias = 'cycle task'

    _title_uniqueness = False

    IMPORTABLE_FIELDS = (
        'slug',
        'title',
        'description',
        'start_date',
        'end_date',
        'finished_date',
        'verified_date',
        'status',
        '__acl__:Task Assignees',
    )

    @classmethod
    def generate_slug_prefix(cls):
        return "CYCLETASK"

    # Note: this statuses are used in utils/query_helpers to filter out the tasks
    # that should be visible on My Tasks pages.

    PROPERTY_TEMPLATE = u"task {}"

    _fulltext_attrs = [
        ft_attributes.DateFullTextAttr(
            "end_date",
            'end_date',
        ),
        ft_attributes.FullTextAttr("group title", 'cycle_task_group',
                                   ['title'], False),
        ft_attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
        ft_attributes.FullTextAttr("group assignee",
                                   lambda x: x.cycle_task_group.contact,
                                   ['email', 'name'], False),
        ft_attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                                   ['email', 'name'], False),
        ft_attributes.DateFullTextAttr(
            "group due date",
            lambda x: x.cycle_task_group.next_due_date,
            with_template=False),
        ft_attributes.DateFullTextAttr("cycle due date",
                                       lambda x: x.cycle.next_due_date,
                                       with_template=False),
        ft_attributes.MultipleSubpropertyFullTextAttr("comments",
                                                      "cycle_task_entries",
                                                      ["description"]),
        "folder",
    ]

    AUTO_REINDEX_RULES = [
        ft_mixin.ReindexRule("CycleTaskEntry",
                             lambda x: x.cycle_task_group_object_task),
    ]

    cycle_id = db.Column(
        db.Integer,
        db.ForeignKey('cycles.id', ondelete="CASCADE"),
        nullable=False,
    )
    cycle_task_group_id = db.Column(
        db.Integer,
        db.ForeignKey('cycle_task_groups.id', ondelete="CASCADE"),
        nullable=False,
    )
    task_group_task_id = db.Column(db.Integer,
                                   db.ForeignKey('task_group_tasks.id'),
                                   nullable=True)
    task_group_task = db.relationship(
        "TaskGroupTask",
        foreign_keys="CycleTaskGroupObjectTask.task_group_task_id")
    task_type = db.Column(db.String(length=250), nullable=False)
    response_options = db.Column(types.JsonType(), nullable=False, default=[])
    selected_response_options = db.Column(types.JsonType(),
                                          nullable=False,
                                          default=[])

    sort_index = db.Column(db.String(length=250), default="", nullable=False)

    finished_date = db.Column(db.DateTime)
    verified_date = db.Column(db.DateTime)

    object_approval = association_proxy('cycle', 'workflow.object_approval')
    object_approval.publish_raw = True

    @builder.simple_property
    def folder(self):
        if self.cycle:
            return self.cycle.folder
        return ""

    @property
    def cycle_task_objects_for_cache(self):
        """Changing task state must invalidate `workflow_state` on objects
    """
        return [(object_.__class__.__name__, object_.id)
                for object_ in self.related_objects]  # pylint: disable=not-an-iterable

    _api_attrs = reflection.ApiAttributes(
        'cycle',
        'cycle_task_group',
        'task_group_task',
        'cycle_task_entries',
        'sort_index',
        'task_type',
        'response_options',
        'selected_response_options',
        reflection.Attribute('object_approval', create=False, update=False),
        reflection.Attribute('finished_date', create=False, update=False),
        reflection.Attribute('verified_date', create=False, update=False),
        reflection.Attribute('allow_change_state', create=False, update=False),
        reflection.Attribute('folder', create=False, update=False),
    )

    default_description = "<ol>"\
                          + "<li>Expand the object review task.</li>"\
                          + "<li>Click on the Object to be reviewed.</li>"\
                          + "<li>Review the object in the Info tab.</li>"\
                          + "<li>Click \"Approve\" to approve the object.</li>"\
                          + "<li>Click \"Decline\" to decline the object.</li>"\
                          + "</ol>"

    _aliases = {
        "title": "Summary",
        "description": "Task Details",
        "finished_date": {
            "display_name":
            "Actual Finish Date",
            "description": ("Make sure that 'Actual Finish Date' isn't set, "
                            "if cycle task state is <'Assigned' / "
                            "'In Progress' / 'Declined' / 'Deprecated'>. "
                            "Type double dash '--' into "
                            "'Actual Finish Date' cell to remove it.")
        },
        "verified_date": {
            "display_name":
            "Actual Verified Date",
            "description": ("Make sure that 'Actual Verified Date' isn't set, "
                            "if cycle task state is <'Assigned' / "
                            "'In Progress' / 'Declined' / 'Deprecated' / "
                            "'Finished'>. Type double dash '--' into "
                            "'Actual Verified Date' cell to remove it.")
        },
        "cycle": {
            "display_name": "Cycle",
            "filter_by": "_filter_by_cycle",
        },
        "cycle_task_group": {
            "display_name": "Task Group",
            "mandatory": True,
            "filter_by": "_filter_by_cycle_task_group",
        },
        "task_type": {
            "display_name": "Task Type",
            "mandatory": True,
        },
        "end_date": "Due Date",
        "start_date": "Start Date",
    }

    @builder.simple_property
    def related_objects(self):
        """Compute and return a list of all the objects related to this cycle task.

    Related objects are those that are found either on the "source" side, or
    on the "destination" side of any of the instance's relations.

    Returns:
      (list) All objects related to the instance.
    """
        # pylint: disable=not-an-iterable
        sources = [r.source for r in self.related_sources]
        destinations = [r.destination for r in self.related_destinations]
        return sources + destinations

    @declared_attr
    def wfo_roles(self):
        """WorkflowOwner UserRoles in parent Workflow.

    Relies on self.context_id = parent_workflow.context_id.
    """
        from ggrc_basic_permissions import models as bp_models

        def primaryjoin():
            """Join UserRoles by context_id = self.context_id and role_id = WFO."""
            workflow_owner_role_id = db.session.query(
                bp_models.Role.id, ).filter(
                    bp_models.Role.name == "WorkflowOwner", ).subquery()
            ur_context_id = sa.orm.foreign(bp_models.UserRole.context_id)
            ur_role_id = sa.orm.foreign(bp_models.UserRole.role_id)
            return sa.and_(self.context_id == ur_context_id,
                           workflow_owner_role_id == ur_role_id)

        return db.relationship(
            bp_models.UserRole,
            primaryjoin=primaryjoin,
            viewonly=True,
        )

    @builder.simple_property
    def allow_change_state(self):
        return self.cycle.is_current and self.current_user_wfo_or_assignee()

    def current_user_wfo_or_assignee(self):
        """Current user is Workflow owner or Assignee for self."""
        wfo_person_ids = {ur.person_id for ur in self.wfo_roles}
        assignees_ids = {
            p.id
            for p in self.get_persons_for_rolename("Task Assignees")
        }
        return login.get_current_user_id() in (wfo_person_ids | assignees_ids)

    @classmethod
    def _filter_by_cycle(cls, predicate):
        """Get query that filters cycle tasks by related cycles.

    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 tasks by related cycles.
    """
        return Cycle.query.filter((Cycle.id == cls.cycle_id)
                                  & (predicate(Cycle.slug)
                                     | predicate(Cycle.title))).exists()

    @classmethod
    def _filter_by_cycle_task_group(cls, predicate):
        """Get query that filters cycle tasks by related 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 tasks by related cycle task groups.
    """
        return CycleTaskGroup.query.filter(
            (CycleTaskGroup.id == cls.cycle_id)
            & (predicate(CycleTaskGroup.slug)
               | predicate(CycleTaskGroup.title))).exists()

    @classmethod
    def eager_query(cls):
        """Add cycle task entries to cycle task eager query

    This function adds cycle_task_entries as a join option when fetching cycles
    tasks, and makes sure that with one query we fetch all cycle task related
    data needed for generating cycle taks json for a response.

    Returns:
      a query object with cycle_task_entries added to joined load options.
    """
        query = super(CycleTaskGroupObjectTask, cls).eager_query()
        return query.options(
            orm.joinedload('cycle').joinedload('workflow').undefer_group(
                'Workflow_complete'),
            orm.joinedload('cycle_task_entries'),
            orm.subqueryload('wfo_roles'),
        )

    @classmethod
    def indexed_query(cls):
        return super(CycleTaskGroupObjectTask, cls).indexed_query().options(
            orm.Load(cls).load_only("end_date", "start_date", "created_at",
                                    "updated_at"),
            orm.Load(cls).joinedload("cycle_task_group").load_only(
                "id",
                "title",
                "end_date",
                "next_due_date",
            ),
            orm.Load(cls).joinedload("cycle").load_only(
                "id", "title", "next_due_date"),
            orm.Load(cls).joinedload("cycle_task_group").joinedload(
                "contact").load_only("email", "name", "id"),
            orm.Load(cls).joinedload("cycle").joinedload("contact").load_only(
                "email", "name", "id"),
            orm.Load(cls).subqueryload("cycle_task_entries").load_only(
                "description", "id"),
            orm.Load(cls).joinedload("cycle").joinedload(
                "workflow").undefer_group("Workflow_complete"),
        )

    def log_json(self):
        out_json = super(CycleTaskGroupObjectTask, self).log_json()
        out_json["folder"] = self.folder
        return out_json

    @classmethod
    def bulk_update(cls, src):
        """Update statuses for bunch of tasks in a bulk.

    Args:
        src: input json with next structure:
          [{"status": "Assigned", "id": 1}, {"status": "InProgress", "id": 2}]

    Returns:
        list of updated_instances
    """
        new_prv_state_map = {
            cls.DEPRECATED: (cls.ASSIGNED, cls.IN_PROGRESS, cls.FINISHED,
                             cls.VERIFIED, cls.DECLINED),
            cls.IN_PROGRESS: (cls.ASSIGNED, ),
            cls.FINISHED: (cls.IN_PROGRESS, cls.DECLINED),
            cls.VERIFIED: (cls.FINISHED, ),
            cls.DECLINED: (cls.FINISHED, ),
            cls.ASSIGNED: (),
        }
        uniq_states = set([item['state'] for item in src])
        if len(list(uniq_states)) != 1:
            raise BadRequest("Request's JSON contains multiple statuses for "
                             "CycleTasks")
        new_state = uniq_states.pop()
        LOGGER.info("Do bulk update CycleTasks with '%s' status", new_state)
        if new_state not in cls.VALID_STATES:
            raise BadRequest("Request's JSON contains invalid statuses for "
                             "CycleTasks")
        prv_states = new_prv_state_map[new_state]
        all_ids = {item['id'] for item in src}
        # Eagerly loading is needed to get user permissions for CycleTask faster
        updatable_objects = cls.eager_query().filter(
            cls.id.in_(list(all_ids)), cls.status.in_(prv_states))
        if new_state in (cls.VERIFIED, cls.DECLINED):
            updatable_objects = [
                obj for obj in updatable_objects
                if obj.cycle.is_verification_needed
            ]
        # Bulk update works only on MyTasks page. Don't need to check for
        # WorkflowMembers' permissions here. User should update only his own tasks.
        updatable_objects = [
            obj for obj in updatable_objects
            if obj.current_user_wfo_or_assignee()
        ]
        # Queries count is constant because we are using eager query for objects.
        for obj in updatable_objects:
            obj.status = new_state
            obj.modified_by_id = login.get_current_user_id()
        return updatable_objects
Beispiel #4
0
class CycleTaskGroup(mixins.WithContact,
                     mixins.Stateful,
                     mixins.Slugged,
                     mixins.Timeboxed,
                     mixins.Described,
                     mixins.Titled,
                     mixins.Base,
                     index_mixin.Indexed,
                     db.Model):
  """Cycle Task Group model.
  """
  __tablename__ = 'cycle_task_groups'
  _title_uniqueness = False

  @classmethod
  def generate_slug_prefix_for(cls, obj):  # pylint: disable=unused-argument
    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 = [
      attributes.MultipleSubpropertyFullTextAttr(
          "task title", 'cycle_task_group_tasks', ["title"], False
      ),
      attributes.MultipleSubpropertyFullTextAttr(
          "task assignee",
          lambda instance: [t.contact for t in
                            instance.cycle_task_group_tasks],
          ["name", "email"],
          False
      ),
      attributes.DateMultipleSubpropertyFullTextAttr(
          "task due date", "cycle_task_group_tasks", ["end_date"], False
      ),
      attributes.DateFullTextAttr("due date", 'next_due_date',),
      attributes.FullTextAttr("assignee", "contact", ['name', 'email']),
      attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
      attributes.FullTextAttr("cycle assignee",
                              lambda x: x.cycle.contact,
                              ['email', 'name'],
                              False),
      attributes.DateFullTextAttr("cycle due date",
                                  lambda x: x.cycle.next_due_date,
                                  with_template=False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task comments",
          lambda instance: itertools.chain(*[
              t.cycle_task_entries for t in instance.cycle_task_group_tasks
          ]),
          ["description"],
          False
      ),
  ]

  AUTO_REINDEX_RULES = [
      index_mixin.ReindexRule(
          "CycleTaskGroupObjectTask", lambda x: x.cycle_task_group
      ),
      index_mixin.ReindexRule(
          "Person", _query_filtered_by_contact
      ),
      index_mixin.ReindexRule(
          "Person",
          lambda x: [i.cycle for i in _query_filtered_by_contact(x)]
      ),
  ]

  @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 indexed_query(cls):
    return super(CycleTaskGroup, cls).indexed_query().options(
        orm.Load(cls).load_only(
            "next_due_date",
        ),
        orm.Load(cls).subqueryload("cycle_task_group_tasks").load_only(
            "id",
            "title",
            "end_date"
        ),
        orm.Load(cls).joinedload("cycle").load_only(
            "id",
            "title",
            "next_due_date"
        ),
        orm.Load(cls).subqueryload("cycle_task_group_tasks").joinedload(
            "contact"
        ).load_only(
            "email",
            "name",
            "id"
        ),
        orm.Load(cls).subqueryload("cycle_task_group_tasks").joinedload(
            "cycle_task_entries"
        ).load_only(
            "description",
            "id"
        ),
        orm.Load(cls).joinedload("cycle").joinedload(
            "contact"
        ).load_only(
            "email",
            "name",
            "id"
        ),
        orm.Load(cls).joinedload("contact").load_only(
            "email",
            "name",
            "id"
        ),
    )

  @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')
    )
Beispiel #5
0
class Cycle(roleable.Roleable, relationship.Relatable, mixins.WithContact,
            wf_mixins.CycleStatusValidatedMixin, mixins.Timeboxed,
            mixins.Described, mixins.Titled, base.ContextRBAC, mixins.Slugged,
            mixins.Notifiable, ft_mixin.Indexed, db.Model):
    """Workflow Cycle model
  """
    # pylint: disable=too-many-instance-attributes

    __tablename__ = 'cycles'
    _title_uniqueness = False

    workflow_id = db.Column(
        db.Integer,
        db.ForeignKey('workflows.id', ondelete="CASCADE"),
        nullable=False,
    )
    cycle_task_groups = db.relationship('CycleTaskGroup',
                                        backref='_cycle',
                                        cascade='all, delete-orphan')
    cycle_task_group_object_tasks = db.relationship(
        'CycleTaskGroupObjectTask',
        backref='cycle',
        cascade='all, delete-orphan')
    is_current = db.Column(db.Boolean, default=True, nullable=False)
    next_due_date = db.Column(db.Date)

    # This parameter is overridden by workflow backref, but is here to ensure
    # pylint does not complain
    _workflow = None

    @hybrid.hybrid_property
    def workflow(self):
        """Getter for workflow foreign key."""
        return self._workflow

    @workflow.setter
    def workflow(self, workflow):
        """Set workflow foreign key and relationship."""
        if not self._workflow and workflow:
            relationship.Relationship(source=workflow, destination=self)
        self._workflow = workflow

    @property
    def is_done(self):
        """Check if cycle's done

    Overrides StatusValidatedMixin method because cycle's is_done state
    depends on is_verification_needed flag
    """
        if super(Cycle, self).is_done:
            return True
        if self.cycle_task_group_object_tasks:
            return False
        return True

    @builder.simple_property
    def folder(self):
        """Get the workflow folder."""
        if self.workflow:
            return self.workflow.folder
        return ""

    _api_attrs = reflection.ApiAttributes(
        'workflow',
        'cycle_task_groups',
        'is_current',
        'next_due_date',
        reflection.Attribute('folder', create=False, update=False),
    )

    _aliases = {
        "cycle_workflow": {
            "display_name": "Workflow",
            "filter_by": "_filter_by_cycle_workflow",
        },
        "contact": "Assignee",
        "secondary_contact": None,
    }

    PROPERTY_TEMPLATE = u"cycle {}"

    _fulltext_attrs = [
        "folder",
        ft_attributes.DateFullTextAttr("due date", "next_due_date"),
        ft_attributes.MultipleSubpropertyFullTextAttr("group title",
                                                      "cycle_task_groups",
                                                      ["title"], False),
        ft_attributes.MultipleSubpropertyFullTextAttr(
            "group assignee",
            lambda instance: [g.contact for g in instance.cycle_task_groups],
            ["email", "name"], False),
        ft_attributes.DateMultipleSubpropertyFullTextAttr(
            "group due date", 'cycle_task_groups', ["next_due_date"], False),
        ft_attributes.MultipleSubpropertyFullTextAttr(
            "task title", 'cycle_task_group_object_tasks', ["title"], False),
        ft_attributes.MultipleSubpropertyFullTextAttr(
            "task state", 'cycle_task_group_object_tasks', ["status"], False),
        ft_attributes.DateMultipleSubpropertyFullTextAttr(
            "task due date", "cycle_task_group_object_tasks", ["end_date"],
            False),
        ft_attributes.MultipleSubpropertyFullTextAttr("task assignees",
                                                      "_task_assignees",
                                                      ["name", "email"],
                                                      False),
        ft_attributes.MultipleSubpropertyFullTextAttr(
            "task secondary assignees", "_task_secondary_assignees",
            ["name", "email"], False),
        ft_attributes.MultipleSubpropertyFullTextAttr(
            "task comment", lambda instance: itertools.chain(
                *[t.comments for t in instance.cycle_task_group_object_tasks]),
            ["description"], False),
    ]

    @property
    def _task_assignees(self):
        """Property. Return the list of persons as assignee of related tasks."""
        people = set()
        for ctask in self.cycle_task_group_object_tasks:
            people.update(ctask.get_persons_for_rolename("Task Assignees"))
        return list(people)

    @property
    def _task_secondary_assignees(self):
        """Property. Returns people list as Secondary Assignee of related tasks."""
        people = set()
        for ctask in self.cycle_task_group_object_tasks:
            people.update(
                ctask.get_persons_for_rolename("Task Secondary Assignees"))
        return list(people)

    AUTO_REINDEX_RULES = [
        ft_mixin.ReindexRule("CycleTaskGroup", lambda x: x.cycle),
        ft_mixin.ReindexRule("CycleTaskGroupObjectTask",
                             lambda x: x.cycle_task_group.cycle),
        ft_mixin.ReindexRule("Person", _query_filtered_by_contact),
    ]

    @classmethod
    def _filter_by_cycle_workflow(cls, predicate):
        """Filter by cycle workflow."""
        from ggrc_workflows.models.workflow import Workflow
        return Workflow.query.filter((Workflow.id == cls.workflow_id)
                                     & (predicate(Workflow.slug)
                                        | predicate(Workflow.title))).exists()

    @classmethod
    def eager_query(cls, **kwargs):
        """Add cycle task groups to cycle eager query

    This function adds cycle_task_groups as a join option when fetching cycles,
    and makes sure we fetch all cycle related data needed for generating cycle
    json, in one query.

    Returns:
      a query object with cycle_task_groups added to joined load options.
    """
        query = super(Cycle, cls).eager_query(**kwargs)
        return query.options(
            orm.joinedload('cycle_task_groups'),
            orm.Load(cls).joinedload("workflow").undefer_group(
                "Workflow_complete"),
        )

    @classmethod
    def indexed_query(cls):
        return super(Cycle, cls).indexed_query().options(
            orm.Load(cls).load_only("next_due_date"),
            orm.Load(cls).subqueryload(
                "cycle_task_group_object_tasks").load_only(
                    "end_date",
                    "id",
                    "status",
                    "title",
                ),
            orm.Load(cls).subqueryload("cycle_task_groups").load_only(
                "id",
                "title",
                "next_due_date",
            ),
            orm.Load(cls).subqueryload(
                "cycle_task_group_object_tasks", ).subqueryload(
                    "_access_control_list").load_only(
                        "ac_role_id", ).subqueryload(
                            "access_control_people").load_only("person_id", ),
            orm.Load(cls).subqueryload("cycle_task_group_object_tasks"),
            orm.Load(cls).subqueryload("cycle_task_groups").joinedload(
                "contact").load_only(
                    "name",
                    "email",
                    "id",
                ),
            orm.Load(cls).joinedload("workflow").undefer_group(
                "Workflow_complete", ),
        )

    def _get_cycle_url(self, widget_name):
        return urljoin(
            get_url_root(),
            "workflows/{workflow_id}#{widget_name}/cycle/{cycle_id}".format(
                workflow_id=self.workflow.id,
                cycle_id=self.id,
                widget_name=widget_name))

    @property
    def cycle_url(self):
        return self._get_cycle_url("current")

    @property
    def cycle_inactive_url(self):
        return self._get_cycle_url("history")

    def log_json(self):
        out_json = super(Cycle, self).log_json()
        out_json["folder"] = self.folder
        return out_json
class Assessment(Assignable, statusable.Statusable, AuditRelationship,
                 AutoStatusChangeable, TestPlanned, CustomAttributable,
                 WithEvidence, Commentable, Personable,
                 reminderable.Reminderable, Relatable, LastDeprecatedTimeboxed,
                 WithSimilarityScore, FinishedDate, VerifiedDate, Notifiable,
                 WithAction, labeled.Labeled,
                 with_last_comment.WithLastComment,
                 issue_tracker_mixins.IssueTrackedWithUrl, base.ContextRBAC,
                 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

    REWORK_NEEDED = u"Rework Needed"
    NOT_DONE_STATES = statusable.Statusable.NOT_DONE_STATES | {
        REWORK_NEEDED,
    }
    VALID_STATES = tuple(NOT_DONE_STATES | statusable.Statusable.DONE_STATES
                         | statusable.Statusable.INACTIVE_STATES)

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

    design = deferred(db.Column(db.String, nullable=False, default=""),
                      "Assessment")
    operationally = deferred(db.Column(db.String, nullable=False, default=""),
                             "Assessment")
    audit_id = deferred(
        db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False),
        'Assessment')
    assessment_type = deferred(
        db.Column(db.String, nullable=False, server_default="Control"),
        "Assessment")
    # whether to use the object test plan on snapshot mapping
    test_plan_procedure = db.Column(db.Boolean, nullable=False, default=True)

    @declared_attr
    def object_level_definitions(cls):  # pylint: disable=no-self-argument
        """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) == cls.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
    _api_attrs = reflection.ApiAttributes(
        'design',
        'operationally',
        'audit',
        'assessment_type',
        'test_plan_procedure',
        reflection.Attribute('archived', create=False, update=False),
        reflection.Attribute('folder', create=False, update=False),
        reflection.Attribute('object', create=False, update=False),
    )

    _fulltext_attrs = [
        'archived',
        'design',
        'operationally',
        'folder',
    ]

    AUTO_REINDEX_RULES = [
        mixin.ReindexRule("Audit", lambda x: x.assessments, ["archived"]),
    ]

    _custom_publish = {
        'audit': audit.build_audit_stub,
    }

    @classmethod
    def _populate_query(cls, query):
        return query.options(
            orm.Load(cls).undefer_group("Assessment_complete"),
            orm.Load(cls).joinedload("audit").undefer_group("Audit_complete"),
            orm.Load(cls).joinedload("audit").joinedload(
                audit.Audit.issuetracker_issue),
        )

    @classmethod
    def eager_query(cls):
        return cls._populate_query(super(Assessment, cls).eager_query())

    @classmethod
    def indexed_query(cls):
        return super(Assessment, cls).indexed_query().options(
            orm.Load(cls).load_only(
                "id",
                "design",
                "operationally",
                "audit_id",
            ),
            orm.Load(cls).joinedload("audit").load_only("archived", "folder"),
        )

    def log_json(self):
        out_json = super(Assessment, self).log_json()
        out_json["folder"] = self.folder
        return out_json

    _aliases = {
        "owners": None,
        "assessment_template": {
            "display_name": "Template",
            "ignore_on_update": True,
            "filter_by": "_ignore_filter",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "assessment_type": {
            "display_name": "Assessment Type",
            "mandatory": False,
        },
        "design": "Conclusion: Design",
        "operationally": "Conclusion: Operation",
        "archived": {
            "display_name": "Archived",
            "mandatory": False,
            "ignore_on_update": True,
            "view_only": True,
        },
        "test_plan": "Assessment Procedure",
        # Currently we decided to have 'Due Date' alias for start_date,
        # but it can be changed in future
        "start_date": "Due Date",
        "status": {
            "display_name": "State",
            "mandatory": False,
            "description": "Options are:\n{}".format('\n'.join(VALID_STATES))
        },
        "issue_tracker": {
            "display_name": "Ticket Tracker",
            "mandatory": False,
            "view_only": True,
        }
    }

    @simple_property
    def archived(self):
        """Returns a boolean whether assessment is archived or not."""
        return self.audit.archived if self.audit else False

    @simple_property
    def folder(self):
        return self.audit.folder if self.audit else ""

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

    @validates("status")
    def validate_status(self, key, value):
        value = super(Assessment, self).validate_status(key, value)
        # pylint: disable=unused-argument
        if self.status == value:
            return value
        if self.status == self.REWORK_NEEDED:
            valid_states = [self.DONE_STATE, self.FINAL_STATE, self.DEPRECATED]
            if value not in valid_states:
                raise ValueError("Assessment in `Rework Needed` "
                                 "state can be only moved to: [{}]".format(
                                     ",".join(valid_states)))
        return value

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

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

    @validates("assessment_type")
    def validate_assessment_type(self, key, value):
        """Validate assessment type to be the same as existing model name"""
        # pylint: disable=unused-argument
        # pylint: disable=no-self-use
        from ggrc.snapshotter.rules import Types
        if value and value not in Types.all:
            raise ValueError(
                "Assessment type '{}' is not snapshotable".format(value))
        return value

    @classmethod
    def _ignore_filter(cls, _):
        return None
Beispiel #7
0
class CycleTaskGroupObjectTask(mixins.WithContact,
                               wf_mixins.CycleTaskStatusValidatedMixin,
                               mixins.Stateful, mixins.Timeboxed,
                               relationship.Relatable, mixins.Notifiable,
                               mixins.Described, mixins.Titled, mixins.Slugged,
                               mixins.Base, ft_mixin.Indexed, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'

    readable_name_alias = 'cycle task'

    _title_uniqueness = False

    IMPORTABLE_FIELDS = (
        'slug',
        'title',
        'description',
        'start_date',
        'end_date',
        'finished_date',
        'verified_date',
        'contact',
    )

    @classmethod
    def generate_slug_prefix(cls):
        return "CYCLETASK"

    # Note: this statuses are used in utils/query_helpers to filter out the tasks
    # that should be visible on My Tasks pages.

    PROPERTY_TEMPLATE = u"task {}"

    _fulltext_attrs = [
        ft_attributes.DateFullTextAttr(
            "end_date",
            'end_date',
        ),
        ft_attributes.FullTextAttr("assignee", 'contact', ['name', 'email']),
        ft_attributes.FullTextAttr("group title", 'cycle_task_group',
                                   ['title'], False),
        ft_attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
        ft_attributes.FullTextAttr("group assignee",
                                   lambda x: x.cycle_task_group.contact,
                                   ['email', 'name'], False),
        ft_attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                                   ['email', 'name'], False),
        ft_attributes.DateFullTextAttr(
            "group due date",
            lambda x: x.cycle_task_group.next_due_date,
            with_template=False),
        ft_attributes.DateFullTextAttr("cycle due date",
                                       lambda x: x.cycle.next_due_date,
                                       with_template=False),
        ft_attributes.MultipleSubpropertyFullTextAttr("comments",
                                                      "cycle_task_entries",
                                                      ["description"]),
    ]

    AUTO_REINDEX_RULES = [
        ft_mixin.ReindexRule("CycleTaskEntry",
                             lambda x: x.cycle_task_group_object_task),
    ]

    cycle_id = db.Column(
        db.Integer,
        db.ForeignKey('cycles.id', ondelete="CASCADE"),
        nullable=False,
    )
    cycle_task_group_id = db.Column(
        db.Integer,
        db.ForeignKey('cycle_task_groups.id', ondelete="CASCADE"),
        nullable=False,
    )
    task_group_task_id = db.Column(db.Integer,
                                   db.ForeignKey('task_group_tasks.id'),
                                   nullable=True)
    task_group_task = db.relationship(
        "TaskGroupTask",
        foreign_keys="CycleTaskGroupObjectTask.task_group_task_id")
    task_type = db.Column(db.String(length=250), nullable=False)
    response_options = db.Column(types.JsonType(), nullable=False, default=[])
    selected_response_options = db.Column(types.JsonType(),
                                          nullable=False,
                                          default=[])

    sort_index = db.Column(db.String(length=250), default="", nullable=False)

    finished_date = db.Column(db.DateTime)
    verified_date = db.Column(db.DateTime)

    object_approval = association_proxy('cycle', 'workflow.object_approval')
    object_approval.publish_raw = True

    @property
    def cycle_task_objects_for_cache(self):
        """Changing task state must invalidate `workflow_state` on objects
    """
        return [(object_.__class__.__name__, object_.id)
                for object_ in self.related_objects]  # pylint: disable=not-an-iterable

    _api_attrs = reflection.ApiAttributes(
        'cycle',
        'cycle_task_group',
        'task_group_task',
        'cycle_task_entries',
        'sort_index',
        'task_type',
        'response_options',
        'selected_response_options',
        reflection.Attribute('object_approval', create=False, update=False),
        reflection.Attribute('finished_date', create=False, update=False),
        reflection.Attribute('verified_date', create=False, update=False),
        reflection.Attribute('allow_change_state', create=False, update=False),
    )

    default_description = "<ol>"\
                          + "<li>Expand the object review task.</li>"\
                          + "<li>Click on the Object to be reviewed.</li>"\
                          + "<li>Review the object in the Info tab.</li>"\
                          + "<li>Click \"Approve\" to approve the object.</li>"\
                          + "<li>Click \"Decline\" to decline the object.</li>"\
                          + "</ol>"

    _aliases = {
        "title": "Summary",
        "description": "Task Details",
        "contact": {
            "display_name": "Assignee",
            "mandatory": True,
        },
        "secondary_contact": None,
        "finished_date": "Actual Finish Date",
        "verified_date": "Actual Verified Date",
        "cycle": {
            "display_name": "Cycle",
            "filter_by": "_filter_by_cycle",
        },
        "cycle_task_group": {
            "display_name": "Task Group",
            "mandatory": True,
            "filter_by": "_filter_by_cycle_task_group",
        },
        "task_type": {
            "display_name": "Task Type",
            "mandatory": True,
        },
        "end_date": "Due Date",
        "start_date": "Start Date",
    }

    @builder.simple_property
    def related_objects(self):
        """Compute and return a list of all the objects related to this cycle task.

    Related objects are those that are found either on the "source" side, or
    on the "destination" side of any of the instance's relations.

    Returns:
      (list) All objects related to the instance.
    """
        # pylint: disable=not-an-iterable
        sources = [r.source for r in self.related_sources]
        destinations = [r.destination for r in self.related_destinations]
        return sources + destinations

    @declared_attr
    def wfo_roles(self):
        """WorkflowOwner UserRoles in parent Workflow.

    Relies on self.context_id = parent_workflow.context_id.
    """
        from ggrc_basic_permissions import models as bp_models

        def primaryjoin():
            """Join UserRoles by context_id = self.context_id and role_id = WFO."""
            workflow_owner_role_id = db.session.query(
                bp_models.Role.id, ).filter(
                    bp_models.Role.name == "WorkflowOwner", ).subquery()
            ur_context_id = sa.orm.foreign(bp_models.UserRole.context_id)
            ur_role_id = sa.orm.foreign(bp_models.UserRole.role_id)
            return sa.and_(self.context_id == ur_context_id,
                           workflow_owner_role_id == ur_role_id)

        return db.relationship(
            bp_models.UserRole,
            primaryjoin=primaryjoin,
            viewonly=True,
        )

    @builder.simple_property
    def allow_change_state(self):
        return self.cycle.is_current and self.current_user_wfo_or_assignee()

    def current_user_wfo_or_assignee(self):
        """Current user is Workflow owner or Assignee for self."""
        current_user_id = login.get_current_user_id()

        # pylint: disable=not-an-iterable
        return (current_user_id == self.contact_id
                or current_user_id in [ur.person_id for ur in self.wfo_roles])

    @classmethod
    def _filter_by_cycle(cls, predicate):
        """Get query that filters cycle tasks by related cycles.

    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 tasks by related cycles.
    """
        return Cycle.query.filter((Cycle.id == cls.cycle_id)
                                  & (predicate(Cycle.slug)
                                     | predicate(Cycle.title))).exists()

    @classmethod
    def _filter_by_cycle_task_group(cls, predicate):
        """Get query that filters cycle tasks by related 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 tasks by related cycle task groups.
    """
        return CycleTaskGroup.query.filter(
            (CycleTaskGroup.id == cls.cycle_id)
            & (predicate(CycleTaskGroup.slug)
               | predicate(CycleTaskGroup.title))).exists()

    @classmethod
    def eager_query(cls):
        """Add cycle task entries to cycle task eager query

    This function adds cycle_task_entries as a join option when fetching cycles
    tasks, and makes sure that with one query we fetch all cycle task related
    data needed for generating cycle taks json for a response.

    Returns:
      a query object with cycle_task_entries added to joined load options.
    """
        query = super(CycleTaskGroupObjectTask, cls).eager_query()
        return query.options(
            orm.joinedload('cycle').joinedload('workflow').undefer_group(
                'Workflow_complete'),
            orm.joinedload('cycle_task_entries'),
            orm.subqueryload('wfo_roles'),
        )

    @classmethod
    def indexed_query(cls):
        return super(CycleTaskGroupObjectTask, cls).indexed_query().options(
            orm.Load(cls).load_only("end_date", "start_date", "created_at",
                                    "updated_at"),
            orm.Load(cls).joinedload("cycle_task_group").load_only(
                "id",
                "title",
                "end_date",
                "next_due_date",
            ),
            orm.Load(cls).joinedload("cycle").load_only(
                "id", "title", "next_due_date"),
            orm.Load(cls).joinedload("cycle_task_group").joinedload(
                "contact").load_only("email", "name", "id"),
            orm.Load(cls).joinedload("cycle").joinedload("contact").load_only(
                "email", "name", "id"),
            orm.Load(cls).subqueryload("cycle_task_entries").load_only(
                "description", "id"),
            orm.Load(cls).joinedload("contact").load_only(
                "email", "name", "id"),
        )
Beispiel #8
0
class CycleTaskGroup(roleable.Roleable, mixins.WithContact,
                     wf_mixins.CycleTaskGroupRelatedStatusValidatedMixin,
                     mixins.Slugged, mixins.Timeboxed, mixins.Described,
                     mixins.Titled, base.ContextRBAC, mixins.Base,
                     index_mixin.Indexed, db.Model):
    """Cycle Task Group model.
  """
    __tablename__ = 'cycle_task_groups'
    _title_uniqueness = False

    @classmethod
    def generate_slug_prefix(cls):  # pylint: disable=unused-argument
        return "CYCLEGROUP"

    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)

    _api_attrs = reflection.ApiAttributes('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 = [
        attributes.DateFullTextAttr(
            "due date",
            'next_due_date',
        ),
        attributes.FullTextAttr("assignee", "contact", ['email', 'name']),
        attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
        attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                                ['email', 'name'], False),
        attributes.DateFullTextAttr("cycle due date",
                                    lambda x: x.cycle.next_due_date,
                                    with_template=False),
    ]

    @property
    def workflow(self):
        """Property which returns parent workflow object."""
        return self.cycle.workflow

    @property
    def _task_assignees(self):
        """Property. Return the list of persons as assignee of related tasks."""
        people = set()
        for ctask in self.cycle_task_group_tasks:
            people.update(ctask.get_persons_for_rolename("Task Assignees"))
        return list(people)

    @property
    def _task_secondary_assignees(self):
        """Property. Returns people list as Secondary Assignee of related tasks."""
        people = set()
        for ctask in self.cycle_task_group_tasks:
            people.update(
                ctask.get_persons_for_rolename("Task Secondary Assignees"))
        return list(people)

    AUTO_REINDEX_RULES = [
        index_mixin.ReindexRule("Person", _query_filtered_by_contact),
        index_mixin.ReindexRule(
            "Person",
            lambda x: [i.cycle for i in _query_filtered_by_contact(x)]),
    ]

    @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 indexed_query(cls):
        return super(CycleTaskGroup, cls).indexed_query().options(
            orm.Load(cls).load_only("next_due_date", ),
            orm.Load(cls).joinedload("cycle").load_only(
                "id", "title", "next_due_date"),
            orm.Load(cls).joinedload("cycle").joinedload("contact").load_only(
                "email", "name", "id"),
            orm.Load(cls).joinedload("contact").load_only(
                "email", "name", "id"),
        )

    @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'))
Beispiel #9
0
class Cycle(mixins.WithContact,
            wf_mixins.CycleStatusValidatedMixin,
            mixins.Timeboxed,
            mixins.Described,
            mixins.Titled,
            mixins.Slugged,
            mixins.Notifiable,
            ft_mixin.Indexed,
            db.Model):
  """Workflow Cycle model
  """
  __tablename__ = 'cycles'
  _title_uniqueness = False

  workflow_id = db.Column(
      db.Integer,
      db.ForeignKey('workflows.id', ondelete="CASCADE"),
      nullable=False,
  )
  cycle_task_groups = db.relationship(
      'CycleTaskGroup', backref='cycle', cascade='all, delete-orphan')
  cycle_task_group_object_tasks = db.relationship(
      'CycleTaskGroupObjectTask', backref='cycle',
      cascade='all, delete-orphan')
  cycle_task_entries = db.relationship(
      'CycleTaskEntry', backref='cycle', cascade='all, delete-orphan')
  is_current = db.Column(db.Boolean, default=True, nullable=False)
  next_due_date = db.Column(db.Date)

  @property
  def is_done(self):
    """Check if cycle's done

    Overrides StatusValidatedMixin method because cycle's is_done state
    depends on is_verification_needed flag
    """
    if super(Cycle, self).is_done:
      return True
    if self.cycle_task_group_object_tasks:
      return False
    return True

  _api_attrs = reflection.ApiAttributes(
      'workflow',
      'cycle_task_groups',
      'is_current',
      'next_due_date',
  )

  _aliases = {
      "cycle_workflow": {
          "display_name": "Workflow",
          "filter_by": "_filter_by_cycle_workflow",
      },
      "contact": "Assignee",
      "secondary_contact": None,
  }

  PROPERTY_TEMPLATE = u"cycle {}"

  _fulltext_attrs = [
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "group title", "cycle_task_groups", ["title"], False,
      ),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "group assignee",
          lambda instance: [g.contact for g in instance.cycle_task_groups],
          ["name", "email"],
          False,
      ),
      ft_attributes.DateMultipleSubpropertyFullTextAttr(
          "group due date",
          'cycle_task_groups',
          ["next_due_date"],
          False,
      ),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "task title",
          'cycle_task_group_object_tasks',
          ["title"],
          False,
      ),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "task assignee",
          lambda instance: [t.contact for t in
                            instance.cycle_task_group_object_tasks],
          ["name", "email"],
          False
      ),
      ft_attributes.DateMultipleSubpropertyFullTextAttr(
          "task due date",
          "cycle_task_group_object_tasks",
          ["end_date"],
          False
      ),
      ft_attributes.DateFullTextAttr("due date", "next_due_date"),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "task comments",
          lambda instance: list(itertools.chain(*[
              t.cycle_task_entries
              for t in instance.cycle_task_group_object_tasks
          ])),
          ["description"],
          False
      ),
  ]

  AUTO_REINDEX_RULES = [
      ft_mixin.ReindexRule("CycleTaskGroup", lambda x: x.cycle),
      ft_mixin.ReindexRule("CycleTaskGroupObjectTask",
                           lambda x: x.cycle_task_group.cycle),
      ft_mixin.ReindexRule("Person", _query_filtered_by_contact)
  ]

  @classmethod
  def _filter_by_cycle_workflow(cls, predicate):
    from ggrc_workflows.models.workflow import Workflow
    return Workflow.query.filter(
        (Workflow.id == cls.workflow_id) &
        (predicate(Workflow.slug) | predicate(Workflow.title))
    ).exists()

  @classmethod
  def eager_query(cls):
    """Add cycle task groups to cycle eager query

    This function adds cycle_task_groups as a join option when fetching cycles,
    and makes sure we fetch all cycle related data needed for generating cycle
    json, in one query.

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

  @classmethod
  def indexed_query(cls):
    return super(Cycle, cls).indexed_query().options(
        orm.Load(cls).load_only("next_due_date"),
        orm.Load(cls).subqueryload("cycle_task_group_object_tasks").load_only(
            "id",
            "title",
            "end_date"
        ),
        orm.Load(cls).subqueryload("cycle_task_groups").load_only(
            "id",
            "title",
            "end_date",
            "next_due_date",
        ),
        orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload(
            "contact"
        ).load_only(
            "email",
            "name",
            "id"
        ),
        orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload(
            "cycle_task_entries"
        ).load_only(
            "description",
            "id"
        ),
        orm.Load(cls).subqueryload("cycle_task_groups").joinedload(
            "contact"
        ).load_only(
            "email",
            "name",
            "id"
        ),
        orm.Load(cls).joinedload("contact").load_only(
            "email",
            "name",
            "id"
        ),
    )
Beispiel #10
0
class Evidence(Roleable, Relatable, mixins.Titled, bfh.BeforeFlushHandleable,
               Statusable, mixins.WithLastDeprecatedDate, comment.Commentable,
               WithAutoDeprecation, mixin.Indexed, base.ContextRBAC,
               mixins.Slugged, db.Model):
    """Evidence (Audit-scope URLs, FILE's) model."""
    __tablename__ = "evidence"

    _title_uniqueness = False

    URL = "URL"
    FILE = "FILE"
    VALID_EVIDENCE_KINDS = [URL, FILE]

    START_STATE = 'Active'
    DEPRECATED = 'Deprecated'

    VALID_STATES = (
        START_STATE,
        DEPRECATED,
    )

    kind = deferred(
        db.Column(db.Enum(*VALID_EVIDENCE_KINDS), default=URL, nullable=False),
        "Evidence")
    source_gdrive_id = deferred(
        db.Column(db.String, nullable=False, default=u""), "Evidence")
    gdrive_id = deferred(db.Column(db.String, nullable=False, default=u""),
                         "Evidence")

    link = deferred(db.Column(db.String), "Evidence")

    description = deferred(db.Column(db.Text, nullable=False, default=u""),
                           "Evidence")

    # Override from Commentable mixin (can be removed after GGRC-5192)
    send_by_default = db.Column(db.Boolean, nullable=False, default=True)

    _api_attrs = reflection.ApiAttributes(
        "title",
        reflection.Attribute("link", update=False),
        reflection.Attribute("source_gdrive_id", update=False),
        "description",
        "status",
        reflection.Attribute("kind", update=False),
        reflection.Attribute("parent_obj", read=False, update=False),
        reflection.Attribute('archived', create=False, update=False),
        reflection.Attribute('is_uploaded', read=False, update=False),
    )

    _fulltext_attrs = ["link", "description", "kind", "status", "archived"]

    AUTO_REINDEX_RULES = [
        mixin.ReindexRule("Audit", lambda x: x.all_related_evidences,
                          ["archived"]),
    ]

    _sanitize_html = [
        "title",
        "description",
    ]

    _aliases = {
        "title": "Title",
        "link": "Link",
        "description": "Description",
        "kind": "Type",
        "archived": {
            "display_name": "Archived",
            "mandatory": False
        },
    }

    _allowed_parents = {'Assessment', 'Audit'}
    FILE_NAME_SEPARATOR = '_ggrc'

    @orm.validates("kind")
    def validate_kind(self, key, kind):
        """Returns correct option, otherwise rises an error"""
        if kind is None:
            kind = self.URL
        if kind not in self.VALID_EVIDENCE_KINDS:
            raise exceptions.ValidationError(
                "Invalid value for attribute {attr}. "
                "Expected options are `{url}`, `{file}`".format(
                    attr=key, url=self.URL, file=self.FILE))
        return kind

    @classmethod
    def indexed_query(cls):
        return super(Evidence, cls).indexed_query().options(
            orm.Load(cls).undefer_group("Evidence_complete", ),
            orm.Load(cls).subqueryload('related_sources'),
            orm.Load(cls).subqueryload('related_destinations'),
        )

    @simple_property
    def archived(self):
        """Returns a boolean whether parent is archived or not."""
        parent_candidates = self.related_objects(
            _types=Evidence._allowed_parents)
        if parent_candidates:
            parent = parent_candidates.pop()
            return parent.archived
        return False

    def log_json(self):
        tmp = super(Evidence, self).log_json()
        tmp['type'] = 'Evidence'
        return tmp

    @simple_property
    def is_uploaded(self):
        """This flag is used to know if file uploaded from a local user folder.

    In that case we need just rename file, not copy.
    """
        return self._is_uploaded if hasattr(self, '_is_uploaded') else False

    @is_uploaded.setter
    def is_uploaded(self, value):
        # pylint: disable=attribute-defined-outside-init
        self._is_uploaded = value

    @simple_property
    def parent_obj(self):
        """Getter for local parent object property."""
        # pylint: disable=attribute-defined-outside-init
        return self._parent_obj

    @parent_obj.setter
    def parent_obj(self, value):
        # pylint: disable=attribute-defined-outside-init
        self._parent_obj = value

    def _get_parent_obj(self):
        """Get parent object specified"""
        if 'id' not in self._parent_obj:
            raise exceptions.ValidationError(
                '"id" is mandatory for parent_obj')
        if 'type' not in self._parent_obj:
            raise exceptions.ValidationError(
                '"type" is mandatory for parent_obj')
        if self._parent_obj['type'] not in self._allowed_parents:
            raise exceptions.ValidationError('Allowed types are: {}.'.format(
                ', '.join(self._allowed_parents)))

        parent_type = self._parent_obj['type']
        parent_id = self._parent_obj['id']
        obj = referenced_objects.get(parent_type, parent_id)

        if not obj:
            raise ValueError('Parent object not found: {type} {id}'.format(
                type=parent_type, id=parent_id))
        return obj

    @staticmethod
    def _build_file_name_postfix(parent_obj):
        """Build postfix for given parent object"""
        postfix_parts = [Evidence.FILE_NAME_SEPARATOR, parent_obj.slug]

        related_snapshots = parent_obj.related_objects(_types=['Snapshot'])
        related_snapshots = sorted(related_snapshots, key=lambda it: it.id)

        slugs = (sn.revision.content['slug'] for sn in related_snapshots
                 if sn.child_type == parent_obj.assessment_type)

        postfix_parts.extend(slugs)
        postfix_sting = '_'.join(postfix_parts).lower()

        return postfix_sting

    def _build_relationship(self, parent_obj):
        """Build relationship between evidence and parent object"""
        from ggrc.models import all_models
        rel = all_models.Relationship(source=parent_obj, destination=self)
        db.session.add(rel)
        signals.Restful.model_put.send(rel.__class__, obj=rel, service=self)

    def _update_fields(self, response):
        """Update fields of evidence with values of the copied file"""
        self.gdrive_id = response['id']
        self.link = response['webViewLink']
        self.title = response['name']
        self.kind = Evidence.FILE

    @staticmethod
    def _get_folder(parent):
        return parent.folder if hasattr(parent, 'folder') else ''

    def exec_gdrive_file_copy_flow(self):
        """Execute google gdrive file copy flow

    Build file name, destination folder and copy file to that folder.
    After coping fills evidence object fields with new gdrive URL
    """
        if self.is_with_parent_obj() and \
           self.kind == Evidence.FILE and \
           self.source_gdrive_id:

            parent = self._get_parent_obj()
            postfix = self._build_file_name_postfix(parent)
            folder_id = self._get_folder(parent)
            file_id = self.source_gdrive_id
            from ggrc.gdrive.file_actions import process_gdrive_file
            response = process_gdrive_file(
                file_id,
                folder_id,
                postfix,
                separator=Evidence.FILE_NAME_SEPARATOR,
                is_uploaded=self.is_uploaded)
            self._update_fields(response)

    def is_with_parent_obj(self):
        return bool(hasattr(self, '_parent_obj') and self._parent_obj)

    def add_admin_role(self):
        """Add current user as Evidence admin"""
        self.add_person_with_role_name(login.get_current_user(), "Admin")

    def handle_before_flush(self):
        """Handler that called  before SQLAlchemy flush event"""
        self.exec_gdrive_file_copy_flow()
Beispiel #11
0
class Cycle(roleable.Roleable, mixins.WithContact,
            wf_mixins.CycleStatusValidatedMixin, mixins.Timeboxed,
            mixins.Described, mixins.Titled, base.ContextRBAC, mixins.Slugged,
            mixins.Notifiable, ft_mixin.Indexed, db.Model):
    """Workflow Cycle model
  """

    __tablename__ = 'cycles'
    _title_uniqueness = False

    workflow_id = db.Column(
        db.Integer,
        db.ForeignKey('workflows.id', ondelete="CASCADE"),
        nullable=False,
    )
    cycle_task_groups = db.relationship('CycleTaskGroup',
                                        backref='cycle',
                                        cascade='all, delete-orphan')
    cycle_task_group_object_tasks = db.relationship(
        'CycleTaskGroupObjectTask',
        backref='cycle',
        cascade='all, delete-orphan')
    cycle_task_entries = db.relationship('CycleTaskEntry',
                                         backref='cycle',
                                         cascade='all, delete-orphan')
    is_current = db.Column(db.Boolean, default=True, nullable=False)
    next_due_date = db.Column(db.Date)

    @property
    def is_done(self):
        """Check if cycle's done

    Overrides StatusValidatedMixin method because cycle's is_done state
    depends on is_verification_needed flag
    """
        if super(Cycle, self).is_done:
            return True
        if self.cycle_task_group_object_tasks:
            return False
        return True

    @builder.simple_property
    def folder(self):
        """Get the workflow folder."""
        if self.workflow:
            return self.workflow.folder
        return ""

    _api_attrs = reflection.ApiAttributes(
        'workflow',
        'cycle_task_groups',
        'is_current',
        'next_due_date',
        reflection.Attribute('folder', create=False, update=False),
    )

    _aliases = {
        "cycle_workflow": {
            "display_name": "Workflow",
            "filter_by": "_filter_by_cycle_workflow",
        },
        "contact": "Assignee",
        "secondary_contact": None,
    }

    PROPERTY_TEMPLATE = u"cycle {}"

    _fulltext_attrs = [
        ft_attributes.DateFullTextAttr("due date", "next_due_date"),
        "folder",
    ]

    @property
    def _task_assignees(self):
        """Property. Return the list of persons as assignee of related tasks."""
        people = set()
        for ctask in self.cycle_task_group_object_tasks:
            people.update(ctask.get_persons_for_rolename("Task Assignees"))
        return list(people)

    @property
    def _task_secondary_assignees(self):
        """Property. Returns people list as Secondary Assignee of related tasks."""
        people = set()
        for ctask in self.cycle_task_group_object_tasks:
            people.update(
                ctask.get_persons_for_rolename("Task Secondary Assignees"))
        return list(people)

    AUTO_REINDEX_RULES = [
        ft_mixin.ReindexRule("Person", _query_filtered_by_contact),
    ]

    @classmethod
    def _filter_by_cycle_workflow(cls, predicate):
        """Filter by cycle workflow."""
        from ggrc_workflows.models.workflow import Workflow
        return Workflow.query.filter((Workflow.id == cls.workflow_id)
                                     & (predicate(Workflow.slug)
                                        | predicate(Workflow.title))).exists()

    @classmethod
    def eager_query(cls):
        """Add cycle task groups to cycle eager query

    This function adds cycle_task_groups as a join option when fetching cycles,
    and makes sure we fetch all cycle related data needed for generating cycle
    json, in one query.

    Returns:
      a query object with cycle_task_groups added to joined load options.
    """
        query = super(Cycle, cls).eager_query()
        return query.options(
            orm.joinedload('cycle_task_groups'),
            orm.Load(cls).joinedload("workflow").undefer_group(
                "Workflow_complete"),
        )

    @classmethod
    def indexed_query(cls):
        return super(Cycle, cls).indexed_query().options(
            orm.Load(cls).load_only("next_due_date"),
            orm.Load(cls).joinedload("workflow").undefer_group(
                "Workflow_complete"),
        )

    def _get_cycle_url(self, widget_name):
        return urljoin(
            get_url_root(),
            "workflows/{workflow_id}#{widget_name}/cycle/{cycle_id}".format(
                workflow_id=self.workflow.id,
                cycle_id=self.id,
                widget_name=widget_name))

    @property
    def cycle_url(self):
        return self._get_cycle_url("current")

    @property
    def cycle_inactive_url(self):
        return self._get_cycle_url("history")

    def log_json(self):
        out_json = super(Cycle, self).log_json()
        out_json["folder"] = self.folder
        return out_json
Beispiel #12
0
class CycleTaskGroup(roleable.Roleable,
                     relationship.Relatable,
                     mixins.WithContact,
                     wf_mixins.CycleTaskGroupRelatedStatusValidatedMixin,
                     mixins.Slugged,
                     mixins.Timeboxed,
                     mixins.Described,
                     mixins.Titled,
                     base.ContextRBAC,
                     mixins.Base,
                     index_mixin.Indexed,
                     db.Model):
  """Cycle Task Group model.
  """
  __tablename__ = 'cycle_task_groups'
  _title_uniqueness = False

  @classmethod
  def generate_slug_prefix(cls):  # pylint: disable=unused-argument
    return "CYCLEGROUP"

  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'
  )
  next_due_date = db.Column(db.Date)

  _api_attrs = reflection.ApiAttributes(
      'cycle',
      'task_group',
      'cycle_task_group_tasks',
      'next_due_date'
  )

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

  PROPERTY_TEMPLATE = u"group {}"

  _fulltext_attrs = [
      attributes.DateFullTextAttr("due date", 'next_due_date',),
      attributes.FullTextAttr("assignee", "contact", ['email', 'name']),
      attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
      attributes.FullTextAttr(
          "cycle assignee",
          lambda x: x.cycle.contact,
          ['email', 'name'],
          False),
      attributes.DateFullTextAttr(
          "cycle due date",
          lambda x: x.cycle.next_due_date,
          with_template=False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task title",
          "cycle_task_group_tasks",
          ["title"],
          False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task assignees",
          "_task_assignees",
          ["email", "name"],
          False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task state",
          "cycle_task_group_tasks",
          ["status"],
          False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task secondary assignees",
          "_task_secondary_assignees",
          ["email", "name"],
          False),
      attributes.DateMultipleSubpropertyFullTextAttr(
          "task due date",
          "cycle_task_group_tasks",
          ["end_date"],
          False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task comment",
          lambda instance: itertools.chain(*[
              t.comments for t in instance.cycle_task_group_tasks
          ]),
          ["description"],
          False),
  ]

  # This parameter is overridden by cycle backref, but is here to ensure
  # pylint does not complain
  _cycle = None

  @hybrid.hybrid_property
  def cycle(self):
    """Getter for cycle foreign key."""
    return self._cycle

  @cycle.setter
  def cycle(self, cycle):
    """Set cycle foreign key and relationship."""
    if not self._cycle and cycle:
      relationship.Relationship(source=cycle, destination=self)
    self._cycle = cycle

  @property
  def workflow(self):
    """Property which returns parent workflow object."""
    return self.cycle.workflow

  @property
  def _task_assignees(self):
    """Property. Return the list of persons as assignee of related tasks."""
    people = set()
    for ctask in self.cycle_task_group_tasks:
      people.update(ctask.get_persons_for_rolename("Task Assignees"))
    return list(people)

  @property
  def _task_secondary_assignees(self):
    """Property. Returns people list as Secondary Assignee of related tasks."""
    people = set()
    for ctask in self.cycle_task_group_tasks:
      people.update(ctask.get_persons_for_rolename("Task Secondary Assignees"))
    return list(people)

  AUTO_REINDEX_RULES = [
      index_mixin.ReindexRule(
          "CycleTaskGroupObjectTask",
          lambda x: x.cycle_task_group
      ),
      index_mixin.ReindexRule(
          "Person",
          _query_filtered_by_contact
      ),
      index_mixin.ReindexRule(
          "Person",
          lambda x: [i.cycle for i in _query_filtered_by_contact(x)]
      ),
  ]

  @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 indexed_query(cls):
    return super(CycleTaskGroup, cls).indexed_query().options(
        orm.Load(cls).load_only(
            "next_due_date",
        ),
        orm.Load(cls).subqueryload("cycle_task_group_tasks").load_only(
            "id",
            "title",
            "end_date",
            "status",
        ),
        orm.Load(cls).subqueryload("cycle_task_group_tasks").subqueryload(
            "_access_control_list",
        ).load_only(
            "ac_role_id",
        ).subqueryload(
            "access_control_people",
        ).load_only(
            "person_id",
        ),
        orm.Load(cls).subqueryload("cycle_task_group_tasks"),
        orm.Load(cls).joinedload("cycle").load_only(
            "id",
            "title",
            "next_due_date",
        ),
        orm.Load(cls).joinedload("cycle").joinedload(
            "contact"
        ).load_only(
            "name",
            "email",
            "id",
        ),
        orm.Load(cls).joinedload("contact").load_only(
            "name",
            "email",
            "id",
        ),
    )

  @classmethod
  def eager_query(cls, **kwargs):
    """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(**kwargs)
    return query.options(
        orm.subqueryload("cycle_task_group_tasks"),
        orm.joinedload("cycle").undefer_group("Cycle_complete"),
        orm.joinedload("cycle").joinedload("contact")
    )
Beispiel #13
0
class Evidence(Roleable, Relatable, mixins.Titled, bfh.BeforeFlushHandleable,
               Statusable, mixins.WithLastDeprecatedDate, comment.Commentable,
               WithAutoDeprecation, base.ContextRBAC, mixins.Slugged,
               mixin.Indexed, db.Model):
    """Evidence (Audit-scope URLs, FILE's) model."""
    __tablename__ = "evidence"

    _title_uniqueness = False

    URL = "URL"
    FILE = "FILE"
    VALID_EVIDENCE_KINDS = [URL, FILE]

    START_STATE = 'Active'
    DEPRECATED = 'Deprecated'

    VALID_STATES = (
        START_STATE,
        DEPRECATED,
    )

    kind = deferred(
        db.Column(db.Enum(*VALID_EVIDENCE_KINDS), default=URL, nullable=False),
        "Evidence")
    source_gdrive_id = deferred(
        db.Column(db.String, nullable=False, default=u""), "Evidence")
    gdrive_id = deferred(db.Column(db.String, nullable=False, default=u""),
                         "Evidence")

    link = deferred(db.Column(db.String), "Evidence")

    description = deferred(db.Column(db.Text, nullable=False, default=u""),
                           "Evidence")

    # Override from Commentable mixin (can be removed after GGRC-5192)
    send_by_default = db.Column(db.Boolean, nullable=False, default=True)

    _api_attrs = reflection.ApiAttributes(
        "title",
        reflection.Attribute("link", update=False),
        reflection.Attribute("source_gdrive_id", update=False),
        "description",
        "status",
        reflection.Attribute("kind", update=False),
        reflection.Attribute("parent_obj", read=False, update=False),
        reflection.Attribute("archived", create=False, update=False),
        reflection.Attribute("is_uploaded", read=False, update=False),
    )

    _fulltext_attrs = [
        "title", "link", "description", "kind", "status", "archived"
    ]

    AUTO_REINDEX_RULES = [
        mixin.ReindexRule("Audit", lambda x: x.assessments, ["archived"]),
    ]

    _sanitize_html = [
        "title",
        "description",
    ]

    _aliases = {
        "title": "Title",
        "link": "Link",
        "description": "Description",
        "kind": "Type",
        "archived": {
            "display_name": "Archived",
            "mandatory": False
        },
    }

    _allowed_parents = {"Assessment", "Audit"}
    FILE_NAME_SEPARATOR = "_ggrc"

    @orm.validates("kind")
    def validate_kind(self, key, kind):
        """Returns correct option, otherwise rises an error"""
        if kind is None:
            kind = self.URL
        if kind not in self.VALID_EVIDENCE_KINDS:
            raise exceptions.ValidationError(
                "Invalid value for attribute {attr}. "
                "Expected options are `{url}`, `{file}`".format(
                    attr=key, url=self.URL, file=self.FILE))
        return kind

    @classmethod
    def _populate_query(cls, query):
        return query.options(
            orm.subqueryload(cls._related_assessment),
            orm.subqueryload(cls._related_audit).load_only("archived"),
            orm.Load(cls).undefer_group("Evidence_complete", ),
        )

    @classmethod
    def indexed_query(cls):
        return cls._populate_query(super(Evidence, cls).indexed_query())

    @classmethod
    def eager_query(cls):
        return cls._populate_query(super(Evidence, cls).eager_query())

    @simple_property
    def archived(self):
        """Evidence archived if related Assessment/Audit is archived"""
        # pylint: disable=unsubscriptable-object
        if self._related_assessment:
            return self._related_assessment.audit.archived
        elif self._related_audit:
            return self._related_audit.archived
        return False

    def log_json(self):
        tmp = super(Evidence, self).log_json()
        tmp["type"] = "Evidence"
        return tmp

    @simple_property
    def is_uploaded(self):
        """This flag is used to know if file uploaded from a local user folder.

    In that case we need just rename file, not copy.
    """
        return self._is_uploaded if hasattr(self, "_is_uploaded") else False

    @is_uploaded.setter
    def is_uploaded(self, value):
        # pylint: disable=attribute-defined-outside-init
        self._is_uploaded = value

    @simple_property
    def parent_obj(self):
        return self._parent_obj

    @parent_obj.setter
    def parent_obj(self, value):
        # pylint: disable=attribute-defined-outside-init
        self._parent_obj = value

    def _get_parent_obj(self):
        """Get parent object specified"""
        if "id" not in self._parent_obj:
            raise exceptions.ValidationError(
                "'id' is mandatory for parent_obj")
        if "type" not in self._parent_obj:
            raise exceptions.ValidationError(
                "'type' is mandatory for parent_obj")
        if self._parent_obj["type"] not in self._allowed_parents:
            raise exceptions.ValidationError("Allowed types are: {}.".format(
                ", ".join(self._allowed_parents)))

        parent_type = self._parent_obj["type"]
        parent_id = self._parent_obj["id"]
        obj = referenced_objects.get(parent_type, parent_id)

        if not obj:
            raise ValueError("Parent object not found: {type} {id}".format(
                type=parent_type, id=parent_id))
        return obj

    @staticmethod
    def _build_file_name_postfix(parent_obj):
        """Build postfix for given parent object"""
        postfix_parts = [Evidence.FILE_NAME_SEPARATOR, parent_obj.slug]

        related_snapshots = parent_obj.related_objects(_types=["Snapshot"])
        related_snapshots = sorted(related_snapshots, key=lambda it: it.id)

        slugs = (sn.revision.content["slug"] for sn in related_snapshots
                 if sn.child_type == parent_obj.assessment_type)

        postfix_parts.extend(slugs)
        postfix_sting = "_".join(postfix_parts).lower()

        return postfix_sting

    def _build_relationship(self, parent_obj):
        """Build relationship between evidence and parent object"""
        from ggrc.models import all_models
        rel = all_models.Relationship(source=parent_obj, destination=self)
        db.session.add(rel)
        signals.Restful.model_put.send(rel.__class__, obj=rel, service=self)

    def _update_fields(self, response):
        """Update fields of evidence with values of the copied file"""
        self.gdrive_id = response["id"]
        self.link = response["webViewLink"]
        self.title = response["name"]
        self.kind = Evidence.FILE

    @staticmethod
    def _get_folder(parent):
        return parent.folder if hasattr(parent, "folder") else ""

    def _map_parent(self):
        """Maps evidence to parent object

    If Document.FILE and source_gdrive_id => copy file
    """
        if self.is_with_parent_obj():
            parent = self._get_parent_obj()
            if self.kind == Evidence.FILE and self.source_gdrive_id:
                self.exec_gdrive_file_copy_flow(parent)
            self._build_relationship(parent)
            self._parent_obj = None

    def exec_gdrive_file_copy_flow(self, parent):
        """Execute google gdrive file copy flow

    Build file name, destination folder and copy file to that folder.
    After coping fills evidence object fields with new gdrive URL
    """
        postfix = self._build_file_name_postfix(parent)
        folder_id = self._get_folder(parent)
        file_id = self.source_gdrive_id
        from ggrc.gdrive.file_actions import process_gdrive_file
        response = process_gdrive_file(file_id,
                                       folder_id,
                                       postfix,
                                       separator=Evidence.FILE_NAME_SEPARATOR,
                                       is_uploaded=self.is_uploaded)
        self._update_fields(response)

    def is_with_parent_obj(self):
        return bool(hasattr(self, "_parent_obj") and self._parent_obj)

    def add_admin_role(self):
        """Add current user as Evidence admin"""
        from ggrc.models import all_models
        admin_role = db.session.query(all_models.AccessControlRole).filter_by(
            name="Admin", object_type=self.type).one()
        self.extend_access_control_list([{
            "ac_role": admin_role,
            "person": login.get_current_user()
        }])

    def handle_before_flush(self):
        """Handler that called  before SQLAlchemy flush event"""
        self._map_parent()

    @declared_attr
    def _related_audit(cls):  # pylint: disable=no-self-argument
        """Audits mapped to Evidence"""
        def primary_join_function():
            return or_(
                and_(Relationship.source_id == cls.id,
                     Relationship.source_type == cls.__name__,
                     Relationship.destination_type == "Audit"),
                and_(Relationship.destination_id == cls.id,
                     Relationship.destination_type == cls.__name__,
                     Relationship.source_type == "Audit"))

        def secondary_join_function():
            from ggrc.models import all_models
            return or_(
                and_(
                    all_models.Audit.id == Relationship.destination_id,
                    Relationship.destination_type == "Audit",
                ),
                and_(
                    all_models.Audit.id == Relationship.source_id,
                    Relationship.source_type == "Audit",
                ))

        return db.relationship("Audit",
                               primaryjoin=primary_join_function,
                               secondary=Relationship.__table__,
                               secondaryjoin=secondary_join_function,
                               viewonly=True,
                               uselist=False)

    @declared_attr
    def _related_assessment(cls):  # pylint: disable=no-self-argument
        """Assessments mapped to Evidence"""
        def primary_join_function():
            return or_(
                and_(Relationship.source_id == cls.id,
                     Relationship.source_type == cls.__name__,
                     Relationship.destination_type == "Assessment"),
                and_(Relationship.destination_id == cls.id,
                     Relationship.destination_type == cls.__name__,
                     Relationship.source_type == "Assessment"))

        def secondary_join_function():
            from ggrc.models import all_models
            return or_(
                and_(
                    all_models.Assessment.id == Relationship.destination_id,
                    Relationship.destination_type == "Assessment",
                ),
                and_(
                    all_models.Assessment.id == Relationship.source_id,
                    Relationship.source_type == "Assessment",
                ))

        return db.relationship("Assessment",
                               primaryjoin=primary_join_function,
                               secondary=Relationship.__table__,
                               secondaryjoin=secondary_join_function,
                               viewonly=True,
                               uselist=False)