class CycleTaskGroupObjectTask(WithContact, Stateful, Slugged, Timeboxed,
                               Relatable, Notifiable, Described, Titled,
                               Indexed, Base, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'
    _title_uniqueness = False

    IMPORTABLE_FIELDS = (
        'slug',
        'title',
        'description',
        'start_date',
        'end_date',
        'finished_date',
        'verified_date',
        'contact',
    )

    @classmethod
    def generate_slug_prefix_for(cls, obj):
        return "CYCLETASK"

    VALID_STATES = (None, 'InProgress', 'Assigned', 'Finished', 'Declined',
                    'Verified')

    # Note: this statuses are used in utils/query_helpers to filter out the tasks
    # that should be visible on My Tasks pages.
    ACTIVE_STATES = ("Assigned", "InProgress", "Finished", "Declined")

    PROPERTY_TEMPLATE = u"task {}"

    _fulltext_attrs = [
        DateFullTextAttr(
            "due 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("comment", "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,
            "filter_by": "_filter_by_contact",
        },
        "secondary_contact": None,
        "start_date": "Start Date",
        "end_date": "End Date",
        "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)))
        }
    }

    @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'),
        )
class AssessmentTemplate(assessment.AuditRelationship, relationship.Relatable,
                         mixins.Titled, mixins.CustomAttributable,
                         mixins.Slugged, db.Model):
    """A class representing the assessment template entity.

  An Assessment Template is a template that allows users for easier creation of
  multiple Assessments that are somewhat similar to each other, avoiding the
  need to repeatedly define the same set of properties for every new Assessment
  object.
  """
    __tablename__ = "assessment_templates"
    _mandatory_default_people = ("assessors", "verifiers")

    PER_OBJECT_CUSTOM_ATTRIBUTABLE = True

    # the type of the object under assessment
    template_object_type = db.Column(db.String, nullable=True)

    # whether to use the control test plan as a procedure
    test_plan_procedure = db.Column(db.Boolean, nullable=False, default=False)

    # procedure description
    procedure_description = db.Column(db.Text, nullable=True)

    # the people that should be assigned by default to each assessment created
    # within the releated audit
    default_people = db.Column(JsonType, nullable=False)

    # labels to show to the user in the UI for various default people values
    DEFAULT_PEOPLE_LABELS = {
        "Object Owners": "Object Owners",
        "Audit Lead": "Audit Lead",
        "Auditors": "Auditors",
        "Primary Assessor": "Principal Assessor",
        "Secondary Assessors": "Secondary Assessors",
        "Primary Contact": "Primary Contact",
        "Secondary Contact": "Secondary Contact",
    }

    _title_uniqueness = False

    # REST properties
    _publish_attrs = [
        "template_object_type", "test_plan_procedure", "procedure_description",
        "default_people",
        PublishOnly("DEFAULT_PEOPLE_LABELS")
    ]

    _aliases = {
        "default_assessors": {
            "display_name": "Default Assessors",
            "mandatory": True,
            "filter_by": "_nop_filter",
        },
        "default_verifier": {
            "display_name": "Default Verifier",
            "mandatory": True,
            "filter_by": "_nop_filter",
        },
        "default_test_plan": {
            "display_name": "Default Test Plan",
            "filter_by": "_nop_filter",
        },
        "test_plan_procedure": {
            "display_name": "Use Control Test Plan",
            "mandatory": False,
        },
        "template_object_type": {
            "display_name": "Object Under Assessment",
            "mandatory": True,
        },
        "template_custom_attributes": {
            "display_name":
            "Custom Attributes",
            "type":
            AttributeInfo.Type.SPECIAL_MAPPING,
            "filter_by":
            "_nop_filter",
            "description":
            ("List of custom attributes for the assessment template\n"
             "One attribute per line. fields are separated by commas ','\n\n"
             "<attribute type>, <attribute name>, [<attribute value1>, "
             "<attribute value2>, ...]\n\n"
             "Valid attribute types: Text, Rich Text, Date, Checkbox, Person,"
             "Dropdown.\n"
             "attribute name: Any single line string without commas. Leading "
             "and trailing spaces are ignored.\n"
             "list of attribute values: Comma separated list, only used if "
             "attribute type is 'Dropdown'. Prepend '(a)' if the value has a "
             "mandatory attachment and/or (c) if the value requires a "
             "mandatory comment.\n\n"
             "Limitations: Dropdown values can not start with either '(a)' or"
             "'(c)' and attribute names can not contain commas ','."),
        },
    }

    @classmethod
    def _nop_filter(cls, _):
        """No operation filter.

    This is used for objects for which we can not implement a normal sql query
    filter. Example is default_verifier field that is a json string in the db
    and we can not create direct queries on json fields.
    """
        return None

    @classmethod
    def generate_slug_prefix_for(cls, obj):
        return "TEMPLATE"

    def _clone(self):
        """Clone Assessment Template.

    Returns:
      Instance of assessment template copy.
    """
        data = {
            "title": self.title,
            "template_object_type": self.template_object_type,
            "test_plan_procedure": self.test_plan_procedure,
            "procedure_description": self.procedure_description,
            "default_people": self.default_people,
        }
        assessment_template_copy = AssessmentTemplate(**data)
        db.session.add(assessment_template_copy)
        db.session.flush()
        return assessment_template_copy

    def clone(self, target):
        """Clone Assessment Template and related custom attributes."""
        assessment_template_copy = self._clone()
        rel = relationship.Relationship(source=target,
                                        destination=assessment_template_copy)
        db.session.add(rel)
        db.session.flush()

        for cad in self.custom_attribute_definitions:
            # pylint: disable=protected-access
            cad._clone(assessment_template_copy)

        return (assessment_template_copy, rel)

    @validates('default_people')
    def validate_default_people(self, key, value):
        """Check that default people lists are not empty.

    Check if the default_people contains both assessors and verifiers. The
    values of those fields must be truthy, and if the value is a string it
    must be a valid default people label. If the value is not a string, it
    should be a list of valid user ids, but that is too expensive to test in
    this validator.
    """
        # pylint: disable=unused-argument
        for mandatory in self._mandatory_default_people:
            mandatory_value = value.get(mandatory)
            if (not mandatory_value or isinstance(mandatory_value, list)
                    and any(not isinstance(p_id, (int, long))
                            for p_id in mandatory_value)
                    or isinstance(mandatory_value, basestring)
                    and mandatory_value not in self.DEFAULT_PEOPLE_LABELS):
                raise ValidationError(
                    'Invalid value for default_people.{field}. Expected a people '
                    'label in string or a list of int people ids, received {value}.'
                    .format(field=mandatory, value=mandatory_value), )

        return value
Esempio n. 3
0
class Assessment(statusable.Statusable, AuditRelationship,
                 AutoStatusChangeable, Assignable, HasObjectState, TestPlanned,
                 CustomAttributable, EvidenceURL, Commentable, Personable,
                 reminderable.Reminderable, Timeboxed, Relatable,
                 WithSimilarityScore, FinishedDate, VerifiedDate,
                 ValidateOnComplete, Notifiable, BusinessObject, 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")

    @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
    audit = {}

    VALID_CONCLUSIONS = frozenset(
        ["Effective", "Ineffective", "Needs improvement", "Not Applicable"])

    # REST properties
    _publish_attrs = [
        'design', 'operationally',
        PublishOnly('audit'),
        PublishOnly('object')
    ]

    _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": "Creator",
            "mandatory": True,
            "filter_by": "_filter_by_related_creators",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_assessors": {
            "display_name": "Assignee",
            "mandatory": True,
            "filter_by": "_filter_by_related_assessors",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_verifiers": {
            "display_name": "Verifier",
            "filter_by": "_filter_by_related_verifiers",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
    }

    similarity_options = similarity_options_module.ASSESSMENT

    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 _filter_by_related_creators(cls, predicate):
        return cls._get_relate_filter(predicate, "Creator")

    @classmethod
    def _filter_by_related_assessors(cls, predicate):
        return cls._get_relate_filter(predicate, "Assessor")

    @classmethod
    def _filter_by_related_verifiers(cls, predicate):
        return cls._get_relate_filter(predicate, "Verifier")

    @classmethod
    def _ignore_filter(cls, _):
        return None
Esempio n. 4
0
class Person(CustomAttributable, CustomAttributeMapable, HasOwnContext,
             Relatable, Base, Indexed, db.Model):

  __tablename__ = 'people'

  email = deferred(db.Column(db.String, nullable=False), 'Person')
  name = deferred(db.Column(db.String), 'Person')
  language_id = deferred(db.Column(db.Integer), 'Person')
  company = deferred(db.Column(db.String), 'Person')

  object_people = db.relationship(
      'ObjectPerson', backref='person', cascade='all, delete-orphan')
  object_owners = db.relationship(
      'ObjectOwner', backref='person', cascade='all, delete-orphan')
  access_control_list = db.relationship(
      'AccessControlList', backref='person', cascade='all, delete-orphan')
  language = db.relationship(
      'Option',
      primaryjoin='and_(foreign(Person.language_id) == Option.id, '
      'Option.role == "person_language")',
      uselist=False,
  )

  @staticmethod
  def _extra_table_args(cls):
    return (
        db.Index('ix_people_name_email', 'name', 'email'),
        db.Index('uq_people_email', 'email', unique=True),
    )

  _fulltext_attrs = [
      'company',
      'email',
      'name',
  ]
  _publish_attrs = [
      'company',
      'email',
      'language',
      'name',
      PublishOnly('object_people'),
      PublishOnly('system_wide_role'),
  ]
  _sanitize_html = [
      'company',
      'name',
  ]
  _include_links = []
  _aliases = {
      "name": "Name",
      "email": {
          "display_name": "Email",
          "unique": True,
      },
      "company": "Company",
      "user_role": {
          "display_name": "Role",
          "type": "user_role",
          "filter_by": "_filter_by_user_role",
      },
  }

  @classmethod
  def _filter_by_user_role(cls, predicate):
    from ggrc_basic_permissions.models import Role, UserRole
    return UserRole.query.join(Role).filter(
        (UserRole.person_id == cls.id) &
        (UserRole.context_id == None) &  # noqa
        predicate(Role.name)
    ).exists()

  # Methods required by Flask-Login
    # pylint: disable=no-self-use
  def is_authenticated(self):
    return self.system_wide_role != 'No Access'

  @property
  def user_name(self):
    return self.email.split("@")[0]

  def is_active(self):
    # pylint: disable=no-self-use
    return True  # self.active

  def is_anonymous(self):
    # pylint: disable=no-self-use
    return False

  def get_id(self):
    return unicode(self.id)  # noqa

  @validates('language')
  def validate_person_options(self, key, option):
    return validate_option(self.__class__.__name__, key, option,
                           'person_language')

  @validates('email')
  def validate_email(self, key, email):
    if not Person.is_valid_email(email):
      message = "Must provide a valid email address"
      raise ValidationError(message)
    return email

  @staticmethod
  def is_valid_email(val):
    # Borrowed from Django
    # literal form, ipv4 address (SMTP 4.1.3)
    email_re = re.compile(
        '^[-!#$%&\'*+\\.\/0-9=?A-Z^_`{|}~]+@([-0-9A-Z]+\.)+([0-9A-Z]){2,4}$',
        re.IGNORECASE)
    return email_re.match(val) if val else False

  @classmethod
  def eager_query(cls):
    from sqlalchemy import orm

    # query = super(Person, cls).eager_query()
    # Completely overriding eager_query to avoid eager loading of the
    # modified_by relationship
    return super(Person, cls).eager_query().options(
        orm.joinedload('language'),
        orm.subqueryload('object_people'),
    )

  @classmethod
  def indexed_query(cls):
    from sqlalchemy import orm

    return super(Person, cls).indexed_query().options(
        orm.Load(cls).undefer_group(
            "Person_complete",
        ),
    )

  def _display_name(self):
    return self.email

  @builder.simple_property
  def system_wide_role(self):
    """For choosing the role string to show to the user; of all the roles in
    the system-wide context, it shows the highest ranked one (if there are
    multiple) or "No Access" if there are none.
    """
    # FIXME: This method should be in `ggrc_basic_permissions`, since it
    #   depends on `Role` and `UserRole` objects

    if self.email in getattr(settings, "BOOTSTRAP_ADMIN_USERS", []):
      return u"Superuser"

    role_hierarchy = {
        u'Administrator': 0,
        u'Editor': 1,
        u'Reader': 2,
        u'Creator': 3,
    }
    unique_roles = set([
        user_role.role.name
        for user_role in self.user_roles
        if user_role.role.name in role_hierarchy
    ])
    if len(unique_roles) == 0:
      return u"No Access"
    else:
      # -1 as default to make items not in this list appear on top
      # and thus shown to the user
      sorted_roles = sorted(unique_roles,
                            key=lambda x: role_hierarchy.get(x, -1))
      return sorted_roles[0]
Esempio n. 5
0
class Audit(Snapshotable, clonable.Clonable, CustomAttributable, Personable,
            HasOwnContext, Relatable, Timeboxed, Noted, Described, Hyperlinked,
            WithContact, Titled, 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)
    # TODO: this should be stateful mixin
    status = deferred(db.Column(db.Enum(*VALID_STATES), nullable=False),
                      'Audit')
    gdrive_evidence_folder = deferred(db.Column(db.String), 'Audit')
    program_id = deferred(
        db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False),
        'Audit')
    requests = db.relationship('Request',
                               backref='audit',
                               cascade='all, delete-orphan')
    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', 'requests', '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": "Status",
        "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('requests'),
            orm.subqueryload('object_people').joinedload('person'),
            orm.subqueryload('audit_objects'),
        )
Esempio n. 6
0
class TaskGroup(WithContact, Timeboxed, Described, Titled, Slugged, db.Model):
    """Workflow TaskGroup model."""

    __tablename__ = 'task_groups'
    _title_uniqueness = False

    workflow_id = db.Column(
        db.Integer,
        db.ForeignKey('workflows.id', ondelete="CASCADE"),
        nullable=False,
    )
    lock_task_order = db.Column(db.Boolean(), nullable=True)

    task_group_objects = db.relationship('TaskGroupObject',
                                         backref='task_group',
                                         cascade='all, delete-orphan')

    objects = association_proxy('task_group_objects', 'object',
                                'TaskGroupObject')

    task_group_tasks = db.relationship('TaskGroupTask',
                                       backref='task_group',
                                       cascade='all, delete-orphan')

    cycle_task_groups = db.relationship('CycleTaskGroup', backref='task_group')

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

    _publish_attrs = [
        'workflow',
        'task_group_objects',
        PublishOnly('objects'),
        'task_group_tasks',
        'lock_task_order',
        'sort_index',
        # Intentionally do not include `cycle_task_groups`
        # 'cycle_task_groups',
    ]

    _aliases = {
        "title": "Summary",
        "description": "Details",
        "contact": {
            "display_name": "Assignee",
            "mandatory": True,
            "filter_by": "_filter_by_contact",
        },
        "secondary_contact": None,
        "start_date": None,
        "end_date": None,
        "workflow": {
            "display_name": "Workflow",
            "mandatory": True,
            "filter_by": "_filter_by_workflow",
        },
        "task_group_objects": {
            "display_name": "Objects",
            "type": AttributeInfo.Type.SPECIAL_MAPPING,
            "filter_by": "_filter_by_objects",
        },
    }

    def copy(self, _other=None, **kwargs):
        columns = [
            'title', 'description', 'workflow', 'sort_index', 'modified_by',
            'context'
        ]

        if kwargs.get('clone_people', False) and getattr(self, "contact"):
            columns.append("contact")
        else:
            kwargs["contact"] = get_current_user()

        target = self.copy_into(_other, columns, **kwargs)

        if kwargs.get('clone_objects', False):
            self.copy_objects(target, **kwargs)

        if kwargs.get('clone_tasks', False):
            self.copy_tasks(target, **kwargs)

        return target

    def copy_objects(self, target, **kwargs):
        # pylint: disable=unused-argument
        for task_group_object in self.task_group_objects:
            target.task_group_objects.append(
                task_group_object.copy(
                    task_group=target,
                    context=target.context,
                ))

        return target

    def copy_tasks(self, target, **kwargs):
        for task_group_task in self.task_group_tasks:
            target.task_group_tasks.append(
                task_group_task.copy(
                    None,
                    task_group=target,
                    context=target.context,
                    clone_people=kwargs.get("clone_people", False),
                ))

        return target

    @classmethod
    def _filter_by_workflow(cls, predicate):
        from ggrc_workflows.models import Workflow
        return Workflow.query.filter((Workflow.id == cls.workflow_id)
                                     & (predicate(Workflow.slug)
                                        | predicate(Workflow.title))).exists()

    @classmethod
    def _filter_by_objects(cls, predicate):
        parts = []
        for model_name in all_models.__all__:
            model = getattr(all_models, model_name)
            query = getattr(model, "query", None)
            field = getattr(model, "slug", getattr(model, "email", None))
            if query is None or field is None or not hasattr(model, "id"):
                continue
            parts.append(
                query.filter((TaskGroupObject.object_type == model_name)
                             & (model.id == TaskGroupObject.object_id)
                             & predicate(field)).exists())
        return TaskGroupObject.query.filter(
            (TaskGroupObject.task_group_id == cls.id) & or_(*parts)).exists()
Esempio n. 7
0
class Assessment(statusable.Statusable, AuditRelationship,
                 AutoStatusChangeable, Assignable, HasObjectState, TestPlanned,
                 CustomAttributable, EvidenceURL, Commentable, Personable,
                 reminderable.Reminderable, Timeboxed, Relatable,
                 WithSimilarityScore, FinishedDate, VerifiedDate,
                 ValidateOnComplete, BusinessObject, 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")

    object = {}  # we add this for the sake of client side error checking
    audit = {}

    VALID_CONCLUSIONS = frozenset(
        ["Effective", "Ineffective", "Needs improvement", "Not Applicable"])

    # REST properties
    _publish_attrs = [
        'design', 'operationally',
        PublishOnly('audit'),
        PublishOnly('object')
    ]

    _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_object": {
            "display_name":
            "Object",
            "mandatory":
            True,
            "ignore_on_update":
            True,
            "filter_by":
            "_ignore_filter",
            "type":
            reflection.AttributeInfo.Type.MAPPING,
            "description":
            ("A single object that will be mapped to the audit.\n"
             "Example:\n\nControl: Control-slug-1\n"
             "Market : MARKET-55"),
        },
        "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": "Creator",
            "mandatory": True,
            "filter_by": "_filter_by_related_creators",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_assessors": {
            "display_name": "Assessor",
            "mandatory": True,
            "filter_by": "_filter_by_related_assessors",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_verifiers": {
            "display_name": "Verifier",
            "filter_by": "_filter_by_related_verifiers",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
    }

    similarity_options = similarity_options_module.ASSESSMENT

    def validate_conclusion(self, value):
        return value if value in self.VALID_CONCLUSIONS else ""

    @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 _filter_by_related_creators(cls, predicate):
        return cls._get_relate_filter(predicate, "Creator")

    @classmethod
    def _filter_by_related_assessors(cls, predicate):
        return cls._get_relate_filter(predicate, "Assessor")

    @classmethod
    def _filter_by_related_verifiers(cls, predicate):
        return cls._get_relate_filter(predicate, "Verifier")

    @classmethod
    def _ignore_filter(cls, predicate):
        return None
class CustomAttributeValue(Base, Indexed, db.Model):
  """Custom attribute value model"""

  __tablename__ = 'custom_attribute_values'

  _publish_attrs = [
      'custom_attribute_id',
      'attributable_id',
      'attributable_type',
      'attribute_value',
      'attribute_object',
      PublishOnly('preconditions_failed'),
  ]
  _fulltext_attrs = ["attribute_value"]

  _sanitize_html = [
      "attribute_value",
  ]

  custom_attribute_id = db.Column(
      db.Integer,
      db.ForeignKey('custom_attribute_definitions.id', ondelete="CASCADE")
  )
  attributable_id = db.Column(db.Integer)
  attributable_type = db.Column(db.String)
  attribute_value = db.Column(db.String)

  # When the attibute is of a mapping type this will hold the id of the mapped
  # object while attribute_value will hold the type name.
  # For example an instance of attribute type Map:Person will have a person id
  # in attribute_object_id and string 'Person' in attribute_value.
  attribute_object_id = db.Column(db.Integer)

  # pylint: disable=protected-access
  # This is just a mapping for accessing local functions so protected access
  # warning is a false positive
  _validator_map = {
      "Date": lambda self: self._validate_date(),
      "Dropdown": lambda self: self._validate_dropdown(),
      "Map:Person": lambda self: self._validate_map_person(),
  }

  @property
  def latest_revision(self):
    """Latest revision of CAV (used for comment precondition check)."""
    # TODO: make eager_query fetch only the first Revision
    return self._related_revisions[0]

  def delere_record(self):
    get_indexer().delete_record(self.attributable_id,
                                self.attributable_type,
                                False)

  def get_reindex_pair(self):
    return (self.attributable_type, self.attributable_id)

  @declared_attr
  def _related_revisions(self):
    def join_function():
      """Function to join CAV to its latest revision."""
      resource_id = foreign(Revision.resource_id)
      resource_type = foreign(Revision.resource_type)
      return and_(resource_id == self.id,
                  resource_type == "CustomAttributeValue")

    return db.relationship(
        Revision,
        primaryjoin=join_function,
        viewonly=True,
        order_by=Revision.created_at.desc(),
    )

  @classmethod
  def eager_query(cls):
    query = super(CustomAttributeValue, cls).eager_query()
    query = query.options(
        orm.subqueryload('_related_revisions'),
        orm.joinedload('custom_attribute'),
    )
    return query

  @property
  def attributable_attr(self):
    return '{0}_custom_attributable'.format(self.attributable_type)

  @property
  def attributable(self):
    return getattr(self, self.attributable_attr)

  @attributable.setter
  def attributable(self, value):
    self.attributable_id = value.id if value is not None else None
    self.attributable_type = value.__class__.__name__ if value is not None \
        else None
    return setattr(self, self.attributable_attr, value)

  @property
  def attribute_object(self):
    """Fetch the object referred to by attribute_object_id.

    Use backrefs defined in CustomAttributeMapable.

    Returns:
        A model instance of type specified in attribute_value
    """
    return getattr(self, self._attribute_object_attr)

  @attribute_object.setter
  def attribute_object(self, value):
    """Set attribute_object_id via whole object.

    Args:
        value: model instance
    """
    if value is None:
      # We get here if "attribute_object" does not get resolved.
      # TODO: make sure None value can be set for removing CA attribute object
      # value
      return
    self.attribute_object_id = value.id
    return setattr(self, self._attribute_object_attr, value)

  @property
  def attribute_object_type(self):
    """Fetch the mapped object pointed to by attribute_object_id.

    Returns:
       A model of type referenced in attribute_value
    """
    attr_type = self.custom_attribute.attribute_type
    if not attr_type.startswith("Map:"):
      return None
    return self.attribute_object.__class__.__name__

  @property
  def _attribute_object_attr(self):
    """Compute the relationship property based on object type.

    Returns:
        Property name
    """
    attr_type = self.custom_attribute.attribute_type
    if not attr_type.startswith("Map:"):
      return None
    return 'attribute_{0}'.format(self.attribute_value)

  @classmethod
  def mk_filter_by_custom(cls, obj_class, custom_attribute_id):
    """Get filter for custom attributable object.

    This returns an exists filter for the given predicate, matching it to
    either a custom attribute value, or a value of the matched object.

    Args:
      obj_class: Class of the attributable object.
      custom_attribute_id: Id of the attribute definition.
    Returns:
      A function that will generate a filter for a given predicate.
    """
    from ggrc.models import all_models
    attr_def = all_models.CustomAttributeDefinition.query.filter_by(
        id=custom_attribute_id
    ).first()
    if attr_def and attr_def.attribute_type.startswith("Map:"):
      map_type = attr_def.attribute_type[4:]
      map_class = getattr(all_models, map_type, None)
      if map_class:
        fields = [getattr(map_class, name, None)
                  for name in ["email", "title", "slug"]]
        fields = [field for field in fields if field is not None]

        def filter_by_mapping(predicate):
          return cls.query.filter(
              (cls.custom_attribute_id == custom_attribute_id) &
              (cls.attributable_type == obj_class.__name__) &
              (cls.attributable_id == obj_class.id) &
              (map_class.query.filter(
                  (map_class.id == cls.attribute_object_id) &
                  or_(*[predicate(f) for f in fields])).exists())
          ).exists()
        return filter_by_mapping

    def filter_by_custom(predicate):
      return cls.query.filter(
          (cls.custom_attribute_id == custom_attribute_id) &
          (cls.attributable_type == obj_class.__name__) &
          (cls.attributable_id == obj_class.id) &
          predicate(cls.attribute_value)
      ).exists()
    return filter_by_custom

  def _clone(self, obj):
    """Clone a custom value to a new object."""
    data = {
        "custom_attribute_id": self.custom_attribute_id,
        "attributable_id": obj.id,
        "attributable_type": self.attributable_type,
        "attribute_value": self.attribute_value,
        "attribute_object_id": self.attribute_object_id
    }
    ca_value = CustomAttributeValue(**data)
    db.session.add(ca_value)
    db.session.flush()
    return ca_value

  @staticmethod
  def _extra_table_args(_):
    return (
        db.UniqueConstraint('attributable_id', 'custom_attribute_id'),
    )

  def _validate_map_person(self):
    """Validate and correct mapped person values

    Mapped person custom attribute is only valid if both attribute_value and
    attribute_object_id are set. To keep the custom attribute api consistent
    with other types, we allow setting the value to a string containing both
    in this way "attribute_value:attribute_object_id". This validator checks
    Both scenarios and changes the string value to proper values needed by
    this custom attribute.

    Note: this validator does not check if id is a proper person id.
    """
    if self.attribute_value and ":" in self.attribute_value:
      value, id_ = self.attribute_value.split(":")
      self.attribute_value = value
      self.attribute_object_id = id_

  def _validate_dropdown(self):
    """Validate dropdown opiton."""
    valid_options = set(self.custom_attribute.multi_choice_options.split(","))
    if self.attribute_value:
      self.attribute_value = self.attribute_value.strip()
      if self.attribute_value not in valid_options:
        raise ValueError("Invalid custom attribute dropdown option: {v}, "
                         "expected one of {l}"
                         .format(v=self.attribute_value, l=valid_options))

  def _validate_date(self):
    """Convert date format."""
    if self.attribute_value:
      # Validate the date format by trying to parse it
      self.attribute_value = utils.convert_date_format(
          self.attribute_value,
          utils.DATE_FORMAT_ISO,
          utils.DATE_FORMAT_ISO,
      )

  def validate(self):
    """Validate custom attribute value."""
    # pylint: disable=protected-access
    attributable_type = self.attributable._inflector.table_singular
    if not self.custom_attribute:
      raise ValueError("Custom attribute definition not found: Can not "
                       "validate custom attribute value")
    if self.custom_attribute.definition_type != attributable_type:
      raise ValueError("Invalid custom attribute definition used.")
    validator = self._validator_map.get(self.custom_attribute.attribute_type)
    if validator:
      validator(self)

  @computed_property
  def is_empty(self):
    """Return True if the CAV is empty or holds a logically empty value."""
    # The CAV is considered empty when:
    # - the value is empty
    if not self.attribute_value:
      return True
    # - the type is Checkbox and the value is 0
    if (self.custom_attribute.attribute_type ==
            self.custom_attribute.ValidTypes.CHECKBOX and
            str(self.attribute_value) == "0"):
      return True
    # - the type is a mapping and the object value id is empty
    if (self.attribute_object_type is not None and
            not self.attribute_object_id):
      return True
    # Otherwise it the CAV is not empty
    return False

  @computed_property
  def preconditions_failed(self):
    """A list of requirements self introduces that are unsatisfied.

    Returns:
      [str] - a list of unsatisfied requirements; possible items are: "value" -
              missing mandatory value, "comment" - missing mandatory comment,
              "evidence" - missing mandatory evidence.

    """
    failed_preconditions = []
    if self.custom_attribute.mandatory and self.is_empty:
      failed_preconditions += ["value"]
    if (self.custom_attribute.attribute_type ==
            self.custom_attribute.ValidTypes.DROPDOWN):
      failed_preconditions += self._check_dropdown_requirements()
    return failed_preconditions or None

  def _check_dropdown_requirements(self):
    """Check mandatory comment and mandatory evidence for dropdown CAV."""
    failed_preconditions = []
    options_to_flags = self._multi_choice_options_to_flags(
        self.custom_attribute,
    )
    flags = options_to_flags.get(self.attribute_value)
    if flags:
      if flags.comment_required:
        failed_preconditions += self._check_mandatory_comment()
      if flags.evidence_required:
        failed_preconditions += self._check_mandatory_evidence()
    return failed_preconditions

  def _check_mandatory_comment(self):
    """Check presence of mandatory comment."""
    if hasattr(self.attributable, "comments"):
      comment_found = any(
          self.custom_attribute_id == (comment
                                       .custom_attribute_definition_id) and
          self.latest_revision.id == comment.revision_id
          for comment in self.attributable.comments
      )
    else:
      comment_found = False
    if not comment_found:
      return ["comment"]
    else:
      return []

  def _check_mandatory_evidence(self):
    """Check presence of mandatory evidence."""
    if hasattr(self.attributable, "object_documents"):
      # Note: this is a suboptimal implementation of mandatory evidence check;
      # it should be refactored once Evicence-CA mapping is introduced
      def evidence_required(cav):
        """Return True if an evidence is required for this `cav`."""
        flags = (self._multi_choice_options_to_flags(cav.custom_attribute)
                 .get(cav.attribute_value))
        return flags and flags.evidence_required
      evidence_found = (len(self.attributable.object_documents) >=
                        len([cav
                             for cav in self.attributable
                                            .custom_attribute_values
                             if evidence_required(cav)]))
    else:
      evidence_found = False
    if not evidence_found:
      return ["evidence"]
    else:
      return []

  @staticmethod
  def _multi_choice_options_to_flags(cad):
    """Parse mandatory comment and evidence flags from dropdown CA definition.

    Args:
      cad - a CA definition object

    Returns:
      {option_value: Flags} - a dict from dropdown options values to Flags
                              objects where Flags.comment_required and
                              Flags.evidence_required correspond to the values
                              from multi_choice_mandatory bitmasks
    """
    flags = namedtuple("Flags", ["comment_required", "evidence_required"])

    def make_flags(multi_choice_mandatory):
      flags_mask = int(multi_choice_mandatory)
      return flags(comment_required=flags_mask & (cad
                                                  .MultiChoiceMandatoryFlags
                                                  .COMMENT_REQUIRED),
                   evidence_required=flags_mask & (cad
                                                   .MultiChoiceMandatoryFlags
                                                   .EVIDENCE_REQUIRED))

    if not cad.multi_choice_options or not cad.multi_choice_mandatory:
      return {}
    else:
      return dict(zip(
          cad.multi_choice_options.split(","),
          (make_flags(mask)
           for mask in cad.multi_choice_mandatory.split(",")),
      ))