Exemplo n.º 1
0
class Relationship(base.ContextRBAC, Base, db.Model):
  """Relationship model."""
  __tablename__ = 'relationships'
  source_id = db.Column(db.Integer, nullable=False)
  source_type = db.Column(db.String, nullable=False)
  destination_id = db.Column(db.Integer, nullable=False)
  destination_type = db.Column(db.String, nullable=False)
  parent_id = db.Column(
      db.Integer,
      db.ForeignKey('relationships.id', ondelete='SET NULL'),
      nullable=True,
  )
  parent = db.relationship(
      lambda: Relationship,
      remote_side=lambda: Relationship.id
  )
  automapping_id = db.Column(
      db.Integer,
      db.ForeignKey('automappings.id', ondelete='CASCADE'),
      nullable=True,
  )
  is_external = db.Column(db.Boolean, nullable=False, default=False)

  def get_related_for(self, object_type):
    """Return related object for sent type."""
    if object_type == self.source_type:
      return self.destination
    if object_type == self.destination_type:
      return self.source

  @property
  def source_attr(self):
    return '{0}_source'.format(self.source_type)

  @property
  def source(self):
    """Source getter."""
    if not hasattr(self, self.source_attr):
      logger.warning(
          "Relationship source attr '%s' does not exist. "
          "This indicates invalid data in our database!",
          self.source_attr
      )
      return None
    return getattr(self, self.source_attr)

  @source.setter
  def source(self, value):
    self.source_id = getattr(value, 'id', None)
    self.source_type = getattr(value, 'type', None)
    self.validate_relatable_type("source", value)
    return setattr(self, self.source_attr, value)

  @property
  def destination_attr(self):
    return '{0}_destination'.format(self.destination_type)

  @property
  def destination(self):
    """Destination getter."""
    if not hasattr(self, self.destination_attr):
      logger.warning(
          "Relationship destination attr '%s' does not exist. "
          "This indicates invalid data in our database!",
          self.destination_attr
      )
      return None
    return getattr(self, self.destination_attr)

  @destination.setter
  def destination(self, value):
    self.destination_id = getattr(value, 'id', None)
    self.destination_type = getattr(value, 'type', None)
    self.validate_relatable_type("destination", value)
    return setattr(self, self.destination_attr, value)

  @classmethod
  def find_related(cls, object1, object2):
    return cls.get_related_query(object1, object2).first()

  @classmethod
  def get_related_query(cls, object1, object2):
    def predicate(src, dst):
      return and_(
          Relationship.source_type == src.type,
          or_(Relationship.source_id == src.id, src.id == None),  # noqa
          Relationship.destination_type == dst.type,
          or_(Relationship.destination_id == dst.id, dst.id == None),  # noqa
      )
    return Relationship.query.filter(
        or_(predicate(object1, object2), predicate(object2, object1))
    )

  @staticmethod
  def _extra_table_args(cls):
    return (
        db.UniqueConstraint(
            'source_id', 'source_type', 'destination_id', 'destination_type'),
        db.Index(
            'ix_relationships_source',
            'source_type', 'source_id'),
        db.Index(
            'ix_relationships_destination',
            'destination_type', 'destination_id'),
    )

  _api_attrs = reflection.ApiAttributes(
      'source',
      'destination',
      reflection.Attribute(
          'is_external', create=True, update=False, read=True),
  )

  def _display_name(self):
    return "{}:{} <-> {}:{}".format(self.source_type, self.source_id,
                                    self.destination_type, self.destination_id)

  def validate_relatable_type(self, field, value):
    if value is None:
      raise ValidationError(u"{}.{} can't be None."
                            .format(self.__class__.__name__, field))
    if not isinstance(value, Relatable):
      raise ValidationError(u"You are trying to create relationship with not "
                            u"Relatable type: {}".format(value.type))
    tgt_type = self.source_type
    tgt_id = self.source_id
    self.validate_relation_by_type(self.source_type, self.destination_type)

    if field == "source":
      tgt_type = self.destination_type
      tgt_id = self.destination_id
    if value and getattr(value, "type") == "Snapshot":
      if not tgt_type:
        return
      if value.child_type == tgt_type and value.child_id == tgt_id:
        raise ValidationError(
            u"Invalid source-destination types pair for {}: "
            u"source_type={!r}, destination_type={!r}"
            .format(self.type, self.source_type, self.destination_type)
        )
    # else check if the opposite is a Snapshot
    elif tgt_type == "Snapshot":
      from ggrc.models import Snapshot
      snapshot = db.session.query(Snapshot).get(tgt_id)
      if snapshot.child_type == value.type and snapshot.child_id == value.id:
        raise ValidationError(
            u"Invalid source-destination types pair for {}: "
            u"source_type={!r}, destination_type={!r}"
            .format(self.type, self.source_type, self.destination_type)
        )

  # pylint:disable=unused-argument
  @validates("is_external")
  def validate_is_external(self, key, value):
    """Validates is change of is_external column value allowed."""
    if is_external_app_user() and (not value or self.is_external is False):
      raise ValidationError(
          'External application can create only external relationships.')
    return value

  # pylint:disable=unused-argument
  @staticmethod
  def validate_delete(mapper, connection, target):
    """Validates is delete of Relationship is allowed."""
    Relationship.validate_relation_by_type(target.source_type,
                                           target.destination_type)
    if is_external_app_user() and not target.is_external:
      raise ValidationError(
          'External application can delete only external relationships.')

  @staticmethod
  def validate_relation_by_type(source_type, destination_type):
    """Checks if a mapping is allowed between given types."""
    if is_external_app_user():
      # external users can map and unmap scoping objects
      # check that relationship is external is done in a separate validator
      return

    from ggrc.models import all_models
    scoping_models_names = [m.__name__ for m in all_models.all_models
                            if issubclass(m, ScopeObject)]
    if source_type in scoping_models_names and \
       destination_type in ("Regulation", "Standard") or \
       destination_type in scoping_models_names and \
       source_type in ("Regulation", "Standard"):
      raise ValidationError(
          u"You do not have the necessary permissions to map and unmap "
          u"scoping objects to directives in this application. Please "
          u"contact your administrator if you have any questions.")
Exemplo n.º 2
0
class TaskGroupTask(roleable.Roleable, relationship.Relatable, mixins.Titled,
                    mixins.Described, base.ContextRBAC, mixins.Slugged,
                    mixins.Timeboxed, Indexed, db.Model):
    """Workflow TaskGroupTask model."""

    __tablename__ = 'task_group_tasks'
    _extra_table_args = (schema.CheckConstraint('start_date <= end_date'), )
    _title_uniqueness = False
    _start_changed = False

    @classmethod
    def default_task_type(cls):
        return cls.TEXT

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

    task_group_id = db.Column(
        db.Integer,
        db.ForeignKey('task_groups.id', ondelete="CASCADE"),
        nullable=False,
    )

    object_approval = db.Column(db.Boolean, nullable=False, default=False)

    task_type = db.Column(db.String(length=250),
                          default=default_task_type,
                          nullable=False)

    response_options = db.Column(JsonType(), nullable=False, default=[])

    relative_start_day = deferred(db.Column(db.Integer, default=None),
                                  "TaskGroupTask")
    relative_end_day = deferred(db.Column(db.Integer, default=None),
                                "TaskGroupTask")

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

    @hybrid.hybrid_property
    def task_group(self):
        """Getter for task group foreign key."""
        return self._task_group

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

    TEXT = 'text'
    CHECKBOX = 'checkbox'
    VALID_TASK_TYPES = [TEXT, CHECKBOX]

    @orm.validates('task_type')
    def validate_task_type(self, key, value):
        # pylint: disable=unused-argument
        if value is None:
            value = self.default_task_type()
        if value not in self.VALID_TASK_TYPES:
            raise ValueError(u"Invalid type '{}'".format(value))
        return value

    # pylint: disable=unused-argument
    @orm.validates("start_date", "end_date")
    def validate_date(self, key, value):
        """Validates date's itself correctness, start_ end_ dates relative to each
    other correctness is checked with 'before_insert' hook
    """
        if value is None:
            return None
        if isinstance(value, datetime.datetime):
            value = value.date()
        if value < datetime.date(100, 1, 1):
            current_century = datetime.date.today().year / 100
            return datetime.date(value.year + current_century * 100,
                                 value.month, value.day)
        return value

    _api_attrs = reflection.ApiAttributes(
        'task_group',
        'object_approval',
        'task_type',
        'response_options',
        reflection.Attribute('view_start_date', update=False, create=False),
        reflection.Attribute('view_end_date', update=False, create=False),
    )
    DATE_HINT = "Allowed value is date in one of formats listed" \
                " below:\nYYYY-MM-DD\nMM/DD/YYYY."
    _sanitize_html = []
    _aliases = {
        "title": "Task Title",
        "description": {
            "display_name": "Task Description",
            "handler_key": "task_description",
        },
        "start_date": {
            "display_name": "Task Start Date",
            "mandatory": True,
            "description":
            "{}\nOnly working days are accepted".format(DATE_HINT),
        },
        "end_date": {
            "display_name": "Task Due Date",
            "mandatory": True,
            "description":
            "{}\nOnly working days are accepted".format(DATE_HINT),
        },
        "task_group": {
            "display_name": "Task Group Code",
            "mandatory": True,
            "filter_by": "_filter_by_task_group",
        },
        "task_type": {
            "display_name": "Task Type",
            "mandatory": True,
            "description": ("Accepted values are:"
                            "\n'Rich Text'\n'Checkbox'"),
        },
    }

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

    @classmethod
    def _filter_by_task_group(cls, predicate):
        return TaskGroup.query.filter((TaskGroup.id == cls.task_group_id) & (
            predicate(TaskGroup.slug) | predicate(TaskGroup.title))).exists()

    def _get_view_date(self, date):
        if date and self.task_group and self.task_group.workflow:
            return self.task_group.workflow.calc_next_adjusted_date(date)
        return None

    @simple_property
    def view_start_date(self):
        return self._get_view_date(self.start_date)

    @simple_property
    def view_end_date(self):
        return self._get_view_date(self.end_date)

    @classmethod
    def _populate_query(cls, query):
        return query.options(
            orm.Load(cls).joinedload("task_group").undefer_group(
                "TaskGroup_complete"),
            orm.Load(cls).joinedload("task_group").joinedload(
                "workflow").undefer_group("Workflow_complete"),
        )

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

    def _display_name(self):
        return self.title + '<->' + self.task_group.display_name

    def copy(self, _other=None, **kwargs):
        columns = [
            'title', 'description', 'task_group', 'start_date', 'end_date',
            'access_control_list', 'modified_by', 'task_type',
            'response_options'
        ]

        if kwargs.get('clone_people', False):
            access_control_list = [{
                "ac_role_id": acl.ac_role_id,
                "person": {
                    "id": person.id
                }
            } for person, acl in self.access_control_list]
        else:
            role_id = {
                v: k
                for (k, v) in role.get_custom_roles_for(self.type).iteritems()
            }['Task Assignees']
            access_control_list = [{
                "ac_role_id": role_id,
                "person": {
                    "id": get_current_user().id
                }
            }]
        kwargs['modified_by'] = get_current_user()
        return self.copy_into(_other,
                              columns,
                              access_control_list=access_control_list,
                              **kwargs)
Exemplo n.º 3
0
class RiskAssessment(Documentable, Timeboxed, Noted, Described,
                     CustomAttributable, Titled, Relatable, Slugged, Indexed,
                     db.Model):
    """Risk Assessment model."""
    __tablename__ = 'risk_assessments'
    _title_uniqueness = False

    ra_manager_id = deferred(db.Column(db.Integer, db.ForeignKey('people.id')),
                             'RiskAssessment')
    ra_manager = db.relationship('Person',
                                 uselist=False,
                                 foreign_keys='RiskAssessment.ra_manager_id')

    ra_counsel_id = deferred(db.Column(db.Integer, db.ForeignKey('people.id')),
                             'RiskAssessment')
    ra_counsel = db.relationship('Person',
                                 uselist=False,
                                 foreign_keys='RiskAssessment.ra_counsel_id')

    program_id = deferred(
        db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False),
        'RiskAssessment')
    program = db.relationship('Program',
                              backref='risk_assessments',
                              uselist=False,
                              foreign_keys='RiskAssessment.program_id')

    _fulltext_attrs = []

    _publish_attrs = [
        'ra_manager',
        'ra_counsel',
        'program',
    ]

    _aliases = {
        "ra_manager": {
            "display_name": "Risk Manager",
            "filter_by": "_filter_by_risk_manager",
        },
        "ra_counsel": {
            "display_name": "Risk Counsel",
            "filter_by": "_filter_by_risk_counsel",
        },
        "start_date": {
            "display_name": "Start Date",
            "mandatory": True,
        },
        "end_date": {
            "display_name": "End Date",
            "mandatory": True,
        },
        "program": {
            "display_name": "Program",
            "mandatory": True,
            "filter_by": "_filter_by_program",
        }
    }

    @classmethod
    def _filter_by_program(cls, predicate):
        return Program.query.filter((Program.id == cls.program_id)
                                    & (predicate(Program.slug)
                                       | predicate(Program.title))).exists()

    @classmethod
    def _filter_by_risk_manager(cls, predicate):
        return Person.query.filter((Person.id == cls.ra_manager_id)
                                   & (predicate(Person.name)
                                      | predicate(Person.email))).exists()

    @classmethod
    def _filter_by_risk_counsel(cls, predicate):
        return Person.query.filter((Person.id == cls.ra_counsel_id)
                                   & (predicate(Person.name)
                                      | predicate(Person.email))).exists()
Exemplo n.º 4
0
 def updated_by_id(cls):  # pylint: disable=no-self-argument
     """Id of user who did the last modification of the object."""
     return db.Column(db.Integer, db.ForeignKey('people.id'), nullable=True)
Exemplo n.º 5
0
class Relationship(Base, db.Model):
    __tablename__ = 'relationships'
    source_id = db.Column(db.Integer, nullable=False)
    source_type = db.Column(db.String, nullable=False)
    destination_id = db.Column(db.Integer, nullable=False)
    destination_type = db.Column(db.String, nullable=False)
    parent_id = db.Column(
        db.Integer,
        db.ForeignKey('relationships.id', ondelete='SET NULL'),
        nullable=True,
    )
    parent = db.relationship(lambda: Relationship,
                             remote_side=lambda: Relationship.id)
    automapping_id = db.Column(
        db.Integer,
        db.ForeignKey('automappings.id', ondelete='CASCADE'),
        nullable=True,
    )
    relationship_attrs = db.relationship(
        lambda: RelationshipAttr,
        collection_class=attribute_mapped_collection("attr_name"),
        lazy='joined',  # eager loading
        cascade='all, delete-orphan')
    attrs = association_proxy(
        "relationship_attrs",
        "attr_value",
        creator=lambda k, v: RelationshipAttr(attr_name=k, attr_value=v))

    @property
    def source_attr(self):
        return '{0}_source'.format(self.source_type)

    @property
    def source(self):
        return getattr(self, self.source_attr)

    @source.setter
    def source(self, value):
        self.source_id = getattr(value, 'id', None)
        self.source_type = getattr(value, 'type', None)
        return setattr(self, self.source_attr, value)

    @property
    def destination_attr(self):
        return '{0}_destination'.format(self.destination_type)

    @property
    def destination(self):
        return getattr(self, self.destination_attr)

    @destination.setter
    def destination(self, value):
        self.destination_id = getattr(value, 'id', None)
        self.destination_type = getattr(value, 'type', None)
        return setattr(self, self.destination_attr, value)

    @staticmethod
    def validate_attrs(mapper, connection, relationship):
        """
      Only white-listed attributes can be stored, so users don't use this
      for storing arbitrary data.
    """
        # pylint: disable=unused-argument
        for attr_name, attr_value in relationship.attrs.iteritems():
            attr = RelationshipAttr(attr_name=attr_name, attr_value=attr_value)
            RelationshipAttr.validate_attr(relationship.source,
                                           relationship.destination,
                                           relationship.attrs, attr)

    @classmethod
    def find_related(cls, object1, object2):
        return cls.get_related_query(object1, object2).first()

    @classmethod
    def get_related_query(cls, object1, object2):
        def predicate(src, dst):
            return and_(
                Relationship.source_type == src.type,
                or_(Relationship.source_id == src.id, src.id == None),  # noqa
                Relationship.destination_type == dst.type,
                or_(Relationship.destination_id == dst.id,
                    dst.id == None),  # noqa
            )

        return Relationship.query.filter(
            or_(predicate(object1, object2), predicate(object2, object1)))

    @classmethod
    def update_attributes(cls, object1, object2, new_attrs):
        r = cls.find_related(object1, object2)
        for attr_name, attr_value in new_attrs.iteritems():
            attr = RelationshipAttr(attr_name=attr_name, attr_value=attr_value)
            attr = RelationshipAttr.validate_attr(r.source, r.destination,
                                                  r.attrs, attr)
            r.attrs[attr.attr_name] = attr.attr_value
        return r

    @staticmethod
    def _extra_table_args(cls):
        return (
            db.UniqueConstraint('source_id', 'source_type', 'destination_id',
                                'destination_type'),
            db.Index('ix_relationships_source', 'source_type', 'source_id'),
            db.Index('ix_relationships_destination', 'destination_type',
                     'destination_id'),
        )

    _api_attrs = reflection.ApiAttributes('source', 'destination', 'attrs')
    attrs.publish_raw = True

    def _display_name(self):
        return "{}:{} <-> {}:{}".format(self.source_type, self.source_id,
                                        self.destination_type,
                                        self.destination_id)

    def log_json(self):
        json = super(Relationship, self).log_json()
        # manually add attrs since the base log_json only captures table columns
        json["attrs"] = self.attrs.copy()  # copy in order to detach from orm
        return json
Exemplo n.º 6
0
class ImportExport(Identifiable, db.Model):
  """ImportExport Model."""

  __tablename__ = 'import_exports'

  IMPORT_JOB_TYPE = 'Import'
  EXPORT_JOB_TYPE = 'Export'

  ANALYSIS_STATUS = 'Analysis'
  BLOCKED_STATUS = 'Blocked'
  FAILED_STATUS = 'Failed'
  IN_PROGRESS_STATUS = 'In Progress'
  NOT_STARTED_STATUS = 'Not Started'

  IMPORT_EXPORT_STATUSES = [
      NOT_STARTED_STATUS,
      ANALYSIS_STATUS,
      IN_PROGRESS_STATUS,
      BLOCKED_STATUS,
      'Analysis Failed',
      'Stopped',
      FAILED_STATUS,
      'Finished',
  ]

  DEFAULT_COLUMNS = ['id', 'title', 'created_at', 'status']

  job_type = db.Column(db.Enum(IMPORT_JOB_TYPE, EXPORT_JOB_TYPE),
                       nullable=False)
  status = db.Column(db.Enum(*IMPORT_EXPORT_STATUSES), nullable=False,
                     default=NOT_STARTED_STATUS)
  description = db.Column(db.Text)
  created_at = db.Column(db.DateTime, nullable=False)
  start_at = db.Column(db.DateTime)
  end_at = db.Column(db.DateTime)
  created_by_id = db.Column(db.Integer,
                            db.ForeignKey('people.id'), nullable=False)
  created_by = db.relationship('Person',
                               foreign_keys='ImportExport.created_by_id',
                               uselist=False)
  results = db.Column(mysql.LONGTEXT)
  title = db.Column(db.Text)
  content = db.Column(mysql.LONGTEXT)
  gdrive_metadata = db.Column('gdrive_metadata', db.Text)

  def log_json(self, is_default=False):
    """JSON representation"""
    if is_default:
      columns = self.DEFAULT_COLUMNS
    else:
      columns = (column.name for column in self.__table__.columns
                 if column.name not in ('content', 'gdrive_metadata'))

    res = {}
    for column in columns:
      if column == "results":
        res[column] = json.loads(self.results) if self.results \
            else self.results
      elif column == "created_at":
        res[column] = self.created_at.isoformat()
      else:
        res[column] = getattr(self, column)

    return res
Exemplo n.º 7
0
class Audit(Snapshotable, clonable.SingleClonable, WithEvidence,
            mixins.CustomAttributable, Personable, HasOwnContext, Relatable,
            Roleable, issue_tracker_mixins.IssueTrackedWithConfig,
            WithLastDeprecatedDate, mixins.Timeboxed, base.ContextRBAC,
            mixins.BusinessObject, mixins.Folderable,
            rest_handable_mixins.WithDeleteHandable, Indexed, db.Model):
    """Audit model."""

    __tablename__ = 'audits'
    _slug_uniqueness = False

    VALID_STATES = (u'Planned', u'In Progress', u'Manager Review',
                    u'Ready for External Review', u'Completed', u'Deprecated')

    CLONEABLE_CHILDREN = {"AssessmentTemplate"}

    report_start_date = deferred(db.Column(db.Date), 'Audit')
    report_end_date = deferred(db.Column(db.Date), 'Audit')
    audit_firm_id = deferred(
        db.Column(db.Integer, db.ForeignKey('org_groups.id')), 'Audit')
    audit_firm = db.relationship('OrgGroup', uselist=False)
    gdrive_evidence_folder = deferred(db.Column(db.String), 'Audit')
    program_id = deferred(
        db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False),
        'Audit')
    object_type = db.Column(db.String(length=250),
                            nullable=False,
                            default='Control')

    assessments = db.relationship('Assessment', backref='audit')
    issues = db.relationship('Issue', backref='audit')
    snapshots = db.relationship('Snapshot', backref='audit')
    archived = deferred(db.Column(db.Boolean, nullable=False, default=False),
                        'Audit')
    manual_snapshots = deferred(
        db.Column(db.Boolean, nullable=False, default=False), 'Audit')
    assessment_templates = db.relationship('AssessmentTemplate',
                                           backref='audit')

    _api_attrs = reflection.ApiAttributes(
        'report_start_date',
        'report_end_date',
        'audit_firm',
        'gdrive_evidence_folder',
        'program',
        'object_type',
        'archived',
        'manual_snapshots',
    )

    _fulltext_attrs = [
        'archived',
        'report_start_date',
        'report_end_date',
        'audit_firm',
        'gdrive_evidence_folder',
    ]

    @classmethod
    def indexed_query(cls):
        return super(Audit, cls).indexed_query().options(
            orm.Load(cls).undefer_group("Audit_complete", ), )

    _sanitize_html = [
        'gdrive_evidence_folder',
        'description',
    ]

    _include_links = []

    _aliases = {
        "program": {
            "display_name": "Program",
            "filter_by": "_filter_by_program",
            "mandatory": True,
        },
        "start_date": "Planned Start Date",
        "end_date": "Planned End Date",
        "report_start_date": "Planned Report Period from",
        "report_end_date": "Planned Report Period to",
        "notes": None,
        "archived": {
            "display_name": "Archived",
            "mandatory": False
        },
        "status": {
            "display_name": "State",
            "mandatory": True,
            "description": "Options are:\n{}".format('\n'.join(VALID_STATES))
        }
    }

    def _clone(self, source_object):
        """Clone audit and all relevant attributes.

    Keeps the internals of actual audit cloning and everything that is related
    to audit itself (auditors, audit firm, context setting,
    custom attribute values, etc.)
    """
        from ggrc_basic_permissions import create_audit_context

        data = {
            "title": source_object.generate_attribute("title"),
            "description": source_object.description,
            "audit_firm": source_object.audit_firm,
            "start_date": source_object.start_date,
            "end_date": source_object.end_date,
            "last_deprecated_date": source_object.last_deprecated_date,
            "program": source_object.program,
            "status": source_object.VALID_STATES[0],
            "report_start_date": source_object.report_start_date,
            "report_end_date": source_object.report_end_date
        }

        self.update_attrs(data)
        db.session.flush()

        create_audit_context(self)
        self.clone_acls(source_object)
        self.clone_custom_attribute_values(source_object)

    def clone_acls(self, audit):
        """Clone acl roles like auditors and audit captains

    Args:
      audit: Audit instance
    """
        for person, acl in audit.access_control_list:
            self.add_person_with_role(person, acl.ac_role)

    def clone(self, source_id, mapped_objects=None):
        """Clone audit with specified whitelisted children.

    Children that can be cloned should be specified in CLONEABLE_CHILDREN.

    Args:
      mapped_objects: A list of related objects that should also be copied and
      linked to a new audit.
    """
        if not mapped_objects:
            mapped_objects = []

        source_object = Audit.query.get(source_id)
        self._clone(source_object)

        if any(mapped_objects):
            related_children = source_object.related_objects(mapped_objects)

            for obj in related_children:
                obj.clone(self)

    @orm.validates("archived")
    def archived_check(self, _, value):
        """Only Admins and Program Managers are allowed to (un)archive Audit."""
        user = get_current_user()
        if getattr(user, 'system_wide_role', None) in SystemWideRoles.admins:
            return value

        if self.archived is not None and self.archived != value and \
           not any(acl for person, acl in list(self.program.access_control_list)
                   if acl.ac_role.name == "Program Managers" and
                   person.id == user.id):
            raise wzg_exceptions.Forbidden()
        return value

    @classmethod
    def _filter_by_program(cls, predicate):
        """Helper for filtering by program"""
        return Program.query.filter((Program.id == Audit.program_id)
                                    & (predicate(Program.slug)
                                       | predicate(Program.title))).exists()

    @classmethod
    def eager_query(cls):
        query = super(Audit, cls).eager_query()
        return query.options(
            orm.joinedload('program'),
            orm.subqueryload('object_people').joinedload('person'),
        )

    def get_evidences_from_assessments(self, objects=False):
        """Return all related evidences from assessments.
      audit <--> assessment -> evidence

    :param objects: bool. optional argument.
          If True object Evidence ORM objects return
    :return: sqlalchemy.Query or sqlalchemy.orm.query.Query objects
    """
        from ggrc.models.assessment import Assessment
        evid_as_dest = db.session.query(
            Relationship.destination_id.label("id"), ).join(
                Assessment,
                Assessment.id == Relationship.source_id,
            ).filter(
                Relationship.destination_type == Evidence.__name__,
                Relationship.source_type == Assessment.__name__,
                Assessment.audit_id == self.id,
            )
        evid_as_source = db.session.query(
            Relationship.source_id.label("id"), ).join(
                Assessment,
                Assessment.id == Relationship.destination_id,
            ).filter(
                Relationship.source_type == Evidence.__name__,
                Relationship.destination_type == Assessment.__name__,
                Assessment.audit_id == self.id,
            )
        evidence_assessment = evid_as_dest.union(evid_as_source)
        if objects:
            return db.session.query(Evidence).filter(
                Evidence.id.in_(evidence_assessment), )
        return evidence_assessment

    def get_evidences_from_audit(self, objects=False):
        """Return all related evidence. In relation audit <--> evidence

    :param objects: bool. optional argument.
          If True object Evidence ORM objects return
    :return: sqlalchemy.Query or sqlalchemy.orm.query.Query objects
    """

        evid_a_source = db.session.query(
            Relationship.source_id.label("id"), ).filter(
                Relationship.source_type == Evidence.__name__,
                Relationship.destination_type == Audit.__name__,
                Relationship.destination_id == self.id,
            )
        evid_a_dest = db.session.query(
            Relationship.destination_id.label("id"), ).filter(
                Relationship.destination_type == Evidence.__name__,
                Relationship.source_type == Audit.__name__,
                Relationship.source_id == self.id,
            )
        evidence_audit = evid_a_dest.union(evid_a_source)
        if objects:
            return db.session.query(Evidence).filter(
                Evidence.id.in_(evidence_audit), )
        return evidence_audit

    @simple_property
    def all_related_evidences(self):
        """Return all related evidences of audit"""
        evidence_assessment = self.get_evidences_from_assessments()
        evidence_audit = self.get_evidences_from_audit()
        evidence_ids = evidence_assessment.union(evidence_audit)
        return db.session.query(Evidence).filter(Evidence.id.in_(evidence_ids))

    def _check_no_assessments(self):
        """Check that audit has no assessments before delete."""
        if self.assessments or self.assessment_templates:
            db.session.rollback()
            raise wzg_exceptions.Conflict(errors.MAPPED_ASSESSMENT)

    def handle_delete(self):
        """Handle model_deleted signals."""
        self._check_no_assessments()
Exemplo n.º 8
0
class Audit(Snapshotable, clonable.Clonable, CustomAttributable, Personable,
            HasOwnContext, Relatable, Timeboxed, Noted, Described, Hyperlinked,
            WithContact, Titled, Stateful, Slugged, db.Model):
    """Audit model."""

    __tablename__ = 'audits'
    _slug_uniqueness = False

    VALID_STATES = (u'Planned', u'In Progress', u'Manager Review',
                    u'Ready for External Review', u'Completed')

    CLONEABLE_CHILDREN = {"AssessmentTemplate"}

    report_start_date = deferred(db.Column(db.Date), 'Audit')
    report_end_date = deferred(db.Column(db.Date), 'Audit')
    audit_firm_id = deferred(
        db.Column(db.Integer, db.ForeignKey('org_groups.id')), 'Audit')
    audit_firm = db.relationship('OrgGroup', uselist=False)
    gdrive_evidence_folder = deferred(db.Column(db.String), 'Audit')
    program_id = deferred(
        db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False),
        'Audit')
    audit_objects = db.relationship('AuditObject',
                                    backref='audit',
                                    cascade='all, delete-orphan')
    object_type = db.Column(db.String(length=250),
                            nullable=False,
                            default='Control')

    _publish_attrs = [
        'report_start_date', 'report_end_date', 'audit_firm', 'status',
        'gdrive_evidence_folder', 'program', 'object_type',
        PublishOnly('audit_objects')
    ]

    _sanitize_html = [
        'gdrive_evidence_folder',
        'description',
    ]

    _include_links = []

    _aliases = {
        "program": {
            "display_name": "Program",
            "filter_by": "_filter_by_program",
            "mandatory": True,
        },
        "user_role:Auditor": {
            "display_name": "Auditors",
            "type": AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_auditor",
        },
        "status": {
            "display_name": "Status",
            "mandatory": True,
        },
        "start_date": "Planned Start Date",
        "end_date": "Planned End Date",
        "report_start_date": "Planned Report Period from",
        "report_end_date": "Planned Report Period to",
        "contact": {
            "display_name": "Internal Audit Lead",
            "mandatory": True,
            "filter_by": "_filter_by_contact",
        },
        "secondary_contact": None,
        "notes": None,
        "url": None,
        "reference_url": None,
    }

    def _clone(self, source_object):
        """Clone audit and all relevant attributes.

    Keeps the internals of actual audit cloning and everything that is related
    to audit itself (auditors, audit firm, context setting,
    custom attribute values, etc.)
    """
        from ggrc_basic_permissions import create_audit_context

        data = {
            "title": source_object.generate_attribute("title"),
            "description": source_object.description,
            "audit_firm": source_object.audit_firm,
            "start_date": source_object.start_date,
            "end_date": source_object.end_date,
            "program": source_object.program,
            "status": source_object.VALID_STATES[0],
            "report_start_date": source_object.report_start_date,
            "report_end_date": source_object.report_end_date,
            "contact": source_object.contact
        }

        self.update_attrs(data)
        db.session.flush()

        create_audit_context(self)
        self._clone_auditors(source_object)
        self.clone_custom_attribute_values(source_object)

    def _clone_auditors(self, audit):
        """Clone auditors of specified audit.

    Args:
      audit: Audit instance
    """
        from ggrc_basic_permissions.models import Role, UserRole

        role = Role.query.filter_by(name="Auditor").first()
        auditors = [
            ur.person
            for ur in UserRole.query.filter_by(role=role,
                                               context=audit.context).all()
        ]

        for auditor in auditors:
            user_role = UserRole(context=self.context,
                                 person=auditor,
                                 role=role)
            db.session.add(user_role)
        db.session.flush()

    def clone(self, source_id, mapped_objects=None):
        """Clone audit with specified whitelisted children.

    Children that can be cloned should be specified in CLONEABLE_CHILDREN.

    Args:
      mapped_objects: A list of related objects that should also be copied and
      linked to a new audit.
    """
        if not mapped_objects:
            mapped_objects = []

        source_object = Audit.query.get(source_id)
        self._clone(source_object)

        if any(mapped_objects):
            related_children = source_object.related_objects(mapped_objects)

            for obj in related_children:
                obj.clone(self)

    @classmethod
    def _filter_by_program(cls, predicate):
        return Program.query.filter((Program.id == Audit.program_id)
                                    & (predicate(Program.slug)
                                       | predicate(Program.title))).exists()

    @classmethod
    def _filter_by_auditor(cls, predicate):
        from ggrc_basic_permissions.models import Role, UserRole
        return UserRole.query.join(
            Role, Person).filter((Role.name == "Auditor")
                                 & (UserRole.context_id == cls.context_id)
                                 & (predicate(Person.name)
                                    | predicate(Person.email))).exists()

    @classmethod
    def eager_query(cls):
        from sqlalchemy import orm

        query = super(Audit, cls).eager_query()
        return query.options(
            orm.joinedload('program'),
            orm.subqueryload('object_people').joinedload('person'),
            orm.subqueryload('audit_objects'),
        )
Exemplo n.º 9
0
class SavedSearch(CreationTimeTracked, Dictable, Identifiable, db.Model):
    """
    Represents table which stores queary API filters for
    given user.
  """

    __tablename__ = "saved_searches"
    __table_args__ = (UniqueConstraint(
        "person_id",
        "name",
        name="unique_pair_saved_search_name_person_id",
    ), )

    ADVANCED_SEARCH = "AdvancedSearch"
    GLOBAL_SEARCH = "GlobalSearch"
    VALID_SAVED_SEARCH_TYPES = [ADVANCED_SEARCH, GLOBAL_SEARCH]

    name = db.Column(db.String, nullable=False)
    object_type = db.Column(db.String, nullable=False)
    person_id = db.Column(db.Integer, db.ForeignKey("people.id"))
    filters = db.Column(db.Text, nullable=True)
    search_type = db.Column(db.String, nullable=False)

    # pylint: disable-msg=too-many-arguments
    def __init__(self, name, object_type, user, search_type, filters=""):
        self.validate_name_uniqueness(user, name, search_type, object_type)

        super(SavedSearch, self).__init__(
            name=name,
            object_type=object_type,
            person_id=user.id,
            search_type=search_type,
            filters=filters,
        )

    @staticmethod
    def validate_name_uniqueness(user, name, search_type, object_type):
        """Check that for given user there are no saved searches with name."""
        if search_type == SavedSearch.GLOBAL_SEARCH:
            saved_seqrch_q = user.saved_searches.filter(
                SavedSearch.name == name,
                SavedSearch.search_type == search_type,
            )
            if db.session.query(saved_seqrch_q.exists()).scalar():
                raise ValidationError(
                    u"Global Saved search with name '{}' already exists".
                    format(name))
        else:
            saved_seqrch_q = user.saved_searches.filter(
                SavedSearch.name == name,
                SavedSearch.search_type == search_type,
                SavedSearch.object_type == object_type,
            )
            if db.session.query(saved_seqrch_q.exists()).scalar():
                raise ValidationError(u"Advanced Saved search for {} with "
                                      u"name '{}' already exists".format(
                                          object_type, name))

    @validates("name")
    def validate_name(self, _, name):
        """
      Validate that name is not blank.
    """
        # pylint: disable=no-self-use
        if not name:
            raise ValidationError("Saved search name can't be blank")

        return name

    @validates("object_type")
    def validate_object_type(self, _, object_type):
        """
      Validate that supplied object type supports search api filters saving.
    """
        # pylint: disable=no-self-use
        if object_type and object_type not in SUPPORTED_OBJECT_TYPES:
            raise ValidationError(
                u"Object of type '{}' does not support search saving".format(
                    object_type, ), )

        return object_type

    @validates("filters")
    def validate_filters(self, _, filters):
        """Validate correctness of supplied search filters.

    Validates that filters is valid json formatted string.

    Args:
      filters: string value with filters

    Returns:
      JSON object with filters

    """
        # pylint: disable=no-self-use
        if filters:
            return json.dumps(filters)
        return None

    @validates('search_type')
    def validate_search_type(self, _, saved_search_type):
        """Valid that saved search type is correct

    Args: Type of saved search. Valid values are AdvancedSearch and
    GlobalSearch

    Returns:
      Correct saved search type

    Raises:
      ValidationError: if saved_search_type is missing or not valid value
    """
        # pylint: disable=no-self-use
        if not saved_search_type:
            raise ValidationError("Saved search type can't be blank")

        if saved_search_type not in self.VALID_SAVED_SEARCH_TYPES:
            raise ValidationError("Invalid saved search type")

        return saved_search_type
Exemplo n.º 10
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],
          ["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.DateMultipleSubpropertyFullTextAttr(
          "task due date",
          "cycle_task_group_object_tasks",
          ["end_date"],
          False
      ),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "task assignees",
          "_task_assignees",
          ["name", "email"],
          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
      ),
  ]

  @property
  def _task_assignees(self):
    """Property. Return the list of persons as assignee of related tasks."""
    persons = {}
    for task in self.cycle_task_group_object_tasks:
      for person in task.get_persons_for_rolename("Task Assignees"):
        persons[person.id] = person
    return persons.values()

  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(
            "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"
        ),
    )

  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_widget")

  @property
  def cycle_inactive_url(self):
    return self._get_cycle_url("history_widget")
Exemplo n.º 11
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')
    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),
    ]

    # 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("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.subqueryload("cycle_task_group_tasks"),
            orm.joinedload("cycle").undefer_group("Cycle_complete"),
            orm.joinedload("cycle").joinedload("contact"))
Exemplo n.º 12
0
 def secondary_contact_id(cls):
   return deferred(
       db.Column(db.Integer, db.ForeignKey('people.id')), cls.__name__)
Exemplo n.º 13
0
 def context_id(cls):
   return db.Column(db.Integer, db.ForeignKey('contexts.id'))
Exemplo n.º 14
0
 def parent_id(cls):
   return deferred(db.Column(
       db.Integer, db.ForeignKey('{0}.id'.format(cls.__tablename__))),
       cls.__name__)
Exemplo n.º 15
0
class Snapshot(rest_handable.WithDeleteHandable, roleable.Roleable,
               relationship.Relatable,
               with_last_assessment_date.WithLastAssessmentDate,
               base.ContextRBAC, mixins.Base, db.Model):
    """Snapshot object that holds a join of parent object, revision, child object
  and parent object's context.

  Conceptual model is that we have a parent snapshotable object (e.g. Audit)
  which will not create relationships to objects with automapper at the time of
  creation but will instead create snapshots of those objects based on the
  latest revision of the object at the time of create / update of the object.
  Objects that were supposed to be mapped are called child objects.
  """
    __tablename__ = "snapshots"

    _api_attrs = reflection.ApiAttributes(
        "parent",
        "child_id",
        "child_type",
        reflection.Attribute("revision", create=False, update=False),
        reflection.Attribute("revision_id", create=False, update=False),
        reflection.Attribute("archived", create=False, update=False),
        reflection.Attribute("revisions", create=False, update=False),
        reflection.Attribute("is_latest_revision", create=False, update=False),
        reflection.Attribute("original_object_deleted",
                             create=False,
                             update=False),
        reflection.Attribute("update_revision", read=False),
    )

    _include_links = ["revision"]
    _aliases = {
        "attributes": "Attributes",
        "archived": "Archived",
        "mappings": {
            "display_name": "Mappings",
            "type": "mapping",
        }
    }

    parent_id = deferred(
        db.Column(db.Integer,
                  db.ForeignKey("audits.id", ondelete='CASCADE'),
                  nullable=False),
        "Snapshot",
    )
    parent_type = deferred(
        db.Column(db.String, nullable=False, default="Audit"),
        "Snapshot",
    )

    @orm.validates("parent_type")
    def validate_parent_type(self, _, value):
        """Validates parent_type equals 'Audit'"""
        # pylint: disable=no-self-use
        if value != "Audit":
            raise ValueError(
                "Wrong 'parent_type' value. Only 'Audit' supported")
        return value

    # Child ID and child type are data denormalisations - we could easily get
    # them from revision.content, but since that is a JSON field it will be
    # easier for development to just denormalise on write and not worry
    # about it.
    child_id = deferred(db.Column(db.Integer, nullable=False), "Snapshot")
    child_type = deferred(db.Column(db.String, nullable=False), "Snapshot")

    revision_id = deferred(
        db.Column(db.Integer, db.ForeignKey("revisions.id"), nullable=False),
        "Snapshot")
    revision = db.relationship("Revision", )
    _update_revision = None

    revisions = db.relationship(
        "Revision",
        primaryjoin="and_(Revision.resource_id == foreign(Snapshot.child_id),"
        "Revision.resource_type == foreign(Snapshot.child_type))",
        uselist=True,
    )

    @builder.simple_property
    def archived(self):
        return self.audit.archived if self.audit else False

    @builder.simple_property
    def is_latest_revision(self):
        """Flag if the snapshot has the latest revision."""
        return self.revisions and self.revision == self.revisions[-1]

    @builder.simple_property
    def original_object_deleted(self):
        """Flag if the snapshot has the latest revision."""
        return self.revisions and self.revisions[-1].action == "deleted"

    @classmethod
    def eager_query(cls, **kwargs):
        query = super(Snapshot, cls).eager_query(**kwargs)
        return cls.eager_inclusions(query, Snapshot._include_links).options(
            orm.subqueryload('revision'),
            orm.subqueryload('revisions'),
            orm.joinedload('audit').load_only("id", "archived"),
        )

    @hybrid_property
    def update_revision(self):
        return self.revision_id

    @update_revision.setter
    def update_revision(self, value):
        self._update_revision = value
        if value == "latest":
            _set_latest_revisions([self])

    @property
    def parent(self):
        return self.audit

    @parent.setter
    def parent(self, value):
        setattr(self, "audit", value)
        self.parent_type = "Audit"

    @staticmethod
    def _extra_table_args(_):
        return (
            db.UniqueConstraint("parent_type", "parent_id", "child_type",
                                "child_id"),
            db.Index("ix_snapshots_parent", "parent_type", "parent_id"),
            db.Index("ix_snapshots_child", "child_type", "child_id"),
        )

    def _check_related_objects(self):
        """Checks that Snapshot mapped only to Audits before deletion"""
        for obj in self.related_objects():
            if obj.type not in ("Audit", "Snapshot"):
                db.session.rollback()
                raise exceptions.Conflict(
                    description="Snapshot should be mapped "
                    "to Audit only before deletion")
            elif obj.type == "Snapshot":
                rel = relationship.Relationship
                related_originals = db.session.query(
                    rel.query.filter(
                        or_(
                            and_(rel.source_id == obj.child_id,
                                 rel.source_type == obj.child_type,
                                 rel.destination_id == self.child_id,
                                 rel.destination_type == self.child_type),
                            and_(rel.destination_id == obj.child_id,
                                 rel.destination_type == obj.child_type,
                                 rel.source_id == self.child_id,
                                 rel.source_type ==
                                 self.child_type))).exists()).scalar()
                if related_originals:
                    db.session.rollback()
                    raise exceptions.Conflict(
                        description="Snapshot should be mapped to "
                        "Audit only before deletion")

    def handle_delete(self):
        """Handle model_deleted signal for Snapshot"""
        self._check_related_objects()
Exemplo n.º 16
0
class Control(WithLastAssessmentDate,
              review.Reviewable,
              Roleable,
              Relatable,
              mixins.CustomAttributable,
              Personable,
              ControlCategorized,
              PublicDocumentable,
              AssertionCategorized,
              mixins.LastDeprecatedTimeboxed,
              mixins.TestPlanned,
              Commentable,
              WithSimilarityScore,
              base.ContextRBAC,
              mixins.BusinessObject,
              Indexed,
              mixins.Folderable,
              proposal.Proposalable,
              db.Model):
  """Control model definition."""
  __tablename__ = 'controls'

  company_control = deferred(db.Column(db.Boolean), 'Control')
  directive_id = deferred(
      db.Column(db.Integer, db.ForeignKey('directives.id')), 'Control')
  kind_id = deferred(db.Column(db.Integer), 'Control')
  means_id = deferred(db.Column(db.Integer), 'Control')
  version = deferred(db.Column(db.String), 'Control')
  verify_frequency_id = deferred(db.Column(db.Integer), 'Control')
  fraud_related = deferred(db.Column(db.Boolean), 'Control')
  key_control = deferred(db.Column(db.Boolean), 'Control')
  active = deferred(db.Column(db.Boolean), 'Control')

  kind = db.relationship(
      'Option',
      primaryjoin='and_(foreign(Control.kind_id) == Option.id, '
                  'Option.role == "control_kind")',
      uselist=False)
  means = db.relationship(
      'Option',
      primaryjoin='and_(foreign(Control.means_id) == Option.id, '
                  'Option.role == "control_means")',
      uselist=False)
  verify_frequency = db.relationship(
      'Option',
      primaryjoin='and_(foreign(Control.verify_frequency_id) == Option.id, '
                  'Option.role == "verify_frequency")',
      uselist=False)

  # REST properties
  _api_attrs = reflection.ApiAttributes(
      'active',
      'company_control',
      'directive',
      'fraud_related',
      'key_control',
      'kind',
      'means',
      'verify_frequency',
      'version',
  )

  _fulltext_attrs = [
      'active',
      'company_control',
      'directive',
      attributes.BooleanFullTextAttr(
          'fraud_related',
          'fraud_related',
          true_value="yes", false_value="no"),
      attributes.BooleanFullTextAttr(
          'key_control',
          'key_control',
          true_value="key", false_value="non-key"),
      'kind',
      'means',
      'verify_frequency',
      'version',
  ]

  _sanitize_html = [
      'version',
  ]

  VALID_RECIPIENTS = frozenset([
      "Assignees",
      "Creators",
      "Verifiers",
      "Admin",
      "Control Operators",
      "Control Owners",
      "Other Contacts",
  ])

  @classmethod
  def indexed_query(cls):
    return super(Control, cls).indexed_query().options(
        orm.Load(cls).undefer_group(
            "Control_complete"
        ),
        orm.Load(cls).joinedload(
            "directive"
        ).undefer_group(
            "Directive_complete"
        ),
        orm.Load(cls).joinedload(
            'kind',
        ).load_only(
            "title"
        ),
        orm.Load(cls).joinedload(
            'means',
        ).load_only(
            "title"
        ),
        orm.Load(cls).joinedload(
            'verify_frequency',
        ).load_only(
            "title"
        ),
    )

  _include_links = []

  _aliases = {
      "kind": "Kind/Nature",
      "means": "Type/Means",
      "verify_frequency": "Frequency",
      "fraud_related": "Fraud Related",
      "key_control": {
          "display_name": "Significance",
          "description": "Allowed values are:\nkey\nnon-key\n---",
      },
      "test_plan": "Assessment Procedure",
  }

  @validates('kind', 'means', 'verify_frequency')
  def validate_control_options(self, key, option):
    """Validate control 'kind', 'means', 'verify_frequency'"""
    desired_role = key if key == 'verify_frequency' else 'control_' + key
    return validate_option(self.__class__.__name__, key, option, desired_role)

  @classmethod
  def eager_query(cls):
    query = super(Control, cls).eager_query()
    return cls.eager_inclusions(query, Control._include_links).options(
        orm.joinedload('directive'),
        orm.joinedload('kind'),
        orm.joinedload('means'),
        orm.joinedload('verify_frequency'),
    )

  def log_json(self):
    out_json = super(Control, self).log_json()
    # so that event log can refer to deleted directive
    if self.directive:
      out_json["mapped_directive"] = self.directive.display_name
    return out_json
Exemplo n.º 17
0
 def context_id(cls):  # pylint: disable=no-self-argument
     return db.Column(db.Integer, db.ForeignKey('contexts.id'))
Exemplo n.º 18
0
class Revision(Base, db.Model):
    """Revision object holds a JSON snapshot of the object at a time."""

    __tablename__ = 'revisions'

    resource_id = db.Column(db.Integer, nullable=False)
    resource_type = db.Column(db.String, nullable=False)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.id'),
                         nullable=False)
    action = db.Column(db.Enum(u'created', u'modified', u'deleted'),
                       nullable=False)
    _content = db.Column('content', LongJsonType, nullable=False)

    resource_slug = db.Column(db.String, nullable=True)
    source_type = db.Column(db.String, nullable=True)
    source_id = db.Column(db.Integer, nullable=True)
    destination_type = db.Column(db.String, nullable=True)
    destination_id = db.Column(db.Integer, nullable=True)

    @staticmethod
    def _extra_table_args(_):
        return (
            db.Index("revisions_modified_by", "modified_by_id"),
            db.Index("fk_revisions_resource", "resource_type", "resource_id"),
            db.Index("fk_revisions_source", "source_type", "source_id"),
            db.Index("fk_revisions_destination", "destination_type",
                     "destination_id"),
            db.Index('ix_revisions_resource_slug', 'resource_slug'),
        )

    _api_attrs = reflection.ApiAttributes(
        'resource_id',
        'resource_type',
        'source_type',
        'source_id',
        'destination_type',
        'destination_id',
        'action',
        'content',
        'description',
    )

    @classmethod
    def eager_query(cls):
        from sqlalchemy import orm

        query = super(Revision, cls).eager_query()
        return query.options(
            orm.subqueryload('modified_by'),
            orm.subqueryload('event'),  # used in description
        )

    def __init__(self, obj, modified_by_id, action, content):
        self.resource_id = obj.id
        self.resource_type = obj.__class__.__name__
        self.resource_slug = getattr(obj, "slug", None)
        self.modified_by_id = modified_by_id
        self.action = action
        if "access_control_list" in content and content["access_control_list"]:
            for acl in content["access_control_list"]:
                acl["person"] = {
                    "id": acl["person_id"],
                    "type": "Person",
                    "href": "/api/people/{}".format(acl["person_id"]),
                }

        self._content = content

        for attr in [
                "source_type", "source_id", "destination_type",
                "destination_id"
        ]:
            setattr(self, attr, getattr(obj, attr, None))

    @builder.simple_property
    def description(self):
        """Compute a human readable description from action and content."""
        if 'display_name' not in self._content:
            return ''
        display_name = self._content['display_name']
        if not display_name:
            result = u"{0} {1}".format(self.resource_type, self.action)
        elif u'<->' in display_name:
            if self.action == 'created':
                msg = u"{destination} linked to {source}"
            elif self.action == 'deleted':
                msg = u"{destination} unlinked from {source}"
            else:
                msg = u"{display_name} {action}"
            source, destination = self._content['display_name'].split(
                '<->')[:2]
            result = msg.format(source=source,
                                destination=destination,
                                display_name=self._content['display_name'],
                                action=self.action)
        elif 'mapped_directive' in self._content:
            # then this is a special case of combined map/creation
            # should happen only for Section and Control
            mapped_directive = self._content['mapped_directive']
            if self.action == 'created':
                result = u"New {0}, {1}, created and mapped to {2}".format(
                    self.resource_type, display_name, mapped_directive)
            elif self.action == 'deleted':
                result = u"{0} unmapped from {1} and deleted".format(
                    display_name, mapped_directive)
            else:
                result = u"{0} {1}".format(display_name, self.action)
        else:
            # otherwise, it's a normal creation event
            result = u"{0} {1}".format(display_name, self.action)
        if self.event.action == "BULK":
            result += ", via bulk action"
        return result

    @builder.simple_property
    def content(self):
        """Property. Contains the revision content dict.

    Updated by required values, generated from saved content dict."""
        # pylint: disable=too-many-locals
        roles_dict = role.get_custom_roles_for(self.resource_type)
        reverted_roles_dict = {n: i for i, n in roles_dict.iteritems()}
        access_control_list = self._content.get("access_control_list") or []
        map_field_to_role = {
            "principal_assessor":
            reverted_roles_dict.get("Principal Assignees"),
            "secondary_assessor":
            reverted_roles_dict.get("Secondary Assignees"),
            "contact": reverted_roles_dict.get("Primary Contacts"),
            "secondary_contact": reverted_roles_dict.get("Secondary Contacts"),
            "owners": reverted_roles_dict.get("Admin"),
        }
        exists_roles = {i["ac_role_id"] for i in access_control_list}
        for field, role_id in map_field_to_role.items():
            if field not in self._content:
                continue
            if role_id in exists_roles or role_id is None:
                continue
            field_content = self._content.get(field) or {}
            if not field_content:
                continue
            if not isinstance(field_content, list):
                field_content = [field_content]
            person_ids = {fc.get("id") for fc in field_content if fc.get("id")}
            for person_id in person_ids:
                access_control_list.append({
                    "display_name": roles_dict[role_id],
                    "ac_role_id": role_id,
                    "context_id": None,
                    "created_at": None,
                    "object_type": self.resource_type,
                    "updated_at": None,
                    "object_id": self.resource_id,
                    "modified_by_id": None,
                    "person_id": person_id,
                    # Frontend require data in such format
                    "person": {
                        "id": person_id,
                        "type": "Person",
                        "href": "/api/people/{}".format(person_id)
                    },
                    "modified_by": None,
                    "id": None,
                })
        populated_content = self._content.copy()

        # Add person with id and type for old snapshots compatibility
        for acl in access_control_list:
            if "person" not in acl:
                acl["person"] = {"id": acl.get("person_id"), "type": "Person"}
        populated_content["access_control_list"] = access_control_list

        if 'url' in self._content:
            reference_url_list = []
            for key in ('url', 'reference_url'):
                link = self._content[key]
                # link might exist, but can be an empty string - we treat those values
                # as non-existing (empty) reference URLs
                if not link:
                    continue

                # if creation/modification date is not available, we estimate it by
                # using the corresponding information from the Revision itself
                created_at = (self._content.get("created_at")
                              or self.created_at.isoformat())
                updated_at = (self._content.get("updated_at")
                              or self.updated_at.isoformat())

                reference_url_list.append({
                    "display_name": link,
                    "document_type": "REFERENCE_URL",
                    "link": link,
                    "title": link,
                    "id": None,
                    "created_at": created_at,
                    "updated_at": updated_at,
                })
            populated_content['reference_url'] = reference_url_list

        return populated_content

    @content.setter
    def content(self, value):
        """ Setter for content property."""
        self._content = value
Exemplo n.º 19
0
class AccessControlRole(attributevalidator.AttributeValidator,
                        base.ContextRBAC, mixins.Base, db.Model):
  """Access Control Role

  Model holds all roles in the application. These roles can be added
  by the users.
  """
  __tablename__ = 'access_control_roles'

  name = db.Column(db.String, nullable=False)
  object_type = db.Column(db.String)
  tooltip = db.Column(db.String)

  read = db.Column(db.Boolean, nullable=False, default=True)
  update = db.Column(db.Boolean, nullable=False, default=True)
  delete = db.Column(db.Boolean, nullable=False, default=True)
  my_work = db.Column(db.Boolean, nullable=False, default=True)
  mandatory = db.Column(db.Boolean, nullable=False, default=False)
  non_editable = db.Column(db.Boolean, nullable=False, default=False)
  internal = db.Column(db.Boolean, nullable=False, default=False)
  default_to_current_user = db.Column(
      db.Boolean, nullable=False, default=False)
  notify_about_proposal = db.Column(db.Boolean, nullable=False, default=False)
  notify_about_review_status = db.Column(db.Boolean,
                                         nullable=False, default=False)

  access_control_list = db.relationship(
      'AccessControlList', backref='ac_role', cascade='all, delete-orphan')

  parent_id = db.Column(
      db.Integer,
      db.ForeignKey('access_control_roles.id', ondelete='CASCADE'),
      nullable=True,
  )
  parent = db.relationship(
      # pylint: disable=undefined-variable
      lambda: AccessControlRole,
      remote_side=lambda: AccessControlRole.id
  )

  _reserved_names = {}

  @staticmethod
  def _extra_table_args(_):
    return (
        db.UniqueConstraint('name', 'object_type'),
    )

  @classmethod
  def eager_query(cls):
    """Define fields to be loaded eagerly to lower the count of DB queries."""
    return super(AccessControlRole, cls).eager_query()

  _api_attrs = reflection.ApiAttributes(
      "name",
      "object_type",
      "tooltip",
      "read",
      "update",
      "delete",
      "my_work",
      "mandatory",
      "default_to_current_user",
      reflection.Attribute("non_editable", create=False, update=False),
  )

  @sa.orm.validates("name", "object_type")
  def validates_name(self, key, value):  # pylint: disable=no-self-use
    """Validate Custom Role name uniquness.

    Custom Role names need to follow 2 uniqueness rules:
      1) Names must not match any attribute name on any existing object.
      2) Object level CAD names must not match any global CAD name.

    This validator should check for name collisions for 1st and 2nd rule.

    This validator works, because object_type is never changed. It only
    gets set when the role is created and after that only name filed can
    change. This makes validation using both fields possible.

    Args:
      value: access control role name

    Returns:
      value if the name passes all uniqueness checks.
    """
    value = value.strip()
    if key == "name" and self.object_type:
      name = value
      object_type = self.object_type
    elif key == "object_type" and self.name:
      name = self.name.strip()
      object_type = value
    else:
      return value

    if name in self._get_reserved_names(object_type):
      raise ValueError(u"Attribute name '{}' is reserved for this object type."
                       .format(name))

    if self._get_global_cad_names(object_type).get(name) is not None:
      raise ValueError(u"Global custom attribute '{}' "
                       u"already exists for this object type"
                       .format(name))
    return value
Exemplo n.º 20
0
class UserRole(rest_handable.WithDeleteHandable,
               rest_handable.WithPostHandable, base.ContextRBAC, Base,
               db.Model):
    """`UserRole` model represents mapping between `User` and `Role` models."""

    __tablename__ = 'user_roles'

    # Override default from `ContextRBAC` to provide backref
    context = db.relationship('Context', backref='user_roles')

    role_id = db.Column(db.Integer(),
                        db.ForeignKey('roles.id'),
                        nullable=False)
    role = db.relationship('Role',
                           backref=backref('user_roles',
                                           cascade='all, delete-orphan'))
    person_id = db.Column(db.Integer(),
                          db.ForeignKey('people.id'),
                          nullable=False)
    person = db.relationship('Person',
                             backref=backref('user_roles',
                                             cascade='all, delete-orphan'))

    @staticmethod
    def _extra_table_args(model):
        return (db.UniqueConstraint('person_id',
                                    name='uq_{}'.format(model.__tablename__)),
                db.Index('ix_user_roles_person', 'person_id'))

    _api_attrs = reflection.ApiAttributes('role', 'person')

    @classmethod
    def role_assignments_for(cls, context):
        context_id = context.id if type(context) is Context else context
        all_assignments = db.session.query(UserRole)\
            .filter(UserRole.context_id == context_id)
        assignments_by_user = {}
        for assignment in all_assignments:
            assignments_by_user.setdefault(assignment.person.email, [])\
                .append(assignment.role)
        return assignments_by_user

    @classmethod
    def eager_query(cls, **kwargs):
        from sqlalchemy import orm

        query = super(UserRole, cls).eager_query(**kwargs)
        return query.options(orm.joinedload('role'),
                             orm.subqueryload('person'),
                             orm.subqueryload('context'))

    def _display_name(self):
        if self.context and self.context.related_object_type and \
           self.context.related_object:
            context_related = ' in ' + self.context.related_object.display_name
        elif hasattr(self, '_display_related_title'):
            context_related = ' in ' + self._display_related_title
        elif self.context:
            logger.warning('Unable to identify context.related for UserRole')
            context_related = ''
        else:
            context_related = ''
        return u'{0} <-> {1}{2}'.format(self.person.display_name,
                                        self.role.display_name,
                                        context_related)

    def _recalculate_permissions_cache(self):
        """Recalculate permissions cache for user `UserRole` relates to."""
        with utils.benchmark(
                "Invalidate permissions cache for user in UserRole"):
            from ggrc_basic_permissions import load_permissions_for
            load_permissions_for(self.person, expire_old=True)

    def handle_delete(self):
        """Handle `model_deleted` signals invoked on `UserRole` instance.

    HTTP DELTE method on `UserRole` model triggers following actions:
      - Recalculate permissions cache for user the `UserRole` object is
        related to.
    """
        self._recalculate_permissions_cache()

    def handle_post(self):
        """Handle `model_posted` signals invoked on `UserRole` instance.

    HTTP POST method on `UserRole` model triggers following actions:
      - Recalculate permissions cache for user the `UserRole` object is
        related to.
    """
        self._recalculate_permissions_cache()
Exemplo n.º 21
0
 def created_by_id(cls):  # pylint: disable=no-self-argument
     return db.Column(db.Integer, db.ForeignKey('people.id'), nullable=True)
Exemplo n.º 22
0
class TaskGroupTask(WithContact, Titled, Described, RelativeTimeboxed, Slugged,
                    Indexed, db.Model):
    """Workflow TaskGroupTask model."""

    __tablename__ = 'task_group_tasks'
    _extra_table_args = (schema.CheckConstraint('start_date <= end_date'), )
    _title_uniqueness = False
    _start_changed = False

    @classmethod
    def default_task_type(cls):
        return "text"

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

    task_group_id = db.Column(
        db.Integer,
        db.ForeignKey('task_groups.id', ondelete="CASCADE"),
        nullable=False,
    )
    sort_index = db.Column(db.String(length=250), default="", nullable=False)

    object_approval = db.Column(db.Boolean, nullable=False, default=False)

    task_type = db.Column(db.String(length=250),
                          default=default_task_type,
                          nullable=False)

    response_options = db.Column(JsonType(), nullable=False, default=[])

    VALID_TASK_TYPES = ['text', 'menu', 'checkbox']

    @orm.validates('task_type')
    def validate_task_type(self, key, value):
        # pylint: disable=unused-argument
        if value is None:
            value = self.default_task_type()
        if value not in self.VALID_TASK_TYPES:
            raise ValueError(u"Invalid type '{}'".format(value))
        return value

    def validate_date(self, value):
        if isinstance(value, datetime):
            value = value.date()
        if value is not None and value.year <= 1900:
            current_century = date.today().year / 100 * 100
            year = current_century + value.year % 100
            return date(year, value.month, value.day)
        return value

    @orm.validates("start_date", "end_date")
    def validate_end_date(self, key, value):
        value = self.validate_date(value)
        if key == "start_date":
            self._start_changed = True
        if key == "end_date" and self._start_changed and self.start_date > value:
            self._start_changed = False
            raise ValueError("Start date can not be after end date.")
        return value

    _api_attrs = reflection.ApiAttributes(
        'task_group', 'sort_index', 'relative_start_month',
        'relative_start_day', 'relative_end_month', 'relative_end_day',
        'object_approval', 'task_type', 'response_options')
    _sanitize_html = []
    _aliases = {
        "title": "Summary",
        "description": {
            "display_name": "Task Description",
            "handler_key": "task_description",
        },
        "contact": {
            "display_name": "Assignee",
            "mandatory": True,
        },
        "secondary_contact": None,
        "start_date": None,
        "end_date": None,
        "task_group": {
            "display_name": "Task Group",
            "mandatory": True,
            "filter_by": "_filter_by_task_group",
        },
        "relative_start_date": {
            "display_name":
            "Start",
            "mandatory":
            True,
            "description":
            ("Enter the task start date in the following format:\n"
             "'mm/dd/yyyy' for one time workflows\n"
             "'#' for weekly workflows (where # represents day "
             "of the week & Monday = day 1)\n"
             "'dd' for monthly workflows\n"
             "'mmm/mmm/mmm/mmm dd' for monthly workflows "
             "e.g. feb/may/aug/nov 17\n"
             "'mm/dd' for yearly workflows"),
        },
        "relative_end_date": {
            "display_name":
            "End",
            "mandatory":
            True,
            "description":
            ("Enter the task end date in the following format:\n"
             "'mm/dd/yyyy' for one time workflows\n"
             "'#' for weekly workflows (where # represents day "
             "of the week & Monday = day 1)\n"
             "'dd' for monthly workflows\n"
             "'mmm/mmm/mmm/mmm dd' for monthly workflows "
             "e.g. feb/may/aug/nov 17\n"
             "'mm/dd' for yearly workflows"),
        },
        "task_type": {
            "display_name":
            "Task Type",
            "mandatory":
            True,
            "description": ("Accepted values are:"
                            "\n'Rich Text'\n'Dropdown'\n'Checkbox'"),
        }
    }

    @classmethod
    def _filter_by_task_group(cls, predicate):
        return TaskGroup.query.filter((TaskGroup.id == cls.task_group_id) & (
            predicate(TaskGroup.slug) | predicate(TaskGroup.title))).exists()

    @classmethod
    def eager_query(cls):
        query = super(TaskGroupTask, cls).eager_query()
        return query.options(orm.subqueryload('task_group'), )

    def _display_name(self):
        return self.title + '<->' + self.task_group.display_name

    def copy(self, _other=None, **kwargs):
        columns = [
            'title',
            'description',
            'task_group',
            'sort_index',
            'relative_start_month',
            'relative_start_day',
            'relative_end_month',
            'relative_end_day',
            'start_date',
            'end_date',
            'contact',
            'modified_by',
            'task_type',
            'response_options',
        ]

        contact = None
        if kwargs.get('clone_people', False):
            contact = self.contact
        else:
            contact = get_current_user()

        kwargs['modified_by'] = get_current_user()

        target = self.copy_into(_other, columns, contact=contact, **kwargs)
        return target
Exemplo n.º 23
0
class Assessment(Roleable, statusable.Statusable, AuditRelationship,
                 AutoStatusChangeable, Assignable, HasObjectState, TestPlanned,
                 CustomAttributable, PublicDocumentable, Commentable,
                 Personable, reminderable.Reminderable, Timeboxed, Relatable,
                 WithSimilarityScore, FinishedDate, VerifiedDate,
                 ValidateOnComplete, Notifiable, WithAction, BusinessObject,
                 labeled.Labeled, 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)

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

    class Labels(object):  # pylint: disable=too-few-public-methods
        """Choices for label enum."""
        AUDITOR_PULLS_EVIDENCE = u'Auditor pulls evidence'
        FOLLOWUP = u'Followup'
        NEEDS_REWORK = u'Needs Rework'
        NEEDS_DISCUSSION = u'Needs Discussion'

    POSSIBLE_LABELS = [
        Labels.AUDITOR_PULLS_EVIDENCE, Labels.FOLLOWUP, Labels.NEEDS_REWORK,
        Labels.NEEDS_DISCUSSION
    ]

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

    design = deferred(db.Column(db.String, 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")

    @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',
        reflection.Attribute('archived', create=False, update=False),
        reflection.Attribute('object', create=False, update=False),
    )

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

    @classmethod
    def indexed_query(cls):
        query = super(Assessment, cls).indexed_query()
        return query.options(
            orm.Load(cls).undefer_group("Assessment_complete", ),
            orm.Load(cls).joinedload("audit").undefer_group(
                "Audit_complete", ),
        )

    _tracked_attrs = {
        'description', 'design', 'notes', 'operationally', 'test_plan',
        'title', 'start_date', 'end_date'
    }

    _aliases = {
        "owners": None,
        "assessment_template": {
            "display_name": "Template",
            "ignore_on_update": True,
            "filter_by": "_ignore_filter",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "assessment_type": {
            "display_name": "Assessment Type",
            "mandatory": False,
        },
        "design": "Conclusion: Design",
        "operationally": "Conclusion: Operation",
        "related_creators": {
            "display_name": "Creators",
            "mandatory": True,
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_assessors": {
            "display_name": "Assignees",
            "mandatory": True,
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_verifiers": {
            "display_name": "Verifiers",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "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))
        },
    }

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

    similarity_options = {
        "relevant_types": {
            "Objective": {
                "weight": 2
            },
            "Control": {
                "weight": 2
            },
        },
        "threshold": 1,
    }

    @simple_property
    def archived(self):
        return self.audit.archived if self.audit else False

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

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

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

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

    @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]
            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):
        # pylint: disable=unused-argument
        return self.validate_conclusion(value)

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

    @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
Exemplo n.º 24
0
class Revision(before_flush_handleable.BeforeFlushHandleable,
               synchronizable.ChangesSynchronized, filterable.Filterable,
               base.ContextRBAC, mixins.Base, db.Model):
    """Revision object holds a JSON snapshot of the object at a time."""

    __tablename__ = 'revisions'

    resource_id = db.Column(db.Integer, nullable=False)
    resource_type = db.Column(db.String, nullable=False)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.id'),
                         nullable=False)
    action = db.Column(db.Enum(u'created', u'modified', u'deleted'),
                       nullable=False)
    _content = db.Column('content', types.LongJsonType, nullable=False)

    resource_slug = db.Column(db.String, nullable=True)
    source_type = db.Column(db.String, nullable=True)
    source_id = db.Column(db.Integer, nullable=True)
    destination_type = db.Column(db.String, nullable=True)
    destination_id = db.Column(db.Integer, nullable=True)

    is_empty = db.Column(db.Boolean, nullable=False, default=False)

    @staticmethod
    def _extra_table_args(_):
        return (
            db.Index("revisions_modified_by", "modified_by_id"),
            db.Index("ix_revisions_resource_action", "resource_type",
                     "resource_id", "action"),
            db.Index("fk_revisions_source", "source_type", "source_id"),
            db.Index("fk_revisions_destination", "destination_type",
                     "destination_id"),
            db.Index('ix_revisions_resource_slug', 'resource_slug'),
        )

    _api_attrs = reflection.ApiAttributes(
        'resource_id',
        'resource_type',
        'source_type',
        'source_id',
        'destination_type',
        'destination_id',
        'action',
        'content',
        'description',
        reflection.Attribute('diff_with_current', create=False, update=False),
        reflection.Attribute('meta', create=False, update=False),
    )

    _filterable_attrs = [
        'action',
        'resource_id',
        'resource_type',
        'source_type',
        'source_id',
        'destination_type',
        'destination_id',
    ]

    @classmethod
    def eager_query(cls, **kwargs):
        from sqlalchemy import orm

        query = super(Revision, cls).eager_query(**kwargs)
        return query.options(
            orm.subqueryload('modified_by'),
            orm.subqueryload('event'),  # used in description
        )

    def __init__(self, obj, modified_by_id, action, content):
        self.resource_id = obj.id
        self.resource_type = obj.__class__.__name__
        self.resource_slug = getattr(obj, "slug", None)
        self.modified_by_id = modified_by_id
        self.action = action
        if "access_control_list" in content and content["access_control_list"]:
            for acl in content["access_control_list"]:
                acl["person"] = {
                    "id": acl["person_id"],
                    "type": "Person",
                    "href": "/api/people/{}".format(acl["person_id"]),
                }

        self._content = content

        for attr in [
                "source_type", "source_id", "destination_type",
                "destination_id"
        ]:
            setattr(self, attr, getattr(obj, attr, None))

    @builder.callable_property
    def diff_with_current(self):
        """Callable lazy property for revision."""
        referenced_objects.mark_to_cache(self.resource_type, self.resource_id)
        revisions_diff.mark_for_latest_content(self.resource_type,
                                               self.resource_id)

        def lazy_loader():
            """Lazy load diff for revisions."""
            referenced_objects.rewarm_cache()
            revisions_diff.rewarm_latest_content()
            instance = referenced_objects.get(self.resource_type,
                                              self.resource_id)
            if instance:
                return revisions_diff.prepare(instance, self.content)
            # return empty diff object has already been removed
            return {}

        return lazy_loader

    @builder.callable_property
    def meta(self):
        """Callable lazy property for revision."""
        referenced_objects.mark_to_cache(self.resource_type, self.resource_id)

        def lazy_loader():
            """Lazy load diff for revisions."""
            referenced_objects.rewarm_cache()
            instance = referenced_objects.get(self.resource_type,
                                              self.resource_id)
            meta_dict = {}
            if instance:
                instance_meta_info = meta_info.MetaInfo(instance)
                meta_dict["mandatory"] = instance_meta_info.mandatory
            return meta_dict

        return lazy_loader

    @builder.simple_property
    def description(self):
        """Compute a human readable description from action and content."""
        if 'display_name' not in self._content:
            return ''
        display_name = self._content['display_name']
        if not display_name:
            result = u"{0} {1}".format(self.resource_type, self.action)
        elif u'<->' in display_name:
            if self.action == 'created':
                msg = u"{destination} linked to {source}"
            elif self.action == 'deleted':
                msg = u"{destination} unlinked from {source}"
            else:
                msg = u"{display_name} {action}"
            source, destination = self._content['display_name'].split(
                '<->')[:2]
            result = msg.format(source=source,
                                destination=destination,
                                display_name=self._content['display_name'],
                                action=self.action)
        elif 'mapped_directive' in self._content:
            # then this is a special case of combined map/creation
            # should happen only for Requirement and Control
            mapped_directive = self._content['mapped_directive']
            if self.action == 'created':
                result = u"New {0}, {1}, created and mapped to {2}".format(
                    self.resource_type, display_name, mapped_directive)
            elif self.action == 'deleted':
                result = u"{0} unmapped from {1} and deleted".format(
                    display_name, mapped_directive)
            else:
                result = u"{0} {1}".format(display_name, self.action)
        else:
            # otherwise, it's a normal creation event
            result = u"{0} {1}".format(display_name, self.action)
        if self.event.action == "BULK":
            result += ", via bulk action"
        return result

    def populate_reference_url(self):
        """Add reference_url info for older revisions."""
        if 'url' not in self._content:
            return {}
        reference_url_list = []
        for key in ('url', 'reference_url'):
            link = self._content[key]
            # link might exist, but can be an empty string - we treat those values
            # as non-existing (empty) reference URLs
            if not link:
                continue

            # if creation/modification date is not available, we estimate it by
            # using the corresponding information from the Revision itself
            created_at = (self._content.get("created_at")
                          or self.created_at.isoformat())
            updated_at = (self._content.get("updated_at")
                          or self.updated_at.isoformat())

            reference_url_list.append({
                "display_name": link,
                "kind": "REFERENCE_URL",
                "link": link,
                "title": link,
                "id": None,
                "created_at": created_at,
                "updated_at": updated_at,
            })
        return {'reference_url': reference_url_list}

    @classmethod
    def _filter_internal_acls(cls, access_control_list):
        """Remove internal access control list entries.

    This is needed due to bugs in older code that in some cases the revisions
    stored internal ACL entries.
    Due to possible role removal, the parent_id is the only true flag that we
    can use for filtering

    Args:
      access_control_list: list of dicts containing ACL entries.

    Returns:
      access_control_list but without any ACL entry that was generated from
        some other ACL entry.
    """
        return [
            acl for acl in access_control_list if acl.get("parent_id") is None
        ]

    @classmethod
    def _populate_acl_with_people(cls, access_control_list):
        """Add person property with person stub on access control list."""
        for acl in access_control_list:
            if "person" not in acl:
                acl["person"] = {"id": acl.get("person_id"), "type": "Person"}
        return access_control_list

    def populate_acl(self):
        """Add access_control_list info for older revisions."""
        # pylint: disable=too-many-locals
        roles_dict = role.get_custom_roles_for(self.resource_type)
        reverted_roles_dict = {n: i for i, n in roles_dict.iteritems()}
        access_control_list = self._content.get("access_control_list") or []
        map_field_to_role = {
            "principal_assessor":
            reverted_roles_dict.get("Principal Assignees"),
            "secondary_assessor":
            reverted_roles_dict.get("Secondary Assignees"),
            "contact": reverted_roles_dict.get("Primary Contacts"),
            "secondary_contact": reverted_roles_dict.get("Secondary Contacts"),
            "owners": reverted_roles_dict.get("Admin"),
        }

        is_control = bool(self.resource_type == "Control")
        is_control_snapshot = bool(
            self.resource_type == "Snapshot"
            and self._content["child_type"] == "Control")
        # for Control type we do not have Primary and Secondary Contacts roles.
        if is_control or is_control_snapshot:
            map_field_to_role.update({
                "contact":
                reverted_roles_dict.get("Control Operators"),
                "secondary_contact":
                reverted_roles_dict.get("Control Owners")
            })

        exists_roles = {i["ac_role_id"] for i in access_control_list}

        for field, role_id in map_field_to_role.items():
            if role_id in exists_roles or role_id is None:
                continue
            if field not in self._content:
                continue
            field_content = self._content.get(field) or {}
            if not field_content:
                continue
            if not isinstance(field_content, list):
                field_content = [field_content]
            person_ids = {fc.get("id") for fc in field_content if fc.get("id")}
            for person_id in person_ids:
                access_control_list.append({
                    "display_name": roles_dict[role_id],
                    "ac_role_id": role_id,
                    "context_id": None,
                    "created_at": None,
                    "object_type": self.resource_type,
                    "updated_at": None,
                    "object_id": self.resource_id,
                    "modified_by_id": None,
                    "person_id": person_id,
                    # Frontend require data in such format
                    "person": {
                        "id": person_id,
                        "type": "Person",
                        "href": "/api/people/{}".format(person_id)
                    },
                    "modified_by": None,
                    "id": None,
                })

        acl_with_people = self._populate_acl_with_people(access_control_list)
        filtered_acl = self._filter_internal_acls(acl_with_people)
        result_acl = [
            acl for acl in filtered_acl if acl["ac_role_id"] in roles_dict
        ]
        return {
            "access_control_list": result_acl,
        }

    def populate_folder(self):
        """Add folder info for older revisions."""
        if "folder" in self._content:
            return {}
        folders = self._content.get("folders") or [{"id": ""}]
        return {"folder": folders[0]["id"]}

    def populate_labels(self):
        """Add labels info for older revisions."""
        if "label" not in self._content:
            return {}
        label = self._content["label"]
        return {
            "labels": [{
                "id": None,
                "name": label
            }]
        } if label else {
            "labels": []
        }

    def populate_status(self):
        """Update status for older revisions or add it if status does not exist."""
        workflow_models = {
            "Cycle",
            "CycleTaskGroup",
            "CycleTaskGroupObjectTask",
        }
        statuses_mapping = {"InProgress": "In Progress"}
        status = statuses_mapping.get(self._content.get("status"))
        if self.resource_type in workflow_models and status:
            return {"status": status}

        pop_models = {
            # ggrc
            "AccessGroup",
            "AccountBalance",
            "Control",
            "DataAsset",
            "Directive",
            "Facility",
            "Issue",
            "KeyReport",
            "Market",
            "Objective",
            "OrgGroup",
            "Product",
            "Program",
            "Project",
            "Requirement",
            "System",
            "Vendor",
            "Risk",
            "Threat",
        }
        if self.resource_type not in pop_models:
            return {}
        statuses_mapping = {
            "Active": "Active",
            "Deprecated": "Deprecated",
            "Effective": "Active",
            "Final": "Active",
            "In Scope": "Active",
            "Ineffective": "Active",
            "Launched": "Active",
        }
        return {
            "status": statuses_mapping.get(self._content.get("status"),
                                           "Draft")
        }

    def populate_review_status(self):
        """Replace os_state with review state for old revisions"""
        from ggrc.models import review
        result = {}
        if "os_state" in self._content:
            if self._content["os_state"] is not None:
                result["review_status"] = self._content["os_state"]
            else:
                result["review_status"] = review.Review.STATES.UNREVIEWED
        return result

    def populate_review_status_display_name(self, result):
        """Get review_status if review_status_display_name is not found"""
        # pylint: disable=invalid-name

        if self.resource_type != "Control":
            return

        if "review_status_display_name" in self._content:
            result["review_status_display_name"] = self._content[
                "review_status_display_name"]
        elif "review_status" in result:
            result["review_status_display_name"] = result["review_status"]

    def populate_readonly(self):
        """Add readonly=False to older revisions of WithReadOnlyAccess models"""

        from ggrc.models import all_models

        model = getattr(all_models, self.resource_type, None)
        if not model or not issubclass(model, wroa.WithReadOnlyAccess):
            return dict()

        if "readonly" in self._content:
            # revision has flag "readonly", use it
            return {"readonly": self._content["readonly"]}

        # not flag "readonly" in revision, use default value False
        return {"readonly": False}

    def _document_evidence_hack(self):
        """Update display_name on evideces

    Evidences have display names from links and titles, and until now they used
    slug property to calculate the display name. This hack is here since we
    must support older revisions with bad data, and to avoid using slug
    differently than everywhere else in the app.

    This function only modifies existing evidence entries on any given object.
    If an object does not have and document evidences then an empty dict is
    returned.

    Returns:
      dict with updated display name for each of the evidence entries if there
      are any.
    """
        if "document_evidence" not in self._content:
            return {}
        document_evidence = self._content.get("document_evidence")
        for evidence in document_evidence:
            evidence[u"display_name"] = u"{link} {title}".format(
                link=evidence.get("link"),
                title=evidence.get("title"),
            ).strip()
        return {u"documents_file": document_evidence}

    def populate_categoies(self, key_name):
        """Return names of categories."""
        if self.resource_type != "Control":
            return {}
        result = []
        categories = self._content.get(key_name)
        if isinstance(categories, (str, unicode)) and categories:
            result = json.loads(categories)
        elif isinstance(categories, list):
            for category in categories:
                if isinstance(category, dict):
                    result.append(category.get("name"))
                elif isinstance(category, (str, unicode)):
                    result.append(category)

        return {key_name: result}

    def _get_cavs(self):
        """Return cavs values from content."""
        if "custom_attribute_values" in self._content:
            return self._content["custom_attribute_values"]
        if "custom_attributes" in self._content:
            return self._content["custom_attributes"]
        return []

    def populate_cavs(self):
        """Setup cads in cav list if they are not presented in content

    but now they are associated to instance."""
        from ggrc.models import custom_attribute_definition
        cads = custom_attribute_definition.get_custom_attributes_for(
            self.resource_type, self.resource_id)
        cavs = {int(i["custom_attribute_id"]): i for i in self._get_cavs()}
        cads_ids = set()
        for cad in cads:
            custom_attribute_id = int(cad["id"])
            cads_ids.add(custom_attribute_id)
            if custom_attribute_id in cavs:
                # Old revisions can contain falsy values for a Checkbox
                if cad["attribute_type"] == "Checkbox" \
                        and not cavs[custom_attribute_id]["attribute_value"]:
                    cavs[custom_attribute_id]["attribute_value"] = cad[
                        "default_value"]
                continue
            if cad["attribute_type"] == "Map:Person":
                value = "Person"
            else:
                value = cad["default_value"]
            cavs[custom_attribute_id] = {
                "attribute_value": value,
                "custom_attribute_id": custom_attribute_id,
                "attributable_id": self.resource_id,
                "attributable_type": self.resource_type,
                "display_name": "",
                "attribute_object": None,
                "type": "CustomAttributeValue",
                "context_id": None,
            }

        cavs = {
            cad_id: value
            for cad_id, value in cavs.iteritems() if cad_id in cads_ids
        }

        return {
            "custom_attribute_values": cavs.values(),
            "custom_attribute_definitions": cads
        }

    def populate_cad_default_values(self):
        """Setup default_value to CADs if it's needed."""
        from ggrc.models import all_models
        if "custom_attribute_definitions" not in self._content:
            return {}
        cads = []
        for cad in self._content["custom_attribute_definitions"]:
            if "default_value" not in cad:
                cad["default_value"] = (
                    all_models.CustomAttributeDefinition.get_default_value_for(
                        cad["attribute_type"]))
            cads.append(cad)
        return {"custom_attribute_definitions": cads}

    def populate_requirements(self, populated_content):  # noqa pylint: disable=too-many-branches
        """Populates revision content for Requirement models and models with fields

    that can contain Requirement old names. This fields would be checked and
    updated where necessary
    """
        # change to add Requirement old names
        requirement_type = ["Section", "Clause"]
        # change to add models and fields that can contain Requirement old names
        affected_models = {
            "AccessControlList": [
                "object_type",
            ],
            "AccessControlRole": [
                "object_type",
            ],
            "Assessment": [
                "assessment_type",
            ],
            "AssessmentTemplate": [
                "template_object_type",
            ],
            "Automapping": [
                "source_type",
                "destination_type",
            ],
            "CustomAttributeValue": [
                "attributable_type",
            ],
            "Event": [
                "resource_type",
            ],
            "ObjectPerson": [
                "personable_type",
            ],
            "Relationship": [
                "source_type",
                "destination_type",
            ],
            "Revision": [
                "resource_type",
            ],
            "Label": [
                "object_type",
            ],
            "Context": [
                "related_object_type",
            ],
            "IssuetrackerIssue": [
                "object_type",
            ],
            "ObjectLabel": [
                "object_type",
            ],
            "ObjectTemplates": [
                "name",
            ],
            "Proposal": [
                "instance_type",
            ],
            "Snapshot": [
                "child_type",
                "parent_type",
            ],
        }
        # change to add special values cases
        special_cases = {
            "CustomAttributeDefinition": {
                "fields": [
                    "definition_type",
                ],
                "old_values": ["section", "clause"],
                "new_value": "requirement",
            }
        }

        obj_type = self.resource_type

        # populate fields if they contain old names
        if obj_type in affected_models.keys():
            for field in affected_models[obj_type]:
                if populated_content.get(field) in requirement_type:
                    populated_content[field] = "Requirement"

        # populate fields for models that contain old names in special spelling
        if obj_type in special_cases.keys():
            for field in special_cases[obj_type]["fields"]:
                if populated_content[field] in special_cases[obj_type][
                        "old_values"]:
                    populated_content[field] = special_cases[obj_type][
                        "new_value"]

        # populate Requirements revisions
        if obj_type == "Requirement":
            populated_content["type"] = "Requirement"

            acls = populated_content.get("access_control_list", {})
            if acls:
                for acl in acls:
                    if acl.get("object_type") in requirement_type:
                        acl["object_type"] = "Requirement"
                populated_content["access_control_list"] = acls

            cavs = populated_content.get("custom_attribute_values", {})
            if cavs:
                for cav in cavs:
                    if cav.get("attributable_type") in requirement_type:
                        cav["attributable_type"] = "Requirement"
                populated_content["custom_attribute_values"] = cavs

    def populate_options(self, populated_content):
        """Update revisions for Sync models to have Option fields as string."""
        if self.resource_type == "Control":
            for attr in ["kind", "means", "verify_frequency"]:
                attr_value = populated_content.get(attr)
                if isinstance(attr_value, dict):
                    populated_content[attr] = attr_value.get("title")
                elif isinstance(attr_value, (str, unicode)):
                    populated_content[attr] = attr_value
                else:
                    populated_content[attr] = None

    def populate_automappings(self):
        """Add automapping info in revisions.

    Populate Relationship revisions with automapping info to help FE
    show Change Log, but we should not show automapping info
    in case of deleted relationship"""
        if ("automapping_id" not in self._content
                or not self._content["automapping_id"]
                or self.action != "created"):
            return {}
        automapping_id = self._content["automapping_id"]
        if not hasattr(flask.g, "automappings_cache"):
            flask.g.automappings_cache = dict()
        if automapping_id not in flask.g.automappings_cache:
            automapping_obj = automapping.Automapping.query.get(automapping_id)
            if automapping_obj is None:
                return {}
            automapping_json = automapping_obj.log_json()
            flask.g.automappings_cache[automapping_id] = automapping_json
        else:
            automapping_json = flask.g.automappings_cache[automapping_id]
        return {"automapping": automapping_json}

    @builder.simple_property
    def content(self):
        """Property. Contains the revision content dict.

    Updated by required values, generated from saved content dict."""
        # pylint: disable=too-many-locals
        populated_content = self._content.copy()
        populated_content.update(self.populate_acl())
        populated_content.update(self.populate_reference_url())
        populated_content.update(self.populate_folder())
        populated_content.update(self.populate_labels())
        populated_content.update(self.populate_status())
        populated_content.update(self.populate_review_status())
        populated_content.update(self._document_evidence_hack())
        populated_content.update(self.populate_categoies("categories"))
        populated_content.update(self.populate_categoies("assertions"))
        populated_content.update(self.populate_cad_default_values())
        populated_content.update(self.populate_cavs())
        populated_content.update(self.populate_readonly())
        populated_content.update(self.populate_automappings())

        self.populate_requirements(populated_content)
        self.populate_options(populated_content)
        self.populate_review_status_display_name(populated_content)
        # remove custom_attributes,
        # it's old style interface and now it's not needed
        populated_content.pop("custom_attributes", None)
        # remove attribute_object_id not used by FE anymore
        for item in populated_content["custom_attribute_values"]:
            item.pop("attribute_object_id", None)
        return populated_content

    @content.setter
    def content(self, value):
        """ Setter for content property."""
        self._content = value

    def _handle_if_empty(self):
        """Check if revision is empty and update is_empty flag if true."""

        # Check if new revision contains any changes in resource state. Revisions
        # created with "created" or "deleted" action are not considered empty.
        if self in db.session.new and self.action == u"modified":
            obj = referenced_objects.get(self.resource_type, self.resource_id)
            # Content serialization and deserialization is needed since content of
            # prev revision stored in DB was serialized before storing and due to
            # this couldn't be correctly compared to content of revision in hands.
            content = json.loads(utils.as_json(self.content))
            self.is_empty = bool(
                obj and not revisions_diff.changes_present(obj, content))

    def handle_before_flush(self):
        """Handler that called  before SQLAlchemy flush event."""
        self._handle_if_empty()
Exemplo n.º 25
0
class Control(WithLastAssessmentDate, HasObjectState, Roleable, Relatable,
              CustomAttributable, Personable, ControlCategorized,
              PublicDocumentable, AssertionCategorized, Hierarchical,
              LastDeprecatedTimeboxed, Auditable, TestPlanned, Commentable,
              BusinessObject, Indexed, db.Model):
    __tablename__ = 'controls'

    company_control = deferred(db.Column(db.Boolean), 'Control')
    directive_id = deferred(
        db.Column(db.Integer, db.ForeignKey('directives.id')), 'Control')
    kind_id = deferred(db.Column(db.Integer), 'Control')
    means_id = deferred(db.Column(db.Integer), 'Control')
    version = deferred(db.Column(db.String), 'Control')
    documentation_description = deferred(db.Column(db.Text), 'Control')
    verify_frequency_id = deferred(db.Column(db.Integer), 'Control')
    fraud_related = deferred(db.Column(db.Boolean), 'Control')
    key_control = deferred(db.Column(db.Boolean), 'Control')
    active = deferred(db.Column(db.Boolean), 'Control')
    principal_assessor_id = deferred(
        db.Column(db.Integer, db.ForeignKey('people.id')), 'Control')
    secondary_assessor_id = deferred(
        db.Column(db.Integer, db.ForeignKey('people.id')), 'Control')

    principal_assessor = db.relationship(
        'Person', uselist=False, foreign_keys='Control.principal_assessor_id')
    secondary_assessor = db.relationship(
        'Person', uselist=False, foreign_keys='Control.secondary_assessor_id')

    kind = db.relationship(
        'Option',
        primaryjoin='and_(foreign(Control.kind_id) == Option.id, '
        'Option.role == "control_kind")',
        uselist=False)
    means = db.relationship(
        'Option',
        primaryjoin='and_(foreign(Control.means_id) == Option.id, '
        'Option.role == "control_means")',
        uselist=False)
    verify_frequency = db.relationship(
        'Option',
        primaryjoin='and_(foreign(Control.verify_frequency_id) == Option.id, '
        'Option.role == "verify_frequency")',
        uselist=False)

    @staticmethod
    def _extra_table_args(_):
        return (
            db.Index('ix_controls_principal_assessor',
                     'principal_assessor_id'),
            db.Index('ix_controls_secondary_assessor',
                     'secondary_assessor_id'),
        )

    # REST properties
    _api_attrs = reflection.ApiAttributes(
        'active',
        'company_control',
        'directive',
        'documentation_description',
        'fraud_related',
        'key_control',
        'kind',
        'means',
        'verify_frequency',
        'version',
        'principal_assessor',
        'secondary_assessor',
    )

    _fulltext_attrs = [
        'active',
        'company_control',
        'directive',
        'documentation_description',
        attributes.BooleanFullTextAttr('fraud_related',
                                       'fraud_related',
                                       true_value="yes",
                                       false_value="no"),
        attributes.BooleanFullTextAttr('key_control',
                                       'key_control',
                                       true_value="key",
                                       false_value="non-key"),
        'kind',
        'means',
        'verify_frequency',
        'version',
        attributes.FullTextAttr("principal_assessor", "principal_assessor",
                                ["email", "name"]),
        attributes.FullTextAttr('secondary_assessor', 'secondary_assessor',
                                ["email", "name"]),
    ]

    _sanitize_html = [
        'documentation_description',
        'version',
    ]

    @classmethod
    def indexed_query(cls):
        return super(Control, cls).indexed_query().options(
            orm.Load(cls).undefer_group("Control_complete"),
            orm.Load(cls).joinedload("directive").undefer_group(
                "Directive_complete"),
            orm.Load(cls).joinedload("principal_assessor").undefer_group(
                "Person_complete"),
            orm.Load(cls).joinedload("secondary_assessor").undefer_group(
                "Person_complete"),
            orm.Load(cls).joinedload(
                'kind', ).undefer_group("Option_complete"),
            orm.Load(cls).joinedload(
                'means', ).undefer_group("Option_complete"),
            orm.Load(cls).joinedload(
                'verify_frequency', ).undefer_group("Option_complete"),
        )

    _include_links = []

    _aliases = {
        "kind": "Kind/Nature",
        "means": "Type/Means",
        "verify_frequency": "Frequency",
        "fraud_related": "Fraud Related",
        "key_control": {
            "display_name": "Significance",
            "description": "Allowed values are:\nkey\nnon-key\n---",
        },
        # overrides values from PublicDocumentable mixin
        "document_url": None,
        "test_plan": "Assessment Procedure",
    }

    @validates('kind', 'means', 'verify_frequency')
    def validate_control_options(self, key, option):
        desired_role = key if key == 'verify_frequency' else 'control_' + key
        return validate_option(self.__class__.__name__, key, option,
                               desired_role)

    @classmethod
    def eager_query(cls):
        query = super(Control, cls).eager_query()
        return cls.eager_inclusions(query, Control._include_links).options(
            orm.joinedload('directive'),
            orm.joinedload('principal_assessor'),
            orm.joinedload('secondary_assessor'),
            orm.joinedload('kind'),
            orm.joinedload('means'),
            orm.joinedload('verify_frequency'),
        )

    def log_json(self):
        out_json = super(Control, self).log_json()
        # so that event log can refer to deleted directive
        if self.directive:
            out_json["mapped_directive"] = self.directive.display_name
        return out_json
Exemplo n.º 26
0
 def parent_id(cls):  # pylint: disable=no-self-argument
     return deferred(
         db.Column(db.Integer,
                   db.ForeignKey('{0}.id'.format(cls.__tablename__))),
         cls.__name__)
Exemplo n.º 27
0
class Request(statusable.Statusable, AutoStatusChangeable, Assignable,
              EvidenceURL, Personable, CustomAttributable, Notifiable,
              relationship.Relatable, WithSimilarityScore, Titled, Slugged,
              Described, Commentable, FinishedDate, VerifiedDate, Base,
              db.Model):
    """Class representing Requests.

  Request is an object representing a request from a Requester to Assignee
  to provide feedback, evidence or attachment in the form of comments,
  documents or URLs that (if specified) Verifier has to approve of
  before Request is considered finished.
  """
    __tablename__ = 'requests'
    _title_uniqueness = False

    VALID_TYPES = (u'documentation', u'interview')

    ASSIGNEE_TYPES = (u'Assignee', u'Requester', u'Verifier')

    similarity_options = similarity_options_module.REQUEST

    # TODO Remove requestor and requestor_id on database cleanup
    requestor_id = db.Column(db.Integer, db.ForeignKey('people.id'))
    requestor = db.relationship('Person', foreign_keys=[requestor_id])

    # TODO Remove request_type on database cleanup
    request_type = deferred(db.Column(db.Enum(*VALID_TYPES), nullable=False),
                            'Request')

    start_date = deferred(
        db.Column(db.Date, nullable=False, default=date.today), 'Request')

    end_date = deferred(
        db.Column(db.Date,
                  nullable=False,
                  default=lambda: date.today() + timedelta(7)), 'Request')

    # TODO Remove audit_id audit_object_id on database cleanup
    audit_id = db.Column(db.Integer,
                         db.ForeignKey('audits.id'),
                         nullable=False)
    audit_object_id = db.Column(db.Integer,
                                db.ForeignKey('audit_objects.id'),
                                nullable=True)
    gdrive_upload_path = deferred(db.Column(db.String, nullable=True),
                                  'Request')
    # TODO Remove test and notes columns on database cleanup
    test = deferred(db.Column(db.Text, nullable=True), 'Request')
    notes = deferred(db.Column(db.Text, nullable=True), 'Request')

    _publish_attrs = [
        'requestor', 'request_type', 'gdrive_upload_path', 'start_date',
        'end_date', 'status', 'audit', 'test', 'notes', 'title', 'description'
    ]

    _tracked_attrs = ((set(_publish_attrs) | {'slug'}) - {'status'})

    _sanitize_html = [
        'gdrive_upload_path', 'test', 'notes', 'description', 'title'
    ]

    _aliases = {
        "request_audit": {
            "display_name": "Audit",
            "filter_by": "_filter_by_request_audit",
            "mandatory": True,
        },
        "end_date": "Due On",
        "notes": "Notes",
        "request_type": "Request Type",
        "start_date": "Starts On",
        "status": {
            "display_name": "Status",
            "handler_key": "request_status",
        },
        "test": "Test",
        "related_assignees": {
            "display_name": "Assignee",
            "mandatory": True,
            "filter_by": "_filter_by_related_assignees",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_requesters": {
            "display_name": "Requester",
            "mandatory": True,
            "filter_by": "_filter_by_related_requesters",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_verifiers": {
            "display_name": "Verifier",
            "filter_by": "_filter_by_related_verifiers",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
    }

    def _display_name(self):
        # pylint: disable=unsubscriptable-object
        if len(self.title) > 32:
            display_string = self.description[:32] + u'...'
        elif self.title:
            display_string = self.title
        elif len(self.description) > 32:
            display_string = self.description[:32] + u'...'
        else:
            display_string = self.description
        return u'Request with id {0} "{1}" for Audit "{2}"'.format(
            self.id, display_string, self.audit.display_name)

    @classmethod
    def eager_query(cls):
        query = super(Request, cls).eager_query()
        return query.options(orm.joinedload('audit'))

    @classmethod
    def _filter_by_related_assignees(cls, predicate):
        return cls._get_relate_filter(predicate, "Assignee")

    @classmethod
    def _filter_by_related_requesters(cls, predicate):
        return cls._get_relate_filter(predicate, "Requester")

    @classmethod
    def _filter_by_related_verifiers(cls, predicate):
        return cls._get_relate_filter(predicate, "Verifier")

    @classmethod
    def _filter_by_request_audit(cls, predicate):
        return cls.query.filter((audit.Audit.id == cls.audit_id)
                                & (predicate(audit.Audit.slug)
                                   | predicate(audit.Audit.title))).exists()

    @classmethod
    def default_request_type(cls):
        return cls.VALID_TYPES[0]
Exemplo n.º 28
0
 def secondary_contact_id(cls):  # pylint: disable=no-self-argument
     return deferred(db.Column(db.Integer, db.ForeignKey('people.id')),
                     cls.__name__)
Exemplo n.º 29
0
class Cycle(WithContact, Stateful, Timeboxed, Described, Titled, Slugged,
            Notifiable, Indexed, db.Model):
  """Workflow Cycle model
  """
  __tablename__ = 'cycles'
  _title_uniqueness = False

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

  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)

  _publish_attrs = [
      'workflow',
      'cycle_task_groups',
      'is_current',
      'next_due_date',
  ]

  _aliases = {
      "cycle_workflow": {
          "display_name": "Workflow",
          "filter_by": "_filter_by_cycle_workflow",
      },
      "status": {
          "display_name": "State",
          "mandatory": False,
          "description": "Options are: \n{} ".format('\n'.join(VALID_STATES))
      },
      "contact": "Assignee",
      "secondary_contact": None,
  }

  PROPERTY_TEMPLATE = u"cycle {}"

  _fulltext_attrs = [
      MultipleSubpropertyFullTextAttr(
          "group title", "cycle_task_groups", ["title"], False,
      ),
      MultipleSubpropertyFullTextAttr(
          "group assignee",
          lambda instance: [g.contact for g in instance.cycle_task_groups],
          ["name", "email"],
          False,
      ),
      DateMultipleSubpropertyFullTextAttr(
          "group due date",
          'cycle_task_groups',
          ["next_due_date"],
          False,
      ),
      MultipleSubpropertyFullTextAttr(
          "task title",
          'cycle_task_group_object_tasks',
          ["title"],
          False,
      ),
      MultipleSubpropertyFullTextAttr(
          "task assignee",
          lambda instance: [t.contact for t in
                            instance.cycle_task_group_object_tasks],
          ["name", "email"],
          False
      ),
      DateMultipleSubpropertyFullTextAttr(
          "task due date",
          "cycle_task_group_object_tasks",
          ["end_date"],
          False
      ),
      DateFullTextAttr("due date", "next_due_date"),
      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 = [
      ReindexRule("CycleTaskGroup", lambda x: x.cycle),
      ReindexRule("CycleTaskGroupObjectTask",
                  lambda x: x.cycle_task_group.cycle),
      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"
        ),
    )
Exemplo n.º 30
0
class CycleTaskGroupObjectTask(
        WithContact, Stateful, Timeboxed, Relatable, Notifiable,
        Described, Titled, Slugged, Base, 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_for(cls, obj):
    return "CYCLETASK"

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

  # Note: this statuses are used in utils/query_helpers to filter out the tasks
  # that should be visible on My Tasks pages.
  ACTIVE_STATES = ("Assigned", "InProgress", "Finished", "Declined")

  PROPERTY_TEMPLATE = u"task {}"

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

  AUTO_REINDEX_RULES = [
      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(
      JsonType(), nullable=False, default=[])
  selected_response_options = db.Column(
      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

  _publish_attrs = [
      'cycle',
      'cycle_task_group',
      'task_group_task',
      'cycle_task_entries',
      'sort_index',
      'task_type',
      'response_options',
      'selected_response_options',
      PublishOnly('object_approval'),
      PublishOnly('finished_date'),
      PublishOnly('verified_date')
  ]

  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,
      },
      "status": {
          "display_name": "State",
          "mandatory": False,
          "description": "Options are:\n{}".
                          format('\n'.join((item for item in VALID_STATES
                                            if item)))
      },
      "end_date": "Due Date",
      "start_date": "Start Date",
  }

  @computed_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

  @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'),
    )

  @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"
        ),
    )