Ejemplo n.º 1
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'
  MENU = 'menu'
  CHECKBOX = 'checkbox'
  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

  # 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
    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),
  )
  _sanitize_html = []
  _aliases = {
      "title": "Summary",
      "description": {
          "display_name": "Task Description",
          "handler_key": "task_description",
      },
      "start_date": {
          "display_name": "Start Date",
          "mandatory": True,
          "description": (
              "Enter the task start date\nin the following format:\n"
              "'mm/dd/yyyy'"
          ),
      },
      "end_date": {
          "display_name": "End Date",
          "mandatory": True,
          "description": (
              "Enter the task end date\nin the following format:\n"
              "'mm/dd/yyyy'"
          ),
      },
      "task_group": {
          "display_name": "Task Group",
          "mandatory": True,
          "filter_by": "_filter_by_task_group",
      },
      "task_type": {
          "display_name": "Task Type",
          "mandatory": True,
          "description": ("Accepted values are:"
                          "\n'Rich Text'\n'Dropdown'\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)

  @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)
Ejemplo n.º 2
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

    _publish_attrs = [
        '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
class CycleTaskGroupObjectTask(WithContact, Stateful, Timeboxed, Relatable,
                               Notifiable, Described, Titled, Slugged, Base,
                               Indexed, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'
    _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"),
        )
Ejemplo n.º 4
0
class CycleTaskGroupObjectTask(WithContact, Stateful, Slugged, Timeboxed,
                               Relatable, Described, Titled, Base, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'
    _title_uniqueness = False

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

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

    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,
            "filter_by": "_filter_by_contact",
        },
        "secondary_contact": None,
        "start_date": "Start Date",
        "end_date": "End Date",
        "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,
        },
    }

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