Ejemplo n.º 1
0
class VerifiedDate(object):
  """Adds 'Verified Date' which is set when status is set to 'Verified'.

  When object is verified the status is overridden to 'Final' and the
  information about verification exposed as the 'verified' boolean.
  Requires Stateful to be mixed in as well.
  """

  VERIFIED_STATES = {u"Verified"}
  DONE_STATES = {}

  # pylint: disable=method-hidden
  # because validator only sets date per model instance
  @declared_attr
  def verified_date(cls):
    return deferred(
        db.Column(db.Date, nullable=True),
        cls.__name__
    )

  @hybrid_property
  def verified(self):
    return self.verified_date != None  # noqa

  _publish_attrs = [
      reflection.PublishOnly('verified'),
      reflection.PublishOnly('verified_date'),
  ]

  _aliases = {
      "verified_date": "Verified Date"
  }

  _fulltext_attrs = [
      "verified_date",
      "verified",
  ]

  @validates('status')
  def validate_status(self, key, value):
    """Update verified_date on status change, make verified status final."""
    # Sqlalchemy only uses one validator per status (not necessarily the
    # first) and ignores others. This enables cooperation between validators
    # since 'status' is not defined here.
    if hasattr(super(VerifiedDate, self), "validate_status"):
      value = super(VerifiedDate, self).validate_status(key, value)
    if (value in self.VERIFIED_STATES and
            self.status not in self.VERIFIED_STATES):
      self.verified_date = datetime.datetime.now()
      value = self.FINAL_STATE
    elif (value not in self.VERIFIED_STATES and
          value not in self.DONE_STATES and
          (self.status in self.VERIFIED_STATES or
           self.status in self.DONE_STATES)):
      self.verified_date = None
    return value
Ejemplo n.º 2
0
class Documentable(object):
  @declared_attr
  def object_documents(cls):
    cls.documents = association_proxy(
        'object_documents', 'document',
        creator=lambda document: ObjectDocument(
            document=document,
            documentable_type=cls.__name__,
        )
    )
    joinstr = ('and_(foreign(ObjectDocument.documentable_id) == {type}.id, '
               '     foreign(ObjectDocument.documentable_type) == "{type}")')
    joinstr = joinstr.format(type=cls.__name__)
    return db.relationship(
        'ObjectDocument',
        primaryjoin=joinstr,
        backref='{0}_documentable'.format(cls.__name__),
        cascade='all, delete-orphan',
    )

  _publish_attrs = [
      reflection.PublishOnly('documents'),
      'object_documents',
  ]

  _include_links = [
      # 'object_documents',
  ]

  @classmethod
  def eager_query(cls):
    query = super(Documentable, cls).eager_query()
    return cls.eager_inclusions(query, Documentable._include_links).options(
        orm.subqueryload('object_documents'))
Ejemplo n.º 3
0
class FinishedDate(object):
  """Adds 'Finished Date' which is set when status is set to a finished state.

  Requires Stateful to be mixed in as well.
  """

  NOT_DONE_STATES = None
  DONE_STATES = {}

  # pylint: disable=method-hidden
  # because validator only sets date per model instance
  @declared_attr
  def finished_date(cls):  # pylint: disable=no-self-argument
    return deferred(
        db.Column(db.DateTime, nullable=True),
        cls.__name__
    )

  _publish_attrs = [
      reflection.PublishOnly('finished_date')
  ]

  _aliases = {
      "finished_date": "Finished Date"
  }

  _fulltext_attrs = [
      attributes.DatetimeFullTextAttr('finished_date', 'finished_date'),
  ]

  @validates('status')
  def validate_status(self, key, value):
    """Update finished_date given the right status change."""
    # Sqlalchemy only uses one validator per status (not necessarily the
    # first) and ignores others. This enables cooperation between validators
    # since 'status' is not defined here.
    if hasattr(super(FinishedDate, self), "validate_status"):
      value = super(FinishedDate, self).validate_status(key, value)
    # pylint: disable=unsupported-membership-test
    # short circuit
    if (value in self.DONE_STATES and
        (self.NOT_DONE_STATES is None or
         self.status in self.NOT_DONE_STATES)):
      self.finished_date = datetime.datetime.now()
    elif ((self.NOT_DONE_STATES is None or
           value in self.NOT_DONE_STATES) and
            self.status in self.DONE_STATES):
      self.finished_date = None
    return value

  @classmethod
  def indexed_query(cls):
    return super(FinishedDate, cls).indexed_query().options(
        orm.Load(cls).load_only("finished_date"),
    )
Ejemplo n.º 4
0
class Folderable(object):
    """Mixin adding the ability to attach folders to an object"""
    @classmethod
    def late_init_folderable(cls):
        def make_object_folders(cls):
            joinstr = 'and_(foreign(ObjectFolder.folderable_id) == {type}.id, '\
                'foreign(ObjectFolder.folderable_type) == "{type}")'
            joinstr = joinstr.format(type=cls.__name__)
            return db.relationship(
                'ObjectFolder',
                primaryjoin=joinstr,
                backref='{0}_folderable'.format(cls.__name__),
                cascade='all, delete-orphan',
            )

        cls.object_folders = make_object_folders(cls)

    @simple_property
    def folders(self):
        """Returns a list of associated folders' ids"""
        # pylint: disable=not-an-iterable
        return [{"id": fobject.folder_id} for fobject in self.object_folders]

    _publish_attrs = [
        'object_folders',
        reflection.PublishOnly('folders'),
    ]

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

        query = super(Folderable, cls).eager_query()
        return query.options(orm.subqueryload('object_folders'))

    def log_json(self):
        """Serialize to JSON"""
        out_json = super(Folderable, self).log_json()
        if hasattr(self, "object_folders"):
            out_json["object_folders"] = [
                # pylint: disable=not-an-iterable
                create_stub(fold) for fold in self.object_folders if fold
            ]
        if hasattr(self, "folders"):
            out_json["folders"] = self.folders
        return out_json
Ejemplo n.º 5
0
class StatusValidatedMixin(mixins.Stateful):
    """Mixin setup statuses for Cycle and CycleTaskGroup."""

    ASSIGNED = u"Assigned"
    IN_PROGRESS = u"InProgress"
    FINISHED = u"Finished"
    VERIFIED = u"Verified"

    NO_VALIDATION_STATES = [ASSIGNED, IN_PROGRESS, FINISHED]
    VALID_STATES = NO_VALIDATION_STATES + [VERIFIED]

    _publish_attrs = [
        reflection.PublishOnly("is_verification_needed"),
    ]

    def is_verification_needed(self):
        raise NotImplementedError()

    @classmethod
    def default_status(cls):
        return cls.ASSIGNED

    def valid_statuses(self):
        """Return valid status for self instance."""
        if self.is_verification_needed:
            return self.VALID_STATES
        return self.NO_VALIDATION_STATES

    @property
    def active_states(self):
        return [
            i for i in self.valid_statuses() if i not in self.inactive_states
        ]

    @property
    def inactive_states(self):
        if self.is_verification_needed:
            return [self.VERIFIED]
        else:
            return [self.FINISHED]
Ejemplo n.º 6
0
class Snapshot(relationship.Relatable, 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"

  _publish_attrs = [
      "parent",
      "child_id",
      "child_type",
      "revision",
      "revision_id",
      reflection.PublishOnly("revisions"),
      reflection.PublishOnly("is_latest_revision"),
      reflection.PublishOnly("original_object_deleted"),
  ]

  _update_attrs = [
      "parent",
      "child_id",
      "child_type",
      "update_revision"
  ]

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

  parent_id = deferred(db.Column(db.Integer, nullable=False), "Snapshot")
  parent_type = deferred(db.Column(db.String, nullable=False), "Snapshot")

  # 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,
  )

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

  @computed_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):
    query = super(Snapshot, cls).eager_query()
    return cls.eager_inclusions(query, Snapshot._include_links).options(
        orm.subqueryload('revision'),
        orm.subqueryload('revisions'),
    )

  @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_attr(self):
    return '{0}_parent'.format(self.parent_type)

  @property
  def parent(self):
    return getattr(self, self.parent_attr)

  @parent.setter
  def parent(self, value):
    self.parent_id = getattr(value, 'id', None)
    self.parent_type = getattr(value, 'type', None)
    return setattr(self, self.parent_attr, value)

  @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"),
    )
Ejemplo n.º 7
0
class WorkflowState(object):
  """Object state mixin.

  This is a mixin for adding workflow_state to all objects that can be mapped
  to workflow tasks.
  """

  _publish_attrs = [reflection.PublishOnly('workflow_state')]
  _update_attrs = []

  OVERDUE = "Overdue"
  VERIFIED = "Verified"
  FINISHED = "Finished"
  ASSIGNED = "Assigned"
  IN_PROGRESS = "InProgress"
  UNKNOWN_STATE = None

  @classmethod
  def _get_state(cls, statusable_childs):
    """Get overall state of a group of tasks.

    Rules, the first that is true is selected:
      -if all are verified -> verified
      -if all are finished -> finished
      -if all are at least finished -> finished
      -if any are in progress or declined -> in progress
      -if any are assigned -> assigned

    The function will work correctly only for non Overdue states. If the result
    is overdue, it should be handled outside of this function.

    Args:
      current_tasks: list of tasks that are currently a part of an active
        cycle or cycles that are active in an workflow.

    Returns:
      Overall state according to the rules described above.
    """

    states = {i.status or i.ASSIGNED for i in statusable_childs}
    if states in [{cls.VERIFIED}, {cls.FINISHED}, {cls.ASSIGNED}]:
      return states.pop()
    if states == {cls.FINISHED, cls.VERIFIED}:
      return cls.FINISHED
    return cls.IN_PROGRESS if states else cls.UNKNOWN_STATE

  @classmethod
  def get_object_state(cls, objs):
    """Get lowest state of an object

    Get the lowest possible state of the tasks relevant to one object. States
    are scanned in order: Overdue, InProgress, Finished, Assigned, Verified.

    Args:
      objs: A list of cycle group object tasks, which should all be mapped to
        the same object.

    Returns:
      Name of the lowest state of all active cycle tasks that relate to the
      given objects.
    """
    current_tasks = []
    for task in objs:
      if not task.cycle.is_current:
        continue
      if task.is_overdue:
        return cls.OVERDUE
      current_tasks.append(task)
    return cls._get_state(current_tasks)

  @classmethod
  def get_workflow_state(cls, cycles):
    """Get lowest state of a workflow

    Get the lowest possible state of the tasks relevant to a given workflow.
    States are scanned in order: Overdue, InProgress, Finished, Assigned,
    Verified.

    Args:
      cycles: list of cycles belonging to a single workflow.

    Returns:
      Name of the lowest workflow state, if there are any active cycles.
      Otherwise it returns None.
    """
    current_cycles = []
    for cycle_instance in cycles:
      if not cycle_instance.is_current:
        continue
      for task in cycle_instance.cycle_task_group_object_tasks:
        if task.is_overdue:
          return cls.OVERDUE
      current_cycles.append(cycle_instance)
    return cls._get_state(current_cycles)

  @builder.simple_property
  def workflow_state(self):
    return WorkflowState.get_object_state(self.cycle_task_group_object_tasks)
Ejemplo n.º 8
0
class Workflow(mixins.CustomAttributable, HasOwnContext, mixins.Timeboxed,
               mixins.Described, mixins.Titled, mixins.Notifiable,
               mixins.Stateful, mixins.Slugged, Indexed, db.Model):
  """Basic Workflow first class object.
  """
  __tablename__ = 'workflows'
  _title_uniqueness = False

  VALID_STATES = [u"Draft", u"Active", u"Inactive"]

  # valid Frequency to user readable values mapping
  VALID_FREQUENCIES = {
      "one_time": "one time",
      "weekly": "weekly",
      "monthly": "monthly",
      "quarterly": "quarterly",
      "annually": "annually"
  }

  @classmethod
  def default_frequency(cls):
    return 'one_time'

  @orm.validates('frequency')
  def validate_frequency(self, _, value):
    """Make sure that value is listed in valid frequencies.

    Args:
      value: A string value for requested frequency

    Returns:
      default_frequency which is 'one_time' if the value is None, or the value
      itself.

    Raises:
      Value error, if the value is not in the VALID_FREQUENCIES
    """
    if value is None:
      value = self.default_frequency()
    if value not in self.VALID_FREQUENCIES:
      message = u"Invalid state '{}'".format(value)
      raise ValueError(message)
    return value

  notify_on_change = deferred(
      db.Column(db.Boolean, default=False, nullable=False), 'Workflow')
  notify_custom_message = deferred(
      db.Column(db.Text, nullable=True), 'Workflow')

  frequency = deferred(
      db.Column(db.String, nullable=True, default=default_frequency),
      'Workflow'
  )

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

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

  workflow_people = db.relationship(
      'WorkflowPerson', backref='workflow', cascade='all, delete-orphan')
  people = association_proxy(
      'workflow_people', 'person', 'WorkflowPerson')

  task_groups = db.relationship(
      'TaskGroup', backref='workflow', cascade='all, delete-orphan')

  cycles = db.relationship(
      'Cycle', backref='workflow', cascade='all, delete-orphan')

  next_cycle_start_date = db.Column(db.Date, nullable=True)

  non_adjusted_next_cycle_start_date = db.Column(db.Date, nullable=True)

  # this is an indicator if the workflow exists from before the change where
  # we deleted cycle objects, which changed how the cycle is created and
  # how objects are mapped to the cycle tasks
  is_old_workflow = deferred(
      db.Column(db.Boolean, default=False, nullable=True), 'Workflow')

  # This column needs to be deferred because one of the migrations
  # uses Workflow as a model and breaks since at that point in time
  # there is no 'kind' column yet
  kind = deferred(
      db.Column(db.String, default=None, nullable=True), 'Workflow')
  IS_VERIFICATION_NEEDED_DEFAULT = True
  is_verification_needed = db.Column(
      db.Boolean,
      default=IS_VERIFICATION_NEEDED_DEFAULT,
      nullable=False)

  @orm.validates('is_verification_needed')
  def validate_is_verification_needed(self, key, value):
    # pylint: disable=unused-argument
    """Validate is_verification_needed field for Workflow.

    It's not allowed to change is_verification_needed flag after creation.
    If is_verification_needed doesn't send,
    then is_verification_needed flag is True.
    """
    if self.is_verification_needed is None:
      return self.IS_VERIFICATION_NEEDED_DEFAULT if value is None else value
    if value is None:
      return self.is_verification_needed
    if value != self.is_verification_needed:
      raise ValueError("is_verification_needed value isn't changeble")
    return value

  @builder.simple_property
  def workflow_state(self):
    return WorkflowState.get_workflow_state(self.cycles)

  _sanitize_html = [
      'notify_custom_message',
  ]

  _publish_attrs = [
      'workflow_people',
      reflection.PublishOnly('people'),
      'task_groups',
      'frequency',
      'notify_on_change',
      'notify_custom_message',
      'cycles',
      'object_approval',
      'recurrences',
      'is_verification_needed',
      reflection.PublishOnly('next_cycle_start_date'),
      reflection.PublishOnly('non_adjusted_next_cycle_start_date'),
      reflection.PublishOnly('workflow_state'),
      reflection.PublishOnly('kind'),
  ]

  _fulltext_attrs = [
      ValueMapFullTextAttr(
          "frequency",
          "frequency",
          value_map=VALID_FREQUENCIES,
      )
  ]

  _aliases = {
      "frequency": {
          "display_name": "Frequency",
          "mandatory": True,
      },
      "is_verification_needed": {
          "display_name": "Need Verification",
          "mandatory": True,
      },
      "notify_custom_message": "Custom email message",
      "notify_on_change": "Force real-time email updates",
      "workflow_owner": {
          "display_name": "Manager",
          "type": reflection.AttributeInfo.Type.USER_ROLE,
          "mandatory": True,
          "filter_by": "_filter_by_workflow_owner",
      },
      "workflow_member": {
          "display_name": "Member",
          "type": reflection.AttributeInfo.Type.USER_ROLE,
          "filter_by": "_filter_by_workflow_member",
      },
      "status": None,
      "start_date": None,
      "end_date": None,
  }

  @classmethod
  def _filter_by_workflow_owner(cls, predicate):
    return cls._filter_by_role("WorkflowOwner", predicate)

  @classmethod
  def _filter_by_workflow_member(cls, predicate):
    return cls._filter_by_role("WorkflowMember", predicate)

  def copy(self, _other=None, **kwargs):
    """Create a partial copy of the current workflow.
    """
    columns = [
        'title', 'description', 'notify_on_change', 'notify_custom_message',
        'frequency', 'end_date', 'start_date'
    ]
    target = self.copy_into(_other, columns, **kwargs)
    return target

  def copy_task_groups(self, target, **kwargs):
    """Copy all task groups and tasks mapped to this workflow.
    """
    for task_group in self.task_groups:
      obj = task_group.copy(
          workflow=target,
          context=target.context,
          clone_people=kwargs.get("clone_people", False),
          clone_objects=kwargs.get("clone_objects", False),
          modified_by=get_current_user(),
      )
      target.task_groups.append(obj)

      if kwargs.get("clone_tasks"):
        task_group.copy_tasks(
            obj,
            clone_people=kwargs.get("clone_people", False),
            clone_objects=kwargs.get("clone_objects", True)
        )

    return target

  @classmethod
  def eager_query(cls):
    return super(Workflow, cls).eager_query().options(
        orm.subqueryload('cycles').undefer_group('Cycle_complete')
           .subqueryload("cycle_task_group_object_tasks")
           .undefer_group("CycleTaskGroupObjectTask_complete"),
        orm.subqueryload('task_groups'),
        orm.subqueryload('workflow_people'),
    )

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

  @classmethod
  def ensure_backlog_workflow_exists(cls):
    """Ensures there is at least one backlog workflow with an active cycle.
    If such workflow does not exist it creates one."""

    def any_active_cycle(workflows):
      """Checks if any active cycle exists from given workflows"""
      for workflow in workflows:
        for cur_cycle in workflow.cycles:
          if cur_cycle.is_current:
            return True
      return False

    # Check if backlog workflow already exists
    backlog_workflows = Workflow.query\
                                .filter(and_
                                        (Workflow.kind == "Backlog",
                                         Workflow.frequency == "one_time"))\
                                .all()

    if len(backlog_workflows) > 0 and any_active_cycle(backlog_workflows):
      return "At least one backlog workflow already exists"
    # Create a backlog workflow
    backlog_workflow = Workflow(description="Backlog workflow",
                                title="Backlog (one time)",
                                frequency="one_time",
                                status="Active",
                                recurrences=0,
                                kind="Backlog")

    # create wf context
    wf_ctx = backlog_workflow.get_or_create_object_context(context=1)
    backlog_workflow.context = wf_ctx
    db.session.flush(backlog_workflow)
    # create a cycle
    backlog_cycle = cycle.Cycle(description="Backlog workflow",
                                title="Backlog (one time)",
                                is_current=1,
                                status="Assigned",
                                start_date=None,
                                end_date=None,
                                context=backlog_workflow
                                .get_or_create_object_context(),
                                workflow=backlog_workflow)

    # create a cycletaskgroup
    backlog_ctg = cycle_task_group\
        .CycleTaskGroup(description="Backlog workflow taskgroup",
                        title="Backlog TaskGroup",
                        cycle=backlog_cycle,
                        status="InProgress",
                        start_date=None,
                        end_date=None,
                        context=backlog_workflow
                        .get_or_create_object_context())

    db.session.add_all([backlog_workflow, backlog_cycle, backlog_ctg])
    db.session.flush()

    # add fulltext entries
    indexer = get_indexer()
    indexer.create_record(indexer.fts_record_for(backlog_workflow))
    return "Backlog workflow created"
Ejemplo n.º 9
0
class Snapshot(relationship.Relatable, 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"

    _publish_attrs = [
        "parent",
        "child_id",
        "child_type",
        "revision",
        "revision_id",
        reflection.PublishOnly("revisions"),
        reflection.PublishOnly("is_latest_revision"),
    ]

    _update_attrs = ["parent", "child_id", "child_type", "update_revision"]

    _include_links = ["revision"]

    parent_id = deferred(db.Column(db.Integer, nullable=False), "Snapshot")
    parent_type = deferred(db.Column(db.String, nullable=False), "Snapshot")

    # 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,
    )

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

    @classmethod
    def eager_query(cls):
        query = super(Snapshot, cls).eager_query()
        return cls.eager_inclusions(query, Snapshot._include_links).options(
            orm.subqueryload('revision'),
            orm.subqueryload('revisions'),
        )

    @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":
            latest_revision_id = get_latest_revision_id(self)
            if latest_revision_id:
                self.revision_id = latest_revision_id

    @property
    def parent_attr(self):
        return '{0}_parent'.format(self.parent_type)

    @property
    def parent(self):
        return getattr(self, self.parent_attr)

    @parent.setter
    def parent(self, value):
        self.parent_id = getattr(value, 'id', None)
        self.parent_type = getattr(value, 'type', None)
        return setattr(self, self.parent_attr, value)

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

    @classmethod
    def handle_post_flush(cls, session, flush_context, instances):
        """Handle snapshot objects on api post requests."""
        # pylint: disable=unused-argument
        # Arguments here are set in the event listener and are mandatory.

        with benchmark("Snapshot pre flush handler"):

            snapshots = [o for o in session if isinstance(o, cls)]
            if not snapshots:
                return

            with benchmark("Snapshot revert attrs"):
                cls._revert_attrs(snapshots)

            new_snapshots = [
                o for o in snapshots
                if getattr(o, "_update_revision", "") == "new"
            ]
            if new_snapshots:
                with benchmark("Snapshot post api set revisions"):
                    cls._set_revisions(new_snapshots)
                with benchmark("Snapshot post api ensure relationships"):
                    cls._ensure_relationships(new_snapshots)

    @classmethod
    def _revert_attrs(cls, objects):
        """Revert any modified attributes on snapshot.

    All snapshot attributes that are updatable with API calls should only be
    settable and not editable. This function reverts any possible edits to
    existing values.
    """
        attrs = ["parent_id", "parent_type", "child_id", "child_type"]
        for snapshot in objects:
            for attr in attrs:
                deleted = inspect(snapshot).attrs[attr].history.deleted
                if deleted:
                    setattr(snapshot, attr, deleted[0])

    @classmethod
    def _ensure_relationships(cls, objects):
        """Ensure that snapshotted object is related to audit program.

    This function is made to handle multiple snapshots for a single audit.
    Args:
      objects: list of snapshot objects with child_id and child_type set.
    """
        pairs = [(o.child_type, o.child_id) for o in objects]
        assert len({o.parent.id
                    for o in objects}) == 1  # fail on multiple audits
        program = ("Program", objects[0].parent.program_id)
        rel = relationship.Relationship
        columns = db.session.query(
            rel.destination_type,
            rel.destination_id,
            rel.source_type,
            rel.source_id,
        )
        query = columns.filter(
            tuple_(rel.destination_type, rel.destination_id) == (program),
            tuple_(rel.source_type, rel.source_id).in_(pairs)).union(
                columns.filter(
                    tuple_(rel.source_type, rel.source_id) == (program),
                    tuple_(rel.destination_type,
                           rel.destination_id).in_(pairs)))
        existing_pairs = set(
            sum([[(r.destination_type, r.destination_id),
                  (r.source_type, r.source_id)] for r in query],
                []))  # build a set of all found type-id pairs
        missing_pairs = set(pairs).difference(existing_pairs)
        cls._insert_relationships(program, missing_pairs)

    @classmethod
    def _insert_relationships(cls, program, missing_pairs):
        """Insert missing obj-program relationships."""
        if not missing_pairs:
            return
        current_user_id = get_current_user_id()
        now = datetime.now()
        # We are doing an INSERT IGNORE INTO here to mitigate a race condition
        # that happens when multiple simultaneous requests create the same
        # automapping. If a relationship object fails our unique constraint
        # it means that the mapping was already created by another request
        # and we can safely ignore it.
        inserter = relationship.Relationship.__table__.insert().prefix_with(
            "IGNORE")
        db.session.execute(
            inserter.values([{
                "id": None,
                "modified_by_id": current_user_id,
                "created_at": now,
                "updated_at": now,
                "source_type": program[0],
                "source_id": program[1],
                "destination_type": dst_type,
                "destination_id": dst_id,
                "context_id": None,
                "status": None,
                "automapping_id": None
            } for dst_type, dst_id in missing_pairs]))

    @classmethod
    def _set_revisions(cls, objects):
        """Set latest revision_id for given child_type.

    Args:
      objects: list of snapshot objects with child_id and child_type set.
    """
        pairs = [(o.child_type, o.child_id) for o in objects]
        query = db.session.query(
            func.max(revision.Revision.id, name="id", identifier="id"),
            revision.Revision.resource_type,
            revision.Revision.resource_id,
        ).filter(
            tuple_(
                revision.Revision.resource_type,
                revision.Revision.resource_id,
            ).in_(pairs)).group_by(
                revision.Revision.resource_type,
                revision.Revision.resource_id,
            )
        id_map = {(r_type, r_id): id_ for id_, r_type, r_id in query}
        for o in objects:
            o.revision_id = id_map.get((o.child_type, o.child_id))
Ejemplo n.º 10
0
class WorkflowState(object):
    """Object state mixin.

  This is a mixin for adding workflow_state to all objects that can be mapped
  to workflow tasks.
  """

    _publish_attrs = [reflection.PublishOnly('workflow_state')]
    _update_attrs = []

    @classmethod
    def _get_state(cls, current_tasks):
        """Get overall state of a group of tasks.

    Rules, the first that is true is selected:
      -if all are verified -> verified
      -if all are finished -> finished
      -if all are at least finished -> finished
      -if any are in progress or declined -> in progress
      -if any are assigned -> assigned

    The function will work correctly only for non Overdue states. If the result
    is overdue, it should be handled outside of this function.

    Args:
      current_tasks: list of tasks that are currently a part of an active
        cycle or cycles that are active in an workflow.

    Returns:
      Overall state according to the rules described above.
    """
        states = [task.status or "Assigned" for task in current_tasks]

        if states.count("Verified") == len(states):
            resulting_state = "Verified"
        elif states.count("Finished") == len(states):
            resulting_state = "Finished"
        elif not set(states).intersection(
            {"InProgress", "Assigned", "Declined"}):
            resulting_state = "Finished"
        elif set(states).intersection(
            {"InProgress", "Declined", "Finished", "Verified"}):
            resulting_state = "InProgress"
        elif "Assigned" in states:
            resulting_state = "Assigned"
        else:
            resulting_state = None

        return resulting_state

    @classmethod
    def get_object_state(cls, objs):
        """Get lowest state of an object

    Get the lowest possible state of the tasks relevant to one object. States
    are scanned in order: Overdue, InProgress, Finished, Assigned, Verified.

    Args:
      objs: A list of cycle group object tasks, which should all be mapped to
        the same object.

    Returns:
      Name of the lowest state of all active cycle tasks that relate to the
      given objects.
    """
        current_tasks = [task for task in objs if task.cycle.is_current]

        if not current_tasks:
            return None

        today = date.today()
        overdue_tasks = any(task.end_date and task.end_date < today
                            and task.status != "Verified"
                            for task in current_tasks)

        if overdue_tasks:
            return "Overdue"

        return cls._get_state(current_tasks)

    @classmethod
    def get_workflow_state(cls, cycles):
        """Get lowest state of a workflow

    Get the lowest possible state of the tasks relevant to a given workflow.
    States are scanned in order: Overdue, InProgress, Finished, Assigned,
    Verified.

    Args:
      cycles: list of cycles belonging to a single workflow.

    Returns:
      Name of the lowest workflow state, if there are any active cycles.
      Otherwise it returns None.
    """
        current_cycles = [cycle for cycle in cycles if cycle.is_current]

        if not current_cycles:
            return None

        today = date.today()
        for cycle in current_cycles:
            for task in cycle.cycle_task_group_object_tasks:
                if (task.status != "Verified" and task.end_date is not None
                        and task.end_date < today):
                    return "Overdue"

        return cls._get_state(current_cycles)

    @computed_property
    def workflow_state(self):
        return WorkflowState.get_object_state(
            self.cycle_task_group_object_tasks)
Ejemplo n.º 11
0
class Workflow(mixins.CustomAttributable, HasOwnContext, mixins.Timeboxed,
               mixins.Described, mixins.Titled, mixins.Slugged,
               mixins.Stateful, mixins.Base, db.Model):
    """Basic Workflow first class object.
  """
    __tablename__ = 'workflows'
    _title_uniqueness = False

    VALID_STATES = [u"Draft", u"Active", u"Inactive"]

    VALID_FREQUENCIES = [
        "one_time", "weekly", "monthly", "quarterly", "annually"
    ]

    @classmethod
    def default_frequency(cls):
        return 'one_time'

    @orm.validates('frequency')
    def validate_frequency(self, _, value):
        """Make sure that value is listed in valid frequencies.

    Args:
      value: A string value for requested frequency

    Returns:
      default_frequency which is 'one_time' if the value is None, or the value
      itself.

    Raises:
      Value error, if the value is not None or in the VALID_FREQUENCIES array.
    """
        if value is None:
            value = self.default_frequency()
        if value not in self.VALID_FREQUENCIES:
            message = u"Invalid state '{}'".format(value)
            raise ValueError(message)
        return value

    notify_on_change = deferred(
        db.Column(db.Boolean, default=False, nullable=False), 'Workflow')
    notify_custom_message = deferred(db.Column(db.Text, nullable=True),
                                     'Workflow')

    frequency = deferred(
        db.Column(db.String, nullable=True, default=default_frequency),
        'Workflow')

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

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

    workflow_people = db.relationship('WorkflowPerson',
                                      backref='workflow',
                                      cascade='all, delete-orphan')
    people = association_proxy('workflow_people', 'person', 'WorkflowPerson')

    task_groups = db.relationship('TaskGroup',
                                  backref='workflow',
                                  cascade='all, delete-orphan')

    cycles = db.relationship('Cycle',
                             backref='workflow',
                             cascade='all, delete-orphan')

    next_cycle_start_date = db.Column(db.Date, nullable=True)

    non_adjusted_next_cycle_start_date = db.Column(db.Date, nullable=True)

    # this is an indicator if the workflow exists from before the change where
    # we deleted cycle objects, which changed how the cycle is created and
    # how objects are mapped to the cycle tasks
    is_old_workflow = deferred(
        db.Column(db.Boolean, default=False, nullable=True), 'Workflow')

    # This column needs to be deferred because one of the migrations
    # uses Workflow as a model and breaks since at that point in time
    # there is no 'kind' column yet
    kind = deferred(db.Column(db.String, default=None, nullable=True),
                    'Workflow')

    @computed_property
    def workflow_state(self):
        return WorkflowState.get_workflow_state(self.cycles)

    _sanitize_html = [
        'notify_custom_message',
    ]

    _publish_attrs = [
        'workflow_people',
        reflection.PublishOnly('people'), 'task_groups', 'frequency',
        'notify_on_change', 'notify_custom_message', 'cycles',
        'object_approval', 'recurrences',
        reflection.PublishOnly('next_cycle_start_date'),
        reflection.PublishOnly('non_adjusted_next_cycle_start_date'),
        reflection.PublishOnly('workflow_state'),
        reflection.PublishOnly('kind')
    ]

    _aliases = {
        "frequency": {
            "display_name": "Frequency",
            "mandatory": True,
        },
        "notify_custom_message": "Custom email message",
        "notify_on_change": "Force real-time email updates",
        "workflow_owner": {
            "display_name": "Manager",
            "type": reflection.AttributeInfo.Type.USER_ROLE,
            "mandatory": True,
            "filter_by": "_filter_by_workflow_owner",
        },
        "workflow_member": {
            "display_name": "Member",
            "type": reflection.AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_workflow_member",
        },
        "workflow_mapped": {
            "display_name": "No Access",
            "type": reflection.AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_no_access",
        },
        "status": None,
        "start_date": None,
        "end_date": None,
    }

    @classmethod
    def _filter_by_workflow_owner(cls, predicate):
        return cls._filter_by_role("WorkflowOwner", predicate)

    @classmethod
    def _filter_by_workflow_member(cls, predicate):
        return cls._filter_by_role("WorkflowMember", predicate)

    @classmethod
    def _filter_by_no_access(cls, predicate):
        """Get query that filters workflows with mapped users.

    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 workflows by no_access users.
    """
        is_no_access = not_(
            UserRole.query.filter((UserRole.person_id == Person.id)
                                  & (UserRole.context_id == workflow_person.
                                     WorkflowPerson.context_id)).exists())
        return workflow_person.WorkflowPerson.query.filter(
            (cls.id == workflow_person.WorkflowPerson.workflow_id)
            & is_no_access).join(Person).filter(
                (predicate(Person.name) | predicate(Person.email))).exists()

    def copy(self, _other=None, **kwargs):
        """Create a partial copy of the current workflow.
    """
        columns = [
            'title', 'description', 'notify_on_change',
            'notify_custom_message', 'frequency', 'end_date', 'start_date'
        ]
        target = self.copy_into(_other, columns, **kwargs)
        return target

    def copy_task_groups(self, target, **kwargs):
        """Copy all task groups and tasks mapped to this workflow.
    """
        for task_group in self.task_groups:
            obj = task_group.copy(
                workflow=target,
                context=target.context,
                clone_people=kwargs.get("clone_people", False),
                clone_objects=kwargs.get("clone_objects", False),
                modified_by=get_current_user(),
            )
            target.task_groups.append(obj)

            if kwargs.get("clone_tasks"):
                task_group.copy_tasks(
                    obj,
                    clone_people=kwargs.get("clone_people", False),
                    clone_objects=kwargs.get("clone_objects", True))

        return target

    @classmethod
    def eager_query(cls):
        return super(Workflow, cls).eager_query().options(
            orm.subqueryload('cycles').undefer_group('Cycle_complete').
            subqueryload("cycle_task_group_object_tasks").undefer_group(
                "CycleTaskGroupObjectTask_complete"),
            orm.subqueryload('task_groups'),
            orm.subqueryload('workflow_people'),
        )

    @classmethod
    def ensure_backlog_workflow_exists(cls):
        """Ensures there is at least one backlog workflow with an active cycle.
    If such workflow does not exist it creates one."""
        def any_active_cycle(workflows):
            """Checks if any active cycle exists from given workflows"""
            for workflow in workflows:
                for cur_cycle in workflow.cycles:
                    if cur_cycle.is_current:
                        return True
            return False

        # Check if backlog workflow already exists
        backlog_workflows = Workflow.query\
                                    .filter(and_
                                            (Workflow.kind == "Backlog",
                                             Workflow.frequency == "one_time"))\
                                    .all()

        if len(backlog_workflows) > 0 and any_active_cycle(backlog_workflows):
            return "At least one backlog workflow already exists"
        # Create a backlog workflow
        backlog_workflow = Workflow(description="Backlog workflow",
                                    title="Backlog (one time)",
                                    frequency="one_time",
                                    status="Active",
                                    recurrences=0,
                                    kind="Backlog")

        # create wf context
        wf_ctx = backlog_workflow.get_or_create_object_context(context=1)
        backlog_workflow.context = wf_ctx
        db.session.flush(backlog_workflow)
        # create a cycle
        backlog_cycle = cycle.Cycle(
            description="Backlog workflow",
            title="Backlog (one time)",
            is_current=1,
            status="Assigned",
            start_date=None,
            end_date=None,
            context=backlog_workflow.get_or_create_object_context(),
            workflow=backlog_workflow)

        # create a cycletaskgroup
        backlog_ctg = cycle_task_group\
            .CycleTaskGroup(description="Backlog workflow taskgroup",
                            title="Backlog TaskGroup",
                            cycle=backlog_cycle,
                            status="InProgress",
                            start_date=None,
                            end_date=None,
                            context=backlog_workflow
                            .get_or_create_object_context())
        db.session.add_all([backlog_workflow, backlog_cycle, backlog_ctg])
        db.session.flush()

        # add fulltext entries
        get_indexer().create_record(fts_record_for(backlog_workflow))
        get_indexer().create_record(fts_record_for(backlog_cycle))
        get_indexer().create_record(fts_record_for(backlog_ctg))
        return "Backlog workflow created"
Ejemplo n.º 12
0
class Documentable(object):
    """Base class for EvidenceURL mixin"""
    @declared_attr
    def object_documents(cls):
        """Returns all the associated documents"""
        cls.documents = association_proxy(
            'object_documents',
            'document',
            creator=lambda document: ObjectDocument(
                document=document,
                documentable_type=cls.__name__,
            ))
        joinstr = (
            'and_(foreign(ObjectDocument.documentable_id) == {type}.id, '
            '     foreign(ObjectDocument.documentable_type) == "{type}")')
        joinstr = joinstr.format(type=cls.__name__)
        return db.relationship(
            'ObjectDocument',
            primaryjoin=joinstr,
            backref='{0}_documentable'.format(cls.__name__),
            cascade='all, delete-orphan',
        )

    _publish_attrs = [
        reflection.PublishOnly('documents'),
        'object_documents',
    ]

    _include_links = [
        # 'object_documents',
    ]

    @classmethod
    def eager_query(cls):
        query = super(Documentable, cls).eager_query()
        return cls.eager_inclusions(query,
                                    Documentable._include_links).options(
                                        orm.subqueryload('object_documents'))

    def log_json(self):
        """Serialize to JSON"""
        out_json = super(Documentable, self).log_json()
        if hasattr(self, "documents"):
            out_json["documents"] = [
                # pylint: disable=not-an-iterable
                create_stub(doc) for doc in self.documents if doc
            ]
        if hasattr(self, "object_documents"):
            out_json["object_documents"] = [
                # pylint: disable=not-an-iterable
                create_stub(doc) for doc in self.object_documents if doc
            ]
        return out_json

    @classmethod
    def indexed_query(cls):
        query = super(Documentable, cls).indexed_query()
        return query.options(
            orm.subqueryload("object_documents").load_only(
                "id",
                "documentable_id",
                "documentable_type",
            ))
Ejemplo n.º 13
0
class CycleTaskGroupObjectTask(mixins.WithContact,
                               wf_mixins.CycleTaskStatusValidatedMixin,
                               mixins.Stateful, mixins.Timeboxed,
                               relationship.Relatable, mixins.Notifiable,
                               mixins.Described, mixins.Titled, mixins.Slugged,
                               mixins.Base, ft_mixin.Indexed, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'

    readable_name_alias = 'cycle task'

    _title_uniqueness = False

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

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

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

    PROPERTY_TEMPLATE = u"task {}"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @classmethod
    def _filter_by_cycle_task_group(cls, predicate):
        """Get query that filters cycle tasks by related cycle task groups.

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

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

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

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

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

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