class Cycle(WithContact, Stateful, Timeboxed, Described, Titled, Slugged, Notifiable, Indexed, db.Model): """Workflow Cycle model """ __tablename__ = 'cycles' _title_uniqueness = False VALID_STATES = (u'Assigned', u'InProgress', u'Finished', u'Verified') workflow_id = db.Column( db.Integer, db.ForeignKey('workflows.id', ondelete="CASCADE"), nullable=False, ) cycle_task_groups = db.relationship( 'CycleTaskGroup', backref='cycle', cascade='all, delete-orphan') cycle_task_group_object_tasks = db.relationship( 'CycleTaskGroupObjectTask', backref='cycle', cascade='all, delete-orphan') cycle_task_entries = db.relationship( 'CycleTaskEntry', backref='cycle', cascade='all, delete-orphan') is_current = db.Column(db.Boolean, default=True, nullable=False) next_due_date = db.Column(db.Date) _publish_attrs = [ 'workflow', 'cycle_task_groups', 'is_current', 'next_due_date', ] _aliases = { "cycle_workflow": { "display_name": "Workflow", "filter_by": "_filter_by_cycle_workflow", }, "status": { "display_name": "State", "mandatory": False, "description": "Options are: \n{} ".format('\n'.join(VALID_STATES)) }, "contact": "Assignee", "secondary_contact": None, } PROPERTY_TEMPLATE = u"cycle {}" _fulltext_attrs = [ MultipleSubpropertyFullTextAttr( "group title", "cycle_task_groups", ["title"], False, ), MultipleSubpropertyFullTextAttr( "group assignee", lambda instance: [g.contact for g in instance.cycle_task_groups], ["name", "email"], False, ), DateMultipleSubpropertyFullTextAttr( "group due date", 'cycle_task_groups', ["next_due_date"], False, ), MultipleSubpropertyFullTextAttr( "task title", 'cycle_task_group_object_tasks', ["title"], False, ), MultipleSubpropertyFullTextAttr( "task assignee", lambda instance: [t.contact for t in instance.cycle_task_group_object_tasks], ["name", "email"], False ), DateMultipleSubpropertyFullTextAttr( "task due date", "cycle_task_group_object_tasks", ["end_date"], False ), DateFullTextAttr("due date", "next_due_date"), MultipleSubpropertyFullTextAttr( "task comments", lambda instance: list(itertools.chain(*[ t.cycle_task_entries for t in instance.cycle_task_group_object_tasks ])), ["description"], False ), ] AUTO_REINDEX_RULES = [ ReindexRule("CycleTaskGroup", lambda x: x.cycle), ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group.cycle), ReindexRule("Person", _query_filtered_by_contact) ] @classmethod def _filter_by_cycle_workflow(cls, predicate): from ggrc_workflows.models.workflow import Workflow return Workflow.query.filter( (Workflow.id == cls.workflow_id) & (predicate(Workflow.slug) | predicate(Workflow.title)) ).exists() @classmethod def eager_query(cls): """Add cycle task groups to cycle eager query This function adds cycle_task_groups as a join option when fetching cycles, and makes sure we fetch all cycle related data needed for generating cycle json, in one query. Returns: a query object with cycle_task_groups added to joined load options. """ query = super(Cycle, cls).eager_query() return query.options( orm.joinedload('cycle_task_groups'), ) @classmethod def indexed_query(cls): return super(Cycle, cls).indexed_query().options( orm.Load(cls).load_only("next_due_date"), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").load_only( "id", "title", "end_date" ), orm.Load(cls).subqueryload("cycle_task_groups").load_only( "id", "title", "end_date", "next_due_date", ), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload( "contact" ).load_only( "email", "name", "id" ), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload( "cycle_task_entries" ).load_only( "description", "id" ), orm.Load(cls).subqueryload("cycle_task_groups").joinedload( "contact" ).load_only( "email", "name", "id" ), orm.Load(cls).joinedload("contact").load_only( "email", "name", "id" ), )
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 CycleTaskGroup(WithContact, Stateful, Slugged, Timeboxed, Described, Titled, Indexed, Base, db.Model): """Cycle Task Group model. """ __tablename__ = 'cycle_task_groups' _title_uniqueness = False @classmethod def generate_slug_prefix_for(cls, obj): return "CYCLEGROUP" VALID_STATES = (u'Assigned', u'InProgress', u'Finished', u'Verified', u'Declined') cycle_id = db.Column( db.Integer, db.ForeignKey('cycles.id', ondelete="CASCADE"), nullable=False, ) task_group_id = db.Column(db.Integer, db.ForeignKey('task_groups.id'), nullable=True) cycle_task_group_tasks = db.relationship('CycleTaskGroupObjectTask', backref='cycle_task_group', cascade='all, delete-orphan') sort_index = db.Column(db.String(length=250), default="", nullable=False) next_due_date = db.Column(db.Date) _publish_attrs = [ 'cycle', 'task_group', 'cycle_task_group_tasks', 'sort_index', 'next_due_date' ] _aliases = { "cycle": { "display_name": "Cycle", "filter_by": "_filter_by_cycle", }, } PROPERTY_TEMPLATE = u"group {}" _fulltext_attrs = [ MultipleSubpropertyFullTextAttr("task title", 'cycle_task_group_tasks', ["title"], False), MultipleSubpropertyFullTextAttr( "task assignee", lambda instance: [t.contact for t in instance.cycle_task_group_tasks], ["name", "email"], False), DateMultipleSubpropertyFullTextAttr("task due date", "cycle_task_group_tasks", ["end_date"], False), DateFullTextAttr( "due date", 'next_due_date', ), FullTextAttr("assignee", "contact", ['name', 'email']), FullTextAttr("cycle title", 'cycle', ['title'], False), FullTextAttr("cycle assignee", lambda x: x.cycle.contact, ['email', 'name'], False), DateFullTextAttr("cycle due date", lambda x: x.cycle.next_due_date, with_template=False), ] AUTO_REINDEX_RULES = [ ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group), ReindexRule( "Person", lambda x: CycleTaskGroup.query.filter( CycleTaskGroup.contact_id == x.id)), ReindexRule( "Person", lambda x: [ i.cycle for i in CycleTaskGroup.query.filter( CycleTaskGroup.contact_id == x.id) ]), ] @classmethod def _filter_by_cycle(cls, predicate): """Get query that filters cycle task groups. Args: predicate: lambda function that excepts a single parameter and returns true of false. Returns: An sqlalchemy query that evaluates to true or false and can be used in filtering cycle task groups by related cycle. """ return Cycle.query.filter((Cycle.id == cls.cycle_id) & (predicate(Cycle.slug) | predicate(Cycle.title))).exists() @classmethod def eager_query(cls): """Add cycle tasks and objects to cycle task group eager query. Make sure we load all cycle task group relevant data in a single query. Returns: a query object with cycle_task_group_tasks added to joined load options. """ query = super(CycleTaskGroup, cls).eager_query() return query.options(orm.joinedload('cycle_task_group_tasks'))