class Relationship(base.ContextRBAC, Base, db.Model): """Relationship model.""" __tablename__ = 'relationships' source_id = db.Column(db.Integer, nullable=False) source_type = db.Column(db.String, nullable=False) destination_id = db.Column(db.Integer, nullable=False) destination_type = db.Column(db.String, nullable=False) parent_id = db.Column( db.Integer, db.ForeignKey('relationships.id', ondelete='SET NULL'), nullable=True, ) parent = db.relationship( lambda: Relationship, remote_side=lambda: Relationship.id ) automapping_id = db.Column( db.Integer, db.ForeignKey('automappings.id', ondelete='CASCADE'), nullable=True, ) is_external = db.Column(db.Boolean, nullable=False, default=False) def get_related_for(self, object_type): """Return related object for sent type.""" if object_type == self.source_type: return self.destination if object_type == self.destination_type: return self.source @property def source_attr(self): return '{0}_source'.format(self.source_type) @property def source(self): """Source getter.""" if not hasattr(self, self.source_attr): logger.warning( "Relationship source attr '%s' does not exist. " "This indicates invalid data in our database!", self.source_attr ) return None return getattr(self, self.source_attr) @source.setter def source(self, value): self.source_id = getattr(value, 'id', None) self.source_type = getattr(value, 'type', None) self.validate_relatable_type("source", value) return setattr(self, self.source_attr, value) @property def destination_attr(self): return '{0}_destination'.format(self.destination_type) @property def destination(self): """Destination getter.""" if not hasattr(self, self.destination_attr): logger.warning( "Relationship destination attr '%s' does not exist. " "This indicates invalid data in our database!", self.destination_attr ) return None return getattr(self, self.destination_attr) @destination.setter def destination(self, value): self.destination_id = getattr(value, 'id', None) self.destination_type = getattr(value, 'type', None) self.validate_relatable_type("destination", value) return setattr(self, self.destination_attr, value) @classmethod def find_related(cls, object1, object2): return cls.get_related_query(object1, object2).first() @classmethod def get_related_query(cls, object1, object2): def predicate(src, dst): return and_( Relationship.source_type == src.type, or_(Relationship.source_id == src.id, src.id == None), # noqa Relationship.destination_type == dst.type, or_(Relationship.destination_id == dst.id, dst.id == None), # noqa ) return Relationship.query.filter( or_(predicate(object1, object2), predicate(object2, object1)) ) @staticmethod def _extra_table_args(cls): return ( db.UniqueConstraint( 'source_id', 'source_type', 'destination_id', 'destination_type'), db.Index( 'ix_relationships_source', 'source_type', 'source_id'), db.Index( 'ix_relationships_destination', 'destination_type', 'destination_id'), ) _api_attrs = reflection.ApiAttributes( 'source', 'destination', reflection.Attribute( 'is_external', create=True, update=False, read=True), ) def _display_name(self): return "{}:{} <-> {}:{}".format(self.source_type, self.source_id, self.destination_type, self.destination_id) def validate_relatable_type(self, field, value): if value is None: raise ValidationError(u"{}.{} can't be None." .format(self.__class__.__name__, field)) if not isinstance(value, Relatable): raise ValidationError(u"You are trying to create relationship with not " u"Relatable type: {}".format(value.type)) tgt_type = self.source_type tgt_id = self.source_id self.validate_relation_by_type(self.source_type, self.destination_type) if field == "source": tgt_type = self.destination_type tgt_id = self.destination_id if value and getattr(value, "type") == "Snapshot": if not tgt_type: return if value.child_type == tgt_type and value.child_id == tgt_id: raise ValidationError( u"Invalid source-destination types pair for {}: " u"source_type={!r}, destination_type={!r}" .format(self.type, self.source_type, self.destination_type) ) # else check if the opposite is a Snapshot elif tgt_type == "Snapshot": from ggrc.models import Snapshot snapshot = db.session.query(Snapshot).get(tgt_id) if snapshot.child_type == value.type and snapshot.child_id == value.id: raise ValidationError( u"Invalid source-destination types pair for {}: " u"source_type={!r}, destination_type={!r}" .format(self.type, self.source_type, self.destination_type) ) # pylint:disable=unused-argument @validates("is_external") def validate_is_external(self, key, value): """Validates is change of is_external column value allowed.""" if is_external_app_user() and (not value or self.is_external is False): raise ValidationError( 'External application can create only external relationships.') return value # pylint:disable=unused-argument @staticmethod def validate_delete(mapper, connection, target): """Validates is delete of Relationship is allowed.""" Relationship.validate_relation_by_type(target.source_type, target.destination_type) if is_external_app_user() and not target.is_external: raise ValidationError( 'External application can delete only external relationships.') @staticmethod def validate_relation_by_type(source_type, destination_type): """Checks if a mapping is allowed between given types.""" if is_external_app_user(): # external users can map and unmap scoping objects # check that relationship is external is done in a separate validator return from ggrc.models import all_models scoping_models_names = [m.__name__ for m in all_models.all_models if issubclass(m, ScopeObject)] if source_type in scoping_models_names and \ destination_type in ("Regulation", "Standard") or \ destination_type in scoping_models_names and \ source_type in ("Regulation", "Standard"): raise ValidationError( u"You do not have the necessary permissions to map and unmap " u"scoping objects to directives in this application. Please " u"contact your administrator if you have any questions.")
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' CHECKBOX = 'checkbox' VALID_TASK_TYPES = [TEXT, 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 None 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), ) DATE_HINT = "Allowed value is date in one of formats listed" \ " below:\nYYYY-MM-DD\nMM/DD/YYYY." _sanitize_html = [] _aliases = { "title": "Task Title", "description": { "display_name": "Task Description", "handler_key": "task_description", }, "start_date": { "display_name": "Task Start Date", "mandatory": True, "description": "{}\nOnly working days are accepted".format(DATE_HINT), }, "end_date": { "display_name": "Task Due Date", "mandatory": True, "description": "{}\nOnly working days are accepted".format(DATE_HINT), }, "task_group": { "display_name": "Task Group Code", "mandatory": True, "filter_by": "_filter_by_task_group", }, "task_type": { "display_name": "Task Type", "mandatory": True, "description": ("Accepted values are:" "\n'Rich Text'\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) return None @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 RiskAssessment(Documentable, Timeboxed, Noted, Described, CustomAttributable, Titled, Relatable, Slugged, Indexed, db.Model): """Risk Assessment model.""" __tablename__ = 'risk_assessments' _title_uniqueness = False ra_manager_id = deferred(db.Column(db.Integer, db.ForeignKey('people.id')), 'RiskAssessment') ra_manager = db.relationship('Person', uselist=False, foreign_keys='RiskAssessment.ra_manager_id') ra_counsel_id = deferred(db.Column(db.Integer, db.ForeignKey('people.id')), 'RiskAssessment') ra_counsel = db.relationship('Person', uselist=False, foreign_keys='RiskAssessment.ra_counsel_id') program_id = deferred( db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False), 'RiskAssessment') program = db.relationship('Program', backref='risk_assessments', uselist=False, foreign_keys='RiskAssessment.program_id') _fulltext_attrs = [] _publish_attrs = [ 'ra_manager', 'ra_counsel', 'program', ] _aliases = { "ra_manager": { "display_name": "Risk Manager", "filter_by": "_filter_by_risk_manager", }, "ra_counsel": { "display_name": "Risk Counsel", "filter_by": "_filter_by_risk_counsel", }, "start_date": { "display_name": "Start Date", "mandatory": True, }, "end_date": { "display_name": "End Date", "mandatory": True, }, "program": { "display_name": "Program", "mandatory": True, "filter_by": "_filter_by_program", } } @classmethod def _filter_by_program(cls, predicate): return Program.query.filter((Program.id == cls.program_id) & (predicate(Program.slug) | predicate(Program.title))).exists() @classmethod def _filter_by_risk_manager(cls, predicate): return Person.query.filter((Person.id == cls.ra_manager_id) & (predicate(Person.name) | predicate(Person.email))).exists() @classmethod def _filter_by_risk_counsel(cls, predicate): return Person.query.filter((Person.id == cls.ra_counsel_id) & (predicate(Person.name) | predicate(Person.email))).exists()
def updated_by_id(cls): # pylint: disable=no-self-argument """Id of user who did the last modification of the object.""" return db.Column(db.Integer, db.ForeignKey('people.id'), nullable=True)
class Relationship(Base, db.Model): __tablename__ = 'relationships' source_id = db.Column(db.Integer, nullable=False) source_type = db.Column(db.String, nullable=False) destination_id = db.Column(db.Integer, nullable=False) destination_type = db.Column(db.String, nullable=False) parent_id = db.Column( db.Integer, db.ForeignKey('relationships.id', ondelete='SET NULL'), nullable=True, ) parent = db.relationship(lambda: Relationship, remote_side=lambda: Relationship.id) automapping_id = db.Column( db.Integer, db.ForeignKey('automappings.id', ondelete='CASCADE'), nullable=True, ) relationship_attrs = db.relationship( lambda: RelationshipAttr, collection_class=attribute_mapped_collection("attr_name"), lazy='joined', # eager loading cascade='all, delete-orphan') attrs = association_proxy( "relationship_attrs", "attr_value", creator=lambda k, v: RelationshipAttr(attr_name=k, attr_value=v)) @property def source_attr(self): return '{0}_source'.format(self.source_type) @property def source(self): return getattr(self, self.source_attr) @source.setter def source(self, value): self.source_id = getattr(value, 'id', None) self.source_type = getattr(value, 'type', None) return setattr(self, self.source_attr, value) @property def destination_attr(self): return '{0}_destination'.format(self.destination_type) @property def destination(self): return getattr(self, self.destination_attr) @destination.setter def destination(self, value): self.destination_id = getattr(value, 'id', None) self.destination_type = getattr(value, 'type', None) return setattr(self, self.destination_attr, value) @staticmethod def validate_attrs(mapper, connection, relationship): """ Only white-listed attributes can be stored, so users don't use this for storing arbitrary data. """ # pylint: disable=unused-argument for attr_name, attr_value in relationship.attrs.iteritems(): attr = RelationshipAttr(attr_name=attr_name, attr_value=attr_value) RelationshipAttr.validate_attr(relationship.source, relationship.destination, relationship.attrs, attr) @classmethod def find_related(cls, object1, object2): return cls.get_related_query(object1, object2).first() @classmethod def get_related_query(cls, object1, object2): def predicate(src, dst): return and_( Relationship.source_type == src.type, or_(Relationship.source_id == src.id, src.id == None), # noqa Relationship.destination_type == dst.type, or_(Relationship.destination_id == dst.id, dst.id == None), # noqa ) return Relationship.query.filter( or_(predicate(object1, object2), predicate(object2, object1))) @classmethod def update_attributes(cls, object1, object2, new_attrs): r = cls.find_related(object1, object2) for attr_name, attr_value in new_attrs.iteritems(): attr = RelationshipAttr(attr_name=attr_name, attr_value=attr_value) attr = RelationshipAttr.validate_attr(r.source, r.destination, r.attrs, attr) r.attrs[attr.attr_name] = attr.attr_value return r @staticmethod def _extra_table_args(cls): return ( db.UniqueConstraint('source_id', 'source_type', 'destination_id', 'destination_type'), db.Index('ix_relationships_source', 'source_type', 'source_id'), db.Index('ix_relationships_destination', 'destination_type', 'destination_id'), ) _api_attrs = reflection.ApiAttributes('source', 'destination', 'attrs') attrs.publish_raw = True def _display_name(self): return "{}:{} <-> {}:{}".format(self.source_type, self.source_id, self.destination_type, self.destination_id) def log_json(self): json = super(Relationship, self).log_json() # manually add attrs since the base log_json only captures table columns json["attrs"] = self.attrs.copy() # copy in order to detach from orm return json
class ImportExport(Identifiable, db.Model): """ImportExport Model.""" __tablename__ = 'import_exports' IMPORT_JOB_TYPE = 'Import' EXPORT_JOB_TYPE = 'Export' ANALYSIS_STATUS = 'Analysis' BLOCKED_STATUS = 'Blocked' FAILED_STATUS = 'Failed' IN_PROGRESS_STATUS = 'In Progress' NOT_STARTED_STATUS = 'Not Started' IMPORT_EXPORT_STATUSES = [ NOT_STARTED_STATUS, ANALYSIS_STATUS, IN_PROGRESS_STATUS, BLOCKED_STATUS, 'Analysis Failed', 'Stopped', FAILED_STATUS, 'Finished', ] DEFAULT_COLUMNS = ['id', 'title', 'created_at', 'status'] job_type = db.Column(db.Enum(IMPORT_JOB_TYPE, EXPORT_JOB_TYPE), nullable=False) status = db.Column(db.Enum(*IMPORT_EXPORT_STATUSES), nullable=False, default=NOT_STARTED_STATUS) description = db.Column(db.Text) created_at = db.Column(db.DateTime, nullable=False) start_at = db.Column(db.DateTime) end_at = db.Column(db.DateTime) created_by_id = db.Column(db.Integer, db.ForeignKey('people.id'), nullable=False) created_by = db.relationship('Person', foreign_keys='ImportExport.created_by_id', uselist=False) results = db.Column(mysql.LONGTEXT) title = db.Column(db.Text) content = db.Column(mysql.LONGTEXT) gdrive_metadata = db.Column('gdrive_metadata', db.Text) def log_json(self, is_default=False): """JSON representation""" if is_default: columns = self.DEFAULT_COLUMNS else: columns = (column.name for column in self.__table__.columns if column.name not in ('content', 'gdrive_metadata')) res = {} for column in columns: if column == "results": res[column] = json.loads(self.results) if self.results \ else self.results elif column == "created_at": res[column] = self.created_at.isoformat() else: res[column] = getattr(self, column) return res
class Audit(Snapshotable, clonable.SingleClonable, WithEvidence, mixins.CustomAttributable, Personable, HasOwnContext, Relatable, Roleable, issue_tracker_mixins.IssueTrackedWithConfig, 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): query = super(Audit, cls).eager_query() 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 Audit(Snapshotable, clonable.Clonable, CustomAttributable, Personable, HasOwnContext, Relatable, Timeboxed, Noted, Described, Hyperlinked, WithContact, Titled, Stateful, Slugged, 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') _publish_attrs = [ 'report_start_date', 'report_end_date', 'audit_firm', 'status', 'gdrive_evidence_folder', 'program', 'object_type', PublishOnly('audit_objects') ] _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", }, "status": { "display_name": "Status", "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", "contact": { "display_name": "Internal Audit Lead", "mandatory": True, "filter_by": "_filter_by_contact", }, "secondary_contact": None, "notes": None, "url": None, "reference_url": None, } 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): from sqlalchemy import orm query = super(Audit, cls).eager_query() return query.options( orm.joinedload('program'), orm.subqueryload('object_people').joinedload('person'), orm.subqueryload('audit_objects'), )
class SavedSearch(CreationTimeTracked, Dictable, Identifiable, db.Model): """ Represents table which stores queary API filters for given user. """ __tablename__ = "saved_searches" __table_args__ = (UniqueConstraint( "person_id", "name", name="unique_pair_saved_search_name_person_id", ), ) ADVANCED_SEARCH = "AdvancedSearch" GLOBAL_SEARCH = "GlobalSearch" VALID_SAVED_SEARCH_TYPES = [ADVANCED_SEARCH, GLOBAL_SEARCH] name = db.Column(db.String, nullable=False) object_type = db.Column(db.String, nullable=False) person_id = db.Column(db.Integer, db.ForeignKey("people.id")) filters = db.Column(db.Text, nullable=True) search_type = db.Column(db.String, nullable=False) # pylint: disable-msg=too-many-arguments def __init__(self, name, object_type, user, search_type, filters=""): self.validate_name_uniqueness(user, name, search_type, object_type) super(SavedSearch, self).__init__( name=name, object_type=object_type, person_id=user.id, search_type=search_type, filters=filters, ) @staticmethod def validate_name_uniqueness(user, name, search_type, object_type): """Check that for given user there are no saved searches with name.""" if search_type == SavedSearch.GLOBAL_SEARCH: saved_seqrch_q = user.saved_searches.filter( SavedSearch.name == name, SavedSearch.search_type == search_type, ) if db.session.query(saved_seqrch_q.exists()).scalar(): raise ValidationError( u"Global Saved search with name '{}' already exists". format(name)) else: saved_seqrch_q = user.saved_searches.filter( SavedSearch.name == name, SavedSearch.search_type == search_type, SavedSearch.object_type == object_type, ) if db.session.query(saved_seqrch_q.exists()).scalar(): raise ValidationError(u"Advanced Saved search for {} with " u"name '{}' already exists".format( object_type, name)) @validates("name") def validate_name(self, _, name): """ Validate that name is not blank. """ # pylint: disable=no-self-use if not name: raise ValidationError("Saved search name can't be blank") return name @validates("object_type") def validate_object_type(self, _, object_type): """ Validate that supplied object type supports search api filters saving. """ # pylint: disable=no-self-use if object_type and object_type not in SUPPORTED_OBJECT_TYPES: raise ValidationError( u"Object of type '{}' does not support search saving".format( object_type, ), ) return object_type @validates("filters") def validate_filters(self, _, filters): """Validate correctness of supplied search filters. Validates that filters is valid json formatted string. Args: filters: string value with filters Returns: JSON object with filters """ # pylint: disable=no-self-use if filters: return json.dumps(filters) return None @validates('search_type') def validate_search_type(self, _, saved_search_type): """Valid that saved search type is correct Args: Type of saved search. Valid values are AdvancedSearch and GlobalSearch Returns: Correct saved search type Raises: ValidationError: if saved_search_type is missing or not valid value """ # pylint: disable=no-self-use if not saved_search_type: raise ValidationError("Saved search type can't be blank") if saved_search_type not in self.VALID_SAVED_SEARCH_TYPES: raise ValidationError("Invalid saved search type") return saved_search_type
class Cycle(mixins.WithContact, wf_mixins.CycleStatusValidatedMixin, mixins.Timeboxed, mixins.Described, mixins.Titled, mixins.Slugged, mixins.Notifiable, ft_mixin.Indexed, db.Model): """Workflow Cycle model """ __tablename__ = 'cycles' _title_uniqueness = False workflow_id = db.Column( db.Integer, db.ForeignKey('workflows.id', ondelete="CASCADE"), nullable=False, ) cycle_task_groups = db.relationship( 'CycleTaskGroup', backref='cycle', cascade='all, delete-orphan') cycle_task_group_object_tasks = db.relationship( 'CycleTaskGroupObjectTask', backref='cycle', cascade='all, delete-orphan') cycle_task_entries = db.relationship( 'CycleTaskEntry', backref='cycle', cascade='all, delete-orphan') is_current = db.Column(db.Boolean, default=True, nullable=False) next_due_date = db.Column(db.Date) @property def is_done(self): """Check if cycle's done Overrides StatusValidatedMixin method because cycle's is_done state depends on is_verification_needed flag """ if super(Cycle, self).is_done: return True if self.cycle_task_group_object_tasks: return False return True _api_attrs = reflection.ApiAttributes( 'workflow', 'cycle_task_groups', 'is_current', 'next_due_date', ) _aliases = { "cycle_workflow": { "display_name": "Workflow", "filter_by": "_filter_by_cycle_workflow", }, "contact": "Assignee", "secondary_contact": None, } PROPERTY_TEMPLATE = u"cycle {}" _fulltext_attrs = [ ft_attributes.MultipleSubpropertyFullTextAttr( "group title", "cycle_task_groups", ["title"], False, ), ft_attributes.MultipleSubpropertyFullTextAttr( "group assignee", lambda instance: [g.contact for g in instance.cycle_task_groups], ["email", "name"], False, ), ft_attributes.DateMultipleSubpropertyFullTextAttr( "group due date", 'cycle_task_groups', ["next_due_date"], False, ), ft_attributes.MultipleSubpropertyFullTextAttr( "task title", 'cycle_task_group_object_tasks', ["title"], False, ), ft_attributes.DateMultipleSubpropertyFullTextAttr( "task due date", "cycle_task_group_object_tasks", ["end_date"], False ), ft_attributes.MultipleSubpropertyFullTextAttr( "task assignees", "_task_assignees", ["name", "email"], False, ), ft_attributes.DateFullTextAttr("due date", "next_due_date"), ft_attributes.MultipleSubpropertyFullTextAttr( "task comments", lambda instance: list(itertools.chain(*[ t.cycle_task_entries for t in instance.cycle_task_group_object_tasks ])), ["description"], False ), ] @property def _task_assignees(self): """Property. Return the list of persons as assignee of related tasks.""" persons = {} for task in self.cycle_task_group_object_tasks: for person in task.get_persons_for_rolename("Task Assignees"): persons[person.id] = person return persons.values() AUTO_REINDEX_RULES = [ ft_mixin.ReindexRule("CycleTaskGroup", lambda x: x.cycle), ft_mixin.ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group.cycle), ft_mixin.ReindexRule("Person", _query_filtered_by_contact) ] @classmethod def _filter_by_cycle_workflow(cls, predicate): from ggrc_workflows.models.workflow import Workflow return Workflow.query.filter( (Workflow.id == cls.workflow_id) & (predicate(Workflow.slug) | predicate(Workflow.title)) ).exists() @classmethod def eager_query(cls): """Add cycle task groups to cycle eager query This function adds cycle_task_groups as a join option when fetching cycles, and makes sure we fetch all cycle related data needed for generating cycle json, in one query. Returns: a query object with cycle_task_groups added to joined load options. """ query = super(Cycle, cls).eager_query() return query.options( orm.joinedload('cycle_task_groups'), ) @classmethod def indexed_query(cls): return super(Cycle, cls).indexed_query().options( orm.Load(cls).load_only("next_due_date"), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").load_only( "id", "title", "end_date" ), orm.Load(cls).subqueryload("cycle_task_groups").load_only( "id", "title", "end_date", "next_due_date", ), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload( "cycle_task_entries" ).load_only( "description", "id" ), orm.Load(cls).subqueryload("cycle_task_groups").joinedload( "contact" ).load_only( "email", "name", "id" ), orm.Load(cls).joinedload("contact").load_only( "email", "name", "id" ), ) def _get_cycle_url(self, widget_name): return urljoin( get_url_root(), "workflows/{workflow_id}#{widget_name}/cycle/{cycle_id}".format( workflow_id=self.workflow.id, cycle_id=self.id, widget_name=widget_name ) ) @property def cycle_url(self): return self._get_cycle_url("current_widget") @property def cycle_inactive_url(self): return self._get_cycle_url("history_widget")
class CycleTaskGroup(roleable.Roleable, relationship.Relatable, mixins.WithContact, wf_mixins.CycleTaskGroupRelatedStatusValidatedMixin, mixins.Slugged, mixins.Timeboxed, mixins.Described, mixins.Titled, base.ContextRBAC, mixins.Base, index_mixin.Indexed, db.Model): """Cycle Task Group model. """ __tablename__ = 'cycle_task_groups' _title_uniqueness = False @classmethod def generate_slug_prefix(cls): # pylint: disable=unused-argument return "CYCLEGROUP" cycle_id = db.Column( db.Integer, db.ForeignKey('cycles.id', ondelete="CASCADE"), nullable=False, ) task_group_id = db.Column(db.Integer, db.ForeignKey('task_groups.id'), nullable=True) cycle_task_group_tasks = db.relationship('CycleTaskGroupObjectTask', backref='_cycle_task_group', cascade='all, delete-orphan') sort_index = db.Column(db.String(length=250), default="", nullable=False) next_due_date = db.Column(db.Date) _api_attrs = reflection.ApiAttributes('cycle', 'task_group', 'cycle_task_group_tasks', 'sort_index', 'next_due_date') _aliases = { "cycle": { "display_name": "Cycle", "filter_by": "_filter_by_cycle", }, } PROPERTY_TEMPLATE = u"group {}" _fulltext_attrs = [ attributes.DateFullTextAttr( "due date", 'next_due_date', ), attributes.FullTextAttr("assignee", "contact", ['email', 'name']), attributes.FullTextAttr("cycle title", 'cycle', ['title'], False), attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact, ['email', 'name'], False), attributes.DateFullTextAttr("cycle due date", lambda x: x.cycle.next_due_date, with_template=False), ] # This parameter is overridden by cycle backref, but is here to ensure # pylint does not complain _cycle = None @hybrid.hybrid_property def cycle(self): """Getter for cycle foreign key.""" return self._cycle @cycle.setter def cycle(self, cycle): """Set cycle foreign key and relationship.""" if not self._cycle and cycle: relationship.Relationship(source=cycle, destination=self) self._cycle = cycle @property def workflow(self): """Property which returns parent workflow object.""" return self.cycle.workflow @property def _task_assignees(self): """Property. Return the list of persons as assignee of related tasks.""" people = set() for ctask in self.cycle_task_group_tasks: people.update(ctask.get_persons_for_rolename("Task Assignees")) return list(people) @property def _task_secondary_assignees(self): """Property. Returns people list as Secondary Assignee of related tasks.""" people = set() for ctask in self.cycle_task_group_tasks: people.update( ctask.get_persons_for_rolename("Task Secondary Assignees")) return list(people) AUTO_REINDEX_RULES = [ index_mixin.ReindexRule("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.subqueryload("cycle_task_group_tasks"), orm.joinedload("cycle").undefer_group("Cycle_complete"), orm.joinedload("cycle").joinedload("contact"))
def secondary_contact_id(cls): return deferred( db.Column(db.Integer, db.ForeignKey('people.id')), cls.__name__)
def context_id(cls): return db.Column(db.Integer, db.ForeignKey('contexts.id'))
def parent_id(cls): return deferred(db.Column( db.Integer, db.ForeignKey('{0}.id'.format(cls.__tablename__))), cls.__name__)
class Snapshot(rest_handable.WithDeleteHandable, roleable.Roleable, relationship.Relatable, with_last_assessment_date.WithLastAssessmentDate, base.ContextRBAC, mixins.Base, db.Model): """Snapshot object that holds a join of parent object, revision, child object and parent object's context. Conceptual model is that we have a parent snapshotable object (e.g. Audit) which will not create relationships to objects with automapper at the time of creation but will instead create snapshots of those objects based on the latest revision of the object at the time of create / update of the object. Objects that were supposed to be mapped are called child objects. """ __tablename__ = "snapshots" _api_attrs = reflection.ApiAttributes( "parent", "child_id", "child_type", reflection.Attribute("revision", create=False, update=False), reflection.Attribute("revision_id", create=False, update=False), reflection.Attribute("archived", create=False, update=False), reflection.Attribute("revisions", create=False, update=False), reflection.Attribute("is_latest_revision", create=False, update=False), reflection.Attribute("original_object_deleted", create=False, update=False), reflection.Attribute("update_revision", read=False), ) _include_links = ["revision"] _aliases = { "attributes": "Attributes", "archived": "Archived", "mappings": { "display_name": "Mappings", "type": "mapping", } } parent_id = deferred( db.Column(db.Integer, db.ForeignKey("audits.id", ondelete='CASCADE'), nullable=False), "Snapshot", ) parent_type = deferred( db.Column(db.String, nullable=False, default="Audit"), "Snapshot", ) @orm.validates("parent_type") def validate_parent_type(self, _, value): """Validates parent_type equals 'Audit'""" # pylint: disable=no-self-use if value != "Audit": raise ValueError( "Wrong 'parent_type' value. Only 'Audit' supported") return value # Child ID and child type are data denormalisations - we could easily get # them from revision.content, but since that is a JSON field it will be # easier for development to just denormalise on write and not worry # about it. child_id = deferred(db.Column(db.Integer, nullable=False), "Snapshot") child_type = deferred(db.Column(db.String, nullable=False), "Snapshot") revision_id = deferred( db.Column(db.Integer, db.ForeignKey("revisions.id"), nullable=False), "Snapshot") revision = db.relationship("Revision", ) _update_revision = None revisions = db.relationship( "Revision", primaryjoin="and_(Revision.resource_id == foreign(Snapshot.child_id)," "Revision.resource_type == foreign(Snapshot.child_type))", uselist=True, ) @builder.simple_property def archived(self): return self.audit.archived if self.audit else False @builder.simple_property def is_latest_revision(self): """Flag if the snapshot has the latest revision.""" return self.revisions and self.revision == self.revisions[-1] @builder.simple_property def original_object_deleted(self): """Flag if the snapshot has the latest revision.""" return self.revisions and self.revisions[-1].action == "deleted" @classmethod def eager_query(cls, **kwargs): query = super(Snapshot, cls).eager_query(**kwargs) return cls.eager_inclusions(query, Snapshot._include_links).options( orm.subqueryload('revision'), orm.subqueryload('revisions'), orm.joinedload('audit').load_only("id", "archived"), ) @hybrid_property def update_revision(self): return self.revision_id @update_revision.setter def update_revision(self, value): self._update_revision = value if value == "latest": _set_latest_revisions([self]) @property def parent(self): return self.audit @parent.setter def parent(self, value): setattr(self, "audit", value) self.parent_type = "Audit" @staticmethod def _extra_table_args(_): return ( db.UniqueConstraint("parent_type", "parent_id", "child_type", "child_id"), db.Index("ix_snapshots_parent", "parent_type", "parent_id"), db.Index("ix_snapshots_child", "child_type", "child_id"), ) def _check_related_objects(self): """Checks that Snapshot mapped only to Audits before deletion""" for obj in self.related_objects(): if obj.type not in ("Audit", "Snapshot"): db.session.rollback() raise exceptions.Conflict( description="Snapshot should be mapped " "to Audit only before deletion") elif obj.type == "Snapshot": rel = relationship.Relationship related_originals = db.session.query( rel.query.filter( or_( and_(rel.source_id == obj.child_id, rel.source_type == obj.child_type, rel.destination_id == self.child_id, rel.destination_type == self.child_type), and_(rel.destination_id == obj.child_id, rel.destination_type == obj.child_type, rel.source_id == self.child_id, rel.source_type == self.child_type))).exists()).scalar() if related_originals: db.session.rollback() raise exceptions.Conflict( description="Snapshot should be mapped to " "Audit only before deletion") def handle_delete(self): """Handle model_deleted signal for Snapshot""" self._check_related_objects()
class Control(WithLastAssessmentDate, review.Reviewable, Roleable, Relatable, mixins.CustomAttributable, Personable, ControlCategorized, PublicDocumentable, AssertionCategorized, mixins.LastDeprecatedTimeboxed, mixins.TestPlanned, Commentable, WithSimilarityScore, base.ContextRBAC, mixins.BusinessObject, Indexed, mixins.Folderable, proposal.Proposalable, db.Model): """Control model definition.""" __tablename__ = 'controls' company_control = deferred(db.Column(db.Boolean), 'Control') directive_id = deferred( db.Column(db.Integer, db.ForeignKey('directives.id')), 'Control') kind_id = deferred(db.Column(db.Integer), 'Control') means_id = deferred(db.Column(db.Integer), 'Control') version = deferred(db.Column(db.String), 'Control') verify_frequency_id = deferred(db.Column(db.Integer), 'Control') fraud_related = deferred(db.Column(db.Boolean), 'Control') key_control = deferred(db.Column(db.Boolean), 'Control') active = deferred(db.Column(db.Boolean), 'Control') kind = db.relationship( 'Option', primaryjoin='and_(foreign(Control.kind_id) == Option.id, ' 'Option.role == "control_kind")', uselist=False) means = db.relationship( 'Option', primaryjoin='and_(foreign(Control.means_id) == Option.id, ' 'Option.role == "control_means")', uselist=False) verify_frequency = db.relationship( 'Option', primaryjoin='and_(foreign(Control.verify_frequency_id) == Option.id, ' 'Option.role == "verify_frequency")', uselist=False) # REST properties _api_attrs = reflection.ApiAttributes( 'active', 'company_control', 'directive', 'fraud_related', 'key_control', 'kind', 'means', 'verify_frequency', 'version', ) _fulltext_attrs = [ 'active', 'company_control', 'directive', attributes.BooleanFullTextAttr( 'fraud_related', 'fraud_related', true_value="yes", false_value="no"), attributes.BooleanFullTextAttr( 'key_control', 'key_control', true_value="key", false_value="non-key"), 'kind', 'means', 'verify_frequency', 'version', ] _sanitize_html = [ 'version', ] VALID_RECIPIENTS = frozenset([ "Assignees", "Creators", "Verifiers", "Admin", "Control Operators", "Control Owners", "Other Contacts", ]) @classmethod def indexed_query(cls): return super(Control, cls).indexed_query().options( orm.Load(cls).undefer_group( "Control_complete" ), orm.Load(cls).joinedload( "directive" ).undefer_group( "Directive_complete" ), orm.Load(cls).joinedload( 'kind', ).load_only( "title" ), orm.Load(cls).joinedload( 'means', ).load_only( "title" ), orm.Load(cls).joinedload( 'verify_frequency', ).load_only( "title" ), ) _include_links = [] _aliases = { "kind": "Kind/Nature", "means": "Type/Means", "verify_frequency": "Frequency", "fraud_related": "Fraud Related", "key_control": { "display_name": "Significance", "description": "Allowed values are:\nkey\nnon-key\n---", }, "test_plan": "Assessment Procedure", } @validates('kind', 'means', 'verify_frequency') def validate_control_options(self, key, option): """Validate control 'kind', 'means', 'verify_frequency'""" desired_role = key if key == 'verify_frequency' else 'control_' + key return validate_option(self.__class__.__name__, key, option, desired_role) @classmethod def eager_query(cls): query = super(Control, cls).eager_query() return cls.eager_inclusions(query, Control._include_links).options( orm.joinedload('directive'), orm.joinedload('kind'), orm.joinedload('means'), orm.joinedload('verify_frequency'), ) def log_json(self): out_json = super(Control, self).log_json() # so that event log can refer to deleted directive if self.directive: out_json["mapped_directive"] = self.directive.display_name return out_json
def context_id(cls): # pylint: disable=no-self-argument return db.Column(db.Integer, db.ForeignKey('contexts.id'))
class Revision(Base, db.Model): """Revision object holds a JSON snapshot of the object at a time.""" __tablename__ = 'revisions' resource_id = db.Column(db.Integer, nullable=False) resource_type = db.Column(db.String, nullable=False) event_id = db.Column(db.Integer, db.ForeignKey('events.id'), nullable=False) action = db.Column(db.Enum(u'created', u'modified', u'deleted'), nullable=False) _content = db.Column('content', LongJsonType, nullable=False) resource_slug = db.Column(db.String, nullable=True) source_type = db.Column(db.String, nullable=True) source_id = db.Column(db.Integer, nullable=True) destination_type = db.Column(db.String, nullable=True) destination_id = db.Column(db.Integer, nullable=True) @staticmethod def _extra_table_args(_): return ( db.Index("revisions_modified_by", "modified_by_id"), db.Index("fk_revisions_resource", "resource_type", "resource_id"), db.Index("fk_revisions_source", "source_type", "source_id"), db.Index("fk_revisions_destination", "destination_type", "destination_id"), db.Index('ix_revisions_resource_slug', 'resource_slug'), ) _api_attrs = reflection.ApiAttributes( 'resource_id', 'resource_type', 'source_type', 'source_id', 'destination_type', 'destination_id', 'action', 'content', 'description', ) @classmethod def eager_query(cls): from sqlalchemy import orm query = super(Revision, cls).eager_query() return query.options( orm.subqueryload('modified_by'), orm.subqueryload('event'), # used in description ) def __init__(self, obj, modified_by_id, action, content): self.resource_id = obj.id self.resource_type = obj.__class__.__name__ self.resource_slug = getattr(obj, "slug", None) self.modified_by_id = modified_by_id self.action = action if "access_control_list" in content and content["access_control_list"]: for acl in content["access_control_list"]: acl["person"] = { "id": acl["person_id"], "type": "Person", "href": "/api/people/{}".format(acl["person_id"]), } self._content = content for attr in [ "source_type", "source_id", "destination_type", "destination_id" ]: setattr(self, attr, getattr(obj, attr, None)) @builder.simple_property def description(self): """Compute a human readable description from action and content.""" if 'display_name' not in self._content: return '' display_name = self._content['display_name'] if not display_name: result = u"{0} {1}".format(self.resource_type, self.action) elif u'<->' in display_name: if self.action == 'created': msg = u"{destination} linked to {source}" elif self.action == 'deleted': msg = u"{destination} unlinked from {source}" else: msg = u"{display_name} {action}" source, destination = self._content['display_name'].split( '<->')[:2] result = msg.format(source=source, destination=destination, display_name=self._content['display_name'], action=self.action) elif 'mapped_directive' in self._content: # then this is a special case of combined map/creation # should happen only for Section and Control mapped_directive = self._content['mapped_directive'] if self.action == 'created': result = u"New {0}, {1}, created and mapped to {2}".format( self.resource_type, display_name, mapped_directive) elif self.action == 'deleted': result = u"{0} unmapped from {1} and deleted".format( display_name, mapped_directive) else: result = u"{0} {1}".format(display_name, self.action) else: # otherwise, it's a normal creation event result = u"{0} {1}".format(display_name, self.action) if self.event.action == "BULK": result += ", via bulk action" return result @builder.simple_property def content(self): """Property. Contains the revision content dict. Updated by required values, generated from saved content dict.""" # pylint: disable=too-many-locals roles_dict = role.get_custom_roles_for(self.resource_type) reverted_roles_dict = {n: i for i, n in roles_dict.iteritems()} access_control_list = self._content.get("access_control_list") or [] map_field_to_role = { "principal_assessor": reverted_roles_dict.get("Principal Assignees"), "secondary_assessor": reverted_roles_dict.get("Secondary Assignees"), "contact": reverted_roles_dict.get("Primary Contacts"), "secondary_contact": reverted_roles_dict.get("Secondary Contacts"), "owners": reverted_roles_dict.get("Admin"), } exists_roles = {i["ac_role_id"] for i in access_control_list} for field, role_id in map_field_to_role.items(): if field not in self._content: continue if role_id in exists_roles or role_id is None: continue field_content = self._content.get(field) or {} if not field_content: continue if not isinstance(field_content, list): field_content = [field_content] person_ids = {fc.get("id") for fc in field_content if fc.get("id")} for person_id in person_ids: access_control_list.append({ "display_name": roles_dict[role_id], "ac_role_id": role_id, "context_id": None, "created_at": None, "object_type": self.resource_type, "updated_at": None, "object_id": self.resource_id, "modified_by_id": None, "person_id": person_id, # Frontend require data in such format "person": { "id": person_id, "type": "Person", "href": "/api/people/{}".format(person_id) }, "modified_by": None, "id": None, }) populated_content = self._content.copy() # Add person with id and type for old snapshots compatibility for acl in access_control_list: if "person" not in acl: acl["person"] = {"id": acl.get("person_id"), "type": "Person"} populated_content["access_control_list"] = access_control_list if 'url' in self._content: reference_url_list = [] for key in ('url', 'reference_url'): link = self._content[key] # link might exist, but can be an empty string - we treat those values # as non-existing (empty) reference URLs if not link: continue # if creation/modification date is not available, we estimate it by # using the corresponding information from the Revision itself created_at = (self._content.get("created_at") or self.created_at.isoformat()) updated_at = (self._content.get("updated_at") or self.updated_at.isoformat()) reference_url_list.append({ "display_name": link, "document_type": "REFERENCE_URL", "link": link, "title": link, "id": None, "created_at": created_at, "updated_at": updated_at, }) populated_content['reference_url'] = reference_url_list return populated_content @content.setter def content(self, value): """ Setter for content property.""" self._content = value
class AccessControlRole(attributevalidator.AttributeValidator, base.ContextRBAC, mixins.Base, db.Model): """Access Control Role Model holds all roles in the application. These roles can be added by the users. """ __tablename__ = 'access_control_roles' name = db.Column(db.String, nullable=False) object_type = db.Column(db.String) tooltip = db.Column(db.String) read = db.Column(db.Boolean, nullable=False, default=True) update = db.Column(db.Boolean, nullable=False, default=True) delete = db.Column(db.Boolean, nullable=False, default=True) my_work = db.Column(db.Boolean, nullable=False, default=True) mandatory = db.Column(db.Boolean, nullable=False, default=False) non_editable = db.Column(db.Boolean, nullable=False, default=False) internal = db.Column(db.Boolean, nullable=False, default=False) default_to_current_user = db.Column( db.Boolean, nullable=False, default=False) notify_about_proposal = db.Column(db.Boolean, nullable=False, default=False) notify_about_review_status = db.Column(db.Boolean, nullable=False, default=False) access_control_list = db.relationship( 'AccessControlList', backref='ac_role', cascade='all, delete-orphan') parent_id = db.Column( db.Integer, db.ForeignKey('access_control_roles.id', ondelete='CASCADE'), nullable=True, ) parent = db.relationship( # pylint: disable=undefined-variable lambda: AccessControlRole, remote_side=lambda: AccessControlRole.id ) _reserved_names = {} @staticmethod def _extra_table_args(_): return ( db.UniqueConstraint('name', 'object_type'), ) @classmethod def eager_query(cls): """Define fields to be loaded eagerly to lower the count of DB queries.""" return super(AccessControlRole, cls).eager_query() _api_attrs = reflection.ApiAttributes( "name", "object_type", "tooltip", "read", "update", "delete", "my_work", "mandatory", "default_to_current_user", reflection.Attribute("non_editable", create=False, update=False), ) @sa.orm.validates("name", "object_type") def validates_name(self, key, value): # pylint: disable=no-self-use """Validate Custom Role name uniquness. Custom Role names need to follow 2 uniqueness rules: 1) Names must not match any attribute name on any existing object. 2) Object level CAD names must not match any global CAD name. This validator should check for name collisions for 1st and 2nd rule. This validator works, because object_type is never changed. It only gets set when the role is created and after that only name filed can change. This makes validation using both fields possible. Args: value: access control role name Returns: value if the name passes all uniqueness checks. """ value = value.strip() if key == "name" and self.object_type: name = value object_type = self.object_type elif key == "object_type" and self.name: name = self.name.strip() object_type = value else: return value if name in self._get_reserved_names(object_type): raise ValueError(u"Attribute name '{}' is reserved for this object type." .format(name)) if self._get_global_cad_names(object_type).get(name) is not None: raise ValueError(u"Global custom attribute '{}' " u"already exists for this object type" .format(name)) return value
class UserRole(rest_handable.WithDeleteHandable, rest_handable.WithPostHandable, base.ContextRBAC, Base, db.Model): """`UserRole` model represents mapping between `User` and `Role` models.""" __tablename__ = 'user_roles' # Override default from `ContextRBAC` to provide backref context = db.relationship('Context', backref='user_roles') role_id = db.Column(db.Integer(), db.ForeignKey('roles.id'), nullable=False) role = db.relationship('Role', backref=backref('user_roles', cascade='all, delete-orphan')) person_id = db.Column(db.Integer(), db.ForeignKey('people.id'), nullable=False) person = db.relationship('Person', backref=backref('user_roles', cascade='all, delete-orphan')) @staticmethod def _extra_table_args(model): return (db.UniqueConstraint('person_id', name='uq_{}'.format(model.__tablename__)), db.Index('ix_user_roles_person', 'person_id')) _api_attrs = reflection.ApiAttributes('role', 'person') @classmethod def role_assignments_for(cls, context): context_id = context.id if type(context) is Context else context all_assignments = db.session.query(UserRole)\ .filter(UserRole.context_id == context_id) assignments_by_user = {} for assignment in all_assignments: assignments_by_user.setdefault(assignment.person.email, [])\ .append(assignment.role) return assignments_by_user @classmethod def eager_query(cls, **kwargs): from sqlalchemy import orm query = super(UserRole, cls).eager_query(**kwargs) return query.options(orm.joinedload('role'), orm.subqueryload('person'), orm.subqueryload('context')) def _display_name(self): if self.context and self.context.related_object_type and \ self.context.related_object: context_related = ' in ' + self.context.related_object.display_name elif hasattr(self, '_display_related_title'): context_related = ' in ' + self._display_related_title elif self.context: logger.warning('Unable to identify context.related for UserRole') context_related = '' else: context_related = '' return u'{0} <-> {1}{2}'.format(self.person.display_name, self.role.display_name, context_related) def _recalculate_permissions_cache(self): """Recalculate permissions cache for user `UserRole` relates to.""" with utils.benchmark( "Invalidate permissions cache for user in UserRole"): from ggrc_basic_permissions import load_permissions_for load_permissions_for(self.person, expire_old=True) def handle_delete(self): """Handle `model_deleted` signals invoked on `UserRole` instance. HTTP DELTE method on `UserRole` model triggers following actions: - Recalculate permissions cache for user the `UserRole` object is related to. """ self._recalculate_permissions_cache() def handle_post(self): """Handle `model_posted` signals invoked on `UserRole` instance. HTTP POST method on `UserRole` model triggers following actions: - Recalculate permissions cache for user the `UserRole` object is related to. """ self._recalculate_permissions_cache()
def created_by_id(cls): # pylint: disable=no-self-argument return db.Column(db.Integer, db.ForeignKey('people.id'), nullable=True)
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 _api_attrs = reflection.ApiAttributes( '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 Assessment(Roleable, statusable.Statusable, AuditRelationship, AutoStatusChangeable, Assignable, HasObjectState, TestPlanned, CustomAttributable, PublicDocumentable, Commentable, Personable, reminderable.Reminderable, Timeboxed, Relatable, WithSimilarityScore, FinishedDate, VerifiedDate, ValidateOnComplete, Notifiable, WithAction, BusinessObject, labeled.Labeled, 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 REWORK_NEEDED = u"Rework Needed" NOT_DONE_STATES = statusable.Statusable.NOT_DONE_STATES | { REWORK_NEEDED, } VALID_STATES = tuple(NOT_DONE_STATES | statusable.Statusable.DONE_STATES) ASSIGNEE_TYPES = (u"Creator", u"Assessor", u"Verifier") class Labels(object): # pylint: disable=too-few-public-methods """Choices for label enum.""" AUDITOR_PULLS_EVIDENCE = u'Auditor pulls evidence' FOLLOWUP = u'Followup' NEEDS_REWORK = u'Needs Rework' NEEDS_DISCUSSION = u'Needs Discussion' POSSIBLE_LABELS = [ Labels.AUDITOR_PULLS_EVIDENCE, Labels.FOLLOWUP, Labels.NEEDS_REWORK, Labels.NEEDS_DISCUSSION ] 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, nullable=False, default=""), "Assessment") operationally = deferred(db.Column(db.String, nullable=False, default=""), "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', ['email', 'name']), MultipleSubpropertyFullTextAttr('related_creators', 'creators', ['email', 'name']), MultipleSubpropertyFullTextAttr('related_verifiers', 'verifiers', ['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", # Currently we decided to have 'Due Date' alias for start_date, # but it can be changed in future "start_date": "Due Date", "status": { "display_name": "State", "mandatory": False, "description": "Options are:\n{}".format('\n'.join(VALID_STATES)) }, } 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 "" @validates("status") def validate_status(self, key, value): value = super(Assessment, self).validate_status(key, value) # pylint: disable=unused-argument if self.status == value: return value if self.status == self.REWORK_NEEDED: valid_states = [self.DONE_STATE, self.FINAL_STATE] if value not in valid_states: raise ValueError("Assessment in `Rework Needed` " "state can be only moved to: [{}]".format( ",".join(valid_states))) return value @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 Revision(before_flush_handleable.BeforeFlushHandleable, synchronizable.ChangesSynchronized, filterable.Filterable, base.ContextRBAC, mixins.Base, db.Model): """Revision object holds a JSON snapshot of the object at a time.""" __tablename__ = 'revisions' resource_id = db.Column(db.Integer, nullable=False) resource_type = db.Column(db.String, nullable=False) event_id = db.Column(db.Integer, db.ForeignKey('events.id'), nullable=False) action = db.Column(db.Enum(u'created', u'modified', u'deleted'), nullable=False) _content = db.Column('content', types.LongJsonType, nullable=False) resource_slug = db.Column(db.String, nullable=True) source_type = db.Column(db.String, nullable=True) source_id = db.Column(db.Integer, nullable=True) destination_type = db.Column(db.String, nullable=True) destination_id = db.Column(db.Integer, nullable=True) is_empty = db.Column(db.Boolean, nullable=False, default=False) @staticmethod def _extra_table_args(_): return ( db.Index("revisions_modified_by", "modified_by_id"), db.Index("ix_revisions_resource_action", "resource_type", "resource_id", "action"), db.Index("fk_revisions_source", "source_type", "source_id"), db.Index("fk_revisions_destination", "destination_type", "destination_id"), db.Index('ix_revisions_resource_slug', 'resource_slug'), ) _api_attrs = reflection.ApiAttributes( 'resource_id', 'resource_type', 'source_type', 'source_id', 'destination_type', 'destination_id', 'action', 'content', 'description', reflection.Attribute('diff_with_current', create=False, update=False), reflection.Attribute('meta', create=False, update=False), ) _filterable_attrs = [ 'action', 'resource_id', 'resource_type', 'source_type', 'source_id', 'destination_type', 'destination_id', ] @classmethod def eager_query(cls, **kwargs): from sqlalchemy import orm query = super(Revision, cls).eager_query(**kwargs) return query.options( orm.subqueryload('modified_by'), orm.subqueryload('event'), # used in description ) def __init__(self, obj, modified_by_id, action, content): self.resource_id = obj.id self.resource_type = obj.__class__.__name__ self.resource_slug = getattr(obj, "slug", None) self.modified_by_id = modified_by_id self.action = action if "access_control_list" in content and content["access_control_list"]: for acl in content["access_control_list"]: acl["person"] = { "id": acl["person_id"], "type": "Person", "href": "/api/people/{}".format(acl["person_id"]), } self._content = content for attr in [ "source_type", "source_id", "destination_type", "destination_id" ]: setattr(self, attr, getattr(obj, attr, None)) @builder.callable_property def diff_with_current(self): """Callable lazy property for revision.""" referenced_objects.mark_to_cache(self.resource_type, self.resource_id) revisions_diff.mark_for_latest_content(self.resource_type, self.resource_id) def lazy_loader(): """Lazy load diff for revisions.""" referenced_objects.rewarm_cache() revisions_diff.rewarm_latest_content() instance = referenced_objects.get(self.resource_type, self.resource_id) if instance: return revisions_diff.prepare(instance, self.content) # return empty diff object has already been removed return {} return lazy_loader @builder.callable_property def meta(self): """Callable lazy property for revision.""" referenced_objects.mark_to_cache(self.resource_type, self.resource_id) def lazy_loader(): """Lazy load diff for revisions.""" referenced_objects.rewarm_cache() instance = referenced_objects.get(self.resource_type, self.resource_id) meta_dict = {} if instance: instance_meta_info = meta_info.MetaInfo(instance) meta_dict["mandatory"] = instance_meta_info.mandatory return meta_dict return lazy_loader @builder.simple_property def description(self): """Compute a human readable description from action and content.""" if 'display_name' not in self._content: return '' display_name = self._content['display_name'] if not display_name: result = u"{0} {1}".format(self.resource_type, self.action) elif u'<->' in display_name: if self.action == 'created': msg = u"{destination} linked to {source}" elif self.action == 'deleted': msg = u"{destination} unlinked from {source}" else: msg = u"{display_name} {action}" source, destination = self._content['display_name'].split( '<->')[:2] result = msg.format(source=source, destination=destination, display_name=self._content['display_name'], action=self.action) elif 'mapped_directive' in self._content: # then this is a special case of combined map/creation # should happen only for Requirement and Control mapped_directive = self._content['mapped_directive'] if self.action == 'created': result = u"New {0}, {1}, created and mapped to {2}".format( self.resource_type, display_name, mapped_directive) elif self.action == 'deleted': result = u"{0} unmapped from {1} and deleted".format( display_name, mapped_directive) else: result = u"{0} {1}".format(display_name, self.action) else: # otherwise, it's a normal creation event result = u"{0} {1}".format(display_name, self.action) if self.event.action == "BULK": result += ", via bulk action" return result def populate_reference_url(self): """Add reference_url info for older revisions.""" if 'url' not in self._content: return {} reference_url_list = [] for key in ('url', 'reference_url'): link = self._content[key] # link might exist, but can be an empty string - we treat those values # as non-existing (empty) reference URLs if not link: continue # if creation/modification date is not available, we estimate it by # using the corresponding information from the Revision itself created_at = (self._content.get("created_at") or self.created_at.isoformat()) updated_at = (self._content.get("updated_at") or self.updated_at.isoformat()) reference_url_list.append({ "display_name": link, "kind": "REFERENCE_URL", "link": link, "title": link, "id": None, "created_at": created_at, "updated_at": updated_at, }) return {'reference_url': reference_url_list} @classmethod def _filter_internal_acls(cls, access_control_list): """Remove internal access control list entries. This is needed due to bugs in older code that in some cases the revisions stored internal ACL entries. Due to possible role removal, the parent_id is the only true flag that we can use for filtering Args: access_control_list: list of dicts containing ACL entries. Returns: access_control_list but without any ACL entry that was generated from some other ACL entry. """ return [ acl for acl in access_control_list if acl.get("parent_id") is None ] @classmethod def _populate_acl_with_people(cls, access_control_list): """Add person property with person stub on access control list.""" for acl in access_control_list: if "person" not in acl: acl["person"] = {"id": acl.get("person_id"), "type": "Person"} return access_control_list def populate_acl(self): """Add access_control_list info for older revisions.""" # pylint: disable=too-many-locals roles_dict = role.get_custom_roles_for(self.resource_type) reverted_roles_dict = {n: i for i, n in roles_dict.iteritems()} access_control_list = self._content.get("access_control_list") or [] map_field_to_role = { "principal_assessor": reverted_roles_dict.get("Principal Assignees"), "secondary_assessor": reverted_roles_dict.get("Secondary Assignees"), "contact": reverted_roles_dict.get("Primary Contacts"), "secondary_contact": reverted_roles_dict.get("Secondary Contacts"), "owners": reverted_roles_dict.get("Admin"), } is_control = bool(self.resource_type == "Control") is_control_snapshot = bool( self.resource_type == "Snapshot" and self._content["child_type"] == "Control") # for Control type we do not have Primary and Secondary Contacts roles. if is_control or is_control_snapshot: map_field_to_role.update({ "contact": reverted_roles_dict.get("Control Operators"), "secondary_contact": reverted_roles_dict.get("Control Owners") }) exists_roles = {i["ac_role_id"] for i in access_control_list} for field, role_id in map_field_to_role.items(): if role_id in exists_roles or role_id is None: continue if field not in self._content: continue field_content = self._content.get(field) or {} if not field_content: continue if not isinstance(field_content, list): field_content = [field_content] person_ids = {fc.get("id") for fc in field_content if fc.get("id")} for person_id in person_ids: access_control_list.append({ "display_name": roles_dict[role_id], "ac_role_id": role_id, "context_id": None, "created_at": None, "object_type": self.resource_type, "updated_at": None, "object_id": self.resource_id, "modified_by_id": None, "person_id": person_id, # Frontend require data in such format "person": { "id": person_id, "type": "Person", "href": "/api/people/{}".format(person_id) }, "modified_by": None, "id": None, }) acl_with_people = self._populate_acl_with_people(access_control_list) filtered_acl = self._filter_internal_acls(acl_with_people) result_acl = [ acl for acl in filtered_acl if acl["ac_role_id"] in roles_dict ] return { "access_control_list": result_acl, } def populate_folder(self): """Add folder info for older revisions.""" if "folder" in self._content: return {} folders = self._content.get("folders") or [{"id": ""}] return {"folder": folders[0]["id"]} def populate_labels(self): """Add labels info for older revisions.""" if "label" not in self._content: return {} label = self._content["label"] return { "labels": [{ "id": None, "name": label }] } if label else { "labels": [] } def populate_status(self): """Update status for older revisions or add it if status does not exist.""" workflow_models = { "Cycle", "CycleTaskGroup", "CycleTaskGroupObjectTask", } statuses_mapping = {"InProgress": "In Progress"} status = statuses_mapping.get(self._content.get("status")) if self.resource_type in workflow_models and status: return {"status": status} pop_models = { # ggrc "AccessGroup", "AccountBalance", "Control", "DataAsset", "Directive", "Facility", "Issue", "KeyReport", "Market", "Objective", "OrgGroup", "Product", "Program", "Project", "Requirement", "System", "Vendor", "Risk", "Threat", } if self.resource_type not in pop_models: return {} statuses_mapping = { "Active": "Active", "Deprecated": "Deprecated", "Effective": "Active", "Final": "Active", "In Scope": "Active", "Ineffective": "Active", "Launched": "Active", } return { "status": statuses_mapping.get(self._content.get("status"), "Draft") } def populate_review_status(self): """Replace os_state with review state for old revisions""" from ggrc.models import review result = {} if "os_state" in self._content: if self._content["os_state"] is not None: result["review_status"] = self._content["os_state"] else: result["review_status"] = review.Review.STATES.UNREVIEWED return result def populate_review_status_display_name(self, result): """Get review_status if review_status_display_name is not found""" # pylint: disable=invalid-name if self.resource_type != "Control": return if "review_status_display_name" in self._content: result["review_status_display_name"] = self._content[ "review_status_display_name"] elif "review_status" in result: result["review_status_display_name"] = result["review_status"] def populate_readonly(self): """Add readonly=False to older revisions of WithReadOnlyAccess models""" from ggrc.models import all_models model = getattr(all_models, self.resource_type, None) if not model or not issubclass(model, wroa.WithReadOnlyAccess): return dict() if "readonly" in self._content: # revision has flag "readonly", use it return {"readonly": self._content["readonly"]} # not flag "readonly" in revision, use default value False return {"readonly": False} def _document_evidence_hack(self): """Update display_name on evideces Evidences have display names from links and titles, and until now they used slug property to calculate the display name. This hack is here since we must support older revisions with bad data, and to avoid using slug differently than everywhere else in the app. This function only modifies existing evidence entries on any given object. If an object does not have and document evidences then an empty dict is returned. Returns: dict with updated display name for each of the evidence entries if there are any. """ if "document_evidence" not in self._content: return {} document_evidence = self._content.get("document_evidence") for evidence in document_evidence: evidence[u"display_name"] = u"{link} {title}".format( link=evidence.get("link"), title=evidence.get("title"), ).strip() return {u"documents_file": document_evidence} def populate_categoies(self, key_name): """Return names of categories.""" if self.resource_type != "Control": return {} result = [] categories = self._content.get(key_name) if isinstance(categories, (str, unicode)) and categories: result = json.loads(categories) elif isinstance(categories, list): for category in categories: if isinstance(category, dict): result.append(category.get("name")) elif isinstance(category, (str, unicode)): result.append(category) return {key_name: result} def _get_cavs(self): """Return cavs values from content.""" if "custom_attribute_values" in self._content: return self._content["custom_attribute_values"] if "custom_attributes" in self._content: return self._content["custom_attributes"] return [] def populate_cavs(self): """Setup cads in cav list if they are not presented in content but now they are associated to instance.""" from ggrc.models import custom_attribute_definition cads = custom_attribute_definition.get_custom_attributes_for( self.resource_type, self.resource_id) cavs = {int(i["custom_attribute_id"]): i for i in self._get_cavs()} cads_ids = set() for cad in cads: custom_attribute_id = int(cad["id"]) cads_ids.add(custom_attribute_id) if custom_attribute_id in cavs: # Old revisions can contain falsy values for a Checkbox if cad["attribute_type"] == "Checkbox" \ and not cavs[custom_attribute_id]["attribute_value"]: cavs[custom_attribute_id]["attribute_value"] = cad[ "default_value"] continue if cad["attribute_type"] == "Map:Person": value = "Person" else: value = cad["default_value"] cavs[custom_attribute_id] = { "attribute_value": value, "custom_attribute_id": custom_attribute_id, "attributable_id": self.resource_id, "attributable_type": self.resource_type, "display_name": "", "attribute_object": None, "type": "CustomAttributeValue", "context_id": None, } cavs = { cad_id: value for cad_id, value in cavs.iteritems() if cad_id in cads_ids } return { "custom_attribute_values": cavs.values(), "custom_attribute_definitions": cads } def populate_cad_default_values(self): """Setup default_value to CADs if it's needed.""" from ggrc.models import all_models if "custom_attribute_definitions" not in self._content: return {} cads = [] for cad in self._content["custom_attribute_definitions"]: if "default_value" not in cad: cad["default_value"] = ( all_models.CustomAttributeDefinition.get_default_value_for( cad["attribute_type"])) cads.append(cad) return {"custom_attribute_definitions": cads} def populate_requirements(self, populated_content): # noqa pylint: disable=too-many-branches """Populates revision content for Requirement models and models with fields that can contain Requirement old names. This fields would be checked and updated where necessary """ # change to add Requirement old names requirement_type = ["Section", "Clause"] # change to add models and fields that can contain Requirement old names affected_models = { "AccessControlList": [ "object_type", ], "AccessControlRole": [ "object_type", ], "Assessment": [ "assessment_type", ], "AssessmentTemplate": [ "template_object_type", ], "Automapping": [ "source_type", "destination_type", ], "CustomAttributeValue": [ "attributable_type", ], "Event": [ "resource_type", ], "ObjectPerson": [ "personable_type", ], "Relationship": [ "source_type", "destination_type", ], "Revision": [ "resource_type", ], "Label": [ "object_type", ], "Context": [ "related_object_type", ], "IssuetrackerIssue": [ "object_type", ], "ObjectLabel": [ "object_type", ], "ObjectTemplates": [ "name", ], "Proposal": [ "instance_type", ], "Snapshot": [ "child_type", "parent_type", ], } # change to add special values cases special_cases = { "CustomAttributeDefinition": { "fields": [ "definition_type", ], "old_values": ["section", "clause"], "new_value": "requirement", } } obj_type = self.resource_type # populate fields if they contain old names if obj_type in affected_models.keys(): for field in affected_models[obj_type]: if populated_content.get(field) in requirement_type: populated_content[field] = "Requirement" # populate fields for models that contain old names in special spelling if obj_type in special_cases.keys(): for field in special_cases[obj_type]["fields"]: if populated_content[field] in special_cases[obj_type][ "old_values"]: populated_content[field] = special_cases[obj_type][ "new_value"] # populate Requirements revisions if obj_type == "Requirement": populated_content["type"] = "Requirement" acls = populated_content.get("access_control_list", {}) if acls: for acl in acls: if acl.get("object_type") in requirement_type: acl["object_type"] = "Requirement" populated_content["access_control_list"] = acls cavs = populated_content.get("custom_attribute_values", {}) if cavs: for cav in cavs: if cav.get("attributable_type") in requirement_type: cav["attributable_type"] = "Requirement" populated_content["custom_attribute_values"] = cavs def populate_options(self, populated_content): """Update revisions for Sync models to have Option fields as string.""" if self.resource_type == "Control": for attr in ["kind", "means", "verify_frequency"]: attr_value = populated_content.get(attr) if isinstance(attr_value, dict): populated_content[attr] = attr_value.get("title") elif isinstance(attr_value, (str, unicode)): populated_content[attr] = attr_value else: populated_content[attr] = None def populate_automappings(self): """Add automapping info in revisions. Populate Relationship revisions with automapping info to help FE show Change Log, but we should not show automapping info in case of deleted relationship""" if ("automapping_id" not in self._content or not self._content["automapping_id"] or self.action != "created"): return {} automapping_id = self._content["automapping_id"] if not hasattr(flask.g, "automappings_cache"): flask.g.automappings_cache = dict() if automapping_id not in flask.g.automappings_cache: automapping_obj = automapping.Automapping.query.get(automapping_id) if automapping_obj is None: return {} automapping_json = automapping_obj.log_json() flask.g.automappings_cache[automapping_id] = automapping_json else: automapping_json = flask.g.automappings_cache[automapping_id] return {"automapping": automapping_json} @builder.simple_property def content(self): """Property. Contains the revision content dict. Updated by required values, generated from saved content dict.""" # pylint: disable=too-many-locals populated_content = self._content.copy() populated_content.update(self.populate_acl()) populated_content.update(self.populate_reference_url()) populated_content.update(self.populate_folder()) populated_content.update(self.populate_labels()) populated_content.update(self.populate_status()) populated_content.update(self.populate_review_status()) populated_content.update(self._document_evidence_hack()) populated_content.update(self.populate_categoies("categories")) populated_content.update(self.populate_categoies("assertions")) populated_content.update(self.populate_cad_default_values()) populated_content.update(self.populate_cavs()) populated_content.update(self.populate_readonly()) populated_content.update(self.populate_automappings()) self.populate_requirements(populated_content) self.populate_options(populated_content) self.populate_review_status_display_name(populated_content) # remove custom_attributes, # it's old style interface and now it's not needed populated_content.pop("custom_attributes", None) # remove attribute_object_id not used by FE anymore for item in populated_content["custom_attribute_values"]: item.pop("attribute_object_id", None) return populated_content @content.setter def content(self, value): """ Setter for content property.""" self._content = value def _handle_if_empty(self): """Check if revision is empty and update is_empty flag if true.""" # Check if new revision contains any changes in resource state. Revisions # created with "created" or "deleted" action are not considered empty. if self in db.session.new and self.action == u"modified": obj = referenced_objects.get(self.resource_type, self.resource_id) # Content serialization and deserialization is needed since content of # prev revision stored in DB was serialized before storing and due to # this couldn't be correctly compared to content of revision in hands. content = json.loads(utils.as_json(self.content)) self.is_empty = bool( obj and not revisions_diff.changes_present(obj, content)) def handle_before_flush(self): """Handler that called before SQLAlchemy flush event.""" self._handle_if_empty()
class Control(WithLastAssessmentDate, HasObjectState, Roleable, Relatable, CustomAttributable, Personable, ControlCategorized, PublicDocumentable, AssertionCategorized, Hierarchical, LastDeprecatedTimeboxed, Auditable, TestPlanned, Commentable, BusinessObject, Indexed, db.Model): __tablename__ = 'controls' company_control = deferred(db.Column(db.Boolean), 'Control') directive_id = deferred( db.Column(db.Integer, db.ForeignKey('directives.id')), 'Control') kind_id = deferred(db.Column(db.Integer), 'Control') means_id = deferred(db.Column(db.Integer), 'Control') version = deferred(db.Column(db.String), 'Control') documentation_description = deferred(db.Column(db.Text), 'Control') verify_frequency_id = deferred(db.Column(db.Integer), 'Control') fraud_related = deferred(db.Column(db.Boolean), 'Control') key_control = deferred(db.Column(db.Boolean), 'Control') active = deferred(db.Column(db.Boolean), 'Control') principal_assessor_id = deferred( db.Column(db.Integer, db.ForeignKey('people.id')), 'Control') secondary_assessor_id = deferred( db.Column(db.Integer, db.ForeignKey('people.id')), 'Control') principal_assessor = db.relationship( 'Person', uselist=False, foreign_keys='Control.principal_assessor_id') secondary_assessor = db.relationship( 'Person', uselist=False, foreign_keys='Control.secondary_assessor_id') kind = db.relationship( 'Option', primaryjoin='and_(foreign(Control.kind_id) == Option.id, ' 'Option.role == "control_kind")', uselist=False) means = db.relationship( 'Option', primaryjoin='and_(foreign(Control.means_id) == Option.id, ' 'Option.role == "control_means")', uselist=False) verify_frequency = db.relationship( 'Option', primaryjoin='and_(foreign(Control.verify_frequency_id) == Option.id, ' 'Option.role == "verify_frequency")', uselist=False) @staticmethod def _extra_table_args(_): return ( db.Index('ix_controls_principal_assessor', 'principal_assessor_id'), db.Index('ix_controls_secondary_assessor', 'secondary_assessor_id'), ) # REST properties _api_attrs = reflection.ApiAttributes( 'active', 'company_control', 'directive', 'documentation_description', 'fraud_related', 'key_control', 'kind', 'means', 'verify_frequency', 'version', 'principal_assessor', 'secondary_assessor', ) _fulltext_attrs = [ 'active', 'company_control', 'directive', 'documentation_description', attributes.BooleanFullTextAttr('fraud_related', 'fraud_related', true_value="yes", false_value="no"), attributes.BooleanFullTextAttr('key_control', 'key_control', true_value="key", false_value="non-key"), 'kind', 'means', 'verify_frequency', 'version', attributes.FullTextAttr("principal_assessor", "principal_assessor", ["email", "name"]), attributes.FullTextAttr('secondary_assessor', 'secondary_assessor', ["email", "name"]), ] _sanitize_html = [ 'documentation_description', 'version', ] @classmethod def indexed_query(cls): return super(Control, cls).indexed_query().options( orm.Load(cls).undefer_group("Control_complete"), orm.Load(cls).joinedload("directive").undefer_group( "Directive_complete"), orm.Load(cls).joinedload("principal_assessor").undefer_group( "Person_complete"), orm.Load(cls).joinedload("secondary_assessor").undefer_group( "Person_complete"), orm.Load(cls).joinedload( 'kind', ).undefer_group("Option_complete"), orm.Load(cls).joinedload( 'means', ).undefer_group("Option_complete"), orm.Load(cls).joinedload( 'verify_frequency', ).undefer_group("Option_complete"), ) _include_links = [] _aliases = { "kind": "Kind/Nature", "means": "Type/Means", "verify_frequency": "Frequency", "fraud_related": "Fraud Related", "key_control": { "display_name": "Significance", "description": "Allowed values are:\nkey\nnon-key\n---", }, # overrides values from PublicDocumentable mixin "document_url": None, "test_plan": "Assessment Procedure", } @validates('kind', 'means', 'verify_frequency') def validate_control_options(self, key, option): desired_role = key if key == 'verify_frequency' else 'control_' + key return validate_option(self.__class__.__name__, key, option, desired_role) @classmethod def eager_query(cls): query = super(Control, cls).eager_query() return cls.eager_inclusions(query, Control._include_links).options( orm.joinedload('directive'), orm.joinedload('principal_assessor'), orm.joinedload('secondary_assessor'), orm.joinedload('kind'), orm.joinedload('means'), orm.joinedload('verify_frequency'), ) def log_json(self): out_json = super(Control, self).log_json() # so that event log can refer to deleted directive if self.directive: out_json["mapped_directive"] = self.directive.display_name return out_json
def parent_id(cls): # pylint: disable=no-self-argument return deferred( db.Column(db.Integer, db.ForeignKey('{0}.id'.format(cls.__tablename__))), cls.__name__)
class Request(statusable.Statusable, AutoStatusChangeable, Assignable, EvidenceURL, Personable, CustomAttributable, Notifiable, relationship.Relatable, WithSimilarityScore, Titled, Slugged, Described, Commentable, FinishedDate, VerifiedDate, Base, db.Model): """Class representing Requests. Request is an object representing a request from a Requester to Assignee to provide feedback, evidence or attachment in the form of comments, documents or URLs that (if specified) Verifier has to approve of before Request is considered finished. """ __tablename__ = 'requests' _title_uniqueness = False VALID_TYPES = (u'documentation', u'interview') ASSIGNEE_TYPES = (u'Assignee', u'Requester', u'Verifier') similarity_options = similarity_options_module.REQUEST # TODO Remove requestor and requestor_id on database cleanup requestor_id = db.Column(db.Integer, db.ForeignKey('people.id')) requestor = db.relationship('Person', foreign_keys=[requestor_id]) # TODO Remove request_type on database cleanup request_type = deferred(db.Column(db.Enum(*VALID_TYPES), nullable=False), 'Request') start_date = deferred( db.Column(db.Date, nullable=False, default=date.today), 'Request') end_date = deferred( db.Column(db.Date, nullable=False, default=lambda: date.today() + timedelta(7)), 'Request') # TODO Remove audit_id audit_object_id on database cleanup audit_id = db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False) audit_object_id = db.Column(db.Integer, db.ForeignKey('audit_objects.id'), nullable=True) gdrive_upload_path = deferred(db.Column(db.String, nullable=True), 'Request') # TODO Remove test and notes columns on database cleanup test = deferred(db.Column(db.Text, nullable=True), 'Request') notes = deferred(db.Column(db.Text, nullable=True), 'Request') _publish_attrs = [ 'requestor', 'request_type', 'gdrive_upload_path', 'start_date', 'end_date', 'status', 'audit', 'test', 'notes', 'title', 'description' ] _tracked_attrs = ((set(_publish_attrs) | {'slug'}) - {'status'}) _sanitize_html = [ 'gdrive_upload_path', 'test', 'notes', 'description', 'title' ] _aliases = { "request_audit": { "display_name": "Audit", "filter_by": "_filter_by_request_audit", "mandatory": True, }, "end_date": "Due On", "notes": "Notes", "request_type": "Request Type", "start_date": "Starts On", "status": { "display_name": "Status", "handler_key": "request_status", }, "test": "Test", "related_assignees": { "display_name": "Assignee", "mandatory": True, "filter_by": "_filter_by_related_assignees", "type": reflection.AttributeInfo.Type.MAPPING, }, "related_requesters": { "display_name": "Requester", "mandatory": True, "filter_by": "_filter_by_related_requesters", "type": reflection.AttributeInfo.Type.MAPPING, }, "related_verifiers": { "display_name": "Verifier", "filter_by": "_filter_by_related_verifiers", "type": reflection.AttributeInfo.Type.MAPPING, }, } def _display_name(self): # pylint: disable=unsubscriptable-object if len(self.title) > 32: display_string = self.description[:32] + u'...' elif self.title: display_string = self.title elif len(self.description) > 32: display_string = self.description[:32] + u'...' else: display_string = self.description return u'Request with id {0} "{1}" for Audit "{2}"'.format( self.id, display_string, self.audit.display_name) @classmethod def eager_query(cls): query = super(Request, cls).eager_query() return query.options(orm.joinedload('audit')) @classmethod def _filter_by_related_assignees(cls, predicate): return cls._get_relate_filter(predicate, "Assignee") @classmethod def _filter_by_related_requesters(cls, predicate): return cls._get_relate_filter(predicate, "Requester") @classmethod def _filter_by_related_verifiers(cls, predicate): return cls._get_relate_filter(predicate, "Verifier") @classmethod def _filter_by_request_audit(cls, predicate): return cls.query.filter((audit.Audit.id == cls.audit_id) & (predicate(audit.Audit.slug) | predicate(audit.Audit.title))).exists() @classmethod def default_request_type(cls): return cls.VALID_TYPES[0]
def secondary_contact_id(cls): # pylint: disable=no-self-argument return deferred(db.Column(db.Integer, db.ForeignKey('people.id')), cls.__name__)
class Cycle(WithContact, Stateful, Timeboxed, Described, Titled, Slugged, Notifiable, Indexed, db.Model): """Workflow Cycle model """ __tablename__ = 'cycles' _title_uniqueness = False VALID_STATES = (u'Assigned', u'InProgress', u'Finished', u'Verified') workflow_id = db.Column( db.Integer, db.ForeignKey('workflows.id', ondelete="CASCADE"), nullable=False, ) cycle_task_groups = db.relationship( 'CycleTaskGroup', backref='cycle', cascade='all, delete-orphan') cycle_task_group_object_tasks = db.relationship( 'CycleTaskGroupObjectTask', backref='cycle', cascade='all, delete-orphan') cycle_task_entries = db.relationship( 'CycleTaskEntry', backref='cycle', cascade='all, delete-orphan') is_current = db.Column(db.Boolean, default=True, nullable=False) next_due_date = db.Column(db.Date) _publish_attrs = [ 'workflow', 'cycle_task_groups', 'is_current', 'next_due_date', ] _aliases = { "cycle_workflow": { "display_name": "Workflow", "filter_by": "_filter_by_cycle_workflow", }, "status": { "display_name": "State", "mandatory": False, "description": "Options are: \n{} ".format('\n'.join(VALID_STATES)) }, "contact": "Assignee", "secondary_contact": None, } PROPERTY_TEMPLATE = u"cycle {}" _fulltext_attrs = [ MultipleSubpropertyFullTextAttr( "group title", "cycle_task_groups", ["title"], False, ), MultipleSubpropertyFullTextAttr( "group assignee", lambda instance: [g.contact for g in instance.cycle_task_groups], ["name", "email"], False, ), DateMultipleSubpropertyFullTextAttr( "group due date", 'cycle_task_groups', ["next_due_date"], False, ), MultipleSubpropertyFullTextAttr( "task title", 'cycle_task_group_object_tasks', ["title"], False, ), MultipleSubpropertyFullTextAttr( "task assignee", lambda instance: [t.contact for t in instance.cycle_task_group_object_tasks], ["name", "email"], False ), DateMultipleSubpropertyFullTextAttr( "task due date", "cycle_task_group_object_tasks", ["end_date"], False ), DateFullTextAttr("due date", "next_due_date"), MultipleSubpropertyFullTextAttr( "task comments", lambda instance: list(itertools.chain(*[ t.cycle_task_entries for t in instance.cycle_task_group_object_tasks ])), ["description"], False ), ] AUTO_REINDEX_RULES = [ ReindexRule("CycleTaskGroup", lambda x: x.cycle), ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group.cycle), ReindexRule("Person", _query_filtered_by_contact) ] @classmethod def _filter_by_cycle_workflow(cls, predicate): from ggrc_workflows.models.workflow import Workflow return Workflow.query.filter( (Workflow.id == cls.workflow_id) & (predicate(Workflow.slug) | predicate(Workflow.title)) ).exists() @classmethod def eager_query(cls): """Add cycle task groups to cycle eager query This function adds cycle_task_groups as a join option when fetching cycles, and makes sure we fetch all cycle related data needed for generating cycle json, in one query. Returns: a query object with cycle_task_groups added to joined load options. """ query = super(Cycle, cls).eager_query() return query.options( orm.joinedload('cycle_task_groups'), ) @classmethod def indexed_query(cls): return super(Cycle, cls).indexed_query().options( orm.Load(cls).load_only("next_due_date"), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").load_only( "id", "title", "end_date" ), orm.Load(cls).subqueryload("cycle_task_groups").load_only( "id", "title", "end_date", "next_due_date", ), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload( "contact" ).load_only( "email", "name", "id" ), orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload( "cycle_task_entries" ).load_only( "description", "id" ), orm.Load(cls).subqueryload("cycle_task_groups").joinedload( "contact" ).load_only( "email", "name", "id" ), orm.Load(cls).joinedload("contact").load_only( "email", "name", "id" ), )
class CycleTaskGroupObjectTask( WithContact, Stateful, Timeboxed, Relatable, Notifiable, Described, Titled, Slugged, Base, Indexed, db.Model): """Cycle task model """ __tablename__ = 'cycle_task_group_object_tasks' 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_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" ), )