Esempio n. 1
0
class Audit(Snapshotable, clonable.SingleClonable, PublicDocumentable,
            mixins.CustomAttributable, Personable, HasOwnContext, Relatable,
            Roleable, issuetracker_issue.IssueTracked, WithLastDeprecatedDate,
            mixins.Timeboxed, mixins.BusinessObject, mixins.Folderable,
            Indexed, db.Model):
    """Audit model."""

    __tablename__ = 'audits'
    _slug_uniqueness = False

    VALID_STATES = (u'Planned', u'In Progress', u'Manager Review',
                    u'Ready for External Review', u'Completed', u'Deprecated')

    CLONEABLE_CHILDREN = {"AssessmentTemplate"}

    report_start_date = deferred(db.Column(db.Date), 'Audit')
    report_end_date = deferred(db.Column(db.Date), 'Audit')
    audit_firm_id = deferred(
        db.Column(db.Integer, db.ForeignKey('org_groups.id')), 'Audit')
    audit_firm = db.relationship('OrgGroup', uselist=False)
    gdrive_evidence_folder = deferred(db.Column(db.String), 'Audit')
    program_id = deferred(
        db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False),
        'Audit')
    audit_objects = db.relationship('AuditObject',
                                    backref='audit',
                                    cascade='all, delete-orphan')
    object_type = db.Column(db.String(length=250),
                            nullable=False,
                            default='Control')

    assessments = db.relationship('Assessment', backref='audit')
    issues = db.relationship('Issue', backref='audit')
    archived = deferred(db.Column(db.Boolean, nullable=False, default=False),
                        'Audit')
    assessment_templates = db.relationship('AssessmentTemplate',
                                           backref='audit')

    _api_attrs = reflection.ApiAttributes(
        'report_start_date',
        'report_end_date',
        'audit_firm',
        'gdrive_evidence_folder',
        'program',
        'object_type',
        'archived',
        reflection.Attribute('issue_tracker', create=False, update=False),
        reflection.Attribute('audit_objects', create=False, update=False),
    )

    _fulltext_attrs = [
        'archived',
        'report_start_date',
        'report_end_date',
        'audit_firm',
        'gdrive_evidence_folder',
    ]

    @classmethod
    def indexed_query(cls):
        return super(Audit, cls).indexed_query().options(
            orm.Load(cls).undefer_group("Audit_complete", ), )

    _sanitize_html = [
        'gdrive_evidence_folder',
        'description',
    ]

    _include_links = []

    _aliases = {
        "program": {
            "display_name": "Program",
            "filter_by": "_filter_by_program",
            "mandatory": True,
        },
        "start_date": "Planned Start Date",
        "end_date": "Planned End Date",
        "report_start_date": "Planned Report Period from",
        "report_end_date": "Planned Report Period to",
        "notes": None,
        "reference_url": None,
        "archived": {
            "display_name": "Archived",
            "mandatory": False
        },
        "status": {
            "display_name": "State",
            "mandatory": True,
            "description": "Options are:\n{}".format('\n'.join(VALID_STATES))
        }
    }

    @simple_property
    def issue_tracker(self):
        """Returns representation of issue tracker related info as a dict."""
        issue_obj = issuetracker_issue.IssuetrackerIssue.get_issue(
            'Audit', self.id)
        return issue_obj.to_dict() if issue_obj is not None else {}

    def _clone(self, source_object):
        """Clone audit and all relevant attributes.

    Keeps the internals of actual audit cloning and everything that is related
    to audit itself (auditors, audit firm, context setting,
    custom attribute values, etc.)
    """
        from ggrc_basic_permissions import create_audit_context

        data = {
            "title": source_object.generate_attribute("title"),
            "description": source_object.description,
            "audit_firm": source_object.audit_firm,
            "start_date": source_object.start_date,
            "end_date": source_object.end_date,
            "last_deprecated_date": source_object.last_deprecated_date,
            "program": source_object.program,
            "status": source_object.VALID_STATES[0],
            "report_start_date": source_object.report_start_date,
            "report_end_date": source_object.report_end_date
        }

        self.update_attrs(data)
        db.session.flush()

        create_audit_context(self)
        self.clone_acls(source_object)
        self.clone_custom_attribute_values(source_object)

    def clone_acls(self, audit):
        """Clone acl roles like auditors and audit captains

    Args:
      audit: Audit instance
    """
        for acl in audit.access_control_list:
            data = {
                "person": acl.person,
                "ac_role": acl.ac_role,
                "object": self,
                "context": acl.context,
            }
            new_acl = AccessControlList(**data)
            db.session.add(new_acl)

    def clone(self, source_id, mapped_objects=None):
        """Clone audit with specified whitelisted children.

    Children that can be cloned should be specified in CLONEABLE_CHILDREN.

    Args:
      mapped_objects: A list of related objects that should also be copied and
      linked to a new audit.
    """
        if not mapped_objects:
            mapped_objects = []

        source_object = Audit.query.get(source_id)
        self._clone(source_object)

        if any(mapped_objects):
            related_children = source_object.related_objects(mapped_objects)

            for obj in related_children:
                obj.clone(self)

    @orm.validates("archived")
    def archived_check(self, _, value):
        """Only Admins and Program Managers are allowed to (un)archive Audit."""
        user = get_current_user()
        if getattr(user, 'system_wide_role', None) in SystemWideRoles.admins:
            return value

        if self.archived is not None and self.archived != value and \
           not any(acl for acl in list(self.program.access_control_list)
                   if acl.ac_role.name == "Program Managers" and
                   acl.person.id == user.id):
            raise Forbidden()
        return value

    @classmethod
    def _filter_by_program(cls, predicate):
        """Helper for filtering by program"""
        return Program.query.filter((Program.id == Audit.program_id)
                                    & (predicate(Program.slug)
                                       | predicate(Program.title))).exists()

    @classmethod
    def eager_query(cls):
        query = super(Audit, cls).eager_query()
        return query.options(
            orm.joinedload('program'),
            orm.subqueryload('object_people').joinedload('person'),
            orm.subqueryload('audit_objects'),
        )
Esempio n. 2
0
class CycleTaskGroupObjectTask(roleable.Roleable,
                               wf_mixins.CycleTaskStatusValidatedMixin,
                               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"]),
        "folder",
    ]

    AUTO_REINDEX_RULES = [
        ft_mixin.ReindexRule("CycleTaskEntry",
                             lambda x: x.cycle_task_group_object_task),
    ]

    cycle_id = db.Column(
        db.Integer,
        db.ForeignKey('cycles.id', ondelete="CASCADE"),
        nullable=False,
    )
    cycle_task_group_id = db.Column(
        db.Integer,
        db.ForeignKey('cycle_task_groups.id', ondelete="CASCADE"),
        nullable=False,
    )
    task_group_task_id = db.Column(db.Integer,
                                   db.ForeignKey('task_group_tasks.id'),
                                   nullable=True)
    task_group_task = db.relationship(
        "TaskGroupTask",
        foreign_keys="CycleTaskGroupObjectTask.task_group_task_id")
    task_type = db.Column(db.String(length=250), nullable=False)
    response_options = db.Column(types.JsonType(), nullable=False, default=[])
    selected_response_options = db.Column(types.JsonType(),
                                          nullable=False,
                                          default=[])

    sort_index = db.Column(db.String(length=250), default="", nullable=False)

    finished_date = db.Column(db.DateTime)
    verified_date = db.Column(db.DateTime)

    @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):
        if self.cycle:
            return self.cycle.folder
        return ""

    @property
    def cycle_task_objects_for_cache(self):
        """Changing task state must invalidate `workflow_state` on objects
    """
        return [(object_.__class__.__name__, object_.id)
                for object_ in self.related_objects()]

    _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),
    )

    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.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("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 CustomAttributeDefinition(attributevalidator.AttributeValidator,
                                base.ContextRBAC, mixins.Base, mixins.Titled,
                                db.Model):
    """Custom attribute definition model.

  Attributes:
    multi_choice_mandatory: comma separated values of mandatory bitmaps.
      First lsb is for comment, second bit is for attachement.
  """

    __tablename__ = 'custom_attribute_definitions'

    definition_type = db.Column(db.String, nullable=False)
    definition_id = db.Column(db.Integer)
    attribute_type = db.Column(db.String, nullable=False)
    multi_choice_options = db.Column(db.String)
    multi_choice_mandatory = db.Column(db.String)
    mandatory = db.Column(db.Boolean)
    helptext = db.Column(db.String)
    placeholder = db.Column(db.String)

    attribute_values = db.relationship('CustomAttributeValue',
                                       backref='custom_attribute',
                                       cascade='all, delete-orphan')

    @property
    def definition_attr(self):
        return '{0}_definition'.format(self.definition_type)

    @property
    def definition(self):
        return getattr(self, self.definition_attr)

    @property
    def value_mapping(self):
        return self.ValidTypes.DEFAULT_VALUE_MAPPING.get(
            self.attribute_type) or {}

    @classmethod
    def get_default_value_for(cls, attribute_type):
        return cls.ValidTypes.DEFAULT_VALUE.get(attribute_type)

    @builder.simple_property
    def default_value(self):
        return self.get_default_value_for(self.attribute_type)

    def get_indexed_value(self, value):
        return self.value_mapping.get(value, value)

    @definition.setter
    def definition(self, value):
        self.definition_id = getattr(value, 'id', None)
        if hasattr(value, '_inflector'):
            self.definition_type = value._inflector.table_singular
        else:
            self.definition_type = ''
        return setattr(self, self.definition_attr, value)

    _extra_table_args = (UniqueConstraint('definition_type',
                                          'definition_id',
                                          'title',
                                          name='uq_custom_attribute'),
                         db.Index('ix_custom_attributes_title', 'title'))

    _include_links = [
        'definition_type',
        'definition_id',
        'attribute_type',
        'multi_choice_options',
        'multi_choice_mandatory',
        'mandatory',
        'helptext',
        'placeholder',
    ]

    _api_attrs = reflection.ApiAttributes(
        reflection.Attribute("default_value",
                             read=True,
                             create=False,
                             update=False), *_include_links)

    _sanitize_html = [
        "multi_choice_options",
        "helptext",
        "placeholder",
    ]

    _reserved_names = {}

    def _clone(self, target):
        """Clone custom attribute definitions."""
        data = {
            "title": self.title,
            "definition_type": self.definition_type,
            "definition_id": target.id,
            "context": target.context,
            "attribute_type": self.attribute_type,
            "multi_choice_options": self.multi_choice_options,
            "multi_choice_mandatory": self.multi_choice_mandatory,
            "mandatory": self.mandatory,
            "helptext": self.helptext,
            "placeholder": self.placeholder,
        }
        ca_definition = CustomAttributeDefinition(**data)
        db.session.add(ca_definition)
        return ca_definition

    class ValidTypes(object):
        """Class representing valid custom attribute definitions.

    Basically an enum, therefore no need for public methods.
    """
        # pylint: disable=too-few-public-methods
        TEXT = "Text"
        RICH_TEXT = "Rich Text"
        DROPDOWN = "Dropdown"
        CHECKBOX = "Checkbox"
        DATE = "Date"
        MAP = "Map"

        DEFAULT_VALUE = {
            CHECKBOX: "0",
            RICH_TEXT: "",
            TEXT: "",
            DROPDOWN: "",
            DATE: ""
        }

        DEFAULT_VALUE_MAPPING = {
            CHECKBOX: {
                True: "Yes",
                False: "No",
                "0": "No",
                "1": "Yes",
            },
        }

    class MultiChoiceMandatoryFlags(object):
        """Enum representing flags in multi_choice_mandatory bitmaps."""
        # pylint: disable=too-few-public-methods
        COMMENT_REQUIRED = 0b001
        EVIDENCE_REQUIRED = 0b010
        URL_REQUIRED = 0b100

    VALID_TYPES = {
        "Text": "Text",
        "Rich Text": "Rich Text",
        "Dropdown": "Dropdown",
        "Checkbox": "Checkbox",
        "Date": "Date",
        "Person": "Map:Person",
    }

    @validates("attribute_type")
    def validate_attribute_type(self, _, value):
        """Check that provided attribute_type is allowed."""
        if value not in self.VALID_TYPES.values():
            raise ValidationError("Invalid attribute_type: got {v}, "
                                  "expected one of {l}".format(
                                      v=value,
                                      l=list(self.VALID_TYPES.values())))
        return value

    @validates("multi_choice_options")
    def validate_multi_choice_options(self, _, value):
        """Strip spaces around options in dropdown options."""
        # pylint: disable=no-self-use
        # TODO: this should be "if value is not None" to disallow value == ""
        if value:
            value_list = [part.strip() for part in value.split(",")]
            value_set = set(value_list)
            if len(value_set) != len(value_list):
                raise ValidationError(
                    "Duplicate dropdown options are not allowed: "
                    "'{}'".format(value))
            if "" in value_set:
                raise ValidationError(
                    "Empty dropdown options are not allowed: '{}'".format(
                        value))
            value = ",".join(value_list)

        return value

    @validates("multi_choice_mandatory")
    def validate_multi_choice_mandatory(self, _, value):
        """Strip spaces around bitmas in dropdown options."""
        # pylint: disable=no-self-use
        if value:
            value = ",".join(part.strip() for part in value.split(","))

        return value

    def validate_assessment_title(self, name):
        """Check assessment title uniqueness.

    Assessment CAD should not match any name from assessment_template.
    Currently assessment templates do not have global custom attributes, but
    in the future global CAD on assessment templates could be applied to all
    generated assessments. That's why we do not differentiate between global
    and local CAD here.

    Args:
      name: Assessment custom attribute definition title.
    Raises:
      ValueError if name is an invalid CAD title.
    """
        if self.definition_id:
            # Local Assessment CAD can match local and global Assessment Template
            # CAD.
            # NOTE: This is not the best way of checking if the current CAD is local,
            # since it relies on the fact that if definition_id will be set, it will
            # be set along with definition_type. If we manually set definition_type
            # then title then definition_id, this check would fail.
            return

        if not getattr(flask.g, "template_cad_names", set()):
            query = db.session.query(self.__class__.title).filter(
                self.__class__.definition_type == "assessment_template")
            flask.g.template_cad_names = {cad.title.lower() for cad in query}

        if name in flask.g.template_cad_names:
            raise ValueError(
                u"Local custom attribute '{}' "
                u"already exists for this object type.".format(name))

    @validates("title", "definition_type")
    def validate_title(self, key, value):
        """Validate CAD title/name uniqueness.

    Note: title field is used for storing CAD names.
    CAD names need to follow 4 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.
      3) Object level CAD names can clash, but not for the same Object
         instance. This means we can have two CAD with a name "my cad", with
         different attributable_id fields.
      4) Names must not match any existing custom attribute role name

    Third rule is handled by the database with unique key uq_custom_attribute
    (`definition_type`,`definition_id`,`title`).

    This validator should check for name collisions for 1st and 2nd rule.

    This validator works, because definition_type is never changed. It only
    gets set when the cad is created and after that only title filed can
    change. This makes validation using both fields possible.

    Args:
      value: custom attribute definition name

    Returns:
      value if the name passes all uniqueness checks.
    """

        if key == "title" and self.definition_type:
            name = value.lower()
            definition_type = self.definition_type
        elif key == "definition_type" and self.title:
            name = self.title.lower()
            definition_type = value.lower()
        else:
            return value

        if name in self._get_reserved_names(definition_type):
            raise ReservedNameError(
                u"Attribute '{}' is reserved for this object type.".format(
                    name))

        if (self._get_global_cad_names(definition_type).get(name) is not None
                and self._get_global_cad_names(definition_type).get(name) !=
                self.id):
            raise ValueError(
                u"Global custom attribute '{}' "
                u"already exists for this object type".format(name))
        model_name = get_inflector_model_name_dict()[definition_type]
        acrs = {
            i.lower()
            for i in acr.get_custom_roles_for(model_name).values()
        }
        if name in acrs:
            raise ValueError(
                u"Custom Role with a name of '{}' "
                u"already exists for this object type".format(name))

        if definition_type == "assessment":
            self.validate_assessment_title(name)

        return value

    def log_json(self):
        """Add extra fields to be logged in CADs."""
        results = super(CustomAttributeDefinition, self).log_json()
        results["default_value"] = self.default_value
        return results