class CycleTaskGroup(mixins.WithContact, mixins.Stateful, mixins.Slugged, mixins.Timeboxed, mixins.Described, mixins.Titled, mixins.Base, index_mixin.Indexed, db.Model): """Cycle Task Group model. """ __tablename__ = 'cycle_task_groups' _title_uniqueness = False @classmethod def generate_slug_prefix_for(cls, obj): # pylint: disable=unused-argument 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 = [ attributes.MultipleSubpropertyFullTextAttr( "task title", 'cycle_task_group_tasks', ["title"], False ), attributes.MultipleSubpropertyFullTextAttr( "task assignee", lambda instance: [t.contact for t in instance.cycle_task_group_tasks], ["name", "email"], False ), attributes.DateMultipleSubpropertyFullTextAttr( "task due date", "cycle_task_group_tasks", ["end_date"], False ), attributes.DateFullTextAttr("due date", 'next_due_date',), attributes.FullTextAttr("assignee", "contact", ['name', 'email']), attributes.FullTextAttr("cycle title", 'cycle', ['title'], False), attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact, ['email', 'name'], False), attributes.DateFullTextAttr("cycle due date", lambda x: x.cycle.next_due_date, with_template=False), attributes.MultipleSubpropertyFullTextAttr( "task comments", lambda instance: itertools.chain(*[ t.cycle_task_entries for t in instance.cycle_task_group_tasks ]), ["description"], False ), ] AUTO_REINDEX_RULES = [ index_mixin.ReindexRule( "CycleTaskGroupObjectTask", lambda x: x.cycle_task_group ), index_mixin.ReindexRule( "Person", _query_filtered_by_contact ), index_mixin.ReindexRule( "Person", lambda x: [i.cycle for i in _query_filtered_by_contact(x)] ), ] @classmethod def _filter_by_cycle(cls, predicate): """Get query that filters cycle task groups. Args: predicate: lambda function that excepts a single parameter and returns true of false. Returns: An sqlalchemy query that evaluates to true or false and can be used in filtering cycle task groups by related cycle. """ return Cycle.query.filter( (Cycle.id == cls.cycle_id) & (predicate(Cycle.slug) | predicate(Cycle.title)) ).exists() @classmethod def indexed_query(cls): return super(CycleTaskGroup, cls).indexed_query().options( orm.Load(cls).load_only( "next_due_date", ), orm.Load(cls).subqueryload("cycle_task_group_tasks").load_only( "id", "title", "end_date" ), orm.Load(cls).joinedload("cycle").load_only( "id", "title", "next_due_date" ), orm.Load(cls).subqueryload("cycle_task_group_tasks").joinedload( "contact" ).load_only( "email", "name", "id" ), orm.Load(cls).subqueryload("cycle_task_group_tasks").joinedload( "cycle_task_entries" ).load_only( "description", "id" ), orm.Load(cls).joinedload("cycle").joinedload( "contact" ).load_only( "email", "name", "id" ), orm.Load(cls).joinedload("contact").load_only( "email", "name", "id" ), ) @classmethod def eager_query(cls): """Add cycle tasks and objects to cycle task group eager query. Make sure we load all cycle task group relevant data in a single query. Returns: a query object with cycle_task_group_tasks added to joined load options. """ query = super(CycleTaskGroup, cls).eager_query() return query.options( orm.joinedload('cycle_task_group_tasks') )
class CycleTaskGroup(roleable.Roleable, relationship.Relatable, mixins.WithContact, wf_mixins.CycleTaskGroupRelatedStatusValidatedMixin, mixins.Slugged, mixins.Timeboxed, mixins.Described, mixins.Titled, base.ContextRBAC, mixins.Base, index_mixin.Indexed, db.Model): """Cycle Task Group model. """ __tablename__ = 'cycle_task_groups' _title_uniqueness = False @classmethod def generate_slug_prefix(cls): # pylint: disable=unused-argument return "CYCLEGROUP" cycle_id = db.Column( db.Integer, db.ForeignKey('cycles.id', ondelete="CASCADE"), nullable=False, ) task_group_id = db.Column(db.Integer, db.ForeignKey('task_groups.id'), nullable=True) cycle_task_group_tasks = db.relationship('CycleTaskGroupObjectTask', backref='_cycle_task_group', cascade='all, delete-orphan') sort_index = db.Column(db.String(length=250), default="", nullable=False) next_due_date = db.Column(db.Date) _api_attrs = reflection.ApiAttributes('cycle', 'task_group', 'cycle_task_group_tasks', 'sort_index', 'next_due_date') _aliases = { "cycle": { "display_name": "Cycle", "filter_by": "_filter_by_cycle", }, } PROPERTY_TEMPLATE = u"group {}" _fulltext_attrs = [ attributes.DateFullTextAttr( "due date", 'next_due_date', ), attributes.FullTextAttr("assignee", "contact", ['email', 'name']), attributes.FullTextAttr("cycle title", 'cycle', ['title'], False), attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact, ['email', 'name'], False), attributes.DateFullTextAttr("cycle due date", lambda x: x.cycle.next_due_date, with_template=False), attributes.MultipleSubpropertyFullTextAttr("task title", "cycle_task_group_tasks", ["title"], False), attributes.MultipleSubpropertyFullTextAttr("task assignees", "_task_assignees", ["email", "name"], False), attributes.MultipleSubpropertyFullTextAttr("task state", "cycle_task_group_tasks", ["status"], False), attributes.MultipleSubpropertyFullTextAttr( "task secondary assignees", "_task_secondary_assignees", ["email", "name"], False), attributes.DateMultipleSubpropertyFullTextAttr( "task due date", "cycle_task_group_tasks", ["end_date"], False), attributes.MultipleSubpropertyFullTextAttr( "task comment", lambda instance: itertools.chain( *[t.comments for t in instance.cycle_task_group_tasks]), ["description"], False), ] # This parameter is overridden by cycle backref, but is here to ensure # pylint does not complain _cycle = None @hybrid.hybrid_property def cycle(self): """Getter for cycle foreign key.""" return self._cycle @cycle.setter def cycle(self, cycle): """Set cycle foreign key and relationship.""" if not self._cycle and cycle: relationship.Relationship(source=cycle, destination=self) self._cycle = cycle @property def workflow(self): """Property which returns parent workflow object.""" return self.cycle.workflow @property def _task_assignees(self): """Property. Return the list of persons as assignee of related tasks.""" people = set() for ctask in self.cycle_task_group_tasks: people.update(ctask.get_persons_for_rolename("Task Assignees")) return list(people) @property def _task_secondary_assignees(self): """Property. Returns people list as Secondary Assignee of related tasks.""" people = set() for ctask in self.cycle_task_group_tasks: people.update( ctask.get_persons_for_rolename("Task Secondary Assignees")) return list(people) AUTO_REINDEX_RULES = [ index_mixin.ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group), index_mixin.ReindexRule("Person", _query_filtered_by_contact), index_mixin.ReindexRule( "Person", lambda x: [i.cycle for i in _query_filtered_by_contact(x)]), ] @classmethod def _filter_by_cycle(cls, predicate): """Get query that filters cycle task groups. Args: predicate: lambda function that excepts a single parameter and returns true of false. Returns: An sqlalchemy query that evaluates to true or false and can be used in filtering cycle task groups by related cycle. """ return Cycle.query.filter((Cycle.id == cls.cycle_id) & (predicate(Cycle.slug) | predicate(Cycle.title))).exists() @classmethod def indexed_query(cls): return super(CycleTaskGroup, cls).indexed_query().options( orm.Load(cls).load_only("next_due_date", ), orm.Load(cls).subqueryload("cycle_task_group_tasks").load_only( "id", "title", "end_date", "status", ), orm.Load(cls).subqueryload("cycle_task_group_tasks").subqueryload( "_access_control_list", ).load_only( "ac_role_id", ).subqueryload( "access_control_people", ).load_only("person_id", ), orm.Load(cls).subqueryload("cycle_task_group_tasks"), orm.Load(cls).joinedload("cycle").load_only( "id", "title", "next_due_date", ), orm.Load(cls).joinedload("cycle").joinedload("contact").load_only( "name", "email", "id", ), orm.Load(cls).joinedload("contact").load_only( "name", "email", "id", ), ) @classmethod def eager_query(cls): """Add cycle tasks and objects to cycle task group eager query. Make sure we load all cycle task group relevant data in a single query. Returns: a query object with cycle_task_group_tasks added to joined load options. """ query = super(CycleTaskGroup, cls).eager_query() return query.options( orm.subqueryload("cycle_task_group_tasks"), orm.joinedload("cycle").undefer_group("Cycle_complete"), orm.joinedload("cycle").joinedload("contact"))
class Cycle(roleable.Roleable, relationship.Relatable, mixins.WithContact, wf_mixins.CycleStatusValidatedMixin, mixins.Timeboxed, mixins.Described, mixins.Titled, base.ContextRBAC, mixins.Slugged, mixins.Notifiable, ft_mixin.Indexed, db.Model): """Workflow Cycle model """ # pylint: disable=too-many-instance-attributes __tablename__ = 'cycles' _title_uniqueness = False workflow_id = db.Column( db.Integer, db.ForeignKey('workflows.id', ondelete="CASCADE"), nullable=False, ) cycle_task_groups = db.relationship('CycleTaskGroup', backref='_cycle', cascade='all, delete-orphan') cycle_task_group_object_tasks = db.relationship( 'CycleTaskGroupObjectTask', backref='cycle', cascade='all, delete-orphan') is_current = db.Column(db.Boolean, default=True, nullable=False) next_due_date = db.Column(db.Date) # This parameter is overridden by workflow backref, but is here to ensure # pylint does not complain _workflow = None @hybrid.hybrid_property def workflow(self): """Getter for workflow foreign key.""" return self._workflow @workflow.setter def workflow(self, workflow): """Set workflow foreign key and relationship.""" if not self._workflow and workflow: relationship.Relationship(source=workflow, destination=self) self._workflow = workflow @property def is_done(self): """Check if cycle's done Overrides StatusValidatedMixin method because cycle's is_done state depends on is_verification_needed flag """ if super(Cycle, self).is_done: return True if self.cycle_task_group_object_tasks: return False return True @builder.simple_property def folder(self): """Get the workflow folder.""" if self.workflow: return self.workflow.folder return "" _api_attrs = reflection.ApiAttributes( 'workflow', 'cycle_task_groups', 'is_current', 'next_due_date', reflection.Attribute('folder', create=False, update=False), ) _aliases = { "cycle_workflow": { "display_name": "Workflow", "filter_by": "_filter_by_cycle_workflow", }, "contact": "Assignee", "secondary_contact": None, } PROPERTY_TEMPLATE = u"cycle {}" _fulltext_attrs = [ "folder", ft_attributes.DateFullTextAttr("due date", "next_due_date"), ft_attributes.MultipleSubpropertyFullTextAttr("group title", "cycle_task_groups", ["title"], False), ft_attributes.MultipleSubpropertyFullTextAttr( "group assignee", lambda instance: [g.contact for g in instance.cycle_task_groups], ["email", "name"], False), ft_attributes.DateMultipleSubpropertyFullTextAttr( "group due date", 'cycle_task_groups', ["next_due_date"], False), ft_attributes.MultipleSubpropertyFullTextAttr( "task title", 'cycle_task_group_object_tasks', ["title"], False), ft_attributes.MultipleSubpropertyFullTextAttr( "task state", 'cycle_task_group_object_tasks', ["status"], False), ft_attributes.DateMultipleSubpropertyFullTextAttr( "task due date", "cycle_task_group_object_tasks", ["end_date"], False), ft_attributes.MultipleSubpropertyFullTextAttr("task assignees", "_task_assignees", ["name", "email"], False), ft_attributes.MultipleSubpropertyFullTextAttr( "task secondary assignees", "_task_secondary_assignees", ["name", "email"], False), ft_attributes.MultipleSubpropertyFullTextAttr( "task comment", lambda instance: itertools.chain( *[t.comments for t in instance.cycle_task_group_object_tasks]), ["description"], False), ] @property def _task_assignees(self): """Property. Return the list of persons as assignee of related tasks.""" people = set() for ctask in self.cycle_task_group_object_tasks: people.update(ctask.get_persons_for_rolename("Task Assignees")) return list(people) @property def _task_secondary_assignees(self): """Property. Returns people list as Secondary Assignee of related tasks.""" people = set() for ctask in self.cycle_task_group_object_tasks: people.update( ctask.get_persons_for_rolename("Task Secondary Assignees")) return list(people) AUTO_REINDEX_RULES = [ ft_mixin.ReindexRule("CycleTaskGroup", lambda x: x.cycle), ft_mixin.ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group.cycle), ft_mixin.ReindexRule("Person", _query_filtered_by_contact), ] @classmethod def _filter_by_cycle_workflow(cls, predicate): """Filter by cycle workflow.""" 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, **kwargs): """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(**kwargs) return query.options( orm.joinedload('cycle_task_groups'), orm.Load(cls).joinedload("workflow").undefer_group( "Workflow_complete"), ) @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( "end_date", "id", "status", "title", ), orm.Load(cls).subqueryload("cycle_task_groups").load_only( "id", "title", "next_due_date", ), orm.Load(cls).subqueryload( "cycle_task_group_object_tasks", ).subqueryload( "_access_control_list").load_only( "ac_role_id", ).subqueryload( "access_control_people").load_only("person_id", ), orm.Load(cls).subqueryload("cycle_task_group_object_tasks"), orm.Load(cls).subqueryload("cycle_task_groups").joinedload( "contact").load_only( "name", "email", "id", ), orm.Load(cls).joinedload("workflow").undefer_group( "Workflow_complete", ), ) def _get_cycle_url(self, widget_name): return urljoin( get_url_root(), "workflows/{workflow_id}#{widget_name}/cycle/{cycle_id}".format( workflow_id=self.workflow.id, cycle_id=self.id, widget_name=widget_name)) @property def cycle_url(self): return self._get_cycle_url("current") @property def cycle_inactive_url(self): return self._get_cycle_url("history") def log_json(self): out_json = super(Cycle, self).log_json() out_json["folder"] = self.folder return out_json
class Cycle(mixins.WithContact, wf_mixins.CycleStatusValidatedMixin, mixins.Timeboxed, mixins.Described, mixins.Titled, mixins.Slugged, mixins.Notifiable, ft_mixin.Indexed, db.Model): """Workflow Cycle model """ __tablename__ = 'cycles' _title_uniqueness = False workflow_id = db.Column( db.Integer, db.ForeignKey('workflows.id', ondelete="CASCADE"), nullable=False, ) cycle_task_groups = db.relationship( 'CycleTaskGroup', backref='cycle', cascade='all, delete-orphan') cycle_task_group_object_tasks = db.relationship( 'CycleTaskGroupObjectTask', backref='cycle', cascade='all, delete-orphan') cycle_task_entries = db.relationship( 'CycleTaskEntry', backref='cycle', cascade='all, delete-orphan') is_current = db.Column(db.Boolean, default=True, nullable=False) next_due_date = db.Column(db.Date) @property def is_done(self): """Check if cycle's done Overrides StatusValidatedMixin method because cycle's is_done state depends on is_verification_needed flag """ if super(Cycle, self).is_done: return True if self.cycle_task_group_object_tasks: return False return True _api_attrs = reflection.ApiAttributes( 'workflow', 'cycle_task_groups', 'is_current', 'next_due_date', ) _aliases = { "cycle_workflow": { "display_name": "Workflow", "filter_by": "_filter_by_cycle_workflow", }, "contact": "Assignee", "secondary_contact": None, } PROPERTY_TEMPLATE = u"cycle {}" _fulltext_attrs = [ ft_attributes.MultipleSubpropertyFullTextAttr( "group title", "cycle_task_groups", ["title"], False, ), ft_attributes.MultipleSubpropertyFullTextAttr( "group assignee", lambda instance: [g.contact for g in instance.cycle_task_groups], ["name", "email"], False, ), ft_attributes.DateMultipleSubpropertyFullTextAttr( "group due date", 'cycle_task_groups', ["next_due_date"], False, ), ft_attributes.MultipleSubpropertyFullTextAttr( "task title", 'cycle_task_group_object_tasks', ["title"], False, ), ft_attributes.MultipleSubpropertyFullTextAttr( "task assignee", lambda instance: [t.contact for t in instance.cycle_task_group_object_tasks], ["name", "email"], False ), ft_attributes.DateMultipleSubpropertyFullTextAttr( "task due date", "cycle_task_group_object_tasks", ["end_date"], False ), ft_attributes.DateFullTextAttr("due date", "next_due_date"), ft_attributes.MultipleSubpropertyFullTextAttr( "task comments", lambda instance: list(itertools.chain(*[ t.cycle_task_entries for t in instance.cycle_task_group_object_tasks ])), ["description"], False ), ] AUTO_REINDEX_RULES = [ ft_mixin.ReindexRule("CycleTaskGroup", lambda x: x.cycle), ft_mixin.ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group.cycle), ft_mixin.ReindexRule("Person", _query_filtered_by_contact) ] @classmethod def _filter_by_cycle_workflow(cls, predicate): from ggrc_workflows.models.workflow import Workflow return Workflow.query.filter( (Workflow.id == cls.workflow_id) & (predicate(Workflow.slug) | predicate(Workflow.title)) ).exists() @classmethod def eager_query(cls): """Add cycle task groups to cycle eager query This function adds cycle_task_groups as a join option when fetching cycles, and makes sure we fetch all cycle related data needed for generating cycle json, in one query. Returns: a query object with cycle_task_groups added to joined load options. """ query = super(Cycle, cls).eager_query() return query.options( orm.joinedload('cycle_task_groups'), ) @classmethod def indexed_query(cls): return super(Cycle, cls).indexed_query().options( orm.Load(cls).load_only("next_due_date"), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").load_only( "id", "title", "end_date" ), orm.Load(cls).subqueryload("cycle_task_groups").load_only( "id", "title", "end_date", "next_due_date", ), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload( "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" ), )