class MysqlRecordProperty(db.Model): __tablename__ = 'fulltext_record_properties' key = db.Column(db.Integer, primary_key=True) type = db.Column(db.String(64), primary_key=True) context_id = db.Column(db.Integer) tags = db.Column(db.String) property = db.Column(db.String(64), primary_key=True) subproperty = db.Column(db.String(64), primary_key=True) content = db.Column(db.Text) @declared_attr def __table_args__(self): return ( # NOTE # This is here to prevent Alembic from wanting to drop the index, but # the DDL below or a similar Alembic migration should be used to create # the index. db.Index('{}_text_idx'.format(self.__tablename__), 'content'), # These are real indexes db.Index('ix_{}_key'.format(self.__tablename__), 'key'), db.Index('ix_{}_type'.format(self.__tablename__), 'type'), db.Index('ix_{}_tags'.format(self.__tablename__), 'tags'), db.Index('ix_{}_context_id'.format(self.__tablename__), 'context_id'), # Only MyISAM supports fulltext indexes until newer MySQL/MariaDB { 'mysql_engine': 'myisam' }, )
class MysqlRecordProperty(db.Model): __tablename__ = 'fulltext_record_properties' __table_args__ = {'mysql_engine': 'myisam'} key = db.Column(db.Integer, primary_key=True) type = db.Column(db.String(64), primary_key=True) tags = db.Column(db.String) property = db.Column(db.String(64), primary_key=True) content = db.Column(db.Text)
class Role(base.ContextRBAC, Base, Described, db.Model): """A user role. All roles have a unique name. This name could be a simple string, an email address, or some other form of string identifier. Example: .. code-block:: python { 'create': ['Program', 'Control'], 'read': ['Program', 'Control'], 'update': ['Program', 'Control'], 'delete': ['Program'], } """ __tablename__ = 'roles' name = db.Column(db.String(128), nullable=False) permissions_json = db.Column(db.Text(), nullable=False) scope = db.Column(db.String(64), nullable=True) role_order = db.Column(db.Integer(), nullable=True) @simple_property def permissions(self): if self.permissions_json == DECLARED_ROLE: declared_role = get_declared_role(self.name) permissions = declared_role.permissions else: permissions = json.loads(self.permissions_json) or {} # make sure not to omit actions for action in ['create', 'read', 'update', 'delete']: if action not in permissions: permissions[action] = [] return permissions @permissions.setter def permissions(self, value): self.permissions_json = json.dumps(value) _api_attrs = reflection.ApiAttributes( 'name', 'permissions', 'scope', 'role_order', ) def _display_name(self): return self.name
class Context(Base, db.Model): """Context class. Sign permissions object for specific user.""" __tablename__ = 'contexts' # This block and the 'description' attrs are taken from the Described mixin # which we do not use for Context because indexing Context descriptions # for fulltext search leads to undesirable results @declared_attr def description(cls): # pylint: disable=no-self-argument return deferred(db.Column(db.Text, nullable=False, default=u""), cls.__name__) name = deferred(db.Column(db.String(128), nullable=True), 'Context') related_object_id = deferred( db.Column(db.Integer(), nullable=True), 'Context') related_object_type = deferred( db.Column(db.String(128), nullable=True), 'Context') @property def related_object_attr(self): return '{0}_related_object'.format(self.related_object_type) @property def related_object(self): return getattr(self, self.related_object_attr) @related_object.setter def related_object(self, value): self.related_object_id = value.id if value is not None else None self.related_object_type = value.__class__.__name__ if value is not None \ else None return setattr(self, self.related_object_attr, value) @staticmethod def _extra_table_args(_): return ( db.Index( 'ix_context_related_object', 'related_object_type', 'related_object_id'), ) _api_attrs = reflection.ApiAttributes('name', 'related_object', 'description') _sanitize_html = ['name', 'description'] _include_links = []
class MysqlRecordProperty(db.Model): """ Db model for collect fulltext index records""" __tablename__ = 'fulltext_record_properties' key = db.Column(db.Integer, primary_key=True) type = db.Column(db.String(64), primary_key=True) tags = db.Column(db.String) property = db.Column(db.String(250), primary_key=True) subproperty = db.Column(db.String(64), primary_key=True) content = db.Column(db.Text, nullable=False, default=u"") @declared_attr def __table_args__(cls): # pylint: disable=no-self-argument return ( db.Index('ix_{}_tags'.format(cls.__tablename__), 'tags'), db.Index('ix_{}_key'.format(cls.__tablename__), 'key'), db.Index('ix_{}_type'.format(cls.__tablename__), 'type'), )
class Context(Base, db.Model): __tablename__ = 'contexts' # This block and the 'description' attrs are taken from the Described mixin # which we do not use for Context because indexing Context descriptions # for fulltext search leads to undesirable results @declared_attr def description(cls): return deferred(db.Column(db.Text), cls.__name__) name = deferred(db.Column(db.String(128), nullable=True), 'Context') related_object_id = deferred( db.Column(db.Integer(), nullable=True), 'Context') related_object_type = deferred( db.Column(db.String(128), nullable=True), 'Context') @property def related_object_attr(self): return '{0}_related_object'.format(self.related_object_type) @property def related_object(self): return getattr(self, self.related_object_attr) @related_object.setter def related_object(self, value): self.related_object_id = value.id if value is not None else None self.related_object_type = value.__class__.__name__ if value is not None \ else None return setattr(self, self.related_object_attr, value) @staticmethod def _extra_table_args(cls): return ( db.Index( 'ix_context_related_object', 'related_object_type', 'related_object_id'), ) _publish_attrs = ['name', 'related_object', 'description'] _sanitize_html = ['name', 'description'] _include_links = []
class MysqlRecordProperty(db.Model): """ Db model for collect fulltext index records""" __tablename__ = 'fulltext_record_properties' key = db.Column(db.Integer, primary_key=True) type = db.Column(db.String(64), primary_key=True) context_id = db.Column(db.Integer) tags = db.Column(db.String) property = db.Column(db.String(250), primary_key=True) subproperty = db.Column(db.String(64), primary_key=True) content = db.Column(db.Text) @declared_attr def __table_args__(self): return ( db.Index('ix_{}_tags'.format(self.__tablename__), 'tags'), db.Index('ix_{}_key'.format(self.__tablename__), 'key'), db.Index('ix_{}_type'.format(self.__tablename__), 'type'), db.Index('ix_{}_context_id'.format(self.__tablename__), 'context_id'), )
class TaskGroup(WithContact, Timeboxed, Described, Titled, Slugged, Indexed, db.Model): """Workflow TaskGroup model.""" __tablename__ = 'task_groups' _title_uniqueness = False workflow_id = db.Column( db.Integer, db.ForeignKey('workflows.id', ondelete="CASCADE"), nullable=False, ) lock_task_order = db.Column(db.Boolean(), nullable=True) task_group_objects = db.relationship('TaskGroupObject', backref='task_group', cascade='all, delete-orphan') objects = association_proxy('task_group_objects', 'object', 'TaskGroupObject') task_group_tasks = db.relationship('TaskGroupTask', backref='task_group', cascade='all, delete-orphan') cycle_task_groups = db.relationship('CycleTaskGroup', backref='task_group') sort_index = db.Column(db.String(length=250), default="", nullable=False) _publish_attrs = [ 'workflow', 'task_group_objects', PublishOnly('objects'), 'task_group_tasks', 'lock_task_order', 'sort_index', # Intentionally do not include `cycle_task_groups` # 'cycle_task_groups', ] _aliases = { "title": "Summary", "description": "Details", "contact": { "display_name": "Assignee", "mandatory": True, }, "secondary_contact": None, "start_date": None, "end_date": None, "workflow": { "display_name": "Workflow", "mandatory": True, "filter_by": "_filter_by_workflow", }, "task_group_objects": { "display_name": "Objects", "type": AttributeInfo.Type.SPECIAL_MAPPING, "filter_by": "_filter_by_objects", }, } def copy(self, _other=None, **kwargs): columns = [ 'title', 'description', 'workflow', 'sort_index', 'modified_by', 'context' ] if kwargs.get('clone_people', False) and getattr(self, "contact"): columns.append("contact") else: kwargs["contact"] = get_current_user() target = self.copy_into(_other, columns, **kwargs) if kwargs.get('clone_objects', False): self.copy_objects(target, **kwargs) if kwargs.get('clone_tasks', False): self.copy_tasks(target, **kwargs) return target def copy_objects(self, target, **kwargs): # pylint: disable=unused-argument for task_group_object in self.task_group_objects: target.task_group_objects.append( task_group_object.copy( task_group=target, context=target.context, )) return target def copy_tasks(self, target, **kwargs): for task_group_task in self.task_group_tasks: target.task_group_tasks.append( task_group_task.copy( None, task_group=target, context=target.context, clone_people=kwargs.get("clone_people", False), )) return target @classmethod def _filter_by_workflow(cls, predicate): from ggrc_workflows.models import Workflow return Workflow.query.filter((Workflow.id == cls.workflow_id) & (predicate(Workflow.slug) | predicate(Workflow.title))).exists() @classmethod def _filter_by_objects(cls, predicate): parts = [] for model_name in all_models.__all__: model = getattr(all_models, model_name) query = getattr(model, "query", None) field = getattr(model, "slug", getattr(model, "email", None)) if query is None or field is None or not hasattr(model, "id"): continue parts.append( query.filter((TaskGroupObject.object_type == model_name) & (model.id == TaskGroupObject.object_id) & predicate(field)).exists()) return TaskGroupObject.query.filter( (TaskGroupObject.task_group_id == cls.id) & or_(*parts)).exists()
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 TaskGroup(roleable.Roleable, relationship.Relatable, WithContact, Timeboxed, Described, Titled, base.ContextRBAC, Slugged, Indexed, db.Model): """Workflow TaskGroup model.""" __tablename__ = 'task_groups' _title_uniqueness = False workflow_id = db.Column( db.Integer, db.ForeignKey('workflows.id', ondelete="CASCADE"), nullable=False, ) lock_task_order = db.Column(db.Boolean(), nullable=True) task_group_objects = db.relationship('TaskGroupObject', backref='_task_group', cascade='all, delete-orphan') objects = association_proxy('task_group_objects', 'object', 'TaskGroupObject') task_group_tasks = db.relationship('TaskGroupTask', backref='_task_group', cascade='all, delete-orphan') cycle_task_groups = db.relationship('CycleTaskGroup', backref='task_group') sort_index = db.Column(db.String(length=250), default="", nullable=False) _api_attrs = reflection.ApiAttributes( 'workflow', 'task_group_objects', reflection.Attribute('objects', create=False, update=False), 'task_group_tasks', 'lock_task_order', 'sort_index', # Intentionally do not include `cycle_task_groups` # 'cycle_task_groups', ) _aliases = { "title": "Summary", "description": "Details", "contact": { "display_name": "Assignee", "mandatory": True, "description": ("One person could be added " "as a Task Group assignee") }, "secondary_contact": None, "start_date": None, "end_date": None, "workflow": { "display_name": "Workflow", "mandatory": True, "filter_by": "_filter_by_workflow", }, "task_group_objects": { "display_name": "Objects", "type": AttributeInfo.Type.SPECIAL_MAPPING, "filter_by": "_filter_by_objects", }, } # 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): """Setter for workflow foreign key.""" if not self._workflow and workflow: all_models.Relationship(source=workflow, destination=self) self._workflow = workflow def ensure_assignee_is_workflow_member(self): # pylint: disable=invalid-name """Add Workflow Member role to user without role in scope of Workflow.""" people_with_role_ids = ( self.workflow.get_person_ids_for_rolename("Admin") + self.workflow.get_person_ids_for_rolename("Workflow Member")) if self.contact.id in people_with_role_ids: return self.workflow.add_person_with_role_name(self.contact, "Workflow Member") def copy(self, _other=None, **kwargs): columns = [ 'title', 'description', 'workflow', 'sort_index', 'modified_by', 'context' ] if kwargs.get('clone_people', False) and getattr(self, "contact"): columns.append("contact") else: kwargs["contact"] = get_current_user() target = self.copy_into(_other, columns, **kwargs) target.ensure_assignee_is_workflow_member() if kwargs.get('clone_objects', False): self.copy_objects(target, **kwargs) if kwargs.get('clone_tasks', False): self.copy_tasks(target, **kwargs) return target def copy_objects(self, target, **kwargs): # pylint: disable=unused-argument for task_group_object in self.task_group_objects: target.task_group_objects.append( task_group_object.copy( task_group=target, context=target.context, )) return target def copy_tasks(self, target, **kwargs): for task_group_task in self.task_group_tasks: target.task_group_tasks.append( task_group_task.copy( None, task_group=target, context=target.context, clone_people=kwargs.get("clone_people", False), )) return target @classmethod def eager_query(cls): query = super(TaskGroup, cls).eager_query() return query.options( orm.Load(cls).subqueryload('task_group_objects'), orm.Load(cls).subqueryload('task_group_tasks')) @classmethod def _filter_by_workflow(cls, predicate): from ggrc_workflows.models import Workflow return Workflow.query.filter((Workflow.id == cls.workflow_id) & (predicate(Workflow.slug) | predicate(Workflow.title))).exists() @classmethod def _filter_by_objects(cls, predicate): parts = [] for model_name in all_models.__all__: model = getattr(all_models, model_name) query = getattr(model, "query", None) field = getattr(model, "slug", getattr(model, "email", None)) if query is None or field is None or not hasattr(model, "id"): continue parts.append( query.filter((TaskGroupObject.object_type == model_name) & (model.id == TaskGroupObject.object_id) & predicate(field)).exists()) return TaskGroupObject.query.filter( (TaskGroupObject.task_group_id == cls.id) & or_(*parts)).exists()
class Audit(Snapshotable, clonable.Clonable, PublicDocumentable, CustomAttributable, Personable, HasOwnContext, Relatable, Timeboxed, WithContact, BusinessObject, Indexed, db.Model): """Audit model.""" __tablename__ = 'audits' _slug_uniqueness = False VALID_STATES = (u'Planned', u'In Progress', u'Manager Review', u'Ready for External Review', u'Completed') CLONEABLE_CHILDREN = {"AssessmentTemplate"} report_start_date = deferred(db.Column(db.Date), 'Audit') report_end_date = deferred(db.Column(db.Date), 'Audit') audit_firm_id = deferred( db.Column(db.Integer, db.ForeignKey('org_groups.id')), 'Audit') audit_firm = db.relationship('OrgGroup', uselist=False) gdrive_evidence_folder = deferred(db.Column(db.String), 'Audit') program_id = deferred( db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False), 'Audit') audit_objects = db.relationship('AuditObject', backref='audit', cascade='all, delete-orphan') object_type = db.Column(db.String(length=250), nullable=False, default='Control') assessments = db.relationship('Assessment', backref='audit') issues = db.relationship('Issue', backref='audit') archived = deferred(db.Column(db.Boolean, nullable=False, default=False), 'Audit') _api_attrs = reflection.ApiAttributes( 'report_start_date', 'report_end_date', 'audit_firm', 'gdrive_evidence_folder', 'program', 'object_type', 'archived', reflection.Attribute('audit_objects', create=False, update=False), ) _fulltext_attrs = [ 'archived', 'report_start_date', 'report_end_date', 'audit_firm', 'gdrive_evidence_folder', ] @classmethod def indexed_query(cls): return super(Audit, cls).indexed_query().options( orm.Load(cls).undefer_group("Audit_complete", ), ) _sanitize_html = [ 'gdrive_evidence_folder', 'description', ] _include_links = [] _aliases = { "program": { "display_name": "Program", "filter_by": "_filter_by_program", "mandatory": True, }, "user_role:Auditor": { "display_name": "Auditors", "type": AttributeInfo.Type.USER_ROLE, "filter_by": "_filter_by_auditor", }, "start_date": "Planned Start Date", "end_date": "Planned End Date", "report_start_date": "Planned Report Period from", "report_end_date": "Planned Report Period to", "contact": { "display_name": "Audit Captain", "mandatory": True, }, "secondary_contact": None, "notes": None, "reference_url": None, "archived": { "display_name": "Archived", "mandatory": False }, "status": { "display_name": "Status", "mandatory": True, "description": "Options are:\n{}".format('\n'.join(VALID_STATES)) } } def _clone(self, source_object): """Clone audit and all relevant attributes. Keeps the internals of actual audit cloning and everything that is related to audit itself (auditors, audit firm, context setting, custom attribute values, etc.) """ from ggrc_basic_permissions import create_audit_context data = { "title": source_object.generate_attribute("title"), "description": source_object.description, "audit_firm": source_object.audit_firm, "start_date": source_object.start_date, "end_date": source_object.end_date, "program": source_object.program, "status": source_object.VALID_STATES[0], "report_start_date": source_object.report_start_date, "report_end_date": source_object.report_end_date, "contact": source_object.contact } self.update_attrs(data) db.session.flush() create_audit_context(self) self._clone_auditors(source_object) self.clone_custom_attribute_values(source_object) def _clone_auditors(self, audit): """Clone auditors of specified audit. Args: audit: Audit instance """ from ggrc_basic_permissions.models import Role, UserRole role = Role.query.filter_by(name="Auditor").first() auditors = [ ur.person for ur in UserRole.query.filter_by(role=role, context=audit.context).all() ] for auditor in auditors: user_role = UserRole(context=self.context, person=auditor, role=role) db.session.add(user_role) db.session.flush() def clone(self, source_id, mapped_objects=None): """Clone audit with specified whitelisted children. Children that can be cloned should be specified in CLONEABLE_CHILDREN. Args: mapped_objects: A list of related objects that should also be copied and linked to a new audit. """ if not mapped_objects: mapped_objects = [] source_object = Audit.query.get(source_id) self._clone(source_object) if any(mapped_objects): related_children = source_object.related_objects(mapped_objects) for obj in related_children: obj.clone(self) @classmethod def _filter_by_program(cls, predicate): return Program.query.filter((Program.id == Audit.program_id) & (predicate(Program.slug) | predicate(Program.title))).exists() @classmethod def _filter_by_auditor(cls, predicate): from ggrc_basic_permissions.models import Role, UserRole return UserRole.query.join( Role, Person).filter((Role.name == "Auditor") & (UserRole.context_id == cls.context_id) & (predicate(Person.name) | predicate(Person.email))).exists() @classmethod def eager_query(cls): query = super(Audit, cls).eager_query() return query.options( orm.joinedload('program'), orm.subqueryload('object_people').joinedload('person'), orm.subqueryload('audit_objects'), )
class Audit(Snapshotable, clonable.SingleClonable, WithEvidence, mixins.CustomAttributable, Personable, HasOwnContext, Relatable, Roleable, issue_tracker_mixins.IssueTrackedWithConfig, issue_tracker_mixins.IssueTrackedWithPeopleSync, WithLastDeprecatedDate, mixins.Timeboxed, base.ContextRBAC, mixins.BusinessObject, mixins.Folderable, rest_handable_mixins.WithDeleteHandable, Indexed, db.Model): """Audit model.""" __tablename__ = 'audits' _slug_uniqueness = False VALID_STATES = ( u'Planned', u'In Progress', u'Manager Review', u'Ready for External Review', u'Completed', u'Deprecated' ) CLONEABLE_CHILDREN = {"AssessmentTemplate"} report_start_date = deferred(db.Column(db.Date), 'Audit') report_end_date = deferred(db.Column(db.Date), 'Audit') audit_firm_id = deferred( db.Column(db.Integer, db.ForeignKey('org_groups.id')), 'Audit') audit_firm = db.relationship('OrgGroup', uselist=False) gdrive_evidence_folder = deferred(db.Column(db.String), 'Audit') program_id = deferred( db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False), 'Audit') object_type = db.Column( db.String(length=250), nullable=False, default='Control') assessments = db.relationship('Assessment', backref='audit') issues = db.relationship('Issue', backref='audit') snapshots = db.relationship('Snapshot', backref='audit') archived = deferred(db.Column(db.Boolean, nullable=False, default=False), 'Audit') manual_snapshots = deferred(db.Column(db.Boolean, nullable=False, default=False), 'Audit') assessment_templates = db.relationship('AssessmentTemplate', backref='audit') _api_attrs = reflection.ApiAttributes( 'report_start_date', 'report_end_date', 'audit_firm', 'gdrive_evidence_folder', 'program', 'object_type', 'archived', 'manual_snapshots', ) _fulltext_attrs = [ 'archived', 'report_start_date', 'report_end_date', 'audit_firm', 'gdrive_evidence_folder', ] @classmethod def indexed_query(cls): return super(Audit, cls).indexed_query().options( orm.Load(cls).undefer_group( "Audit_complete", ), ) _sanitize_html = [ 'gdrive_evidence_folder', 'description', ] _include_links = [] _aliases = { "program": { "display_name": "Program", "filter_by": "_filter_by_program", "mandatory": True, }, "start_date": "Planned Start Date", "end_date": "Planned End Date", "report_start_date": "Planned Report Period from", "report_end_date": "Planned Report Period to", "notes": None, "archived": { "display_name": "Archived", "mandatory": False }, "status": { "display_name": "State", "mandatory": True, "description": "Options are:\n{}".format('\n'.join(VALID_STATES)) } } def _clone(self, source_object): """Clone audit and all relevant attributes. Keeps the internals of actual audit cloning and everything that is related to audit itself (auditors, audit firm, context setting, custom attribute values, etc.) """ from ggrc_basic_permissions import create_audit_context data = { "title": source_object.generate_attribute("title"), "description": source_object.description, "audit_firm": source_object.audit_firm, "start_date": source_object.start_date, "end_date": source_object.end_date, "last_deprecated_date": source_object.last_deprecated_date, "program": source_object.program, "status": source_object.VALID_STATES[0], "report_start_date": source_object.report_start_date, "report_end_date": source_object.report_end_date } self.update_attrs(data) db.session.flush() create_audit_context(self) self.clone_acls(source_object) self.clone_custom_attribute_values(source_object) def clone_acls(self, audit): """Clone acl roles like auditors and audit captains Args: audit: Audit instance """ for person, acl in audit.access_control_list: self.add_person_with_role(person, acl.ac_role) def clone(self, source_id, mapped_objects=None): """Clone audit with specified whitelisted children. Children that can be cloned should be specified in CLONEABLE_CHILDREN. Args: mapped_objects: A list of related objects that should also be copied and linked to a new audit. """ if not mapped_objects: mapped_objects = [] source_object = Audit.query.get(source_id) self._clone(source_object) if any(mapped_objects): related_children = source_object.related_objects(mapped_objects) for obj in related_children: obj.clone(self) @orm.validates("archived") def archived_check(self, _, value): """Only Admins and Program Managers are allowed to (un)archive Audit.""" user = get_current_user() if getattr(user, 'system_wide_role', None) in SystemWideRoles.admins: return value if self.archived is not None and self.archived != value and \ not any(acl for person, acl in list(self.program.access_control_list) if acl.ac_role.name == "Program Managers" and person.id == user.id): raise wzg_exceptions.Forbidden() return value @classmethod def _filter_by_program(cls, predicate): """Helper for filtering by program""" return Program.query.filter( (Program.id == Audit.program_id) & (predicate(Program.slug) | predicate(Program.title)) ).exists() @classmethod def eager_query(cls, **kwargs): query = super(Audit, cls).eager_query(**kwargs) return query.options( orm.joinedload('program'), orm.subqueryload('object_people').joinedload('person'), ) def get_evidences_from_assessments(self, objects=False): """Return all related evidences from assessments. audit <--> assessment -> evidence :param objects: bool. optional argument. If True object Evidence ORM objects return :return: sqlalchemy.Query or sqlalchemy.orm.query.Query objects """ from ggrc.models.assessment import Assessment evid_as_dest = db.session.query( Relationship.destination_id.label("id"), ).join( Assessment, Assessment.id == Relationship.source_id, ).filter( Relationship.destination_type == Evidence.__name__, Relationship.source_type == Assessment.__name__, Assessment.audit_id == self.id, ) evid_as_source = db.session.query( Relationship.source_id.label("id"), ).join( Assessment, Assessment.id == Relationship.destination_id, ).filter( Relationship.source_type == Evidence.__name__, Relationship.destination_type == Assessment.__name__, Assessment.audit_id == self.id, ) evidence_assessment = evid_as_dest.union(evid_as_source) if objects: return db.session.query(Evidence).filter( Evidence.id.in_(evidence_assessment), ) return evidence_assessment def get_evidences_from_audit(self, objects=False): """Return all related evidence. In relation audit <--> evidence :param objects: bool. optional argument. If True object Evidence ORM objects return :return: sqlalchemy.Query or sqlalchemy.orm.query.Query objects """ evid_a_source = db.session.query( Relationship.source_id.label("id"), ).filter( Relationship.source_type == Evidence.__name__, Relationship.destination_type == Audit.__name__, Relationship.destination_id == self.id, ) evid_a_dest = db.session.query( Relationship.destination_id.label("id"), ).filter( Relationship.destination_type == Evidence.__name__, Relationship.source_type == Audit.__name__, Relationship.source_id == self.id, ) evidence_audit = evid_a_dest.union(evid_a_source) if objects: return db.session.query(Evidence).filter( Evidence.id.in_(evidence_audit), ) return evidence_audit @simple_property def all_related_evidences(self): """Return all related evidences of audit""" evidence_assessment = self.get_evidences_from_assessments() evidence_audit = self.get_evidences_from_audit() evidence_ids = evidence_assessment.union(evidence_audit) return db.session.query(Evidence).filter( Evidence.id.in_(evidence_ids) ) def _check_no_assessments(self): """Check that audit has no assessments before delete.""" if self.assessments or self.assessment_templates: db.session.rollback() raise wzg_exceptions.Conflict(errors.MAPPED_ASSESSMENT) def handle_delete(self): """Handle model_deleted signals.""" self._check_no_assessments()
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 IssuetrackerIssue(base.ContextRBAC, Base, db.Model): """Class representing IssuetrackerIssue.""" __tablename__ = 'issuetracker_issues' object_id = db.Column(db.Integer, nullable=False) object_type = db.Column(db.String(250), nullable=False) enabled = db.Column(db.Boolean, nullable=False, default=False) title = db.Column(db.String(250), nullable=True) component_id = db.Column(db.String(50), nullable=True) hotlist_id = db.Column(db.String(50), nullable=True) issue_type = db.Column(db.String(50), nullable=True) issue_priority = db.Column(db.String(50), nullable=True) issue_severity = db.Column(db.String(50), nullable=True) assignee = db.Column(db.String(250), nullable=True) cc_list = db.Column(db.Text, nullable=False, default="") due_date = db.Column(db.Date, nullable=True) issue_id = db.Column(db.String(50), nullable=True) issue_url = db.Column(db.String(250), nullable=True) issue_tracked_obj = utils.PolymorphicRelationship("object_id", "object_type", "{}_issue_tracked") @classmethod def get_issue(cls, object_type, object_id): """Returns an issue object by given type and ID or None. Args: object_type: A string representing a model. object_id: An integer identifier of model's instance. Returns: An instance of IssuetrackerIssue or None. """ return cls.query.filter(cls.object_type == object_type, cls.object_id == object_id).first() def to_dict(self, include_issue=False, include_private=False): """Returns representation of object as a dict. Args: include_issue: A boolean whether to include issue related properties. include_private: A boolean whether to include private properties. Returns: A dict representing an instance of IssuetrackerIssue. """ res = { 'enabled': self.enabled, 'component_id': self.component_id, 'hotlist_id': self.hotlist_id, 'issue_type': self.issue_type, 'issue_priority': self.issue_priority, 'issue_severity': self.issue_severity, } if include_issue: res['issue_id'] = self.issue_id res['issue_url'] = self.issue_url res['title'] = self.title if include_private: res['object_id'] = self.object_id res['object_type'] = self.object_type res['assignee'] = self.assignee res['cc_list'] = self.cc_list.split(',') if self.cc_list else [] return res @classmethod def create_or_update_from_dict(cls, obj, info): """Creates or updates issue with given parameters. Args: obj: An object which is an IssueTracked instance. info: A dict with issue properties. Returns: An instance of IssuetrackerIssue. """ if not info: raise ValueError('Issue tracker info cannot be empty.') issue_obj = cls.get_issue(obj.type, obj.id) info = dict(info, issue_tracked_obj=obj) if issue_obj is not None: issue_obj.update_from_dict(info) else: issue_obj = cls.create_from_dict(info) db.session.add(issue_obj) return issue_obj @classmethod def create_from_dict(cls, info): """Creates issue with given parameters. Args: info: A dict with issue properties. Returns: An instance of IssuetrackerIssue. """ cc_list = info.get('cc_list') if cc_list is not None: cc_list = ','.join(cc_list) return cls( issue_tracked_obj=info['issue_tracked_obj'], enabled=bool(info.get('enabled')), title=info.get('title'), component_id=info.get('component_id'), hotlist_id=info.get('hotlist_id'), issue_type=info.get('issue_type'), issue_priority=info.get('issue_priority'), issue_severity=info.get('issue_severity'), assignee=info.get('assignee'), cc_list=cc_list, issue_id=info.get('issue_id'), issue_url=info.get('issue_url'), ) def update_from_dict(self, info): """Updates issue with given parameters. Args: info: A dict with issue properties. Returns: An instance of IssuetrackerIssue. """ cc_list = info.pop('cc_list', None) info = dict(self.to_dict(include_issue=True, include_private=True), **info) if cc_list is not None: info['cc_list'] = cc_list if info['cc_list'] is not None: info['cc_list'] = ','.join(info['cc_list']) self.object_type = info['object_type'] self.object_id = info['object_id'] self.enabled = info['enabled'] self.title = info['title'] self.component_id = info['component_id'] self.hotlist_id = info['hotlist_id'] self.issue_type = info['issue_type'] self.issue_priority = info['issue_priority'] self.issue_severity = info['issue_severity'] self.assignee = info['assignee'] self.cc_list = info['cc_list'] self.issue_id = info['issue_id'] self.issue_url = info['issue_url'] if info.get('due_date'): self.due_date = info.get('due_date') @staticmethod def get_issuetracker_issue_stub(): """Returns dict with all Issue Tracker fields with empty values.""" return { 'enabled': False, 'component_id': None, 'hotlist_id': None, 'issue_type': None, 'issue_priority': None, 'issue_severity': None, 'title': None, 'issue_id': None, 'issue_url': None }
class CycleTaskGroupObjectTask(roleable.Roleable, 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', 'status', '__acl__:Task Assignees', ) @classmethod def generate_slug_prefix(cls): 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("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"]), "folder", ] 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 @builder.simple_property def folder(self): if self.cycle: return self.cycle.folder return "" @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 _api_attrs = reflection.ApiAttributes( 'cycle', 'cycle_task_group', 'task_group_task', 'cycle_task_entries', 'sort_index', 'task_type', 'response_options', 'selected_response_options', reflection.Attribute('object_approval', create=False, update=False), reflection.Attribute('finished_date', create=False, update=False), reflection.Attribute('verified_date', create=False, update=False), reflection.Attribute('allow_change_state', create=False, update=False), reflection.Attribute('folder', create=False, update=False), ) 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", "finished_date": { "display_name": "Actual Finish Date", "description": ("Make sure that 'Actual Finish Date' isn't set, " "if cycle task state is <'Assigned' / " "'In Progress' / 'Declined' / 'Deprecated'>. " "Type double dash '--' into " "'Actual Finish Date' cell to remove it.") }, "verified_date": { "display_name": "Actual Verified Date", "description": ("Make sure that 'Actual Verified Date' isn't set, " "if cycle task state is <'Assigned' / " "'In Progress' / 'Declined' / 'Deprecated' / " "'Finished'>. Type double dash '--' into " "'Actual Verified Date' cell to remove it.") }, "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.""" wfo_person_ids = {ur.person_id for ur in self.wfo_roles} assignees_ids = { p.id for p in self.get_persons_for_rolename("Task Assignees") } return login.get_current_user_id() in (wfo_person_ids | assignees_ids) @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("cycle").joinedload( "workflow").undefer_group("Workflow_complete"), ) def log_json(self): out_json = super(CycleTaskGroupObjectTask, self).log_json() out_json["folder"] = self.folder return out_json @classmethod def bulk_update(cls, src): """Update statuses for bunch of tasks in a bulk. Args: src: input json with next structure: [{"status": "Assigned", "id": 1}, {"status": "InProgress", "id": 2}] Returns: list of updated_instances """ new_prv_state_map = { cls.DEPRECATED: (cls.ASSIGNED, cls.IN_PROGRESS, cls.FINISHED, cls.VERIFIED, cls.DECLINED), cls.IN_PROGRESS: (cls.ASSIGNED, ), cls.FINISHED: (cls.IN_PROGRESS, cls.DECLINED), cls.VERIFIED: (cls.FINISHED, ), cls.DECLINED: (cls.FINISHED, ), cls.ASSIGNED: (), } uniq_states = set([item['state'] for item in src]) if len(list(uniq_states)) != 1: raise BadRequest("Request's JSON contains multiple statuses for " "CycleTasks") new_state = uniq_states.pop() LOGGER.info("Do bulk update CycleTasks with '%s' status", new_state) if new_state not in cls.VALID_STATES: raise BadRequest("Request's JSON contains invalid statuses for " "CycleTasks") prv_states = new_prv_state_map[new_state] all_ids = {item['id'] for item in src} # Eagerly loading is needed to get user permissions for CycleTask faster updatable_objects = cls.eager_query().filter( cls.id.in_(list(all_ids)), cls.status.in_(prv_states)) if new_state in (cls.VERIFIED, cls.DECLINED): updatable_objects = [ obj for obj in updatable_objects if obj.cycle.is_verification_needed ] # Bulk update works only on MyTasks page. Don't need to check for # WorkflowMembers' permissions here. User should update only his own tasks. updatable_objects = [ obj for obj in updatable_objects if obj.current_user_wfo_or_assignee() ] # Queries count is constant because we are using eager query for objects. for obj in updatable_objects: obj.status = new_state obj.modified_by_id = login.get_current_user_id() return updatable_objects
class Audit(Snapshotable, clonable.SingleClonable, PublicDocumentable, mixins.CustomAttributable, Personable, HasOwnContext, Relatable, Roleable, issuetracker_issue.IssueTracked, WithLastDeprecatedDate, mixins.Timeboxed, mixins.BusinessObject, mixins.Folderable, Indexed, db.Model): """Audit model.""" __tablename__ = 'audits' _slug_uniqueness = False VALID_STATES = (u'Planned', u'In Progress', u'Manager Review', u'Ready for External Review', u'Completed', u'Deprecated') CLONEABLE_CHILDREN = {"AssessmentTemplate"} report_start_date = deferred(db.Column(db.Date), 'Audit') report_end_date = deferred(db.Column(db.Date), 'Audit') audit_firm_id = deferred( db.Column(db.Integer, db.ForeignKey('org_groups.id')), 'Audit') audit_firm = db.relationship('OrgGroup', uselist=False) gdrive_evidence_folder = deferred(db.Column(db.String), 'Audit') program_id = deferred( db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False), 'Audit') audit_objects = db.relationship('AuditObject', backref='audit', cascade='all, delete-orphan') object_type = db.Column(db.String(length=250), nullable=False, default='Control') assessments = db.relationship('Assessment', backref='audit') issues = db.relationship('Issue', backref='audit') archived = deferred(db.Column(db.Boolean, nullable=False, default=False), 'Audit') assessment_templates = db.relationship('AssessmentTemplate', backref='audit') _api_attrs = reflection.ApiAttributes( 'report_start_date', 'report_end_date', 'audit_firm', 'gdrive_evidence_folder', 'program', 'object_type', 'archived', reflection.Attribute('issue_tracker', create=False, update=False), reflection.Attribute('audit_objects', create=False, update=False), ) _fulltext_attrs = [ 'archived', 'report_start_date', 'report_end_date', 'audit_firm', 'gdrive_evidence_folder', ] @classmethod def indexed_query(cls): return super(Audit, cls).indexed_query().options( orm.Load(cls).undefer_group("Audit_complete", ), ) _sanitize_html = [ 'gdrive_evidence_folder', 'description', ] _include_links = [] _aliases = { "program": { "display_name": "Program", "filter_by": "_filter_by_program", "mandatory": True, }, "start_date": "Planned Start Date", "end_date": "Planned End Date", "report_start_date": "Planned Report Period from", "report_end_date": "Planned Report Period to", "notes": None, "reference_url": None, "archived": { "display_name": "Archived", "mandatory": False }, "status": { "display_name": "State", "mandatory": True, "description": "Options are:\n{}".format('\n'.join(VALID_STATES)) } } @simple_property def issue_tracker(self): """Returns representation of issue tracker related info as a dict.""" issue_obj = issuetracker_issue.IssuetrackerIssue.get_issue( 'Audit', self.id) return issue_obj.to_dict() if issue_obj is not None else {} def _clone(self, source_object): """Clone audit and all relevant attributes. Keeps the internals of actual audit cloning and everything that is related to audit itself (auditors, audit firm, context setting, custom attribute values, etc.) """ from ggrc_basic_permissions import create_audit_context data = { "title": source_object.generate_attribute("title"), "description": source_object.description, "audit_firm": source_object.audit_firm, "start_date": source_object.start_date, "end_date": source_object.end_date, "last_deprecated_date": source_object.last_deprecated_date, "program": source_object.program, "status": source_object.VALID_STATES[0], "report_start_date": source_object.report_start_date, "report_end_date": source_object.report_end_date } self.update_attrs(data) db.session.flush() create_audit_context(self) self.clone_acls(source_object) self.clone_custom_attribute_values(source_object) def clone_acls(self, audit): """Clone acl roles like auditors and audit captains Args: audit: Audit instance """ for acl in audit.access_control_list: data = { "person": acl.person, "ac_role": acl.ac_role, "object": self, "context": acl.context, } new_acl = AccessControlList(**data) db.session.add(new_acl) def clone(self, source_id, mapped_objects=None): """Clone audit with specified whitelisted children. Children that can be cloned should be specified in CLONEABLE_CHILDREN. Args: mapped_objects: A list of related objects that should also be copied and linked to a new audit. """ if not mapped_objects: mapped_objects = [] source_object = Audit.query.get(source_id) self._clone(source_object) if any(mapped_objects): related_children = source_object.related_objects(mapped_objects) for obj in related_children: obj.clone(self) @orm.validates("archived") def archived_check(self, _, value): """Only Admins and Program Managers are allowed to (un)archive Audit.""" user = get_current_user() if getattr(user, 'system_wide_role', None) in SystemWideRoles.admins: return value if self.archived is not None and self.archived != value and \ not any(acl for acl in list(self.full_access_control_list) if acl.ac_role.name == "Program Managers Mapped" and acl.person.id == user.id): raise Forbidden() return value @classmethod def _filter_by_program(cls, predicate): """Helper for filtering by program""" return Program.query.filter((Program.id == Audit.program_id) & (predicate(Program.slug) | predicate(Program.title))).exists() @classmethod def _filter_by_auditor(cls, predicate): """Helper for filtering by auditor""" from ggrc_basic_permissions.models import Role, UserRole return UserRole.query.join( Role, Person).filter((Role.name == "Auditor") & (UserRole.context_id == cls.context_id) & (predicate(Person.name) | predicate(Person.email))).exists() @classmethod def eager_query(cls): query = super(Audit, cls).eager_query() return query.options( orm.joinedload('program'), orm.subqueryload('object_people').joinedload('person'), orm.subqueryload('audit_objects'), )
class CycleTaskGroupObjectTask( roleable.Roleable, wf_mixins.CycleTaskStatusValidatedMixin, wf_mixins.WorkflowCommentable, mixins.WithLastDeprecatedDate, mixins.Timeboxed, relationship.Relatable, mixins.Notifiable, mixins.Described, mixins.Titled, mixins.Slugged, mixins.Base, base.ContextRBAC, 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', 'status', '__acl__:Task Assignees', '__acl__:Task Secondary Assignees', ) @classmethod def generate_slug_prefix(cls): 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("group title", 'cycle_task_group', ['title'], False), ft_attributes.FullTextAttr("object_approval", 'object_approval', with_template=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"]), ft_attributes.BooleanFullTextAttr("needs verification", "is_verification_needed", with_template=False, true_value="Yes", false_value="No"), "folder", ] # The app should not pass to the json representation of # relationships to the internal models IGNORED_RELATED_TYPES = ["CalendarEvent"] _custom_publish = { "related_sources": lambda obj: [ rel.log_json() for rel in obj.related_sources if rel.source_type not in obj.IGNORED_RELATED_TYPES ], "related_destinations": lambda obj: [ rel.log_json() for rel in obj.related_destinations if rel.destination_type not in obj.IGNORED_RELATED_TYPES ] } 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) # This parameter is overridden by cycle task group backref, but is here to # ensure pylint does not complain _cycle_task_group = None @hybrid.hybrid_property def cycle_task_group(self): """Getter for cycle task group foreign key.""" return self._cycle_task_group @cycle_task_group.setter def cycle_task_group(self, cycle_task_group): """Setter for cycle task group foreign key.""" if not self._cycle_task_group and cycle_task_group: relationship.Relationship(source=cycle_task_group, destination=self) self._cycle_task_group = cycle_task_group @hybrid.hybrid_property def object_approval(self): return self.cycle.workflow.object_approval @object_approval.expression def object_approval(cls): # pylint: disable=no-self-argument return sa.select([ Workflow.object_approval, ]).where( sa.and_( (Cycle.id == cls.cycle_id), (Cycle.workflow_id == Workflow.id))).label('object_approval') @builder.simple_property def folder(self): """Simple property for cycle folder.""" if self.cycle: return self.cycle.folder return "" @builder.simple_property def is_in_history(self): """Used on UI to disable editing finished CycleTask which is in history""" return not self.cycle.is_current @property def cycle_task_objects_for_cache(self): """Get all related objects for this CycleTaskGroupObjectTask Returns: List of tuples with (related_object_type, related_object_id) """ return [(object_.__class__.__name__, object_.id) for object_ in self.related_objects()] _api_attrs = reflection.ApiAttributes( 'cycle', 'cycle_task_group', 'task_group_task', 'cycle_task_entries', 'sort_index', 'task_type', 'response_options', 'selected_response_options', reflection.Attribute('related_sources', create=False, update=False), reflection.Attribute('related_destinations', create=False, update=False), reflection.Attribute('object_approval', create=False, update=False), reflection.Attribute('finished_date', create=False, update=False), reflection.Attribute('verified_date', create=False, update=False), reflection.Attribute('allow_change_state', create=False, update=False), reflection.Attribute('folder', create=False, update=False), reflection.Attribute('workflow', create=False, update=False), reflection.Attribute('workflow_title', create=False, update=False), reflection.Attribute('cycle_task_group_title', create=False, update=False), reflection.Attribute('is_in_history', create=False, update=False), ) 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", "finished_date": { "display_name": "Actual Finish Date", "description": ("Make sure that 'Actual Finish Date' isn't set, " "if cycle task state is <'Assigned' / " "'In Progress' / 'Declined' / 'Deprecated'>. " "Type double dash '--' into " "'Actual Finish Date' cell to remove it.") }, "verified_date": { "display_name": "Actual Verified Date", "description": ("Make sure that 'Actual Verified Date' isn't set, " "if cycle task state is <'Assigned' / " "'In Progress' / 'Declined' / 'Deprecated' / " "'Finished'>. Type double dash '--' into " "'Actual Verified Date' cell to remove it.") }, "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 cycle_task_group_title(self): """Property. Returns parent CycleTaskGroup title.""" return self.cycle_task_group.title @builder.simple_property def workflow_title(self): """Property. Returns parent Workflow's title.""" return self.workflow.title @builder.simple_property def workflow(self): """Property which returns parent workflow object.""" return self.cycle.workflow @builder.simple_property def allow_change_state(self): return self.cycle.is_current and self.current_user_wfa_or_assignee() def current_user_wfa_or_assignee(self): """Current user is WF Admin, Assignee or Secondary Assignee for self.""" wfa_ids = self.workflow.get_person_ids_for_rolename("Admin") ta_ids = self.get_person_ids_for_rolename("Task Assignees") tsa_ids = self.get_person_ids_for_rolename("Task Secondary Assignees") return login.get_current_user_id() in set().union( wfa_ids, ta_ids, tsa_ids) @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.subqueryload('cycle_task_entries'), orm.joinedload('cycle').undefer_group('Cycle_complete'), orm.joinedload('cycle').joinedload('workflow').undefer_group( 'Workflow_complete'), orm.joinedload('cycle').joinedload('workflow').joinedload( '_access_control_list'), orm.joinedload('cycle_task_group').undefer_group( 'CycleTaskGroup_complete'), ) @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", "is_verification_needed", ), 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("cycle").joinedload( "workflow").undefer_group("Workflow_complete"), ) def log_json(self): out_json = super(CycleTaskGroupObjectTask, self).log_json() out_json["folder"] = self.folder return out_json @classmethod def bulk_update(cls, src): """Update statuses for bunch of tasks in a bulk. Args: src: input json with next structure: [{"status": "Assigned", "id": 1}, {"status": "In Progress", "id": 2}] Returns: list of updated_instances """ new_prv_state_map = { cls.DEPRECATED: (cls.ASSIGNED, cls.IN_PROGRESS, cls.FINISHED, cls.VERIFIED, cls.DECLINED), cls.IN_PROGRESS: (cls.ASSIGNED, ), cls.FINISHED: (cls.IN_PROGRESS, cls.DECLINED), cls.VERIFIED: (cls.FINISHED, ), cls.DECLINED: (cls.FINISHED, ), cls.ASSIGNED: (), } uniq_states = set([item['state'] for item in src]) if len(list(uniq_states)) != 1: raise BadRequest("Request's JSON contains multiple statuses for " "CycleTasks") new_state = uniq_states.pop() LOGGER.info("Do bulk update CycleTasks with '%s' status", new_state) if new_state not in cls.VALID_STATES: raise BadRequest("Request's JSON contains invalid statuses for " "CycleTasks") prv_states = new_prv_state_map[new_state] all_ids = {item['id'] for item in src} # Eagerly loading is needed to get user permissions for CycleTask faster updatable_objects = cls.eager_query().filter( cls.id.in_(list(all_ids)), cls.status.in_(prv_states)) if new_state in (cls.VERIFIED, cls.DECLINED): updatable_objects = [ obj for obj in updatable_objects if obj.cycle.is_verification_needed ] # Bulk update works only on MyTasks page. Don't need to check for # WorkflowMembers' permissions here. User should update only his own tasks. updatable_objects = [ obj for obj in updatable_objects if obj.current_user_wfa_or_assignee() ] # Queries count is constant because we are using eager query for objects. for obj in updatable_objects: obj.status = new_state obj.modified_by_id = login.get_current_user_id() return updatable_objects
class CycleTaskGroup(roleable.Roleable, 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), ] @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("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).joinedload("cycle").load_only( "id", "title", "next_due_date"), 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 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(cls): 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 _api_attrs = reflection.ApiAttributes( 'cycle', 'cycle_task_group', 'task_group_task', 'cycle_task_entries', 'sort_index', 'task_type', 'response_options', 'selected_response_options', reflection.Attribute('object_approval', create=False, update=False), reflection.Attribute('finished_date', create=False, update=False), reflection.Attribute('verified_date', create=False, update=False), reflection.Attribute('allow_change_state', create=False, update=False), ) 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"), )
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 CycleTaskGroup(WithContact, Stateful, Slugged, Timeboxed, Described, Titled, 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", }, } @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 and cycle_task_group_objects added to joined load options. """ query = super(CycleTaskGroup, cls).eager_query() return query.options( orm.joinedload('cycle_task_group_tasks') )
class IssuetrackerIssue(Base, db.Model): """Class representing IssuetrackerIssue.""" __tablename__ = 'issuetracker_issues' object_id = db.Column(db.Integer, nullable=False) object_type = db.Column(db.String(250), nullable=False) enabled = db.Column(db.Boolean, nullable=False, default=False) title = db.Column(db.String(250), nullable=True) component_id = db.Column(db.String(50), nullable=True) hotlist_id = db.Column(db.String(50), nullable=True) issue_type = db.Column(db.String(50), nullable=True) issue_priority = db.Column(db.String(50), nullable=True) issue_severity = db.Column(db.String(50), nullable=True) assignee = db.Column(db.String(250), nullable=True) cc_list = db.Column(db.Text, nullable=True) issue_id = db.Column(db.String(50), nullable=True) issue_url = db.Column(db.String(250), nullable=True) _MANDATORY_ATTRS = ( 'object_type', 'object_id', ) @classmethod def get_issue(cls, object_type, object_id): """Returns an issue object by given type and ID or None. Args: object_type: A string representing a model. object_id: An integer identifier of model's instance. Returns: An instance of IssuetrackerIssue or None. """ return cls.query.filter( cls.object_type == object_type, cls.object_id == object_id).first() @classmethod def _validate_info(cls, info): """Validates issue info to have all required properties.""" missing_attrs = [ attr for attr in cls._MANDATORY_ATTRS if attr not in info ] if missing_attrs: raise ValueError( 'Issue tracker info is missing mandatory attributes: %s' % ( ', '.join(missing_attrs))) def to_dict(self, include_issue=False, include_private=False): """Returns representation of object as a dict. Args: include_issue: A boolean whether to include issue related properties. include_private: A boolean whether to include private properties. Returns: A dict representing an instance of IssuetrackerIssue. """ res = { 'enabled': self.enabled, 'component_id': self.component_id, 'hotlist_id': self.hotlist_id, 'issue_type': self.issue_type, 'issue_priority': self.issue_priority, 'issue_severity': self.issue_severity, } if include_issue: res['issue_id'] = self.issue_id res['issue_url'] = self.issue_url res['title'] = self.title if include_private: res['object_id'] = self.object_id res['object_type'] = self.object_type res['assignee'] = self.assignee res['cc_list'] = self.cc_list.split(',') if self.cc_list else [] return res @classmethod def create_or_update_from_dict(cls, object_type, object_id, info): """Creates or updates issue with given parameters. Args: object_type: A string representing a model. object_id: An integer identifier of model's instance. info: A dict with issue properties. Returns: An instance of IssuetrackerIssue. """ if not info: raise ValueError('Issue tracker info cannot be empty.') issue_obj = cls.get_issue(object_type, object_id) info = dict(info, object_type=object_type, object_id=object_id) if issue_obj is not None: issue_obj.update_from_dict(info) else: issue_obj = cls.create_from_dict(info) db.session.add(issue_obj) return issue_obj @classmethod def create_from_dict(cls, info): """Creates issue with given parameters. Args: info: A dict with issue properties. Returns: An instance of IssuetrackerIssue. """ cls._validate_info(info) cc_list = info.get('cc_list') if cc_list is not None: cc_list = ','.join(cc_list) return cls( object_type=info['object_type'], object_id=info['object_id'], enabled=bool(info.get('enabled')), title=info.get('title'), component_id=info.get('component_id'), hotlist_id=info.get('hotlist_id'), issue_type=info.get('issue_type'), issue_priority=info.get('issue_priority'), issue_severity=info.get('issue_severity'), assignee=info.get('assignee'), cc_list=cc_list, issue_id=info.get('issue_id'), issue_url=info.get('issue_url'), ) def update_from_dict(self, info): """Updates issue with given parameters. Args: info: A dict with issue properties. Returns: An instance of IssuetrackerIssue. """ cc_list = info.pop('cc_list', None) info = dict( self.to_dict(include_issue=True, include_private=True), **info) if cc_list is not None: info['cc_list'] = cc_list if info['cc_list'] is not None: info['cc_list'] = ','.join(info['cc_list']) self.object_type = info['object_type'] self.object_id = info['object_id'] self.enabled = info['enabled'] self.title = info['title'] self.component_id = info['component_id'] self.hotlist_id = info['hotlist_id'] self.issue_type = info['issue_type'] self.issue_priority = info['issue_priority'] self.issue_severity = info['issue_severity'] self.assignee = info['assignee'] self.cc_list = info['cc_list'] self.issue_id = info['issue_id'] self.issue_url = info['issue_url']
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 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'), )