class Ownable(object): AUTO_REINDEX_RULES = [ ReindexRule( "ObjectOwner", lambda x: [x.ownable] if isinstance(x.ownable, Indexed) else [] ) ] @declared_attr def object_owners(cls): # pylint: disable=no-self-argument cls.owners = association_proxy( 'object_owners', 'person', creator=lambda person: ObjectOwner( person=person, ownable_type=cls.__name__, ) ) joinstr = 'and_(foreign(ObjectOwner.ownable_id) == {type}.id, '\ 'foreign(ObjectOwner.ownable_type) == "{type}")' joinstr = joinstr.format(type=cls.__name__) return db.relationship( 'ObjectOwner', primaryjoin=joinstr, backref='{0}_ownable'.format(cls.__name__), cascade='all, delete-orphan', ) _publish_attrs = [ 'owners', PublishOnly('object_owners'), ] _include_links = [] _fulltext_attrs = [ MultipleSubpropertyFullTextAttr('owners', 'owners', ['user_name', 'email', 'name']) ] _aliases = { "owners": { "display_name": "Admin", "mandatory": True, } } @classmethod def indexed_query(cls): return super(Ownable, cls).indexed_query().options( orm.Load(cls).joinedload( "object_owners" ) ) @classmethod def eager_query(cls): from sqlalchemy import orm query = super(Ownable, cls).eager_query() return cls.eager_inclusions(query, Ownable._include_links).options( orm.subqueryload('object_owners'))
class ExternalCommentable(object): """Mixin for external commentable objects. This is a mixin for adding external comments (comments that can be created only by sync service) to the object. """ _fulltext_attrs = [ MultipleSubpropertyFullTextAttr("comment", "comments", ["description"]), ] @classmethod def indexed_query(cls): """Indexed query for ExternalCommentable mixin.""" return super(ExternalCommentable, cls).indexed_query().options( orm.Load(cls).subqueryload("comments").load_only("id", "description") ) @classmethod def eager_query(cls): """Eager query for ExternalCommentable mixin.""" query = super(ExternalCommentable, cls).eager_query() return query.options(orm.subqueryload("comments")) @declared_attr def comments(cls): # pylint: disable=no-self-argument """ExternalComment related to self via Relationship table.""" return db.relationship( ExternalComment, primaryjoin=lambda: sa.or_( sa.and_( cls.id == Relationship.source_id, Relationship.source_type == cls.__name__, Relationship.destination_type == ExternalComment.__name__, ), sa.and_( cls.id == Relationship.destination_id, Relationship.destination_type == cls.__name__, Relationship.source_type == ExternalComment.__name__, ) ), secondary=Relationship.__table__, secondaryjoin=lambda: sa.or_( sa.and_( ExternalComment.id == Relationship.source_id, Relationship.source_type == ExternalComment.__name__, ), sa.and_( ExternalComment.id == Relationship.destination_id, Relationship.destination_type == ExternalComment.__name__, ) ), viewonly=True, )
class Labeled(object): """Mixin to add label in required model.""" _update_raw = _include_links = [ 'labels', ] _api_attrs = reflection.ApiAttributes(*_include_links) _aliases = {'labels': 'Labels'} _fulltext_attrs = [ MultipleSubpropertyFullTextAttr("label", "labels", ["name"]), ] @declared_attr def _object_labels(cls): # pylint: disable=no-self-argument """Object labels property""" # pylint: disable=attribute-defined-outside-init cls._labels = association_proxy( '_object_labels', 'label', creator=lambda label: ObjectLabel( label=label, # noqa object_type=cls.__name__)) return db.relationship( ObjectLabel, primaryjoin=lambda: and_(cls.id == ObjectLabel.object_id, cls. __name__ == ObjectLabel.object_type), foreign_keys=ObjectLabel.object_id, backref='{}_labeled'.format(cls.__name__), cascade='all, delete-orphan') @hybrid_property def labels(self): return self._labels @labels.setter def labels(self, values): """Setter function for labeled. Args: values: List of labels in json. Labels mapped on labeled and not represented in values will be unmapped from labeled. label is represented as a dict {"id": <label id>, "name": <label name>} Currently, FE sends the following info: for a newly created label: {"id": None, "name": <label name>} for old label: {"id": <label_id>, "name": <label name>} for being mapped label: {"id": <label id>} """ if values is None: return for value in values: if 'name' in value: value['name'] = value['name'].strip() if values: new_ids = {value['id'] for value in values if value['id']} new_names = {value['name'] for value in values if 'name' in value} # precache labels filter_group = [] if new_ids: filter_group.append(Label.id.in_(new_ids)) if new_names: filter_group.append(Label.name.in_(new_names)) cached_labels = Label.query.filter( and_(or_(*filter_group), Label.object_type == self.__class__.__name__)).all() else: new_ids = set() new_names = set() cached_labels = [] old_ids = {label.id for label in self.labels} self._unmap_labels(old_ids - new_ids, new_names) self._map_labels(new_ids - old_ids, cached_labels) # label comparison has to be case insensitive if new_names: self._add_labels_by_name(new_names, new_ids | old_ids, cached_labels) def _map_labels(self, ids, cached_labels): """Attach new labels to current object.""" labels_dict = {label.id: label for label in cached_labels} for id_ in ids: self._labels.append(labels_dict[id_]) def _add_labels_by_name(self, names, ids, cached_labels): """Creates new labels and map them to current object""" labels_dict = {label.name.lower(): label for label in cached_labels} for name in names: if name.lower() in labels_dict: if labels_dict[name.lower()].id in ids: continue label = labels_dict[name.lower()] else: label = Label(name=name, object_type=self.__class__.__name__) self._labels.append(label) def _unmap_labels(self, ids, names): """Remove labels from current object.""" values_map = { label.id: label for label in self._labels # noqa pylint: disable=not-an-iterable } lower_names = [name.lower() for name in names] for id_ in ids: if values_map[id_].name.lower() not in lower_names: self._labels.remove(values_map[id_]) def log_json(self): """Log label values.""" # pylint: disable=not-an-iterable res = super(Labeled, self).log_json() res["labels"] = [value.log_json() for value in self.labels] return res @classmethod def eager_query(cls, **kwargs): """Eager query classmethod.""" return super(Labeled, cls).eager_query(**kwargs).options( orm.subqueryload('_object_labels')) @classmethod def indexed_query(cls): return super(Labeled, cls).indexed_query().options( orm.subqueryload("_object_labels"))
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 Commentable(object): """Mixin for commentable objects. This is a mixin for adding default options to objects on which people can comment. recipients is used for setting who gets notified (Verifer, Requester, ...). send_by_default should be used for setting the "send notification" flag in the comment modal. """ # pylint: disable=too-few-public-methods VALID_RECIPIENTS = frozenset([ "Assignees", "Creators", "Verifiers", "Admin", "Primary Contacts", "Secondary Contacts", ]) @validates("recipients") def validate_recipients(self, key, value): """ Validate recipients list Args: value (string): Can be either empty, or list of comma separated `VALID_RECIPIENTS` """ # pylint: disable=unused-argument if value: value = set(name for name in value.split(",") if name) if value and value.issubset(self.VALID_RECIPIENTS): # The validator is a bit more smart and also makes some filtering of the # given data - this is intended. return ",".join(value) elif not value: return "" else: raise ValueError( value, 'Value should be either empty ' + 'or comma separated list of ' + ', '.join(sorted(self.VALID_RECIPIENTS))) recipients = db.Column(db.String, nullable=True, default=u"Assignees,Creators,Verifiers") send_by_default = db.Column(db.Boolean, nullable=True, default=True) _api_attrs = reflection.ApiAttributes("recipients", "send_by_default") _aliases = { "recipients": "Recipients", "send_by_default": "Send by default", "comments": { "display_name": "Comments", "description": 'DELIMITER=";;" double semi-colon separated values', }, } _fulltext_attrs = [ MultipleSubpropertyFullTextAttr("comment", "comments", ["description"]), ] @classmethod def indexed_query(cls): return super(Commentable, cls).indexed_query().options( orm.Load(cls).subqueryload("comments").load_only( "id", "description")) @classmethod def eager_query(cls): """Eager Query""" query = super(Commentable, cls).eager_query() return query.options(orm.subqueryload('comments')) @declared_attr def comments(cls): # pylint: disable=no-self-argument """Comments related to self via Relationship table.""" return db.relationship( Comment, primaryjoin=lambda: sa.or_( sa.and_( cls.id == Relationship.source_id, Relationship.source_type == cls.__name__, Relationship.destination_type == "Comment", ), sa.and_( cls.id == Relationship.destination_id, Relationship.destination_type == cls.__name__, Relationship.source_type == "Comment", )), secondary=Relationship.__table__, secondaryjoin=lambda: sa.or_( sa.and_( Comment.id == Relationship.source_id, Relationship.source_type == "Comment", ), sa.and_( Comment.id == Relationship.destination_id, Relationship.destination_type == "Comment", )), viewonly=True, )
class Assessment(statusable.Statusable, AuditRelationship, AutoStatusChangeable, Assignable, HasObjectState, TestPlanned, CustomAttributable, EvidenceURL, Commentable, Personable, reminderable.Reminderable, Timeboxed, Relatable, WithSimilarityScore, FinishedDate, VerifiedDate, ValidateOnComplete, Notifiable, BusinessObject, Indexed, db.Model): """Class representing Assessment. Assessment is an object representing an individual assessment performed on a specific object during an audit to ascertain whether or not certain conditions were met for that object. """ __tablename__ = 'assessments' _title_uniqueness = False ASSIGNEE_TYPES = (u"Creator", u"Assessor", u"Verifier") REMINDERABLE_HANDLERS = { "statusToPerson": { "handler": reminderable.Reminderable.handle_state_to_person_reminder, "data": { statusable.Statusable.START_STATE: "Assessor", "In Progress": "Assessor" }, "reminders": { "assessment_assessor_reminder", } } } design = deferred(db.Column(db.String), "Assessment") operationally = deferred(db.Column(db.String), "Assessment") audit_id = deferred( db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False), 'Assessment') @declared_attr def object_level_definitions(self): """Set up a backref so that we can create an object level custom attribute definition without the need to do a flush to get the assessment id. This is used in the relate_ca method in hooks/assessment.py. """ return db.relationship('CustomAttributeDefinition', primaryjoin=lambda: and_( remote(CustomAttributeDefinition. definition_id) == Assessment.id, remote(CustomAttributeDefinition. definition_type) == "assessment"), foreign_keys=[ CustomAttributeDefinition.definition_id, CustomAttributeDefinition.definition_type ], backref='assessment_definition', cascade='all, delete-orphan') object = {} # we add this for the sake of client side error checking VALID_CONCLUSIONS = frozenset( ["Effective", "Ineffective", "Needs improvement", "Not Applicable"]) # REST properties _publish_attrs = [ 'design', 'operationally', 'audit', PublishOnly('object') ] _fulltext_attrs = [ 'design', 'operationally', MultipleSubpropertyFullTextAttr('related_assessors', 'assessors', ['user_name', 'email', 'name']), MultipleSubpropertyFullTextAttr('related_creators', 'creators', ['user_name', 'email', 'name']), MultipleSubpropertyFullTextAttr('related_verifiers', 'verifiers', ['user_name', 'email', 'name']), MultipleSubpropertyFullTextAttr('document_evidence', 'document_evidence', ['title', 'link']), MultipleSubpropertyFullTextAttr('document_url', 'document_url', ['link']), ] _tracked_attrs = { 'contact_id', 'description', 'design', 'notes', 'operationally', 'reference_url', 'secondary_contact_id', 'test_plan', 'title', 'url', 'start_date', 'end_date' } _aliases = { "owners": None, "assessment_template": { "display_name": "Template", "ignore_on_update": True, "filter_by": "_ignore_filter", "type": reflection.AttributeInfo.Type.MAPPING, }, "url": "Assessment URL", "design": "Conclusion: Design", "operationally": "Conclusion: Operation", "related_creators": { "display_name": "Creators", "mandatory": True, "type": reflection.AttributeInfo.Type.MAPPING, }, "related_assessors": { "display_name": "Assignees", "mandatory": True, "type": reflection.AttributeInfo.Type.MAPPING, }, "related_verifiers": { "display_name": "Verifiers", "type": reflection.AttributeInfo.Type.MAPPING, }, } AUTO_REINDEX_RULES = [ ReindexRule("RelationshipAttr", reindex_by_relationship_attr), ReindexRule("Relationship", reindex_by_relationship) ] similarity_options = similarity_options_module.ASSESSMENT @property def assessors(self): """Get the list of assessor assignees""" return self.assignees_by_type.get("Assessor", []) @property def creators(self): """Get the list of creator assignees""" return self.assignees_by_type.get("Creator", []) @property def verifiers(self): """Get the list of verifier assignees""" return self.assignees_by_type.get("Verifier", []) @property def document_evidence(self): return self.documents_by_type("document_evidence") @property def document_url(self): return self.documents_by_type("document_url") def validate_conclusion(self, value): return value if value in self.VALID_CONCLUSIONS else None @validates("operationally") def validate_opperationally(self, key, value): # pylint: disable=unused-argument return self.validate_conclusion(value) @validates("design") def validate_design(self, key, value): # pylint: disable=unused-argument return self.validate_conclusion(value) @classmethod def _ignore_filter(cls, _): return None
class Assessment(Roleable, statusable.Statusable, AuditRelationship, AutoStatusChangeable, Assignable, HasObjectState, TestPlanned, CustomAttributable, PublicDocumentable, Commentable, Personable, reminderable.Reminderable, Timeboxed, Relatable, WithSimilarityScore, FinishedDate, VerifiedDate, ValidateOnComplete, Notifiable, WithAction, BusinessObject, Indexed, db.Model): """Class representing Assessment. Assessment is an object representing an individual assessment performed on a specific object during an audit to ascertain whether or not certain conditions were met for that object. """ __tablename__ = 'assessments' _title_uniqueness = False ASSIGNEE_TYPES = (u"Creator", u"Assessor", u"Verifier") REMINDERABLE_HANDLERS = { "statusToPerson": { "handler": reminderable.Reminderable.handle_state_to_person_reminder, "data": { statusable.Statusable.START_STATE: "Assessor", "In Progress": "Assessor" }, "reminders": { "assessment_assessor_reminder", } } } design = deferred(db.Column(db.String), "Assessment") operationally = deferred(db.Column(db.String), "Assessment") audit_id = deferred( db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False), 'Assessment') assessment_type = deferred( db.Column(db.String, nullable=False, server_default="Control"), "Assessment") @declared_attr def object_level_definitions(cls): # pylint: disable=no-self-argument """Set up a backref so that we can create an object level custom attribute definition without the need to do a flush to get the assessment id. This is used in the relate_ca method in hooks/assessment.py. """ return db.relationship( 'CustomAttributeDefinition', primaryjoin=lambda: and_( remote(CustomAttributeDefinition.definition_id) == cls.id, remote(CustomAttributeDefinition.definition_type) == "assessment"), foreign_keys=[ CustomAttributeDefinition.definition_id, CustomAttributeDefinition.definition_type ], backref='assessment_definition', cascade='all, delete-orphan') object = {} # we add this for the sake of client side error checking VALID_CONCLUSIONS = frozenset( ["Effective", "Ineffective", "Needs improvement", "Not Applicable"]) # REST properties _api_attrs = reflection.ApiAttributes( 'design', 'operationally', 'audit', 'assessment_type', reflection.Attribute('archived', create=False, update=False), reflection.Attribute('object', create=False, update=False), ) _fulltext_attrs = [ 'archived', 'design', 'operationally', MultipleSubpropertyFullTextAttr('related_assessors', 'assessors', ['user_name', 'email', 'name']), MultipleSubpropertyFullTextAttr('related_creators', 'creators', ['user_name', 'email', 'name']), MultipleSubpropertyFullTextAttr('related_verifiers', 'verifiers', ['user_name', 'email', 'name']), ] @classmethod def indexed_query(cls): query = super(Assessment, cls).indexed_query() return query.options( orm.Load(cls).undefer_group("Assessment_complete", ), orm.Load(cls).joinedload("audit").undefer_group( "Audit_complete", ), ) _tracked_attrs = { 'description', 'design', 'notes', 'operationally', 'test_plan', 'title', 'start_date', 'end_date' } _aliases = { "owners": None, "assessment_template": { "display_name": "Template", "ignore_on_update": True, "filter_by": "_ignore_filter", "type": reflection.AttributeInfo.Type.MAPPING, }, "assessment_type": { "display_name": "Assessment Type", "mandatory": False, }, "design": "Conclusion: Design", "operationally": "Conclusion: Operation", "related_creators": { "display_name": "Creators", "mandatory": True, "type": reflection.AttributeInfo.Type.MAPPING, }, "related_assessors": { "display_name": "Assignees", "mandatory": True, "type": reflection.AttributeInfo.Type.MAPPING, }, "related_verifiers": { "display_name": "Verifiers", "type": reflection.AttributeInfo.Type.MAPPING, }, "archived": { "display_name": "Archived", "mandatory": False, "ignore_on_update": True, "view_only": True, }, "test_plan": "Assessment Procedure", } AUTO_REINDEX_RULES = [ ReindexRule("RelationshipAttr", reindex_by_relationship_attr) ] similarity_options = { "relevant_types": { "Objective": { "weight": 2 }, "Control": { "weight": 2 }, }, "threshold": 1, } @simple_property def archived(self): return self.audit.archived if self.audit else False @property def assessors(self): """Get the list of assessor assignees""" return self.assignees_by_type.get("Assessor", []) @property def creators(self): """Get the list of creator assignees""" return self.assignees_by_type.get("Creator", []) @property def verifiers(self): """Get the list of verifier assignees""" return self.assignees_by_type.get("Verifier", []) def validate_conclusion(self, value): return value if value in self.VALID_CONCLUSIONS else None @validates("operationally") def validate_opperationally(self, key, value): # pylint: disable=unused-argument return self.validate_conclusion(value) @validates("design") def validate_design(self, key, value): # pylint: disable=unused-argument return self.validate_conclusion(value) @validates("assessment_type") def validate_assessment_type(self, key, value): """Validate assessment type to be the same as existing model name""" # pylint: disable=unused-argument # pylint: disable=no-self-use from ggrc.snapshotter.rules import Types if value and value not in Types.all: raise ValueError( "Assessment type '{}' is not snapshotable".format(value)) return value @classmethod def _ignore_filter(cls, _): return None
class Documentable(object): """Documentable mixin.""" _include_links = [] _fulltext_attrs = [ MultipleSubpropertyFullTextAttr('document_evidence', 'document_evidence', ['title', 'link']), MultipleSubpropertyFullTextAttr('document_url', 'document_url', ['link']), ] @classmethod def documents(cls, document_type): """Return documents releated for that instance and sent docuemtn type.""" document_id = case([ ( Relationship.destination_type == "Document", Relationship.destination_id, ), ( Relationship.source_type == "Document", Relationship.source_id, ), ], else_=literal(False)) documentable_id = case([ (Relationship.destination_type == "Document", Relationship.source_id), ( Relationship.source_type == "Document", Relationship.destination_id, ), ], else_=literal(False)) return db.relationship( Document, # at first we check is documentable_id not False (it return id in fact) # after that we can compare values. # this is required for saving logic consistancy # case return 2 types of values BOOL(false) and INT(id) not Null primaryjoin=lambda: and_(documentable_id, cls.id == documentable_id ), secondary=Relationship.__table__, # at first we check is document_id not False (it return id in fact) # after that we can compare values. # this is required for saving logic consistancy # case return 2 types of values BOOL(false) and INT(id) not Null secondaryjoin=lambda: and_(document_id, Document.id == document_id, Document. document_type == document_type), viewonly=True, ) @declared_attr def document_url(cls): # pylint: disable=no-self-argument return cls.documents(Document.URL) @declared_attr def document_evidence(cls): # pylint: disable=no-self-argument return cls.documents(Document.ATTACHMENT) @classmethod def eager_query(cls): """Eager query classmethod.""" query = super(Documentable, cls).eager_query() document_fields = [ "id", "title", "link", "description", "document_type" ] return cls.eager_inclusions( query, Documentable._include_links).options( orm.subqueryload('document_url').load_only(*document_fields), orm.subqueryload('document_evidence').load_only( *document_fields), ) @staticmethod def _log_docs(documents): return [create_stub(d) for d in documents if d] def log_json(self): """Serialize to JSON""" out_json = super(Documentable, self).log_json() if hasattr(self, "urls"): out_json["urls"] = self._log_docs(self.urls) if hasattr(self, "attachments"): out_json["attachments"] = self._log_docs(self.urls) return out_json @classmethod def indexed_query(cls): return super(Documentable, cls).indexed_query().options( orm.subqueryload("document_url").load_only("id", "title", "link"), orm.subqueryload("document_evidence").load_only("id", "link"), )
class Documentable(object): """Documentable mixin.""" _include_links = [] _fulltext_attrs = [ MultipleSubpropertyFullTextAttr('document_evidence', 'document_evidence', ['title', 'link']), MultipleSubpropertyFullTextAttr('document_url', 'document_url', ['link']), MultipleSubpropertyFullTextAttr('reference_url', 'reference_url', ['link']), ] @declared_attr def documents(cls): """Return documents releated for that instance.""" document_id = case([ ( Relationship.destination_type == "Document", Relationship.destination_id, ), ( Relationship.source_type == "Document", Relationship.source_id, ), ], else_=literal(False)) documentable_id = case([ (Relationship.destination_type == "Document", Relationship.source_id), ( Relationship.source_type == "Document", Relationship.destination_id, ), ], else_=literal(False)) documentable_type = case([ (Relationship.destination_type == "Document", Relationship.source_type), ( Relationship.source_type == "Document", Relationship.destination_type, ), ], ) return db.relationship( Document, # at first we check is documentable_id not False (it return id in fact) # after that we can compare values. # this is required for saving logic consistancy # case return 2 types of values BOOL(false) and INT(id) not Null primaryjoin=lambda: and_(documentable_id, cls.id == documentable_id ), secondary=Relationship.__table__, # at first we check is document_id not False (it return id in fact) # after that we can compare values. # this is required for saving logic consistancy # case return 2 types of values BOOL(false) and INT(id) not Null secondaryjoin=lambda: and_(document_id, Document.id == document_id, documentable_type == cls.__name__), viewonly=True, ) @property def document_url(self): # pylint: disable=no-self-argument # pylint: disable=not-an-iterable return [d for d in self.documents if Document.URL == d.document_type] @property def document_evidence(self): # pylint: disable=no-self-argument # pylint: disable=not-an-iterable return [ d for d in self.documents if Document.ATTACHMENT == d.document_type ] @property def reference_url(self): # pylint: disable=no-self-argument # pylint: disable=not-an-iterable return [ d for d in self.documents if Document.REFERENCE_URL == d.document_type ] @classmethod def eager_query(cls): """Eager query classmethod.""" return cls.eager_inclusions( super(Documentable, cls).eager_query(), Documentable._include_links, ).options( orm.subqueryload('documents', ).undefer_group( "Document_complete", ), ) @staticmethod def _log_docs(documents): """Returns serialization of the given docs""" return [d.log_json() for d in documents if d] def log_json(self): """Serialize to JSON""" # This query is required to refresh related documents collection after # they were mapped to an object. Otherwise python uses cached value, # which might not contain newly created documents. out_json = super(Documentable, self).log_json() out_json["document_url"] = self._log_docs(self.document_url) out_json["document_evidence"] = self._log_docs(self.document_evidence) out_json["reference_url"] = self._log_docs(self.reference_url) return out_json @classmethod def indexed_query(cls): return super(Documentable, cls).indexed_query().options( orm.subqueryload("documents").undefer_group("Document_complete"), )
class WithEvidence(object): """WithEvidence mixin.""" _include_links = [] _fulltext_attrs = [ MultipleSubpropertyFullTextAttr('evidences_file', 'evidences_file', ['title', 'link']), MultipleSubpropertyFullTextAttr('evidences_url', 'evidences_url', ['link']) ] _aliases = { "evidences_url": { "display_name": "Evidence URL", "type": reflection.AttributeInfo.Type.SPECIAL_MAPPING, "description": "New line separated list of URLs.", }, "evidences_file": { "display_name": "Evidence File", "type": reflection.AttributeInfo.Type.SPECIAL_MAPPING, "description": ("New line separated list of evidence links and " "titles.\nExample:\n\nhttp://my.gdrive.link/file " "Title of the evidence link"), } } @declared_attr def evidences(cls): # pylint: disable=no-self-argument """Return evidences related for that instance.""" return db.relationship( Evidence, primaryjoin=lambda: sa.or_( sa.and_( cls.id == Relationship.source_id, Relationship.source_type == cls.__name__, Relationship.destination_type == "Evidence", ), sa.and_( cls.id == Relationship.destination_id, Relationship.destination_type == cls.__name__, Relationship.source_type == "Evidence", )), secondary=Relationship.__table__, secondaryjoin=lambda: sa.or_( sa.and_( Evidence.id == Relationship.source_id, Relationship.source_type == "Evidence", ), sa.and_( Evidence.id == Relationship.destination_id, Relationship.destination_type == "Evidence", )), viewonly=True, ) def get_evidences_by_kind(self, kind): return [e for e in self.evidences if e.kind == kind] @property def evidences_url(self): return self.get_evidences_by_kind(Evidence.URL) @property def evidences_file(self): return self.get_evidences_by_kind(Evidence.FILE) @classmethod def eager_query(cls): """Eager query classmethod.""" return cls.eager_inclusions( super(WithEvidence, cls).eager_query(), WithEvidence._include_links, ).options( sa.orm.subqueryload('evidences', ).undefer_group( 'Evidence_complete', ), ) @staticmethod def _log_evidences(evidences): """Returns serialization of the given docs""" return [e.log_json() for e in evidences if e] def log_json(self): """Serialize to JSON""" # This query is required to refresh related documents collection after # they were mapped to an object. Otherwise python uses cached value, # which might not contain newly created documents. out_json = super(WithEvidence, self).log_json() out_json['evidences_url'] = self._log_evidences(self.evidences_url) out_json['evidences_file'] = self._log_evidences(self.evidences_file) return out_json @classmethod def indexed_query(cls): return super(WithEvidence, cls).indexed_query().options( sa.orm.subqueryload("evidences").load_only( "kind", "title", "link", ), )
class Commentable(object): """Mixin for commentable objects. This is a mixin for adding default options to objects on which people can comment. recipients is used for setting who gets notified (Verifer, Requester, ...). send_by_default should be used for setting the "send notification" flag in the comment modal. """ # pylint: disable=too-few-public-methods VALID_RECIPIENTS = frozenset([ "Assessor", "Assignee", "Creator", "Verifier", ]) @validates("recipients") def validate_recipients(self, key, value): """ Validate recipients list Args: value (string): Can be either empty, or list of comma separated `VALID_RECIPIENTS` """ # pylint: disable=unused-argument if value: value = set(name for name in value.split(",") if name) if value and value.issubset(self.VALID_RECIPIENTS): # The validator is a bit more smart and also makes some filtering of the # given data - this is intended. return ",".join(value) elif not value: return None else: raise ValueError( value, 'Value should be either empty ' + 'or comma separated list of ' + ', '.join(sorted(self.VALID_RECIPIENTS))) recipients = db.Column(db.String, nullable=True, default=u"Assessor,Creator,Verifier") send_by_default = db.Column(db.Boolean, nullable=True, default=True) _publish_attrs = [ "recipients", "send_by_default", ] _aliases = { "recipients": "Recipients", "send_by_default": "Send by default", } _fulltext_attrs = [ MultipleSubpropertyFullTextAttr("comment", "comments", ["description"]), ] @classmethod def indexed_query(cls): return super(Commentable, cls).indexed_query().options( orm.Load(cls).subqueryload("comments").load_only( "id", "description")) @declared_attr def comments(self): """Comments related to self via Relationship table.""" comment_id = case( [(Relationship.destination_type == "Comment", Relationship.destination_id)], else_=Relationship.source_id, ) commentable_id = case( [(Relationship.destination_type == "Comment", Relationship.source_id)], else_=Relationship.destination_id, ) return db.relationship( Comment, primaryjoin=lambda: self.id == commentable_id, secondary=Relationship.__table__, secondaryjoin=lambda: Comment.id == comment_id, viewonly=True, )
class Cycle(WithContact, Stateful, Timeboxed, Described, Titled, Slugged, Notifiable, Indexed, db.Model): """Workflow Cycle model """ __tablename__ = 'cycles' _title_uniqueness = False VALID_STATES = (u'Assigned', u'InProgress', u'Finished', u'Verified') workflow_id = db.Column( db.Integer, db.ForeignKey('workflows.id', ondelete="CASCADE"), nullable=False, ) cycle_task_groups = db.relationship('CycleTaskGroup', backref='cycle', cascade='all, delete-orphan') cycle_task_group_object_tasks = db.relationship( 'CycleTaskGroupObjectTask', backref='cycle', cascade='all, delete-orphan') cycle_task_entries = db.relationship('CycleTaskEntry', backref='cycle', cascade='all, delete-orphan') is_current = db.Column(db.Boolean, default=True, nullable=False) next_due_date = db.Column(db.Date) _publish_attrs = [ 'workflow', 'cycle_task_groups', 'is_current', 'next_due_date', ] _aliases = { "cycle_workflow": { "display_name": "Workflow", "filter_by": "_filter_by_cycle_workflow", }, "status": { "display_name": "State", "mandatory": False, "description": "Options are: \n{} ".format('\n'.join(VALID_STATES)) } } PROPERTY_TEMPLATE = u"cycle {}" _fulltext_attrs = [ MultipleSubpropertyFullTextAttr( "group title", "cycle_task_groups", ["title"], False, ), MultipleSubpropertyFullTextAttr( "group assignee", lambda instance: [g.contact for g in instance.cycle_task_groups], ["name", "email"], False, ), MultipleSubpropertyFullTextAttr( "group due date", 'cycle_task_groups', ["next_due_date"], False, ), MultipleSubpropertyFullTextAttr( "task title", 'cycle_task_group_object_tasks', ["title"], False, ), MultipleSubpropertyFullTextAttr( "task assignee", lambda instance: [t.contact for t in instance.cycle_task_group_object_tasks], ["name", "email"], False), MultipleSubpropertyFullTextAttr("task due date", "cycle_task_group_object_tasks", ["end_date"], False), FullTextAttr("due date", "next_due_date"), ] AUTO_REINDEX_RULES = [ ReindexRule("CycleTaskGroup", lambda x: x.cycle), ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group.cycle), ReindexRule("Person", lambda x: Cycle.query.filter(Cycle.contact_id == x.id)) ] @classmethod def _filter_by_cycle_workflow(cls, predicate): from ggrc_workflows.models.workflow import Workflow return Workflow.query.filter((Workflow.id == cls.workflow_id) & (predicate(Workflow.slug) | predicate(Workflow.title))).exists() @classmethod def eager_query(cls): """Add cycle task groups to cycle eager query This function adds cycle_task_groups as a join option when fetching cycles, and makes sure we fetch all cycle related data needed for generating cycle json, in one query. Returns: a query object with cycle_task_groups added to joined load options. """ query = super(Cycle, cls).eager_query() return query.options(orm.joinedload('cycle_task_groups'), )
class CycleTaskGroup(WithContact, Stateful, Slugged, Timeboxed, Described, Titled, Indexed, Base, db.Model): """Cycle Task Group model. """ __tablename__ = 'cycle_task_groups' _title_uniqueness = False @classmethod def generate_slug_prefix_for(cls, obj): return "CYCLEGROUP" VALID_STATES = (u'Assigned', u'InProgress', u'Finished', u'Verified', u'Declined') cycle_id = db.Column( db.Integer, db.ForeignKey('cycles.id', ondelete="CASCADE"), nullable=False, ) task_group_id = db.Column(db.Integer, db.ForeignKey('task_groups.id'), nullable=True) cycle_task_group_tasks = db.relationship('CycleTaskGroupObjectTask', backref='cycle_task_group', cascade='all, delete-orphan') sort_index = db.Column(db.String(length=250), default="", nullable=False) next_due_date = db.Column(db.Date) _publish_attrs = [ 'cycle', 'task_group', 'cycle_task_group_tasks', 'sort_index', 'next_due_date' ] _aliases = { "cycle": { "display_name": "Cycle", "filter_by": "_filter_by_cycle", }, } PROPERTY_TEMPLATE = u"group {}" _fulltext_attrs = [ MultipleSubpropertyFullTextAttr("task title", 'cycle_task_group_tasks', ["title"], False), MultipleSubpropertyFullTextAttr( "task assignee", lambda instance: [t.contact for t in instance.cycle_task_group_tasks], ["name", "email"], False), MultipleSubpropertyFullTextAttr("task due date", "cycle_task_group_tasks", ["end_date"], False), FullTextAttr( "due date", 'next_due_date', ), FullTextAttr("assignee", "contact", ['name', 'email']), FullTextAttr("cycle title", 'cycle', ['title'], False), FullTextAttr("cycle assignee", lambda x: x.cycle.contact, ['email', 'name'], False), FullTextAttr("cycle due date", lambda x: x.cycle.next_due_date, with_template=False), ] AUTO_REINDEX_RULES = [ ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group), ReindexRule( "Person", lambda x: CycleTaskGroup.query.filter( CycleTaskGroup.contact_id == x.id)), ReindexRule( "Person", lambda x: [ i.cycle for i in CycleTaskGroup.query.filter( CycleTaskGroup.contact_id == x.id) ]), ] @classmethod def _filter_by_cycle(cls, predicate): """Get query that filters cycle task groups. Args: predicate: lambda function that excepts a single parameter and returns true of false. Returns: An sqlalchemy query that evaluates to true or false and can be used in filtering cycle task groups by related cycle. """ return Cycle.query.filter((Cycle.id == cls.cycle_id) & (predicate(Cycle.slug) | predicate(Cycle.title))).exists() @classmethod def eager_query(cls): """Add cycle tasks and objects to cycle task group eager query. Make sure we load all cycle task group relevant data in a single query. Returns: a query object with cycle_task_group_tasks added to joined load options. """ query = super(CycleTaskGroup, cls).eager_query() return query.options(orm.joinedload('cycle_task_group_tasks'))
class Documentable(object): """Documentable mixin.""" _include_links = [] _fulltext_attrs = [ MultipleSubpropertyFullTextAttr('documents_file', 'documents_file', ['title', 'link']), MultipleSubpropertyFullTextAttr('documents_reference_url', 'documents_reference_url', ['link']), ] @declared_attr def documents(cls): # pylint: disable=no-self-argument """Return documents related for that instance.""" return db.relationship( Document, primaryjoin=lambda: sa.or_( sa.and_( cls.id == Relationship.source_id, Relationship.source_type == cls.__name__, Relationship.destination_type == "Document", ), sa.and_( cls.id == Relationship.destination_id, Relationship.destination_type == cls.__name__, Relationship.source_type == "Document", )), secondary=Relationship.__table__, secondaryjoin=lambda: sa.or_( sa.and_( Document.id == Relationship.source_id, Relationship.source_type == "Document", ), sa.and_( Document.id == Relationship.destination_id, Relationship.destination_type == "Document", )), viewonly=True, ) def get_documents_by_kind(self, kind): return [e for e in self.documents if e.kind == kind] @property def documents_file(self): """List of documents FILE type""" return self.get_documents_by_kind(Document.FILE) @property def documents_reference_url(self): """List of documents REFERENCE_URL type""" return self.get_documents_by_kind(Document.REFERENCE_URL) @classmethod def eager_query(cls): """Eager query classmethod.""" return cls.eager_inclusions( super(Documentable, cls).eager_query(), Documentable._include_links, ).options( sa.orm.subqueryload('documents', ).undefer_group( 'Document_complete', ), ) @staticmethod def _log_docs(documents): """Returns serialization of the given docs""" return [d.log_json() for d in documents if d] def log_json(self): """Serialize to JSON""" # This query is required to refresh related documents collection after # they were mapped to an object. Otherwise python uses cached value, # which might not contain newly created documents. out_json = super(Documentable, self).log_json() out_json["documents_file"] = self._log_docs(self.documents_file) out_json["documents_reference_url"] = self._log_docs( self.documents_reference_url) return out_json @classmethod def indexed_query(cls): return super(Documentable, cls).indexed_query().options( sa.orm.subqueryload("documents").load_only( "title", "link", "kind", ), )