class WithReadOnlyAccess(object): """Mixin for models which can be marked as read-only""" # pylint: disable=too-few-public-methods _read_only_model_relationships = ( 'Document' ) readonly = db.Column(db.Boolean, nullable=False, default=False) _api_attrs = reflection.ApiAttributes( reflection.Attribute("readonly", create=True, update=True), ) _aliases = { "readonly": { "display_name": "Read-only", "mandatory": False, "hidden": True, }, } _fulltext_attrs = [ attributes.BooleanFullTextAttr( 'readonly', 'readonly', true_value="yes", false_value="no", ) ] def can_change_relationship_with(self, obj): """Check whether relationship from self to obj1 can be changed This function doesn't expect that another obj also has type WithReadOnlyAccess. In this case can_change_relationship_with() of another object have to be called also to ensure that relationship is not read-only. Final read-only flag can be calculated using the following expression: obj1.can_change_relationship_with(obj2) and \ obj2.can_change_relationship_with(obj1) """ if not self.readonly: return True return obj.__class__.__name__ not in self._read_only_model_relationships @validates('readonly') def validate_readonly(self, _, value): # pylint: disable=no-self-use """Validate readonly""" if value is None: # if value is not specified or is set to None, use default value False return self.readonly if not isinstance(value, bool): raise ValidationError("Attribute 'readonly' has invalid value") return value
class CycleTaskGroupObjectTask( roleable.Roleable, wf_mixins.CycleTaskStatusValidatedMixin, wf_mixins.WorkflowCommentable, mixins.WithLastDeprecatedDate, mixins.Timeboxed, relationship.Relatable, mixins.Notifiable, mixins.Described, mixins.Titled, mixins.Slugged, mixins.Base, base.ContextRBAC, ft_mixin.Indexed, db.Model): """Cycle task model """ __tablename__ = 'cycle_task_group_object_tasks' readable_name_alias = 'cycle task' _title_uniqueness = False IMPORTABLE_FIELDS = ( 'slug', 'title', 'description', 'start_date', 'end_date', 'finished_date', 'verified_date', 'status', '__acl__:Task Assignees', '__acl__:Task Secondary Assignees', ) @classmethod def generate_slug_prefix(cls): return "CYCLETASK" # Note: this statuses are used in utils/query_helpers to filter out the tasks # that should be visible on My Tasks pages. PROPERTY_TEMPLATE = u"task {}" _fulltext_attrs = [ ft_attributes.DateFullTextAttr( "end_date", 'end_date', ), ft_attributes.FullTextAttr("group title", 'cycle_task_group', ['title'], False), ft_attributes.FullTextAttr("object_approval", 'object_approval', with_template=False), ft_attributes.FullTextAttr("cycle title", 'cycle', ['title'], False), ft_attributes.FullTextAttr("group assignee", lambda x: x.cycle_task_group.contact, ['email', 'name'], False), ft_attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact, ['email', 'name'], False), ft_attributes.DateFullTextAttr( "group due date", lambda x: x.cycle_task_group.next_due_date, with_template=False), ft_attributes.DateFullTextAttr("cycle due date", lambda x: x.cycle.next_due_date, with_template=False), ft_attributes.MultipleSubpropertyFullTextAttr("comments", "cycle_task_entries", ["description"]), ft_attributes.BooleanFullTextAttr("needs verification", "is_verification_needed", with_template=False, true_value="Yes", false_value="No"), "folder", ] # The app should not pass to the json representation of # relationships to the internal models IGNORED_RELATED_TYPES = ["CalendarEvent"] _custom_publish = { "related_sources": lambda obj: [ rel.log_json() for rel in obj.related_sources if rel.source_type not in obj.IGNORED_RELATED_TYPES ], "related_destinations": lambda obj: [ rel.log_json() for rel in obj.related_destinations if rel.destination_type not in obj.IGNORED_RELATED_TYPES ] } AUTO_REINDEX_RULES = [ ft_mixin.ReindexRule("CycleTaskEntry", lambda x: x.cycle_task_group_object_task), ] cycle_id = db.Column( db.Integer, db.ForeignKey('cycles.id', ondelete="CASCADE"), nullable=False, ) cycle_task_group_id = db.Column( db.Integer, db.ForeignKey('cycle_task_groups.id', ondelete="CASCADE"), nullable=False, ) task_group_task_id = db.Column(db.Integer, db.ForeignKey('task_group_tasks.id'), nullable=True) task_group_task = db.relationship( "TaskGroupTask", foreign_keys="CycleTaskGroupObjectTask.task_group_task_id") task_type = db.Column(db.String(length=250), nullable=False) response_options = db.Column(types.JsonType(), nullable=False, default=[]) selected_response_options = db.Column(types.JsonType(), nullable=False, default=[]) sort_index = db.Column(db.String(length=250), default="", nullable=False) finished_date = db.Column(db.DateTime) verified_date = db.Column(db.DateTime) # This parameter is overridden by cycle task group backref, but is here to # ensure pylint does not complain _cycle_task_group = None @hybrid.hybrid_property def cycle_task_group(self): """Getter for cycle task group foreign key.""" return self._cycle_task_group @cycle_task_group.setter def cycle_task_group(self, cycle_task_group): """Setter for cycle task group foreign key.""" if not self._cycle_task_group and cycle_task_group: relationship.Relationship(source=cycle_task_group, destination=self) self._cycle_task_group = cycle_task_group @hybrid.hybrid_property def object_approval(self): return self.cycle.workflow.object_approval @object_approval.expression def object_approval(cls): # pylint: disable=no-self-argument return sa.select([ Workflow.object_approval, ]).where( sa.and_( (Cycle.id == cls.cycle_id), (Cycle.workflow_id == Workflow.id))).label('object_approval') @builder.simple_property def folder(self): """Simple property for cycle folder.""" if self.cycle: return self.cycle.folder return "" @builder.simple_property def is_in_history(self): """Used on UI to disable editing finished CycleTask which is in history""" return not self.cycle.is_current @property def cycle_task_objects_for_cache(self): """Get all related objects for this CycleTaskGroupObjectTask Returns: List of tuples with (related_object_type, related_object_id) """ return [(object_.__class__.__name__, object_.id) for object_ in self.related_objects()] _api_attrs = reflection.ApiAttributes( 'cycle', 'cycle_task_group', 'task_group_task', 'cycle_task_entries', 'sort_index', 'task_type', 'response_options', 'selected_response_options', reflection.Attribute('related_sources', create=False, update=False), reflection.Attribute('related_destinations', create=False, update=False), reflection.Attribute('object_approval', create=False, update=False), reflection.Attribute('finished_date', create=False, update=False), reflection.Attribute('verified_date', create=False, update=False), reflection.Attribute('allow_change_state', create=False, update=False), reflection.Attribute('folder', create=False, update=False), reflection.Attribute('workflow', create=False, update=False), reflection.Attribute('workflow_title', create=False, update=False), reflection.Attribute('cycle_task_group_title', create=False, update=False), reflection.Attribute('is_in_history', create=False, update=False), ) default_description = "<ol>"\ + "<li>Expand the object review task.</li>"\ + "<li>Click on the Object to be reviewed.</li>"\ + "<li>Review the object in the Info tab.</li>"\ + "<li>Click \"Approve\" to approve the object.</li>"\ + "<li>Click \"Decline\" to decline the object.</li>"\ + "</ol>" _aliases = { "title": "Summary", "description": "Task Details", "finished_date": { "display_name": "Actual Finish Date", "description": ("Make sure that 'Actual Finish Date' isn't set, " "if cycle task state is <'Assigned' / " "'In Progress' / 'Declined' / 'Deprecated'>. " "Type double dash '--' into " "'Actual Finish Date' cell to remove it.") }, "verified_date": { "display_name": "Actual Verified Date", "description": ("Make sure that 'Actual Verified Date' isn't set, " "if cycle task state is <'Assigned' / " "'In Progress' / 'Declined' / 'Deprecated' / " "'Finished'>. Type double dash '--' into " "'Actual Verified Date' cell to remove it.") }, "cycle": { "display_name": "Cycle", "filter_by": "_filter_by_cycle", }, "cycle_task_group": { "display_name": "Task Group", "mandatory": True, "filter_by": "_filter_by_cycle_task_group", }, "task_type": { "display_name": "Task Type", "mandatory": True, }, "end_date": "Due Date", "start_date": "Start Date", } @builder.simple_property def cycle_task_group_title(self): """Property. Returns parent CycleTaskGroup title.""" return self.cycle_task_group.title @builder.simple_property def workflow_title(self): """Property. Returns parent Workflow's title.""" return self.workflow.title @builder.simple_property def workflow(self): """Property which returns parent workflow object.""" return self.cycle.workflow @builder.simple_property def allow_change_state(self): return self.cycle.is_current and self.current_user_wfa_or_assignee() def current_user_wfa_or_assignee(self): """Current user is WF Admin, Assignee or Secondary Assignee for self.""" wfa_ids = self.workflow.get_person_ids_for_rolename("Admin") ta_ids = self.get_person_ids_for_rolename("Task Assignees") tsa_ids = self.get_person_ids_for_rolename("Task Secondary Assignees") return login.get_current_user_id() in set().union( wfa_ids, ta_ids, tsa_ids) @classmethod def _filter_by_cycle(cls, predicate): """Get query that filters cycle tasks by related cycles. Args: predicate: lambda function that excepts a single parameter and returns true of false. Returns: An sqlalchemy query that evaluates to true or false and can be used in filtering cycle tasks by related cycles. """ return Cycle.query.filter((Cycle.id == cls.cycle_id) & (predicate(Cycle.slug) | predicate(Cycle.title))).exists() @classmethod def _filter_by_cycle_task_group(cls, predicate): """Get query that filters cycle tasks by related cycle task groups. Args: predicate: lambda function that excepts a single parameter and returns true of false. Returns: An sqlalchemy query that evaluates to true or false and can be used in filtering cycle tasks by related cycle task groups. """ return CycleTaskGroup.query.filter( (CycleTaskGroup.id == cls.cycle_id) & (predicate(CycleTaskGroup.slug) | predicate(CycleTaskGroup.title))).exists() @classmethod def eager_query(cls): """Add cycle task entries to cycle task eager query This function adds cycle_task_entries as a join option when fetching cycles tasks, and makes sure that with one query we fetch all cycle task related data needed for generating cycle taks json for a response. Returns: a query object with cycle_task_entries added to joined load options. """ query = super(CycleTaskGroupObjectTask, cls).eager_query() return query.options( orm.subqueryload('cycle_task_entries'), orm.joinedload('cycle').undefer_group('Cycle_complete'), orm.joinedload('cycle').joinedload('workflow').undefer_group( 'Workflow_complete'), orm.joinedload('cycle').joinedload('workflow').joinedload( '_access_control_list'), orm.joinedload('cycle_task_group').undefer_group( 'CycleTaskGroup_complete'), ) @classmethod def indexed_query(cls): return super(CycleTaskGroupObjectTask, cls).indexed_query().options( orm.Load(cls).load_only("end_date", "start_date", "created_at", "updated_at"), orm.Load(cls).joinedload("cycle_task_group").load_only( "id", "title", "end_date", "next_due_date", ), orm.Load(cls).joinedload("cycle").load_only( "id", "title", "next_due_date", "is_verification_needed", ), orm.Load(cls).joinedload("cycle_task_group").joinedload( "contact").load_only("email", "name", "id"), orm.Load(cls).joinedload("cycle").joinedload("contact").load_only( "email", "name", "id"), orm.Load(cls).subqueryload("cycle_task_entries").load_only( "description", "id"), orm.Load(cls).joinedload("cycle").joinedload( "workflow").undefer_group("Workflow_complete"), ) def log_json(self): out_json = super(CycleTaskGroupObjectTask, self).log_json() out_json["folder"] = self.folder return out_json @classmethod def bulk_update(cls, src): """Update statuses for bunch of tasks in a bulk. Args: src: input json with next structure: [{"status": "Assigned", "id": 1}, {"status": "In Progress", "id": 2}] Returns: list of updated_instances """ new_prv_state_map = { cls.DEPRECATED: (cls.ASSIGNED, cls.IN_PROGRESS, cls.FINISHED, cls.VERIFIED, cls.DECLINED), cls.IN_PROGRESS: (cls.ASSIGNED, ), cls.FINISHED: (cls.IN_PROGRESS, cls.DECLINED), cls.VERIFIED: (cls.FINISHED, ), cls.DECLINED: (cls.FINISHED, ), cls.ASSIGNED: (), } uniq_states = set([item['state'] for item in src]) if len(list(uniq_states)) != 1: raise BadRequest("Request's JSON contains multiple statuses for " "CycleTasks") new_state = uniq_states.pop() LOGGER.info("Do bulk update CycleTasks with '%s' status", new_state) if new_state not in cls.VALID_STATES: raise BadRequest("Request's JSON contains invalid statuses for " "CycleTasks") prv_states = new_prv_state_map[new_state] all_ids = {item['id'] for item in src} # Eagerly loading is needed to get user permissions for CycleTask faster updatable_objects = cls.eager_query().filter( cls.id.in_(list(all_ids)), cls.status.in_(prv_states)) if new_state in (cls.VERIFIED, cls.DECLINED): updatable_objects = [ obj for obj in updatable_objects if obj.cycle.is_verification_needed ] # Bulk update works only on MyTasks page. Don't need to check for # WorkflowMembers' permissions here. User should update only his own tasks. updatable_objects = [ obj for obj in updatable_objects if obj.current_user_wfa_or_assignee() ] # Queries count is constant because we are using eager query for objects. for obj in updatable_objects: obj.status = new_state obj.modified_by_id = login.get_current_user_id() return updatable_objects
class Control(WithLastAssessmentDate, HasObjectState, Roleable, Relatable, CustomAttributable, Personable, ControlCategorized, PublicDocumentable, AssertionCategorized, Hierarchical, LastDeprecatedTimeboxed, Auditable, TestPlanned, 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", ["name", "email"]), attributes.FullTextAttr('secondary_assessor', 'secondary_assessor', ["name", "email"]), ] _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
class Control(synchronizable.Synchronizable, categorizable.Categorizable, WithLastAssessmentDate, synchronizable.RoleableSynchronizable, Relatable, mixins.CustomAttributable, Personable, PublicDocumentable, mixins.LastDeprecatedTimeboxed, mixins.TestPlanned, comment.ExternalCommentable, 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') version = deferred(db.Column(db.String), '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 = deferred(db.Column(db.String), "Control") means = deferred(db.Column(db.String), "Control") verify_frequency = deferred(db.Column(db.String), "Control") review_status = deferred(db.Column(db.String, nullable=True), "Control") review_status_display_name = deferred(db.Column(db.String, nullable=True), "Control") # GGRCQ attributes due_date = db.Column(db.Date, nullable=True) created_by_id = db.Column(db.Integer, nullable=False) # pylint: disable=no-self-argument @declared_attr def created_by(cls): """Relationship to user referenced by created_by_id.""" return utils.person_relationship(cls.__name__, "created_by_id") last_submitted_at = db.Column(db.DateTime, nullable=True) last_submitted_by_id = db.Column(db.Integer, nullable=True) # pylint: disable=no-self-argument @declared_attr def last_submitted_by(cls): """Relationship to user referenced by last_submitted_by_id.""" return utils.person_relationship(cls.__name__, "last_submitted_by_id") last_verified_at = db.Column(db.DateTime, nullable=True) last_verified_by_id = db.Column(db.Integer, nullable=True) # pylint: disable=no-self-argument @declared_attr def last_verified_by(cls): """Relationship to user referenced by last_verified_by_id.""" return utils.person_relationship(cls.__name__, "last_verified_by_id") _title_uniqueness = False _custom_publish = { 'created_by': ggrc_utils.created_by_stub, 'last_submitted_by': ggrc_utils.last_submitted_by_stub, 'last_verified_by': ggrc_utils.last_verified_by_stub, } # REST properties _api_attrs = reflection.ApiAttributes( 'active', 'company_control', 'directive', 'fraud_related', 'key_control', 'kind', 'means', 'verify_frequency', 'version', 'review_status', 'review_status_display_name', 'due_date', reflection.ExternalUserAttribute('created_by', force_create=True), 'last_submitted_at', reflection.ExternalUserAttribute('last_submitted_by', force_create=True), 'last_verified_at', reflection.ExternalUserAttribute('last_verified_by', force_create=True), ) _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', 'review_status_display_name', ] _sanitize_html = [ '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"), ) _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", "review_status": { "display_name": "Review State", "mandatory": False, "filter_only": True }, "review_status_display_name": { "display_name": "Review Status", "mandatory": False }, } @classmethod def eager_query(cls, **kwargs): query = super(Control, cls).eager_query(**kwargs) return cls.eager_inclusions(query, Control._include_links).options( orm.joinedload('directive'), ) def log_json(self): out_json = super(Control, self).log_json() out_json["created_by"] = ggrc_utils.created_by_stub(self) out_json["last_submitted_by"] = ggrc_utils.last_submitted_by_stub(self) out_json["last_verified_by"] = ggrc_utils.last_verified_by_stub(self) # so that event log can refer to deleted directive if self.directive: out_json["mapped_directive"] = self.directive.display_name return out_json @validates('review_status') def validate_review_status(self, _, value): # pylint: disable=no-self-use """Add explicit non-nullable validation.""" if value is None: raise ValidationError( "review_status for the object is not specified") return value # pylint: disable=invalid-name @validates('review_status_display_name') def validate_review_status_display_name(self, _, value): """Add explicit non-nullable validation.""" # pylint: disable=no-self-use,invalid-name if value is None: raise ValidationError( "review_status_display_name for the object is not specified") return value
class Control(WithLastAssessmentDate, HasObjectState, 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', ] @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', ).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---", }, "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