class Comment(Roleable, Relatable, Described, Notifiable, base.ContextRBAC, Base, Indexed, db.Model): """Basic comment model.""" __tablename__ = "comments" assignee_type = db.Column(db.String, nullable=False, default=u"") revision_id = deferred( db.Column( db.Integer, db.ForeignKey('revisions.id', ondelete='SET NULL'), nullable=True, ), 'Comment') revision = db.relationship( 'Revision', uselist=False, ) custom_attribute_definition_id = deferred( db.Column( db.Integer, db.ForeignKey('custom_attribute_definitions.id', ondelete='SET NULL'), nullable=True, ), 'Comment') custom_attribute_definition = db.relationship( 'CustomAttributeDefinition', uselist=False, ) initiator_instance_id = db.Column(db.Integer, nullable=True) initiator_instance_type = db.Column(db.String, nullable=True) INITIATOR_INSTANCE_TMPL = "{}_comment_initiated_by" initiator_instance = utils.PolymorphicRelationship( "initiator_instance_id", "initiator_instance_type", INITIATOR_INSTANCE_TMPL) # REST properties _api_attrs = reflection.ApiAttributes( "assignee_type", reflection.Attribute("custom_attribute_revision", create=False, update=False), reflection.Attribute("custom_attribute_revision_upd", read=False), reflection.Attribute("header_url_link", create=False, update=False), ) _sanitize_html = [ "description", ] AUTO_REINDEX_RULES = [ ReindexRule("Comment", get_objects_to_reindex), ReindexRule("Relationship", reindex_by_relationship), ] _aliases = { "custom_attribute_definition": "custom_attribute_definition", } @builder.simple_property def header_url_link(self): """Return header url link to comment if that comment related to proposal and that proposal is only proposed.""" if self.initiator_instance_type != "Proposal": return "" proposed_status = self.initiator_instance.STATES.PROPOSED if self.initiator_instance.status == proposed_status: return "proposal_link" return "" @classmethod def eager_query(cls, **kwargs): query = super(Comment, cls).eager_query(**kwargs) return query.options( orm.joinedload('revision'), orm.joinedload('custom_attribute_definition').undefer_group( 'CustomAttributeDefinition_complete'), ) def log_json(self): """Log custom attribute revisions.""" res = super(Comment, self).log_json() res["custom_attribute_revision"] = self.custom_attribute_revision return res @builder.simple_property def custom_attribute_revision(self): """Get the historical value of the relevant CA value.""" if not self.revision: return None revision = self.revision.content cav_stored_value = revision['attribute_value'] cad = self.custom_attribute_definition return { 'custom_attribute': { 'id': cad.id if cad else None, 'title': cad.title if cad else 'DELETED DEFINITION', }, 'custom_attribute_stored_value': cav_stored_value, } def custom_attribute_revision_upd(self, value): """Create a Comment-CA mapping with current CA value stored.""" ca_revision_dict = value.get('custom_attribute_revision_upd') if not ca_revision_dict: return ca_val_dict = self._get_ca_value(ca_revision_dict) ca_val_id = ca_val_dict['id'] ca_val_revision = Revision.query.filter_by( resource_type='CustomAttributeValue', resource_id=ca_val_id, ).order_by(Revision.created_at.desc(), ).limit(1).first() if not ca_val_revision: raise BadRequest( "No Revision found for CA value with id provided under " "'custom_attribute_value': {}".format(ca_val_dict)) self.revision_id = ca_val_revision.id self.revision = ca_val_revision # Here *attribute*_id is assigned to *definition*_id, strange but, # as you can see in src/ggrc/models/custom_attribute_value.py # custom_attribute_id is link to custom_attribute_definitions.id # possible best way is use definition id from request: # ca_revision_dict["custom_attribute_definition"]["id"] # but needs to be checked that is always exist in request self.custom_attribute_definition_id = ca_val_revision.content.get( 'custom_attribute_id', ) self.custom_attribute_definition = CustomAttributeDefinition.query.get( self.custom_attribute_definition_id, ) @staticmethod def _get_ca_value(ca_revision_dict): """Get CA value dict from json and do a basic validation.""" ca_val_dict = ca_revision_dict.get('custom_attribute_value') if not ca_val_dict: raise ValueError( "CA value expected under " "'custom_attribute_value': {}".format(ca_revision_dict)) if not ca_val_dict.get('id'): raise ValueError( "CA value id expected under 'id': {}".format(ca_val_dict)) return ca_val_dict
class Assessment(statusable.Statusable, AuditRelationship, AutoStatusChangeable, Assignable, HasObjectState, TestPlanned, CustomAttributable, EvidenceURL, Commentable, Personable, reminderable.Reminderable, Timeboxed, Relatable, WithSimilarityScore, FinishedDate, VerifiedDate, ValidateOnComplete, Notifiable, BusinessObject, Indexed, db.Model): """Class representing Assessment. Assessment is an object representing an individual assessment performed on a specific object during an audit to ascertain whether or not certain conditions were met for that object. """ __tablename__ = 'assessments' _title_uniqueness = False ASSIGNEE_TYPES = (u"Creator", u"Assessor", u"Verifier") REMINDERABLE_HANDLERS = { "statusToPerson": { "handler": reminderable.Reminderable.handle_state_to_person_reminder, "data": { statusable.Statusable.START_STATE: "Assessor", "In Progress": "Assessor" }, "reminders": { "assessment_assessor_reminder", } } } design = deferred(db.Column(db.String), "Assessment") operationally = deferred(db.Column(db.String), "Assessment") audit_id = deferred( db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False), 'Assessment') @declared_attr def object_level_definitions(self): """Set up a backref so that we can create an object level custom attribute definition without the need to do a flush to get the assessment id. This is used in the relate_ca method in hooks/assessment.py. """ return db.relationship('CustomAttributeDefinition', primaryjoin=lambda: and_( remote(CustomAttributeDefinition. definition_id) == Assessment.id, remote(CustomAttributeDefinition. definition_type) == "assessment"), foreign_keys=[ CustomAttributeDefinition.definition_id, CustomAttributeDefinition.definition_type ], backref='assessment_definition', cascade='all, delete-orphan') object = {} # we add this for the sake of client side error checking VALID_CONCLUSIONS = frozenset( ["Effective", "Ineffective", "Needs improvement", "Not Applicable"]) # REST properties _publish_attrs = [ 'design', 'operationally', 'audit', PublishOnly('object') ] _fulltext_attrs = [ 'design', 'operationally', MultipleSubpropertyFullTextAttr('related_assessors', 'assessors', ['email', 'name']), MultipleSubpropertyFullTextAttr('related_creators', 'creators', ['email', 'name']), MultipleSubpropertyFullTextAttr('related_verifiers', 'verifiers', ['email', 'name']), ] _tracked_attrs = { 'contact_id', 'description', 'design', 'notes', 'operationally', 'reference_url', 'secondary_contact_id', 'test_plan', 'title', 'url', 'start_date', 'end_date' } _aliases = { "owners": None, "assessment_template": { "display_name": "Template", "ignore_on_update": True, "filter_by": "_ignore_filter", "type": reflection.AttributeInfo.Type.MAPPING, }, "url": "Assessment URL", "design": "Conclusion: Design", "operationally": "Conclusion: Operation", "related_creators": { "display_name": "Creators", "mandatory": True, "type": reflection.AttributeInfo.Type.MAPPING, }, "related_assessors": { "display_name": "Assignees", "mandatory": True, "type": reflection.AttributeInfo.Type.MAPPING, }, "related_verifiers": { "display_name": "Verifiers", "type": reflection.AttributeInfo.Type.MAPPING, }, } AUTO_REINDEX_RULES = [ ReindexRule("RelationshipAttr", reindex_by_relationship_attr) ] similarity_options = similarity_options_module.ASSESSMENT @property def assessors(self): """Get the list of assessor assignees""" return self.assignees_by_type.get("Assessor", []) @property def creators(self): """Get the list of creator assignees""" return self.assignees_by_type.get("Creator", []) @property def verifiers(self): """Get the list of verifier assignees""" return self.assignees_by_type.get("Verifier", []) def validate_conclusion(self, value): return value if value in self.VALID_CONCLUSIONS else None @validates("operationally") def validate_opperationally(self, key, value): # pylint: disable=unused-argument return self.validate_conclusion(value) @validates("design") def validate_design(self, key, value): # pylint: disable=unused-argument return self.validate_conclusion(value) @classmethod def _ignore_filter(cls, _): return None
class Comment(Roleable, Relatable, Described, Notifiable, Base, Indexed, db.Model): """Basic comment model.""" __tablename__ = "comments" assignee_type = db.Column(db.String) revision_id = deferred( db.Column( db.Integer, db.ForeignKey('revisions.id', ondelete='SET NULL'), nullable=True, ), 'Comment') revision = db.relationship( 'Revision', uselist=False, ) custom_attribute_definition_id = deferred( db.Column( db.Integer, db.ForeignKey('custom_attribute_definitions.id', ondelete='SET NULL'), nullable=True, ), 'Comment') custom_attribute_definition = db.relationship( 'CustomAttributeDefinition', uselist=False, ) # REST properties _api_attrs = reflection.ApiAttributes( "assignee_type", reflection.Attribute("custom_attribute_revision", create=False, update=False), reflection.Attribute("custom_attribute_revision_upd", read=False), ) _sanitize_html = [ "description", ] def get_objects_to_reindex(self): """Return list required objects for reindex if comment C.U.D.""" source_qs = db.session.query( Relationship.destination_type, Relationship.destination_id).filter( Relationship.source_type == self.__class__.__name__, Relationship.source_id == self.id) destination_qs = db.session.query( Relationship.source_type, Relationship.source_id).filter( Relationship.destination_type == self.__class__.__name__, Relationship.destination_id == self.id) result_qs = source_qs.union(destination_qs) klass_dict = defaultdict(set) for klass, object_id in result_qs: klass_dict[klass].add(object_id) queries = [] for klass, object_ids in klass_dict.iteritems(): model = inflector.get_model(klass) if not model: continue if issubclass(model, (Indexed, Commentable)): queries.append( model.query.filter(model.id.in_(list(object_ids)))) return list(itertools.chain(*queries)) AUTO_REINDEX_RULES = [ ReindexRule("Comment", lambda x: x.get_objects_to_reindex()), ReindexRule("Relationship", reindex_by_relationship), ] @classmethod def eager_query(cls): query = super(Comment, cls).eager_query() return query.options( orm.joinedload('revision'), orm.joinedload('custom_attribute_definition').undefer_group( 'CustomAttributeDefinition_complete'), ) @builder.simple_property def custom_attribute_revision(self): """Get the historical value of the relevant CA value.""" if not self.revision: return None revision = self.revision.content cav_stored_value = revision['attribute_value'] cad = self.custom_attribute_definition return { 'custom_attribute': { 'id': cad.id if cad else None, 'title': cad.title if cad else 'DELETED DEFINITION', }, 'custom_attribute_stored_value': cav_stored_value, } def custom_attribute_revision_upd(self, value): """Create a Comment-CA mapping with current CA value stored.""" ca_revision_dict = value.get('custom_attribute_revision_upd') if not ca_revision_dict: return ca_val_dict = self._get_ca_value(ca_revision_dict) ca_val_id = ca_val_dict['id'] ca_val_revision = Revision.query.filter_by( resource_type='CustomAttributeValue', resource_id=ca_val_id, ).order_by(Revision.created_at.desc(), ).limit(1).first() if not ca_val_revision: raise BadRequest( "No Revision found for CA value with id provided under " "'custom_attribute_value': {}".format(ca_val_dict)) self.revision_id = ca_val_revision.id self.custom_attribute_definition_id = ca_val_revision.content.get( 'custom_attribute_id', ) @staticmethod def _get_ca_value(ca_revision_dict): """Get CA value dict from json and do a basic validation.""" ca_val_dict = ca_revision_dict.get('custom_attribute_value') if not ca_val_dict: raise ValueError( "CA value expected under " "'custom_attribute_value': {}".format(ca_revision_dict)) if not ca_val_dict.get('id'): raise ValueError( "CA value id expected under 'id': {}".format(ca_val_dict)) return ca_val_dict
class CycleTaskGroup(WithContact, Stateful, Slugged, Timeboxed, Described, Titled, Indexed, Base, db.Model): """Cycle Task Group model. """ __tablename__ = 'cycle_task_groups' _title_uniqueness = False @classmethod def generate_slug_prefix_for(cls, obj): return "CYCLEGROUP" VALID_STATES = (u'Assigned', u'InProgress', u'Finished', u'Verified', u'Declined') cycle_id = db.Column( db.Integer, db.ForeignKey('cycles.id', ondelete="CASCADE"), nullable=False, ) task_group_id = db.Column(db.Integer, db.ForeignKey('task_groups.id'), nullable=True) cycle_task_group_tasks = db.relationship('CycleTaskGroupObjectTask', backref='cycle_task_group', cascade='all, delete-orphan') sort_index = db.Column(db.String(length=250), default="", nullable=False) next_due_date = db.Column(db.Date) _publish_attrs = [ 'cycle', 'task_group', 'cycle_task_group_tasks', 'sort_index', 'next_due_date' ] _aliases = { "cycle": { "display_name": "Cycle", "filter_by": "_filter_by_cycle", }, } PROPERTY_TEMPLATE = u"group {}" _fulltext_attrs = [ MultipleSubpropertyFullTextAttr("task title", 'cycle_task_group_tasks', ["title"], False), MultipleSubpropertyFullTextAttr( "task assignee", lambda instance: [t.contact for t in instance.cycle_task_group_tasks], ["name", "email"], False), DateMultipleSubpropertyFullTextAttr("task due date", "cycle_task_group_tasks", ["end_date"], False), DateFullTextAttr( "due date", 'next_due_date', ), FullTextAttr("assignee", "contact", ['name', 'email']), FullTextAttr("cycle title", 'cycle', ['title'], False), FullTextAttr("cycle assignee", lambda x: x.cycle.contact, ['email', 'name'], False), DateFullTextAttr("cycle due date", lambda x: x.cycle.next_due_date, with_template=False), ] AUTO_REINDEX_RULES = [ ReindexRule("CycleTaskGroupObjectTask", lambda x: x.cycle_task_group), ReindexRule( "Person", lambda x: CycleTaskGroup.query.filter( CycleTaskGroup.contact_id == x.id)), ReindexRule( "Person", lambda x: [ i.cycle for i in CycleTaskGroup.query.filter( CycleTaskGroup.contact_id == x.id) ]), ] @classmethod def _filter_by_cycle(cls, predicate): """Get query that filters cycle task groups. Args: predicate: lambda function that excepts a single parameter and returns true of false. Returns: An sqlalchemy query that evaluates to true or false and can be used in filtering cycle task groups by related cycle. """ return Cycle.query.filter((Cycle.id == cls.cycle_id) & (predicate(Cycle.slug) | predicate(Cycle.title))).exists() @classmethod def eager_query(cls): """Add cycle tasks and objects to cycle task group eager query. Make sure we load all cycle task group relevant data in a single query. Returns: a query object with cycle_task_group_tasks added to joined load options. """ query = super(CycleTaskGroup, cls).eager_query() return query.options(orm.joinedload('cycle_task_group_tasks'))