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