Esempio n. 1
0
 def comments(cls):  # pylint: disable=no-self-argument
   """Comments related to self via Relationship table."""
   return db.relationship(
       Comment,
       primaryjoin=lambda: sa.or_(
           sa.and_(
               cls.id == Relationship.source_id,
               Relationship.source_type == cls.__name__,
               Relationship.destination_type == "Comment",
           ),
           sa.and_(
               cls.id == Relationship.destination_id,
               Relationship.destination_type == cls.__name__,
               Relationship.source_type == "Comment",
           )
       ),
       secondary=Relationship.__table__,
       secondaryjoin=lambda: sa.or_(
           sa.and_(
               Comment.id == Relationship.source_id,
               Relationship.source_type == "Comment",
           ),
           sa.and_(
               Comment.id == Relationship.destination_id,
               Relationship.destination_type == "Comment",
           )
       ),
       viewonly=True,
   )
Esempio n. 2
0
 def modified_by(cls):
   return db.relationship(
       'Person',
       primaryjoin='{0}.modified_by_id == Person.id'.format(cls.__name__),
       foreign_keys='{0}.modified_by_id'.format(cls.__name__),
       uselist=False,
       )
Esempio n. 3
0
  def _access_control_list(cls):  # pylint: disable=no-self-argument
    """access_control_list"""
    current_type = cls.__name__

    joinstr = (
        'and_('
        'foreign(remote(AccessControlList.object_id)) == {type}.id,'
        'AccessControlList.object_type == "{type}",'
        'AccessControlList.parent_id_nn == 0'
        ')'
        .format(type=current_type)
    )

    # Since we have some kind of generic relationship here, it is needed
    # to provide custom joinstr for backref. If default, all models having
    # this mixin will be queried, which in turn produce large number of
    # queries returning nothing and one query returning object.
    backref_joinstr = (
        'remote({type}.id) == foreign(AccessControlList.object_id)'
        .format(type=current_type)
    )

    return db.relationship(
        'AccessControlList',
        primaryjoin=joinstr,
        backref=orm.backref(
            '{}_object'.format(current_type),
            primaryjoin=backref_joinstr,
        ),
        cascade='all, delete-orphan'
    )
Esempio n. 4
0
 def evidences(cls):  # pylint: disable=no-self-argument
   """Return evidences related for that instance."""
   return db.relationship(
       Evidence,
       primaryjoin=lambda: sa.or_(
           sa.and_(
               cls.id == Relationship.source_id,
               Relationship.source_type == cls.__name__,
               Relationship.destination_type == "Evidence",
           ),
           sa.and_(
               cls.id == Relationship.destination_id,
               Relationship.destination_type == cls.__name__,
               Relationship.source_type == "Evidence",
           )
       ),
       secondary=Relationship.__table__,
       secondaryjoin=lambda: sa.or_(
           sa.and_(
               Evidence.id == Relationship.source_id,
               Relationship.source_type == "Evidence",
           ),
           sa.and_(
               Evidence.id == Relationship.destination_id,
               Relationship.destination_type == "Evidence",
           )
       ),
       viewonly=True,
   )
Esempio n. 5
0
  def _categorizations(cls, rel_name, proxy_name, scope):
    setattr(cls, proxy_name, association_proxy(
        rel_name, 'category',
        creator=lambda category: Categorization(
            category=category,
            #FIXME add from http session!
            modified_by_id=1,
            categorizable_type=cls.__name__,
            ),
        ))
    joinstr = 'and_(foreign(Categorization.categorizable_id) == {type}.id, '\
                   'foreign(Categorization.categorizable_type) == "{type}", '\
                   'Categorization.category_id == Category.id, '\
                   'Category.scope_id == {scope})'
    joinstr = joinstr.format(type=cls.__name__, scope=scope)
    return db.relationship(
        'Categorization',
        primaryjoin=joinstr,
        backref=BACKREF_NAME_FORMAT.format(type=cls.__name__, scope=scope),
        )

  # FIXME: make eager-loading work for categorizations/assertations
  #@classmethod
  #def eager_query(cls):
  #  from sqlalchemy import orm

  #  query = super(Categorizable, cls).eager_query()
  #  return query.options(
  #      orm.subqueryload_all('categorizations.category'),
  #      orm.subqueryload_all('assertations.category'))
Esempio n. 6
0
 def network_zone(cls):  # pylint: disable=no-self-argument
   return db.relationship(
       "Option",
       primaryjoin="and_(foreign({}.network_zone_id) == Option.id, "
                   "Option.role == 'network_zone')".format(cls.__name__),
       uselist=False,
   )
Esempio n. 7
0
  def related_destinations(cls):  # pylint: disable=no-self-argument
    """List of Relationship where 'destination' points to related object"""
    current_type = cls.__name__

    joinstr = (
        "and_("
        "foreign(remote(Relationship.source_id)) == {type}.id,"
        "Relationship.source_type == '{type}'"
        ")"
        .format(type=current_type)
    )

    # Since we have some kind of generic relationship here, it is needed
    # to provide custom joinstr for backref. If default, all models having
    # this mixin will be queried, which in turn produce large number of
    # queries returning nothing and one query returning object.
    backref_joinstr = (
        "remote({type}.id) == foreign(Relationship.source_id)"
        .format(type=current_type)
    )

    return db.relationship(
        "Relationship",
        primaryjoin=joinstr,
        backref=sa.orm.backref(
            "{}_source".format(current_type),
            primaryjoin=backref_joinstr,
        ),
        cascade="all, delete-orphan"
    )
Esempio n. 8
0
  def _custom_attribute_values(cls):  # pylint: disable=no-self-argument
    """Load custom attribute values"""
    current_type = cls.__name__

    joinstr = (
        "and_("
        "foreign(remote(CustomAttributeValue.attributable_id)) == {type}.id,"
        "CustomAttributeValue.attributable_type == '{type}'"
        ")"
        .format(type=current_type)
    )

    # Since we have some kind of generic relationship here, it is needed
    # to provide custom joinstr for backref. If default, all models having
    # this mixin will be queried, which in turn produce large number of
    # queries returning nothing and one query returning object.
    backref_joinstr = (
        "remote({type}.id) == foreign(CustomAttributeValue.attributable_id)"
        .format(type=current_type)
    )

    return db.relationship(
        "CustomAttributeValue",
        primaryjoin=joinstr,
        backref=orm.backref(
            "{}_custom_attributable".format(current_type),
            primaryjoin=backref_joinstr,
        ),
        cascade="all, delete-orphan"
    )
Esempio n. 9
0
  def declare_categorizable(cls, category_type, single, plural, ation):
    setattr(
        cls, plural,
        association_proxy(
            ation, 'category',
            creator=lambda category: Categorization(
                category_id=category.id,
                category_type=category.__class__.__name__,
                categorizable_type=cls.__name__
            )
        )
    )

    joinstr = (
        'and_('
        'foreign(Categorization.categorizable_id) == {type}.id, '
        'foreign(Categorization.categorizable_type) == "{type}", '
        'foreign(Categorization.category_type) == "{category_type}"'
        ')'
    )
    joinstr = joinstr.format(type=cls.__name__, category_type=category_type)
    backref = '{type}_categorizable_{category_type}'.format(
        type=cls.__name__,
        category_type=category_type,
    )
    return db.relationship(
        'Categorization',
        primaryjoin=joinstr,
        backref=backref,
        cascade='all, delete-orphan',
    )
 def cycle_task_group_object_tasks(cls):  # pylint: disable=no-self-argument
   return db.relationship(
       CycleTaskGroupObjectTask,
       primaryjoin=lambda: sa.or_(
           sa.and_(
               cls.id == relationship.Relationship.source_id,
               relationship.Relationship.source_type == cls.__name__,
               relationship.Relationship.destination_type ==
               "CycleTaskGroupObjectTask",
           ),
           sa.and_(
               cls.id == relationship.Relationship.destination_id,
               relationship.Relationship.destination_type == cls.__name__,
               relationship.Relationship.source_type ==
               "CycleTaskGroupObjectTask",
           )
       ),
       secondary=relationship.Relationship.__table__,
       secondaryjoin=lambda: CycleTaskGroupObjectTask.id == sa.case(
           [(relationship.Relationship.source_type == cls.__name__,
             relationship.Relationship.destination_id)],
           else_=relationship.Relationship.source_id
       ),
       viewonly=True
   )
Esempio n. 11
0
 def created_by(cls):  # pylint: disable=no-self-argument
   """Relationship to user referenced by updated_by_id."""
   return db.relationship(
       'Person',
       primaryjoin='{}.created_by_id == Person.id'.format(cls.__name__),
       foreign_keys='{}.created_by_id'.format(cls.__name__),
       uselist=False,
   )
Esempio n. 12
0
 def related_sources(cls):
   joinstr = 'and_(remote(Relationship.destination_id) == {type}.id, '\
                   'remote(Relationship.destination_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'Relationship',
       primaryjoin=joinstr,
       foreign_keys = 'Relationship.destination_id',
       cascade = 'all, delete-orphan')
Esempio n. 13
0
 def attendee(cls):  # pylint: disable=no-self-argument
   """Relationship to user referenced by attendee_id."""
   return db.relationship(
       'Person',
       primaryjoin='{0}.attendee_id == Person.id'.format(cls.__name__),
       foreign_keys='{0}.attendee_id'.format(cls.__name__),
       remote_side='Person.id',
       uselist=False,
   )
Esempio n. 14
0
def person_relationship(model_name, field_name):
  """Return relationship attribute between Person model and provided model."""
  return db.relationship(
      "Person",
      primaryjoin="{0}.{1} == Person.id".format(model_name, field_name),
      foreign_keys="{0}.{1}".format(model_name, field_name),
      remote_side="Person.id",
      uselist=False,
  )
Esempio n. 15
0
 def related_destinations(cls):  # pylint: disable=no-self-argument
   joinstr = 'and_(remote(Relationship.source_id) == {type}.id, '\
       'remote(Relationship.source_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'Relationship',
       primaryjoin=joinstr,
       foreign_keys='Relationship.source_id',
       backref='{0}_source'.format(cls.__name__),
       cascade='all, delete-orphan')
Esempio n. 16
0
 def attr_declaration(cls):
   return db.relationship(
       'Person',
       primaryjoin='{0}.{1}_id == Person.id'.format(cls.__name__,
                                                    relation_name),
       foreign_keys='{0}.{1}_id'.format(cls.__name__,
                                        relation_name),
       remote_side='Person.id',
       uselist=False,
   )
Esempio n. 17
0
 def _access_control_list(cls):  # pylint: disable=no-self-argument
   """access_control_list"""
   return db.relationship(
       'AccessControlList',
       primaryjoin=lambda: and_(
           remote(AccessControlList.object_id) == cls.id,
           remote(AccessControlList.object_type) == cls.__name__),
       foreign_keys='AccessControlList.object_id',
       backref='{0}_object'.format(cls.__name__),
       cascade='all, delete-orphan')
Esempio n. 18
0
 def make_object_files(cls):
   joinstr = 'and_(foreign(ObjectFile.fileable_id) == {type}.id, '\
       'foreign(ObjectFile.fileable_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'ObjectFile',
       primaryjoin=joinstr,
       backref='{0}_fileable'.format(cls.__name__),
       cascade='all, delete-orphan',
   )
Esempio n. 19
0
 def snapshotted_objects(cls):  # pylint: disable=no-self-argument
   """Return all snapshotted objects"""
   joinstr = "and_(remote(Snapshot.parent_id) == {type}.id, " \
             "remote(Snapshot.parent_type) == '{type}')"
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       lambda: Snapshot,
       primaryjoin=joinstr,
       foreign_keys='Snapshot.parent_id,Snapshot.parent_type,',
       backref='{0}_parent'.format(cls.__name__),
       cascade='all, delete-orphan')
Esempio n. 20
0
 def contexts(cls):  # pylint: disable=no-self-argument
   joinstr = 'and_(foreign(Context.related_object_id) == {type}.id, '\
             'foreign(Context.related_object_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'Context',
       primaryjoin=joinstr,
       # foreign_keys='Context.related_object_id',
       # cascade='all, delete-orphan',
       backref='{0}_related_object'.format(cls.__name__),
       order_by='Context.id',
       post_update=True)
Esempio n. 21
0
 def _child_relationships(cls):  # pylint: disable=no-self-argument
   """Return relationships to children
   used to eagerly query is_mega property"""
   joinstr = (
       "and_(foreign(Relationship.source_id) == {cls_name}.id,"
       "foreign(Relationship.source_type) == '{cls_name}',"
       "foreign(Relationship.destination_type) == '{cls_name}')"
   )
   return db.relationship(
       "Relationship",
       primaryjoin=joinstr.format(cls_name=cls.__name__),
   )
Esempio n. 22
0
  def _related_revisions(cls):  # pylint: disable=no-self-argument
    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 == cls.id,
                  resource_type == "CustomAttributeValue")

    return db.relationship(
        Revision,
        primaryjoin=join_function,
        viewonly=True,
        order_by=Revision.created_at.desc(),
    )
  def related_custom_attributes(cls):  # pylint: disable=no-self-argument
    """CustomAttributeValues that directly map to this object.

    Used just to get the backrefs on the CustomAttributeValue object.

    Returns:
       a sqlalchemy relationship
    """
    return db.relationship(
        'CustomAttributeValue',
        primaryjoin=lambda: (
            (CustomAttributeValue.attribute_value == cls.__name__) &
            (CustomAttributeValue.attribute_object_id == cls.id)),
        foreign_keys="CustomAttributeValue.attribute_object_id",
        backref='attribute_{0}'.format(cls.__name__),
        viewonly=True)
Esempio n. 24
0
 def make_risk_objects(cls):
   cls.risks = association_proxy(
       'risk_objects', 'risk',
       creator=lambda risk: RiskObject(
           risk=risk,
           object_type=cls.__name__,
       )
   )
   joinstr = 'and_(foreign(RiskObject.object_id) == {type}.id, '\
       'foreign(RiskObject.object_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'RiskObject',
       primaryjoin=joinstr,
       backref='{0}_object'.format(cls.__name__),
       cascade='all, delete-orphan',
   )
Esempio n. 25
0
 def object_people(cls):
   cls.people = association_proxy(
       'object_people', 'person',
       creator=lambda person: ObjectPerson(
           person=person,
           modified_by_id=1,
           personable_type=cls.__name__,
           )
       )
   joinstr = 'and_(foreign(ObjectPerson.personable_id) == {type}.id, '\
                  'foreign(ObjectPerson.personable_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'ObjectPerson',
       primaryjoin=joinstr,
       backref='{0}_personable'.format(cls.__name__),
       )
Esempio n. 26
0
 def make_task_group_objects(cls):
   cls.task_groups = association_proxy(
       'task_group_objects', 'task_group',
       creator=lambda task_group: TaskGroupObject(
           task_group=task_group,
           object_type=cls.__name__,
       )
   )
   joinstr = 'and_(foreign(TaskGroupObject.object_id) == {type}.id, '\
             'foreign(TaskGroupObject.object_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'TaskGroupObject',
       primaryjoin=joinstr,
       backref='{0}_object'.format(cls.__name__),
       cascade='all, delete-orphan',
   )
Esempio n. 27
0
 def audit_objects(cls):
   cls.audits = association_proxy(
       'audit_objects', 'audit',
       creator=lambda control: AuditObject(
           audit=audit,  # noqa
           auditable_type=cls.__name__,
       )
   )
   joinstr = 'and_(foreign(AuditObject.auditable_id) == {type}.id, '\
       'foreign(AuditObject.auditable_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'AuditObject',
       primaryjoin=joinstr,
       backref='{0}_auditable'.format(cls.__name__),
       cascade='all, delete-orphan',
   )
Esempio n. 28
0
 def _object_labels(cls):  # pylint: disable=no-self-argument
   """Object labels property"""
   # pylint: disable=attribute-defined-outside-init
   cls._labels = association_proxy(
     '_object_labels', 'label',
     creator=lambda label: ObjectLabel(
           label=label,  # noqa
           object_type=cls.__name__
       )
   )
   return db.relationship(
       ObjectLabel,
       primaryjoin=lambda: and_(cls.id == ObjectLabel.object_id,
                                cls.__name__ == ObjectLabel.object_type),
       foreign_keys=ObjectLabel.object_id,
       backref='{}_labeled'.format(cls.__name__),
       cascade='all, delete-orphan')
Esempio n. 29
0
 def object_sections(cls):
   cls.sections = association_proxy(
       'object_sections', 'section',
       creator=lambda section: ObjectSection(
           section=section,
           sectionable_type=cls.__name__,
           )
       )
   joinstr = 'and_(foreign(ObjectSection.sectionable_id) == {type}.id, '\
                  'foreign(ObjectSection.sectionable_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'ObjectSection',
       primaryjoin=joinstr,
       backref='{0}_sectionable'.format(cls.__name__),
       cascade='all, delete-orphan',
       )
 def object_documents(cls):
   cls.documents = association_proxy(
       'object_documents', 'document',
       creator=lambda document: ObjectDocument(
           document=document,
           documentable_type=cls.__name__,
           )
       )
   joinstr = 'and_(foreign(ObjectDocument.documentable_id) == {type}.id, '\
                  'foreign(ObjectDocument.documentable_type) == "{type}")'
   joinstr = joinstr.format(type=cls.__name__)
   return db.relationship(
       'ObjectDocument',
       primaryjoin=joinstr,
       backref='{0}_documentable'.format(cls.__name__),
       cascade='all, delete-orphan',
       )
Esempio n. 31
0
class Control(WithLastAssessmentDate,
              review.Reviewable,
              Roleable,
              Relatable,
              mixins.CustomAttributable,
              Personable,
              ControlCategorized,
              PublicDocumentable,
              AssertionCategorized,
              mixins.LastDeprecatedTimeboxed,
              mixins.TestPlanned,
              Commentable,
              WithSimilarityScore,
              base.ContextRBAC,
              mixins.BusinessObject,
              Indexed,
              mixins.Folderable,
              proposal.Proposalable,
              db.Model):
  """Control model definition."""
  __tablename__ = 'controls'

  company_control = deferred(db.Column(db.Boolean), 'Control')
  directive_id = deferred(
      db.Column(db.Integer, db.ForeignKey('directives.id')), 'Control')
  kind_id = deferred(db.Column(db.Integer), 'Control')
  means_id = deferred(db.Column(db.Integer), 'Control')
  version = deferred(db.Column(db.String), 'Control')
  verify_frequency_id = deferred(db.Column(db.Integer), 'Control')
  fraud_related = deferred(db.Column(db.Boolean), 'Control')
  key_control = deferred(db.Column(db.Boolean), 'Control')
  active = deferred(db.Column(db.Boolean), 'Control')

  kind = db.relationship(
      'Option',
      primaryjoin='and_(foreign(Control.kind_id) == Option.id, '
                  'Option.role == "control_kind")',
      uselist=False)
  means = db.relationship(
      'Option',
      primaryjoin='and_(foreign(Control.means_id) == Option.id, '
                  'Option.role == "control_means")',
      uselist=False)
  verify_frequency = db.relationship(
      'Option',
      primaryjoin='and_(foreign(Control.verify_frequency_id) == Option.id, '
                  'Option.role == "verify_frequency")',
      uselist=False)

  # REST properties
  _api_attrs = reflection.ApiAttributes(
      'active',
      'company_control',
      'directive',
      'fraud_related',
      'key_control',
      'kind',
      'means',
      'verify_frequency',
      'version',
  )

  _fulltext_attrs = [
      'active',
      'company_control',
      'directive',
      attributes.BooleanFullTextAttr(
          'fraud_related',
          'fraud_related',
          true_value="yes", false_value="no"),
      attributes.BooleanFullTextAttr(
          'key_control',
          'key_control',
          true_value="key", false_value="non-key"),
      'kind',
      'means',
      'verify_frequency',
      'version',
  ]

  _sanitize_html = [
      'version',
  ]

  VALID_RECIPIENTS = frozenset([
      "Assignees",
      "Creators",
      "Verifiers",
      "Admin",
      "Control Operators",
      "Control Owners",
      "Other Contacts",
  ])

  @classmethod
  def indexed_query(cls):
    return super(Control, cls).indexed_query().options(
        orm.Load(cls).undefer_group(
            "Control_complete"
        ),
        orm.Load(cls).joinedload(
            "directive"
        ).undefer_group(
            "Directive_complete"
        ),
        orm.Load(cls).joinedload(
            'kind',
        ).load_only(
            "title"
        ),
        orm.Load(cls).joinedload(
            'means',
        ).load_only(
            "title"
        ),
        orm.Load(cls).joinedload(
            'verify_frequency',
        ).load_only(
            "title"
        ),
    )

  _include_links = []

  _aliases = {
      "kind": "Kind/Nature",
      "means": "Type/Means",
      "verify_frequency": "Frequency",
      "fraud_related": "Fraud Related",
      "key_control": {
          "display_name": "Significance",
          "description": "Allowed values are:\nkey\nnon-key\n---",
      },
      "test_plan": "Assessment Procedure",
  }

  @validates('kind', 'means', 'verify_frequency')
  def validate_control_options(self, key, option):
    """Validate control 'kind', 'means', 'verify_frequency'"""
    desired_role = key if key == 'verify_frequency' else 'control_' + key
    return validate_option(self.__class__.__name__, key, option, desired_role)

  @classmethod
  def eager_query(cls):
    query = super(Control, cls).eager_query()
    return cls.eager_inclusions(query, Control._include_links).options(
        orm.joinedload('directive'),
        orm.joinedload('kind'),
        orm.joinedload('means'),
        orm.joinedload('verify_frequency'),
    )

  def log_json(self):
    out_json = super(Control, self).log_json()
    # so that event log can refer to deleted directive
    if self.directive:
      out_json["mapped_directive"] = self.directive.display_name
    return out_json
Esempio n. 32
0
class Cycle(mixins.WithContact,
            wf_mixins.CycleStatusValidatedMixin,
            mixins.Timeboxed,
            mixins.Described,
            mixins.Titled,
            mixins.Slugged,
            mixins.Notifiable,
            ft_mixin.Indexed,
            db.Model):
  """Workflow Cycle model
  """

  __tablename__ = 'cycles'
  _title_uniqueness = False

  workflow_id = db.Column(
      db.Integer,
      db.ForeignKey('workflows.id', ondelete="CASCADE"),
      nullable=False,
  )
  cycle_task_groups = db.relationship(
      'CycleTaskGroup', backref='cycle', cascade='all, delete-orphan')
  cycle_task_group_object_tasks = db.relationship(
      'CycleTaskGroupObjectTask', backref='cycle',
      cascade='all, delete-orphan')
  cycle_task_entries = db.relationship(
      'CycleTaskEntry', backref='cycle', cascade='all, delete-orphan')
  is_current = db.Column(db.Boolean,
                         default=True,
                         nullable=False)
  next_due_date = db.Column(db.Date)

  @property
  def is_done(self):
    """Check if cycle's done

    Overrides StatusValidatedMixin method because cycle's is_done state
    depends on is_verification_needed flag
    """
    if super(Cycle, self).is_done:
      return True
    if self.cycle_task_group_object_tasks:
      return False
    return True

  _api_attrs = reflection.ApiAttributes(
      'workflow',
      'cycle_task_groups',
      'is_current',
      'next_due_date',
  )

  _aliases = {
      "cycle_workflow": {
          "display_name": "Workflow",
          "filter_by": "_filter_by_cycle_workflow",
      },
      "contact": "Assignee",
      "secondary_contact": None,
  }

  PROPERTY_TEMPLATE = u"cycle {}"

  _fulltext_attrs = [
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "group title", "cycle_task_groups", ["title"], False,
      ),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "group assignee",
          lambda instance: [g.contact for g in instance.cycle_task_groups],
          ["email", "name"],
          False,
      ),
      ft_attributes.DateMultipleSubpropertyFullTextAttr(
          "group due date",
          'cycle_task_groups',
          ["next_due_date"],
          False,
      ),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "task title",
          'cycle_task_group_object_tasks',
          ["title"],
          False,
      ),
      ft_attributes.DateMultipleSubpropertyFullTextAttr(
          "task due date",
          "cycle_task_group_object_tasks",
          ["end_date"],
          False
      ),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "task assignees",
          "_task_assignees",
          ["name", "email"],
          False,
      ),
      ft_attributes.DateFullTextAttr("due date", "next_due_date"),
      ft_attributes.MultipleSubpropertyFullTextAttr(
          "task comments",
          lambda instance: list(itertools.chain(*[
              t.cycle_task_entries
              for t in instance.cycle_task_group_object_tasks
          ])),
          ["description"],
          False
      ),
  ]

  @property
  def _task_assignees(self):
    """Property. Return the list of persons as assignee of related tasks."""
    persons = {}
    for task in self.cycle_task_group_object_tasks:
      for person in task.get_persons_for_rolename("Task Assignees"):
        persons[person.id] = person
    return persons.values()

  AUTO_REINDEX_RULES = [
      ft_mixin.ReindexRule("CycleTaskGroup", lambda x: x.cycle),
      ft_mixin.ReindexRule("CycleTaskGroupObjectTask",
                           lambda x: x.cycle_task_group.cycle),
      ft_mixin.ReindexRule("Person", _query_filtered_by_contact)
  ]

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

  @classmethod
  def eager_query(cls):
    """Add cycle task groups to cycle eager query

    This function adds cycle_task_groups as a join option when fetching cycles,
    and makes sure we fetch all cycle related data needed for generating cycle
    json, in one query.

    Returns:
      a query object with cycle_task_groups added to joined load options.
    """
    query = super(Cycle, cls).eager_query()
    return query.options(
        orm.joinedload('cycle_task_groups'),
    )

  @classmethod
  def indexed_query(cls):
    return super(Cycle, cls).indexed_query().options(
        orm.Load(cls).load_only("next_due_date"),
        orm.Load(cls).subqueryload("cycle_task_group_object_tasks").load_only(
            "id",
            "title",
            "end_date"
        ),
        orm.Load(cls).subqueryload("cycle_task_groups").load_only(
            "id",
            "title",
            "end_date",
            "next_due_date",
        ),
        orm.Load(cls).subqueryload("cycle_task_group_object_tasks").joinedload(
            "cycle_task_entries"
        ).load_only(
            "description",
            "id"
        ),
        orm.Load(cls).subqueryload("cycle_task_groups").joinedload(
            "contact"
        ).load_only(
            "email",
            "name",
            "id"
        ),
        orm.Load(cls).joinedload("contact").load_only(
            "email",
            "name",
            "id"
        ),
    )

  def _get_cycle_url(self, widget_name):
    return urljoin(
        get_url_root(),
        "workflows/{workflow_id}#{widget_name}/cycle/{cycle_id}".format(
            workflow_id=self.workflow.id,
            cycle_id=self.id,
            widget_name=widget_name
        )
    )

  @property
  def cycle_url(self):
    return self._get_cycle_url("current_widget")

  @property
  def cycle_inactive_url(self):
    return self._get_cycle_url("history_widget")
Esempio n. 33
0
class CycleTaskGroup(mixins.WithContact,
                     wf_mixins.CycleTaskGroupRelatedStatusValidatedMixin,
                     mixins.Slugged, mixins.Timeboxed, mixins.Described,
                     mixins.Titled, mixins.Base, index_mixin.Indexed,
                     db.Model):
    """Cycle Task Group model.
  """
    __tablename__ = 'cycle_task_groups'
    _title_uniqueness = False

    @classmethod
    def generate_slug_prefix_for(cls, obj):  # pylint: disable=unused-argument
        return "CYCLEGROUP"

    cycle_id = db.Column(
        db.Integer,
        db.ForeignKey('cycles.id', ondelete="CASCADE"),
        nullable=False,
    )
    task_group_id = db.Column(db.Integer,
                              db.ForeignKey('task_groups.id'),
                              nullable=True)
    cycle_task_group_tasks = db.relationship('CycleTaskGroupObjectTask',
                                             backref='cycle_task_group',
                                             cascade='all, delete-orphan')
    sort_index = db.Column(db.String(length=250), default="", nullable=False)
    next_due_date = db.Column(db.Date)

    _api_attrs = reflection.ApiAttributes('cycle', 'task_group',
                                          'cycle_task_group_tasks',
                                          'sort_index', 'next_due_date')

    _aliases = {
        "cycle": {
            "display_name": "Cycle",
            "filter_by": "_filter_by_cycle",
        },
    }

    PROPERTY_TEMPLATE = u"group {}"

    _fulltext_attrs = [
        attributes.MultipleSubpropertyFullTextAttr("task title",
                                                   'cycle_task_group_tasks',
                                                   ["title"], False),
        attributes.MultipleSubpropertyFullTextAttr(
            "task assignee", lambda instance:
            [t.contact for t in instance.cycle_task_group_tasks],
            ["name", "email"], False),
        attributes.DateMultipleSubpropertyFullTextAttr(
            "task due date", "cycle_task_group_tasks", ["end_date"], False),
        attributes.DateFullTextAttr(
            "due date",
            'next_due_date',
        ),
        attributes.FullTextAttr("assignee", "contact", ['name', 'email']),
        attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
        attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                                ['email', 'name'], False),
        attributes.DateFullTextAttr("cycle due date",
                                    lambda x: x.cycle.next_due_date,
                                    with_template=False),
        attributes.MultipleSubpropertyFullTextAttr(
            "task comments", lambda instance: itertools.chain(*[
                t.cycle_task_entries for t in instance.cycle_task_group_tasks
            ]), ["description"], False),
    ]

    AUTO_REINDEX_RULES = [
        index_mixin.ReindexRule("CycleTaskGroupObjectTask",
                                lambda x: x.cycle_task_group),
        index_mixin.ReindexRule("Person", _query_filtered_by_contact),
        index_mixin.ReindexRule(
            "Person",
            lambda x: [i.cycle for i in _query_filtered_by_contact(x)]),
    ]

    @classmethod
    def _filter_by_cycle(cls, predicate):
        """Get query that filters cycle task groups.

    Args:
      predicate: lambda function that excepts a single parameter and returns
      true of false.

    Returns:
      An sqlalchemy query that evaluates to true or false and can be used in
      filtering cycle task groups by related cycle.
    """
        return Cycle.query.filter((Cycle.id == cls.cycle_id)
                                  & (predicate(Cycle.slug)
                                     | predicate(Cycle.title))).exists()

    @classmethod
    def indexed_query(cls):
        return super(CycleTaskGroup, cls).indexed_query().options(
            orm.Load(cls).load_only("next_due_date", ),
            orm.Load(cls).subqueryload("cycle_task_group_tasks").load_only(
                "id", "title", "end_date"),
            orm.Load(cls).joinedload("cycle").load_only(
                "id", "title", "next_due_date"),
            orm.Load(cls).subqueryload("cycle_task_group_tasks").joinedload(
                "contact").load_only("email", "name", "id"),
            orm.Load(cls).subqueryload("cycle_task_group_tasks").joinedload(
                "cycle_task_entries").load_only("description", "id"),
            orm.Load(cls).joinedload("cycle").joinedload("contact").load_only(
                "email", "name", "id"),
            orm.Load(cls).joinedload("contact").load_only(
                "email", "name", "id"),
        )

    @classmethod
    def eager_query(cls):
        """Add cycle tasks and objects to cycle task group eager query.

    Make sure we load all cycle task group relevant data in a single query.

    Returns:
      a query object with cycle_task_group_tasks added to joined load options.
    """
        query = super(CycleTaskGroup, cls).eager_query()
        return query.options(orm.joinedload('cycle_task_group_tasks'))
Esempio n. 34
0
class Snapshot(relationship.Relatable, WithLastAssessmentDate, mixins.Base,
               db.Model):
  """Snapshot object that holds a join of parent object, revision, child object
  and parent object's context.

  Conceptual model is that we have a parent snapshotable object (e.g. Audit)
  which will not create relationships to objects with automapper at the time of
  creation but will instead create snapshots of those objects based on the
  latest revision of the object at the time of create / update of the object.
  Objects that were supposed to be mapped are called child objects.
  """
  __tablename__ = "snapshots"

  _api_attrs = reflection.ApiAttributes(
      "parent",
      "child_id",
      "child_type",
      reflection.Attribute("revision", create=False, update=False),
      reflection.Attribute("revision_id", create=False, update=False),
      reflection.Attribute("archived", create=False, update=False),
      reflection.Attribute("revisions", create=False, update=False),
      reflection.Attribute("is_latest_revision", create=False, update=False),
      reflection.Attribute("original_object_deleted",
                           create=False,
                           update=False),
      reflection.Attribute("update_revision", read=False),
  )

  _include_links = [
      "revision"
  ]
  _aliases = {
      "attributes": "Attributes",
      "mappings": {
          "display_name": "Mappings",
          "type": "mapping",
      }
  }

  parent_id = deferred(db.Column(db.Integer, nullable=False), "Snapshot")
  parent_type = deferred(db.Column(db.String, nullable=False), "Snapshot")

  # Child ID and child type are data denormalisations - we could easily get
  # them from revision.content, but since that is a JSON field it will be
  # easier for development to just denormalise on write and not worry
  # about it.
  child_id = deferred(db.Column(db.Integer, nullable=False), "Snapshot")
  child_type = deferred(db.Column(db.String, nullable=False), "Snapshot")

  revision_id = deferred(db.Column(
      db.Integer,
      db.ForeignKey("revisions.id"),
      nullable=False
  ), "Snapshot")
  revision = db.relationship(
      "Revision",
  )
  _update_revision = None

  revisions = db.relationship(
      "Revision",
      primaryjoin="and_(Revision.resource_id == foreign(Snapshot.child_id),"
      "Revision.resource_type == foreign(Snapshot.child_type))",
      uselist=True,
  )

  @builder.simple_property
  def archived(self):
    return self.parent.archived if self.parent else False

  @builder.simple_property
  def is_latest_revision(self):
    """Flag if the snapshot has the latest revision."""
    return self.revisions and self.revision == self.revisions[-1]

  @builder.simple_property
  def original_object_deleted(self):
    """Flag if the snapshot has the latest revision."""
    return self.revisions and self.revisions[-1].action == "deleted"

  @classmethod
  def eager_query(cls):
    query = super(Snapshot, cls).eager_query()
    return cls.eager_inclusions(query, Snapshot._include_links).options(
        orm.subqueryload('revision'),
        orm.subqueryload('revisions'),
    )

  @hybrid_property
  def update_revision(self):
    return self.revision_id

  @update_revision.setter
  def update_revision(self, value):
    self._update_revision = value
    if value == "latest":
      _set_latest_revisions([self])

  @property
  def parent_attr(self):
    return '{0}_parent'.format(self.parent_type)

  @property
  def parent(self):
    return getattr(self, self.parent_attr)

  @parent.setter
  def parent(self, value):
    self.parent_id = getattr(value, 'id', None)
    self.parent_type = getattr(value, 'type', None)
    return setattr(self, self.parent_attr, value)

  @staticmethod
  def _extra_table_args(_):
    return (
        db.UniqueConstraint(
            "parent_type", "parent_id",
            "child_type", "child_id"),
        db.Index("ix_snapshots_parent", "parent_type", "parent_id"),
        db.Index("ix_snapshots_child", "child_type", "child_id"),
    )
Esempio n. 35
0
class Relationship(base.ContextRBAC, Base, db.Model):
  """Relationship model."""
  __tablename__ = 'relationships'
  source_id = db.Column(db.Integer, nullable=False)
  source_type = db.Column(db.String, nullable=False)
  destination_id = db.Column(db.Integer, nullable=False)
  destination_type = db.Column(db.String, nullable=False)
  parent_id = db.Column(
      db.Integer,
      db.ForeignKey('relationships.id', ondelete='SET NULL'),
      nullable=True,
  )
  parent = db.relationship(
      lambda: Relationship,
      remote_side=lambda: Relationship.id
  )
  automapping_id = db.Column(
      db.Integer,
      db.ForeignKey('automappings.id', ondelete='CASCADE'),
      nullable=True,
  )
  is_external = db.Column(db.Boolean, nullable=False, default=False)

  def get_related_for(self, object_type):
    """Return related object for sent type."""
    if object_type == self.source_type:
      return self.destination
    if object_type == self.destination_type:
      return self.source

  @property
  def source_attr(self):
    return '{0}_source'.format(self.source_type)

  @property
  def source(self):
    """Source getter."""
    if not hasattr(self, self.source_attr):
      logger.warning(
          "Relationship source attr '%s' does not exist. "
          "This indicates invalid data in our database!",
          self.source_attr
      )
      return None
    return getattr(self, self.source_attr)

  @source.setter
  def source(self, value):
    self.source_id = getattr(value, 'id', None)
    self.source_type = getattr(value, 'type', None)
    self.validate_relatable_type("source", value)
    return setattr(self, self.source_attr, value)

  @property
  def destination_attr(self):
    return '{0}_destination'.format(self.destination_type)

  @property
  def destination(self):
    """Destination getter."""
    if not hasattr(self, self.destination_attr):
      logger.warning(
          "Relationship destination attr '%s' does not exist. "
          "This indicates invalid data in our database!",
          self.destination_attr
      )
      return None
    return getattr(self, self.destination_attr)

  @destination.setter
  def destination(self, value):
    self.destination_id = getattr(value, 'id', None)
    self.destination_type = getattr(value, 'type', None)
    self.validate_relatable_type("destination", value)
    return setattr(self, self.destination_attr, value)

  @classmethod
  def find_related(cls, object1, object2):
    return cls.get_related_query(object1, object2).first()

  @classmethod
  def get_related_query(cls, object1, object2):
    def predicate(src, dst):
      return and_(
          Relationship.source_type == src.type,
          or_(Relationship.source_id == src.id, src.id == None),  # noqa
          Relationship.destination_type == dst.type,
          or_(Relationship.destination_id == dst.id, dst.id == None),  # noqa
      )
    return Relationship.query.filter(
        or_(predicate(object1, object2), predicate(object2, object1))
    )

  @staticmethod
  def _extra_table_args(cls):
    return (
        db.UniqueConstraint(
            'source_id', 'source_type', 'destination_id', 'destination_type'),
        db.Index(
            'ix_relationships_source',
            'source_type', 'source_id'),
        db.Index(
            'ix_relationships_destination',
            'destination_type', 'destination_id'),
    )

  _api_attrs = reflection.ApiAttributes(
      'source',
      'destination',
      reflection.Attribute(
          'is_external', create=True, update=False, read=True),
  )

  def _display_name(self):
    return "{}:{} <-> {}:{}".format(self.source_type, self.source_id,
                                    self.destination_type, self.destination_id)

  def validate_relatable_type(self, field, value):
    if value is None:
      raise ValidationError(u"{}.{} can't be None."
                            .format(self.__class__.__name__, field))
    if not isinstance(value, Relatable):
      raise ValidationError(u"You are trying to create relationship with not "
                            u"Relatable type: {}".format(value.type))
    tgt_type = self.source_type
    tgt_id = self.source_id
    self.validate_relation_by_type(self.source_type, self.destination_type)

    if field == "source":
      tgt_type = self.destination_type
      tgt_id = self.destination_id
    if value and getattr(value, "type") == "Snapshot":
      if not tgt_type:
        return
      if value.child_type == tgt_type and value.child_id == tgt_id:
        raise ValidationError(
            u"Invalid source-destination types pair for {}: "
            u"source_type={!r}, destination_type={!r}"
            .format(self.type, self.source_type, self.destination_type)
        )
    # else check if the opposite is a Snapshot
    elif tgt_type == "Snapshot":
      from ggrc.models import Snapshot
      snapshot = db.session.query(Snapshot).get(tgt_id)
      if snapshot.child_type == value.type and snapshot.child_id == value.id:
        raise ValidationError(
            u"Invalid source-destination types pair for {}: "
            u"source_type={!r}, destination_type={!r}"
            .format(self.type, self.source_type, self.destination_type)
        )

  # pylint:disable=unused-argument
  @validates("is_external")
  def validate_is_external(self, key, value):
    """Validates is change of is_external column value allowed."""
    if is_external_app_user() and (not value or self.is_external is False):
      raise ValidationError(
          'External application can create only external relationships.')
    return value

  # pylint:disable=unused-argument
  @staticmethod
  def validate_delete(mapper, connection, target):
    """Validates is delete of Relationship is allowed."""
    Relationship.validate_relation_by_type(target.source_type,
                                           target.destination_type)
    if is_external_app_user() and not target.is_external:
      raise ValidationError(
          'External application can delete only external relationships.')

  @staticmethod
  def validate_relation_by_type(source_type, destination_type):
    """Checks if a mapping is allowed between given types."""
    if is_external_app_user():
      # external users can map and unmap scoping objects
      # check that relationship is external is done in a separate validator
      return

    from ggrc.models import all_models
    scoping_models_names = [m.__name__ for m in all_models.all_models
                            if issubclass(m, ScopeObject)]
    if source_type in scoping_models_names and \
       destination_type in ("Regulation", "Standard") or \
       destination_type in scoping_models_names and \
       source_type in ("Regulation", "Standard"):
      raise ValidationError(
          u"You do not have the necessary permissions to map and unmap "
          u"scoping objects to directives in this application. Please "
          u"contact your administrator if you have any questions.")
Esempio n. 36
0
class ImportExport(Identifiable, db.Model):
    """ImportExport Model."""

    __tablename__ = 'import_exports'

    IMPORT_JOB_TYPE = 'Import'
    EXPORT_JOB_TYPE = 'Export'

    ANALYSIS_STATUS = 'Analysis'
    BLOCKED_STATUS = 'Blocked'
    IN_PROGRESS_STATUS = 'In Progress'
    NOT_STARTED_STATUS = 'Not Started'

    IMPORT_EXPORT_STATUSES = [
        NOT_STARTED_STATUS,
        ANALYSIS_STATUS,
        IN_PROGRESS_STATUS,
        BLOCKED_STATUS,
        'Analysis Failed',
        'Stopped',
        'Failed',
        'Finished',
    ]

    DEFAULT_COLUMNS = ['id', 'title', 'created_at', 'status']

    job_type = db.Column(db.Enum(IMPORT_JOB_TYPE, EXPORT_JOB_TYPE),
                         nullable=False)
    status = db.Column(db.Enum(*IMPORT_EXPORT_STATUSES),
                       nullable=False,
                       default=NOT_STARTED_STATUS)
    description = db.Column(db.Text)
    created_at = db.Column(db.DateTime, nullable=False)
    start_at = db.Column(db.DateTime)
    end_at = db.Column(db.DateTime)
    created_by_id = db.Column(db.Integer,
                              db.ForeignKey('people.id'),
                              nullable=False)
    created_by = db.relationship('Person',
                                 foreign_keys='ImportExport.created_by_id',
                                 uselist=False)
    results = db.Column(mysql.LONGTEXT)
    title = db.Column(db.Text)
    content = db.Column(mysql.LONGTEXT)
    gdrive_metadata = db.Column('gdrive_metadata', db.Text)

    def log_json(self, is_default=False):
        """JSON representation"""
        if is_default:
            columns = self.DEFAULT_COLUMNS
        else:
            columns = (column.name for column in self.__table__.columns
                       if column.name not in ('content', 'gdrive_metadata'))

        res = {}
        for column in columns:
            if column == "results":
                res[column] = json.loads(self.results) if self.results \
                    else self.results
            elif column == "created_at":
                res[column] = self.created_at.isoformat()
            else:
                res[column] = getattr(self, column)

        return res
Esempio n. 37
0
class Audit(Snapshotable, clonable.SingleClonable, WithEvidence,
            mixins.CustomAttributable, Personable, HasOwnContext, Relatable,
            Roleable, issue_tracker_mixins.IssueTrackedWithConfig,
            issue_tracker_mixins.IssueTrackedWithPeopleSync,
            WithLastDeprecatedDate, mixins.Timeboxed, base.ContextRBAC,
            mixins.BusinessObject, mixins.Folderable,
            rest_handable_mixins.WithDeleteHandable, Indexed, db.Model):
    """Audit model."""

    __tablename__ = 'audits'
    _slug_uniqueness = False

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

    CLONEABLE_CHILDREN = {"AssessmentTemplate"}

    report_start_date = deferred(db.Column(db.Date), 'Audit')
    report_end_date = deferred(db.Column(db.Date), 'Audit')
    audit_firm_id = deferred(
        db.Column(db.Integer, db.ForeignKey('org_groups.id')), 'Audit')
    audit_firm = db.relationship('OrgGroup', uselist=False)
    gdrive_evidence_folder = deferred(db.Column(db.String), 'Audit')
    program_id = deferred(
        db.Column(db.Integer, db.ForeignKey('programs.id'), nullable=False),
        'Audit')
    object_type = db.Column(db.String(length=250),
                            nullable=False,
                            default='Control')

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

    _api_attrs = reflection.ApiAttributes(
        'report_start_date',
        'report_end_date',
        'audit_firm',
        'gdrive_evidence_folder',
        'program',
        'object_type',
        'archived',
        'manual_snapshots',
    )

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

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

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

    _include_links = []

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

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

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

        # NOTE. Currently this function is called from Restful.collection_posted
        # hook. The following operations are performed:
        # 1) create new object, call json_create(), where attributes will be set
        #    with value validation
        # 2) current function is called from(Restful.collection_posted
        #    which overrides some attributes, attribute validator for these
        #    attributes are called
        # So, validation for those attrs are called twice!
        # One corner case of this behavior is validation of field "title".
        # title cannot be None, and because title validation is performed before
        # this function, API request MUST contain non-empty title in dict,
        # however the value will be overridden and re-validated in this function!

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

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

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

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

    Args:
      audit: Audit instance
    """
        for person, acl in audit.access_control_list:
            self.add_person_with_role(person, acl.ac_role)

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

    Children that can be cloned should be specified in CLONEABLE_CHILDREN.

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

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

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

            for obj in related_children:
                obj.clone(self)

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

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

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

    @classmethod
    def eager_query(cls, **kwargs):
        query = super(Audit, cls).eager_query(**kwargs)
        return query.options(
            orm.joinedload('program'),
            orm.subqueryload('object_people').joinedload('person'),
        )

    def get_evidences_from_assessments(self, objects=False):
        """Return all related evidences from assessments.
      audit <--> assessment -> evidence

    :param objects: bool. optional argument.
          If True object Evidence ORM objects return
    :return: sqlalchemy.Query or sqlalchemy.orm.query.Query objects
    """
        from ggrc.models.assessment import Assessment
        evid_as_dest = db.session.query(
            Relationship.destination_id.label("id"), ).join(
                Assessment,
                Assessment.id == Relationship.source_id,
            ).filter(
                Relationship.destination_type == Evidence.__name__,
                Relationship.source_type == Assessment.__name__,
                Assessment.audit_id == self.id,
            )
        evid_as_source = db.session.query(
            Relationship.source_id.label("id"), ).join(
                Assessment,
                Assessment.id == Relationship.destination_id,
            ).filter(
                Relationship.source_type == Evidence.__name__,
                Relationship.destination_type == Assessment.__name__,
                Assessment.audit_id == self.id,
            )
        evidence_assessment = evid_as_dest.union(evid_as_source)
        if objects:
            return db.session.query(Evidence).filter(
                Evidence.id.in_(evidence_assessment), )
        return evidence_assessment

    def get_evidences_from_audit(self, objects=False):
        """Return all related evidence. In relation audit <--> evidence

    :param objects: bool. optional argument.
          If True object Evidence ORM objects return
    :return: sqlalchemy.Query or sqlalchemy.orm.query.Query objects
    """

        evid_a_source = db.session.query(
            Relationship.source_id.label("id"), ).filter(
                Relationship.source_type == Evidence.__name__,
                Relationship.destination_type == Audit.__name__,
                Relationship.destination_id == self.id,
            )
        evid_a_dest = db.session.query(
            Relationship.destination_id.label("id"), ).filter(
                Relationship.destination_type == Evidence.__name__,
                Relationship.source_type == Audit.__name__,
                Relationship.source_id == self.id,
            )
        evidence_audit = evid_a_dest.union(evid_a_source)
        if objects:
            return db.session.query(Evidence).filter(
                Evidence.id.in_(evidence_audit), )
        return evidence_audit

    @simple_property
    def all_related_evidences(self):
        """Return all related evidences of audit"""
        evidence_assessment = self.get_evidences_from_assessments()
        evidence_audit = self.get_evidences_from_audit()
        evidence_ids = evidence_assessment.union(evidence_audit)
        return db.session.query(Evidence).filter(Evidence.id.in_(evidence_ids))

    def _check_no_assessments(self):
        """Check that audit has no assessments before delete."""
        if self.assessments or self.assessment_templates:
            db.session.rollback()
            raise wzg_exceptions.Conflict(errors.MAPPED_ASSESSMENT)

    def handle_delete(self):
        """Handle model_deleted signals."""
        self._check_no_assessments()
Esempio n. 38
0
class Cycle(WithContact, Stateful, Timeboxed, Described, Titled, Slugged,
            Notifiable, Indexed, db.Model):
  """Workflow Cycle model
  """
  __tablename__ = 'cycles'
  _title_uniqueness = False

  VALID_STATES = (u'Assigned', u'InProgress', u'Finished', u'Verified')

  workflow_id = db.Column(
      db.Integer,
      db.ForeignKey('workflows.id', ondelete="CASCADE"),
      nullable=False,
  )
  cycle_task_groups = db.relationship(
      'CycleTaskGroup', backref='cycle', cascade='all, delete-orphan')
  cycle_task_group_object_tasks = db.relationship(
      'CycleTaskGroupObjectTask', backref='cycle',
      cascade='all, delete-orphan')
  cycle_task_entries = db.relationship(
      'CycleTaskEntry', backref='cycle', cascade='all, delete-orphan')
  is_current = db.Column(db.Boolean, default=True, nullable=False)
  next_due_date = db.Column(db.Date)

  _publish_attrs = [
      'workflow',
      'cycle_task_groups',
      'is_current',
      'next_due_date',
  ]

  _aliases = {
      "cycle_workflow": {
          "display_name": "Workflow",
          "filter_by": "_filter_by_cycle_workflow",
      },
      "status": {
          "display_name": "State",
          "mandatory": False,
          "description": "Options are: \n{} ".format('\n'.join(VALID_STATES))
      }
  }

  PROPERTY_TEMPLATE = u"cycle {}"

  _fulltext_attrs = [
      MultipleSubpropertyFullTextAttr(
          "group title", "cycle_task_groups", ["title"], False,
      ),
      MultipleSubpropertyFullTextAttr(
          "group assignee",
          lambda instance: [g.contact for g in instance.cycle_task_groups],
          ["name", "email"],
          False,
      ),
      DateMultipleSubpropertyFullTextAttr(
          "group due date",
          'cycle_task_groups',
          ["next_due_date"],
          False,
      ),
      MultipleSubpropertyFullTextAttr(
          "task title",
          'cycle_task_group_object_tasks',
          ["title"],
          False,
      ),
      MultipleSubpropertyFullTextAttr(
          "task assignee",
          lambda instance: [t.contact for t in
                            instance.cycle_task_group_object_tasks],
          ["name", "email"],
          False
      ),
      DateMultipleSubpropertyFullTextAttr(
          "task due date",
          "cycle_task_group_object_tasks",
          ["end_date"],
          False
      ),
      DateFullTextAttr("due date", "next_due_date"),
  ]

  AUTO_REINDEX_RULES = [
      ReindexRule("CycleTaskGroup", lambda x: x.cycle),
      ReindexRule("CycleTaskGroupObjectTask",
                  lambda x: x.cycle_task_group.cycle),
      ReindexRule("Person",
                  lambda x: Cycle.query.filter(Cycle.contact_id == x.id))
  ]

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

  @classmethod
  def eager_query(cls):
    """Add cycle task groups to cycle eager query

    This function adds cycle_task_groups as a join option when fetching cycles,
    and makes sure we fetch all cycle related data needed for generating cycle
    json, in one query.

    Returns:
      a query object with cycle_task_groups added to joined load options.
    """
    query = super(Cycle, cls).eager_query()
    return query.options(
        orm.joinedload('cycle_task_groups'),
    )
Esempio n. 39
0
class Relationship(Base, db.Model):
  __tablename__ = 'relationships'
  source_id = db.Column(db.Integer, nullable=False)
  source_type = db.Column(db.String, nullable=False)
  destination_id = db.Column(db.Integer, nullable=False)
  destination_type = db.Column(db.String, nullable=False)
  parent_id = db.Column(
      db.Integer,
      db.ForeignKey('relationships.id', ondelete='SET NULL'),
      nullable=True,
  )
  parent = db.relationship(
      lambda: Relationship,
      remote_side=lambda: Relationship.id
  )
  automapping_id = db.Column(
      db.Integer,
      db.ForeignKey('automappings.id', ondelete='CASCADE'),
      nullable=True,
  )
  relationship_attrs = db.relationship(
      lambda: RelationshipAttr,
      collection_class=attribute_mapped_collection("attr_name"),
      lazy='joined',  # eager loading
      cascade='all, delete-orphan'
  )
  attrs = association_proxy(
      "relationship_attrs", "attr_value",
      creator=lambda k, v: RelationshipAttr(attr_name=k, attr_value=v)
  )

  def get_related_for(self, object_type):
    """Return related object for sent type."""
    if object_type == self.source_type:
      return self.destination
    if object_type == self.destination_type:
      return self.source

  @property
  def source_attr(self):
    return '{0}_source'.format(self.source_type)

  @property
  def source(self):
    return getattr(self, self.source_attr)

  @source.setter
  def source(self, value):
    self.source_id = getattr(value, 'id', None)
    self.source_type = getattr(value, 'type', None)
    return setattr(self, self.source_attr, value)

  @property
  def destination_attr(self):
    return '{0}_destination'.format(self.destination_type)

  @property
  def destination(self):
    return getattr(self, self.destination_attr)

  @destination.setter
  def destination(self, value):
    self.destination_id = getattr(value, 'id', None)
    self.destination_type = getattr(value, 'type', None)
    return setattr(self, self.destination_attr, value)

  @staticmethod
  def validate_attrs(mapper, connection, relationship):
    """
      Only white-listed attributes can be stored, so users don't use this
      for storing arbitrary data.
    """
    # pylint: disable=unused-argument
    for attr_name, attr_value in relationship.attrs.iteritems():
      attr = RelationshipAttr(attr_name=attr_name, attr_value=attr_value)
      RelationshipAttr.validate_attr(relationship.source,
                                     relationship.destination,
                                     relationship.attrs,
                                     attr)

  @classmethod
  def find_related(cls, object1, object2):
    return cls.get_related_query(object1, object2).first()

  @classmethod
  def get_related_query(cls, object1, object2):
    def predicate(src, dst):
      return and_(
          Relationship.source_type == src.type,
          or_(Relationship.source_id == src.id, src.id == None),  # noqa
          Relationship.destination_type == dst.type,
          or_(Relationship.destination_id == dst.id, dst.id == None),  # noqa
      )
    return Relationship.query.filter(
        or_(predicate(object1, object2), predicate(object2, object1))
    )

  @classmethod
  def update_attributes(cls, object1, object2, new_attrs):
    r = cls.find_related(object1, object2)
    for attr_name, attr_value in new_attrs.iteritems():
      attr = RelationshipAttr(attr_name=attr_name, attr_value=attr_value)
      attr = RelationshipAttr.validate_attr(r.source, r.destination,
                                            r.attrs, attr)
      r.attrs[attr.attr_name] = attr.attr_value
    return r

  @staticmethod
  def _extra_table_args(cls):
    return (
        db.UniqueConstraint(
            'source_id', 'source_type', 'destination_id', 'destination_type'),
        db.Index(
            'ix_relationships_source',
            'source_type', 'source_id'),
        db.Index(
            'ix_relationships_destination',
            'destination_type', 'destination_id'),
    )

  _api_attrs = reflection.ApiAttributes('source', 'destination', 'attrs')
  attrs.publish_raw = True

  def _display_name(self):
    return "{}:{} <-> {}:{}".format(self.source_type, self.source_id,
                                    self.destination_type, self.destination_id)

  def log_json(self):
    json = super(Relationship, self).log_json()
    # manually add attrs since the base log_json only captures table columns
    json["attrs"] = self.attrs.copy()  # copy in order to detach from orm
    return json
Esempio n. 40
0
class Audit(Snapshotable, clonable.Clonable, CustomAttributable, Personable,
            HasOwnContext, Relatable, Timeboxed, Noted, Described, Hyperlinked,
            WithContact, Titled, Stateful, Slugged, db.Model):
    """Audit model."""

    __tablename__ = 'audits'
    _slug_uniqueness = False

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

    CLONEABLE_CHILDREN = {"AssessmentTemplate"}

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

    _publish_attrs = [
        'report_start_date', 'report_end_date', 'audit_firm', 'status',
        'gdrive_evidence_folder', 'program', 'object_type',
        PublishOnly('audit_objects')
    ]

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

    _include_links = []

    _aliases = {
        "program": {
            "display_name": "Program",
            "filter_by": "_filter_by_program",
            "mandatory": True,
        },
        "user_role:Auditor": {
            "display_name": "Auditors",
            "type": AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_auditor",
        },
        "status": {
            "display_name": "Status",
            "mandatory": True,
        },
        "start_date": "Planned Start Date",
        "end_date": "Planned End Date",
        "report_start_date": "Planned Report Period from",
        "report_end_date": "Planned Report Period to",
        "contact": {
            "display_name": "Internal Audit Lead",
            "mandatory": True,
            "filter_by": "_filter_by_contact",
        },
        "secondary_contact": None,
        "notes": None,
        "url": None,
        "reference_url": None,
    }

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

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

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

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

        create_audit_context(self)
        self._clone_auditors(source_object)
        self.clone_custom_attribute_values(source_object)

    def _clone_auditors(self, audit):
        """Clone auditors of specified audit.

    Args:
      audit: Audit instance
    """
        from ggrc_basic_permissions.models import Role, UserRole

        role = Role.query.filter_by(name="Auditor").first()
        auditors = [
            ur.person
            for ur in UserRole.query.filter_by(role=role,
                                               context=audit.context).all()
        ]

        for auditor in auditors:
            user_role = UserRole(context=self.context,
                                 person=auditor,
                                 role=role)
            db.session.add(user_role)
        db.session.flush()

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

    Children that can be cloned should be specified in CLONEABLE_CHILDREN.

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

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

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

            for obj in related_children:
                obj.clone(self)

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

    @classmethod
    def _filter_by_auditor(cls, predicate):
        from ggrc_basic_permissions.models import Role, UserRole
        return UserRole.query.join(
            Role, Person).filter((Role.name == "Auditor")
                                 & (UserRole.context_id == cls.context_id)
                                 & (predicate(Person.name)
                                    | predicate(Person.email))).exists()

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

        query = super(Audit, cls).eager_query()
        return query.options(
            orm.joinedload('program'),
            orm.subqueryload('object_people').joinedload('person'),
            orm.subqueryload('audit_objects'),
        )
Esempio n. 41
0
class AccessControlRole(attributevalidator.AttributeValidator,
                        base.ContextRBAC, mixins.Base, db.Model):
    """Access Control Role

  Model holds all roles in the application. These roles can be added
  by the users.
  """
    __tablename__ = 'access_control_roles'

    name = db.Column(db.String, nullable=False)
    object_type = db.Column(db.String)
    tooltip = db.Column(db.String)

    read = db.Column(db.Boolean, nullable=False, default=True)
    update = db.Column(db.Boolean, nullable=False, default=True)
    delete = db.Column(db.Boolean, nullable=False, default=True)
    my_work = db.Column(db.Boolean, nullable=False, default=True)
    mandatory = db.Column(db.Boolean, nullable=False, default=False)
    non_editable = db.Column(db.Boolean, nullable=False, default=False)
    internal = db.Column(db.Boolean, nullable=False, default=False)
    default_to_current_user = db.Column(db.Boolean,
                                        nullable=False,
                                        default=False)
    notify_about_proposal = db.Column(db.Boolean,
                                      nullable=False,
                                      default=False)
    notify_about_review_status = db.Column(db.Boolean,
                                           nullable=False,
                                           default=False)

    access_control_list = db.relationship('AccessControlList',
                                          backref='ac_role',
                                          cascade='all, delete-orphan')

    parent_id = db.Column(
        db.Integer,
        db.ForeignKey('access_control_roles.id', ondelete='CASCADE'),
        nullable=True,
    )
    parent = db.relationship(
        # pylint: disable=undefined-variable
        lambda: AccessControlRole,
        remote_side=lambda: AccessControlRole.id)

    _reserved_names = {}

    @staticmethod
    def _extra_table_args(_):
        return (db.UniqueConstraint('name', 'object_type'), )

    @classmethod
    def eager_query(cls):
        """Define fields to be loaded eagerly to lower the count of DB queries."""
        return super(AccessControlRole, cls).eager_query()

    _api_attrs = reflection.ApiAttributes(
        "name",
        "object_type",
        "tooltip",
        "read",
        "update",
        "delete",
        "my_work",
        "mandatory",
        "default_to_current_user",
        reflection.Attribute("non_editable", create=False, update=False),
    )

    @sa.orm.validates("name", "object_type")
    def validates_name(self, key, value):  # pylint: disable=no-self-use
        """Validate Custom Role name uniquness.

    Custom Role names need to follow 3 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) Names should not contains "*" symbol

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

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

    Args:
      value: access control role name

    Returns:
      value if the name passes all uniqueness checks.
    """
        value = value.strip()
        if key == "name" and self.object_type:
            name = value
            object_type = self.object_type
        elif key == "object_type" and self.name:
            name = self.name.strip()
            object_type = value
        else:
            return value

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

        if self._get_global_cad_names(object_type).get(name) is not None:
            raise ValueError(
                u"Global custom attribute '{}' "
                u"already exists for this object type".format(name))

        if key == "name" and "*" in name:
            raise ValueError(u"Attribute name contains unsupported symbol '*'")

        return value
Esempio n. 42
0
class Workflow(roleable.Roleable, mixins.CustomAttributable, HasOwnContext,
               mixins.Timeboxed, mixins.Described, mixins.Titled,
               mixins.Notifiable, mixins.Stateful, mixins.Slugged,
               mixins.Folderable, Indexed, db.Model):
    """Basic Workflow first class object.
  """
    __tablename__ = 'workflows'
    _title_uniqueness = False

    DRAFT = u"Draft"
    ACTIVE = u"Active"
    INACTIVE = u"Inactive"
    VALID_STATES = [DRAFT, ACTIVE, INACTIVE]

    @classmethod
    def default_status(cls):
        return cls.DRAFT

    notify_on_change = deferred(
        db.Column(db.Boolean, default=False, nullable=False), 'Workflow')
    notify_custom_message = deferred(
        db.Column(db.Text, nullable=False, default=u""), 'Workflow')

    object_approval = deferred(
        db.Column(db.Boolean, default=False, nullable=False), 'Workflow')

    recurrences = db.Column(db.Boolean, default=False, nullable=False)

    task_groups = db.relationship('TaskGroup',
                                  backref='workflow',
                                  cascade='all, delete-orphan')

    cycles = db.relationship('Cycle',
                             backref='workflow',
                             cascade='all, delete-orphan')

    next_cycle_start_date = db.Column(db.Date, nullable=True)

    non_adjusted_next_cycle_start_date = db.Column(db.Date, nullable=True)

    # this is an indicator if the workflow exists from before the change where
    # we deleted cycle objects, which changed how the cycle is created and
    # how objects are mapped to the cycle tasks
    is_old_workflow = deferred(
        db.Column(db.Boolean, default=False, nullable=True), 'Workflow')

    # This column needs to be deferred because one of the migrations
    # uses Workflow as a model and breaks since at that point in time
    # there is no 'kind' column yet
    kind = deferred(db.Column(db.String, default=None, nullable=True),
                    'Workflow')
    IS_VERIFICATION_NEEDED_DEFAULT = True
    is_verification_needed = db.Column(db.Boolean,
                                       default=IS_VERIFICATION_NEEDED_DEFAULT,
                                       nullable=False)

    repeat_every = deferred(db.Column(db.Integer, nullable=True, default=None),
                            'Workflow')
    DAY_UNIT = 'day'
    WEEK_UNIT = 'week'
    MONTH_UNIT = 'month'
    VALID_UNITS = (DAY_UNIT, WEEK_UNIT, MONTH_UNIT)
    unit = deferred(
        db.Column(db.Enum(*VALID_UNITS), nullable=True, default=None),
        'Workflow')
    repeat_multiplier = deferred(
        db.Column(db.Integer, nullable=False, default=0), 'Workflow')

    UNIT_FREQ_MAPPING = {
        None: "one_time",
        DAY_UNIT: "daily",
        WEEK_UNIT: "weekly",
        MONTH_UNIT: "monthly"
    }

    @hybrid.hybrid_property
    def frequency(self):
        """Hybrid property for SearchAPI filtering backward compatibility"""
        return self.UNIT_FREQ_MAPPING[self.unit]

    @frequency.expression
    def frequency(self):
        """Hybrid property for SearchAPI filtering backward compatibility"""
        return case([
            (self.unit.is_(None), self.UNIT_FREQ_MAPPING[None]),
            (self.unit == self.DAY_UNIT,
             self.UNIT_FREQ_MAPPING[self.DAY_UNIT]),
            (self.unit == self.WEEK_UNIT,
             self.UNIT_FREQ_MAPPING[self.WEEK_UNIT]),
            (self.unit == self.MONTH_UNIT,
             self.UNIT_FREQ_MAPPING[self.MONTH_UNIT]),
        ])

    @builder.simple_property
    def can_start_cycle(self):
        """Can start cycle.

    Boolean property, returns True if all task groups have at least one
    task group task, False otherwise.
    """
        return not any(tg
                       for tg in self.task_groups if not tg.task_group_tasks)

    @property
    def tasks(self):
        return list(
            itertools.chain(*[t.task_group_tasks for t in self.task_groups]))

    @property
    def min_task_start_date(self):
        """Fetches non adjusted setup cycle start date based on TGT user's setup.

    Args:
        self: Workflow instance.

    Returns:
        Date when first cycle should be started based on user's setup.
    """
        tasks = self.tasks
        min_date = None
        for task in tasks:
            min_date = min(task.start_date, min_date or task.start_date)
        return min_date

    WORK_WEEK_LEN = 5

    @classmethod
    def first_work_day(cls, day):
        holidays = google_holidays.GoogleHolidays()
        while day.isoweekday() > cls.WORK_WEEK_LEN or day in holidays:
            day -= relativedelta.relativedelta(days=1)
        return day

    def calc_next_adjusted_date(self, setup_date):
        """Calculates adjusted date which are expected in next cycle.

    Args:
        setup_date: Date which was setup by user.

    Returns:
        Adjusted date which are expected to be in next Workflow cycle.
    """
        if self.repeat_every is None or self.unit is None:
            return self.first_work_day(setup_date)
        try:
            key = {
                self.WEEK_UNIT: "weeks",
                self.MONTH_UNIT: "months",
                self.DAY_UNIT: "days",
            }[self.unit]
        except KeyError:
            raise ValueError("Invalid Workflow unit")
        repeater = self.repeat_every * self.repeat_multiplier
        if self.unit == self.DAY_UNIT:
            weeks = repeater / self.WORK_WEEK_LEN
            days = repeater % self.WORK_WEEK_LEN
            # append weekends if it's needed
            days += ((setup_date.isoweekday() + days) > self.WORK_WEEK_LEN) * 2
            return setup_date + relativedelta.relativedelta(
                setup_date, weeks=weeks, days=days)
        calc_date = setup_date + relativedelta.relativedelta(
            setup_date, **{key: repeater})
        if self.unit == self.MONTH_UNIT:
            # check if setup date is the last day of the month
            # and if it is then calc_date should be the last day of hte month too
            setup_day = calendar.monthrange(setup_date.year,
                                            setup_date.month)[1]
            if setup_day == setup_date.day:
                calc_date = datetime.date(
                    calc_date.year, calc_date.month,
                    calendar.monthrange(calc_date.year, calc_date.month)[1])
        return self.first_work_day(calc_date)

    @orm.validates('repeat_every')
    def validate_repeat_every(self, _, value):
        """Validate repeat_every field for Workflow.

    repeat_every shouldn't have 0 value.
    """
        if value is not None and not isinstance(value, (int, long)):
            raise ValueError("'repeat_every' should be integer or 'null'")
        if value is not None and value <= 0:
            raise ValueError(
                "'repeat_every' should be strictly greater than 0")
        return value

    @orm.validates('unit')
    def validate_unit(self, _, value):
        """Validate unit field for Workflow.

    Unit should have one of the value from VALID_UNITS list or None.
    """
        if value is not None and value not in self.VALID_UNITS:
            raise ValueError("'unit' field should be one of the "
                             "value: null, {}".format(", ".join(
                                 self.VALID_UNITS)))
        return value

    @orm.validates('is_verification_needed')
    def validate_is_verification_needed(self, _, value):
        # pylint: disable=unused-argument
        """Validate is_verification_needed field for Workflow.

    It's not allowed to change is_verification_needed flag after creation.
    If is_verification_needed doesn't send,
    then is_verification_needed flag is True.
    """
        if self.is_verification_needed is None:
            return self.IS_VERIFICATION_NEEDED_DEFAULT if value is None else value
        if value is None:
            return self.is_verification_needed
        if self.status != self.DRAFT and value != self.is_verification_needed:
            raise ValueError("is_verification_needed value isn't changeble "
                             "on workflow with '{}' status".format(
                                 self.status))
        return value

    @builder.simple_property
    def workflow_state(self):
        return WorkflowState.get_workflow_state(self.cycles)

    _sanitize_html = [
        'notify_custom_message',
    ]

    _api_attrs = reflection.ApiAttributes(
        'task_groups',
        'notify_on_change',
        'notify_custom_message',
        'cycles',
        'object_approval',
        'recurrences',
        'is_verification_needed',
        'repeat_every',
        'unit',
        reflection.Attribute('next_cycle_start_date',
                             create=False,
                             update=False),
        reflection.Attribute('can_start_cycle', create=False, update=False),
        reflection.Attribute('non_adjusted_next_cycle_start_date',
                             create=False,
                             update=False),
        reflection.Attribute('workflow_state', create=False, update=False),
        reflection.Attribute('kind', create=False, update=False),
    )

    _aliases = {
        "repeat_every": {
            "display_name":
            "Repeat Every",
            "description":
            "'Repeat Every' value\nmust fall into\nthe range 1~30"
            "\nor '-' for None",
        },
        "unit": {
            "display_name":
            "Unit",
            "description":
            "Allowed values for\n'Unit' are:\n{}"
            "\nor '-' for None".format("\n".join(VALID_UNITS)),
        },
        "is_verification_needed": {
            "display_name": "Need Verification",
            "mandatory": True,
            "description": "This field is not changeable\nafter creation.",
        },
        "notify_custom_message": "Custom email message",
        "notify_on_change": {
            "display_name": "Force real-time email updates",
            "mandatory": False,
        },
        "status": None,
        "start_date": None,
        "end_date": None,
    }

    def copy(self, _other=None, **kwargs):
        """Create a partial copy of the current workflow.
    """
        columns = [
            'title', 'description', 'notify_on_change',
            'notify_custom_message', 'end_date', 'start_date', 'repeat_every',
            'unit', 'is_verification_needed'
        ]
        if kwargs.get('clone_people', False):
            access_control_list = [{
                "ac_role": acl.ac_role,
                "person": acl.person
            } for acl in self.access_control_list]
        else:
            role_id = {
                name: ind
                for (ind,
                     name) in role.get_custom_roles_for(self.type).iteritems()
            }['Admin']
            access_control_list = [{
                "ac_role_id": role_id,
                "person": {
                    "id": get_current_user().id
                }
            }]
        target = self.copy_into(_other,
                                columns,
                                access_control_list=access_control_list,
                                **kwargs)
        return target

    def copy_task_groups(self, target, **kwargs):
        """Copy all task groups and tasks mapped to this workflow.
    """
        for task_group in self.task_groups:
            obj = task_group.copy(
                workflow=target,
                context=target.context,
                clone_people=kwargs.get("clone_people", False),
                clone_objects=kwargs.get("clone_objects", False),
                modified_by=get_current_user(),
            )
            target.task_groups.append(obj)

            if kwargs.get("clone_tasks"):
                task_group.copy_tasks(
                    obj,
                    clone_people=kwargs.get("clone_people", False),
                    clone_objects=kwargs.get("clone_objects", True))

        return target

    @classmethod
    def eager_query(cls):
        return super(Workflow, cls).eager_query().options(
            orm.subqueryload('cycles').undefer_group('Cycle_complete').
            subqueryload("cycle_task_group_object_tasks").undefer_group(
                "CycleTaskGroupObjectTask_complete"),
            orm.subqueryload('task_groups').undefer_group(
                'TaskGroup_complete'),
            orm.subqueryload('task_groups').subqueryload(
                "task_group_tasks").undefer_group('TaskGroupTask_complete'),
        )

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

    @classmethod
    def ensure_backlog_workflow_exists(cls):
        """Ensures there is at least one backlog workflow with an active cycle.
    If such workflow does not exist it creates one."""
        def any_active_cycle(workflows):
            """Checks if any active cycle exists from given workflows"""
            for workflow in workflows:
                for cur_cycle in workflow.cycles:
                    if cur_cycle.is_current:
                        return True
            return False

        # Check if backlog workflow already exists
        backlog_workflows = Workflow.query.filter(
            and_(
                Workflow.kind == "Backlog",
                # the following means one_time wf
                Workflow.unit is None)).all()

        if len(backlog_workflows) > 0 and any_active_cycle(backlog_workflows):
            return "At least one backlog workflow already exists"
        # Create a backlog workflow
        backlog_workflow = Workflow(description="Backlog workflow",
                                    title="Backlog (one time)",
                                    status="Active",
                                    recurrences=0,
                                    kind="Backlog")

        # create wf context
        wf_ctx = backlog_workflow.get_or_create_object_context(context=1)
        backlog_workflow.context = wf_ctx
        db.session.flush(backlog_workflow)
        # create a cycle
        backlog_cycle = cycle.Cycle(
            description="Backlog workflow",
            title="Backlog (one time)",
            is_current=1,
            status="Assigned",
            start_date=None,
            end_date=None,
            context=backlog_workflow.get_or_create_object_context(),
            workflow=backlog_workflow)

        # create a cycletaskgroup
        backlog_ctg = cycle_task_group\
            .CycleTaskGroup(description="Backlog workflow taskgroup",
                            title="Backlog TaskGroup",
                            cycle=backlog_cycle,
                            status="InProgress",
                            start_date=None,
                            end_date=None,
                            context=backlog_workflow
                            .get_or_create_object_context())

        db.session.add_all([backlog_workflow, backlog_cycle, backlog_ctg])
        db.session.flush()

        # add fulltext entries
        indexer = get_indexer()
        indexer.create_record(indexer.fts_record_for(backlog_workflow))
        return "Backlog workflow created"
Esempio n. 43
0
class CycleTaskGroup(roleable.Roleable, relationship.Relatable,
                     mixins.WithContact,
                     wf_mixins.CycleTaskGroupRelatedStatusValidatedMixin,
                     mixins.Slugged, mixins.Timeboxed, mixins.Described,
                     mixins.Titled, base.ContextRBAC, mixins.Base,
                     index_mixin.Indexed, db.Model):
    """Cycle Task Group model.
  """
    __tablename__ = 'cycle_task_groups'
    _title_uniqueness = False

    @classmethod
    def generate_slug_prefix(cls):  # pylint: disable=unused-argument
        return "CYCLEGROUP"

    cycle_id = db.Column(
        db.Integer,
        db.ForeignKey('cycles.id', ondelete="CASCADE"),
        nullable=False,
    )
    task_group_id = db.Column(db.Integer,
                              db.ForeignKey('task_groups.id'),
                              nullable=True)
    cycle_task_group_tasks = db.relationship('CycleTaskGroupObjectTask',
                                             backref='_cycle_task_group',
                                             cascade='all, delete-orphan')
    sort_index = db.Column(db.String(length=250), default="", nullable=False)
    next_due_date = db.Column(db.Date)

    _api_attrs = reflection.ApiAttributes('cycle', 'task_group',
                                          'cycle_task_group_tasks',
                                          'sort_index', 'next_due_date')

    _aliases = {
        "cycle": {
            "display_name": "Cycle",
            "filter_by": "_filter_by_cycle",
        },
    }

    PROPERTY_TEMPLATE = u"group {}"

    _fulltext_attrs = [
        attributes.DateFullTextAttr(
            "due date",
            'next_due_date',
        ),
        attributes.FullTextAttr("assignee", "contact", ['email', 'name']),
        attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
        attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                                ['email', 'name'], False),
        attributes.DateFullTextAttr("cycle due date",
                                    lambda x: x.cycle.next_due_date,
                                    with_template=False),
    ]

    # This parameter is overridden by cycle backref, but is here to ensure
    # pylint does not complain
    _cycle = None

    @hybrid.hybrid_property
    def cycle(self):
        """Getter for cycle foreign key."""
        return self._cycle

    @cycle.setter
    def cycle(self, cycle):
        """Set cycle foreign key and relationship."""
        if not self._cycle and cycle:
            relationship.Relationship(source=cycle, destination=self)
        self._cycle = cycle

    @property
    def workflow(self):
        """Property which returns parent workflow object."""
        return self.cycle.workflow

    @property
    def _task_assignees(self):
        """Property. Return the list of persons as assignee of related tasks."""
        people = set()
        for ctask in self.cycle_task_group_tasks:
            people.update(ctask.get_persons_for_rolename("Task Assignees"))
        return list(people)

    @property
    def _task_secondary_assignees(self):
        """Property. Returns people list as Secondary Assignee of related tasks."""
        people = set()
        for ctask in self.cycle_task_group_tasks:
            people.update(
                ctask.get_persons_for_rolename("Task Secondary Assignees"))
        return list(people)

    AUTO_REINDEX_RULES = [
        index_mixin.ReindexRule("Person", _query_filtered_by_contact),
        index_mixin.ReindexRule(
            "Person",
            lambda x: [i.cycle for i in _query_filtered_by_contact(x)]),
    ]

    @classmethod
    def _filter_by_cycle(cls, predicate):
        """Get query that filters cycle task groups.

    Args:
      predicate: lambda function that excepts a single parameter and returns
      true of false.

    Returns:
      An sqlalchemy query that evaluates to true or false and can be used in
      filtering cycle task groups by related cycle.
    """
        return Cycle.query.filter((Cycle.id == cls.cycle_id)
                                  & (predicate(Cycle.slug)
                                     | predicate(Cycle.title))).exists()

    @classmethod
    def indexed_query(cls):
        return super(CycleTaskGroup, cls).indexed_query().options(
            orm.Load(cls).load_only("next_due_date", ),
            orm.Load(cls).joinedload("cycle").load_only(
                "id", "title", "next_due_date"),
            orm.Load(cls).joinedload("cycle").joinedload("contact").load_only(
                "email", "name", "id"),
            orm.Load(cls).joinedload("contact").load_only(
                "email", "name", "id"),
        )

    @classmethod
    def eager_query(cls):
        """Add cycle tasks and objects to cycle task group eager query.

    Make sure we load all cycle task group relevant data in a single query.

    Returns:
      a query object with cycle_task_group_tasks added to joined load options.
    """
        query = super(CycleTaskGroup, cls).eager_query()
        return query.options(
            orm.subqueryload("cycle_task_group_tasks"),
            orm.joinedload("cycle").undefer_group("Cycle_complete"),
            orm.joinedload("cycle").joinedload("contact"))
Esempio n. 44
0
class TaskGroup(roleable.Roleable,
                relationship.Relatable,
                WithContact,
                Timeboxed,
                Described,
                Titled,
                base.ContextRBAC,
                Slugged,
                Indexed,
                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)

  _api_attrs = reflection.ApiAttributes(
      'workflow',
      'task_group_objects',
      reflection.Attribute('objects', create=False, update=False),
      '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,
          "description": ("One person could be added "
                          "as a Task Group assignee")
      },
      "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",
      },
  }

  # This parameter is overridden by workflow backref, but is here to ensure
  # pylint does not complain
  _workflow = None

  @hybrid.hybrid_property
  def workflow(self):
    """Getter for workflow foreign key."""
    return self._workflow

  @workflow.setter
  def workflow(self, workflow):
    """Setter for workflow foreign key."""
    if not self._workflow and workflow:
      all_models.Relationship(source=workflow, destination=self)
    self._workflow = workflow

  def ensure_assignee_is_workflow_member(self):  # pylint: disable=invalid-name
    """Add Workflow Member role to user without role in scope of Workflow."""
    people_with_role_ids = (
        self.workflow.get_person_ids_for_rolename("Admin") +
        self.workflow.get_person_ids_for_rolename("Workflow Member"))
    if self.contact.id in people_with_role_ids:
      return
    self.workflow.add_person_with_role_name(self.contact, "Workflow Member")

  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)

    target.ensure_assignee_is_workflow_member()

    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 eager_query(cls):
    query = super(TaskGroup, cls).eager_query()
    return query.options(
        orm.Load(cls).subqueryload('task_group_objects'),
        orm.Load(cls).subqueryload('task_group_tasks')
    )

  @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. 45
0
 def children(cls):
   return db.relationship(
       cls.__name__,
       backref=db.backref(
           'parent', remote_side='{0}.id'.format(cls.__name__)),
   )
Esempio n. 46
0
class Request(statusable.Statusable, AutoStatusChangeable, Assignable,
              EvidenceURL, Personable, CustomAttributable,
              relationship.Relatable, WithSimilarityScore, Titled, Slugged,
              Described, Commentable, FinishedDate, VerifiedDate, Base,
              db.Model):
    """Class representing Requests.

  Request is an object representing a request from a Requester to Assignee
  to provide feedback, evidence or attachment in the form of comments,
  documents or URLs that (if specified) Verifier has to approve of
  before Request is considered finished.
  """
    __tablename__ = 'requests'
    _title_uniqueness = False

    VALID_TYPES = (u'documentation', u'interview')

    ASSIGNEE_TYPES = (u'Assignee', u'Requester', u'Verifier')

    similarity_options = similarity_options_module.REQUEST

    # TODO Remove requestor and requestor_id on database cleanup
    requestor_id = db.Column(db.Integer, db.ForeignKey('people.id'))
    requestor = db.relationship('Person', foreign_keys=[requestor_id])

    # TODO Remove request_type on database cleanup
    request_type = deferred(db.Column(db.Enum(*VALID_TYPES), nullable=False),
                            'Request')

    start_date = deferred(
        db.Column(db.Date, nullable=False, default=date.today), 'Request')

    end_date = deferred(
        db.Column(db.Date,
                  nullable=False,
                  default=lambda: date.today() + timedelta(7)), 'Request')

    # TODO Remove audit_id audit_object_id on database cleanup
    audit_id = db.Column(db.Integer,
                         db.ForeignKey('audits.id'),
                         nullable=False)
    audit_object_id = db.Column(db.Integer,
                                db.ForeignKey('audit_objects.id'),
                                nullable=True)
    gdrive_upload_path = deferred(db.Column(db.String, nullable=True),
                                  'Request')
    # TODO Remove test and notes columns on database cleanup
    test = deferred(db.Column(db.Text, nullable=True), 'Request')
    notes = deferred(db.Column(db.Text, nullable=True), 'Request')

    _publish_attrs = [
        'requestor', 'request_type', 'gdrive_upload_path', 'start_date',
        'end_date', 'status', 'audit', 'test', 'notes', 'title', 'description'
    ]

    _tracked_attrs = ((set(_publish_attrs) | {'slug'}) - {'status'})

    _sanitize_html = [
        'gdrive_upload_path', 'test', 'notes', 'description', 'title'
    ]

    _aliases = {
        "request_audit": {
            "display_name": "Audit",
            "filter_by": "_filter_by_request_audit",
            "mandatory": True,
        },
        "end_date": "Due On",
        "notes": "Notes",
        "request_type": "Request Type",
        "start_date": "Starts On",
        "status": {
            "display_name": "Status",
            "handler_key": "request_status",
        },
        "test": "Test",
        "related_assignees": {
            "display_name": "Assignee",
            "mandatory": True,
            "filter_by": "_filter_by_related_assignees",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_requesters": {
            "display_name": "Requester",
            "mandatory": True,
            "filter_by": "_filter_by_related_requesters",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_verifiers": {
            "display_name": "Verifier",
            "filter_by": "_filter_by_related_verifiers",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
    }

    def _display_name(self):
        # pylint: disable=unsubscriptable-object
        if len(self.title) > 32:
            display_string = self.description[:32] + u'...'
        elif self.title:
            display_string = self.title
        elif len(self.description) > 32:
            display_string = self.description[:32] + u'...'
        else:
            display_string = self.description
        return u'Request with id {0} "{1}" for Audit "{2}"'.format(
            self.id, display_string, self.audit.display_name)

    @classmethod
    def eager_query(cls):
        query = super(Request, cls).eager_query()
        return query.options(orm.joinedload('audit'))

    @classmethod
    def _filter_by_related_assignees(cls, predicate):
        return cls._get_relate_filter(predicate, "Assignee")

    @classmethod
    def _filter_by_related_requesters(cls, predicate):
        return cls._get_relate_filter(predicate, "Requester")

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

    @classmethod
    def _filter_by_request_audit(cls, predicate):
        return cls.query.filter((audit.Audit.id == cls.audit_id)
                                & (predicate(audit.Audit.slug)
                                   | predicate(audit.Audit.title))).exists()

    @classmethod
    def default_request_type(cls):
        return cls.VALID_TYPES[0]
Esempio n. 47
0
class Program(HasObjectState, CustomAttributable, Documentable, Personable,
              Relatable, HasOwnContext, Timeboxed, Ownable, BusinessObject,
              db.Model):
    __tablename__ = 'programs'

    KINDS = ['Directive']
    KINDS_HIDDEN = ['Company Controls Policy']

    kind = deferred(db.Column(db.String), 'Program')

    audits = db.relationship('Audit',
                             backref='program',
                             cascade='all, delete-orphan')

    _publish_attrs = [
        'kind',
        'audits',
    ]
    _include_links = []
    _aliases = {
        "url": "Program URL",
        "owners": None,
        "program_owner": {
            "display_name": "Manager",
            "mandatory": True,
            "type": AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_program_owner",
        },
        "program_editor": {
            "display_name": "Editor",
            "type": AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_program_editor",
        },
        "program_reader": {
            "display_name": "Reader",
            "type": AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_program_reader",
        },
        "program_mapped": {
            "display_name": "No Access",
            "type": AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_program_mapped",
        },
    }

    @classmethod
    def _filter_by_program_owner(cls, predicate):
        return cls._filter_by_role("ProgramOwner", predicate)

    @classmethod
    def _filter_by_program_editor(cls, predicate):
        return cls._filter_by_role("ProgramEditor", predicate)

    @classmethod
    def _filter_by_program_reader(cls, predicate):
        return cls._filter_by_role("ProgramReader", predicate)

    @classmethod
    def _filter_by_program_mapped(cls, predicate):
        return ObjectPerson.query.join(Person).filter(
            (ObjectPerson.personable_type == "Program")
            & (ObjectPerson.personable_id == cls.id)
            & (predicate(Person.email) | predicate(Person.name))).exists()

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

        query = super(Program, cls).eager_query()
        return cls.eager_inclusions(query, Program._include_links).options(
            orm.subqueryload('audits'))
class CycleTaskGroupObjectTask(roleable.Roleable,
                               wf_mixins.CycleTaskStatusValidatedMixin,
                               mixins.Stateful,
                               mixins.Timeboxed,
                               relationship.Relatable,
                               mixins.Notifiable,
                               mixins.Described,
                               mixins.Titled,
                               mixins.Slugged,
                               mixins.Base,
                               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',
  )

  @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("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)

  object_approval = association_proxy('cycle', 'workflow.object_approval')
  object_approval.publish_raw = True

  @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]  # pylint: disable=not-an-iterable

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

  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 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

  @declared_attr
  def wfo_roles(self):
    """WorkflowOwner UserRoles in parent Workflow.

    Relies on self.context_id = parent_workflow.context_id.
    """
    from ggrc_basic_permissions import models as bp_models

    def primaryjoin():
      """Join UserRoles by context_id = self.context_id and role_id = WFO."""
      workflow_owner_role_id = db.session.query(
          bp_models.Role.id,
      ).filter(
          bp_models.Role.name == "WorkflowOwner",
      ).subquery()
      ur_context_id = sa.orm.foreign(bp_models.UserRole.context_id)
      ur_role_id = sa.orm.foreign(bp_models.UserRole.role_id)
      return sa.and_(self.context_id == ur_context_id,
                     workflow_owner_role_id == ur_role_id)

    return db.relationship(
        bp_models.UserRole,
        primaryjoin=primaryjoin,
        viewonly=True,
    )

  @builder.simple_property
  def allow_change_state(self):
    return self.cycle.is_current and self.current_user_wfo_or_assignee()

  def current_user_wfo_or_assignee(self):
    """Current user is Workflow owner or Assignee for self."""
    wfo_person_ids = {ur.person_id for ur in self.wfo_roles}
    assignees_ids = {p.id for p in
                     self.get_persons_for_rolename("Task Assignees")}
    return login.get_current_user_id() in (wfo_person_ids | assignees_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'),
        orm.subqueryload('wfo_roles'),
    )

  @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": "InProgress", "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_wfo_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
Esempio n. 49
0
class Workflow(roleable.Roleable, relationship.Relatable,
               mixins.CustomAttributable, HasOwnContext, mixins.Timeboxed,
               mixins.Described, mixins.Titled, mixins.Notifiable,
               mixins.Stateful, base.ContextRBAC, mixins.Slugged,
               mixins.Folderable, Indexed, db.Model):
    """Basic Workflow first class object.
  """
    __tablename__ = 'workflows'
    _title_uniqueness = False

    DRAFT = u"Draft"
    ACTIVE = u"Active"
    INACTIVE = u"Inactive"
    VALID_STATES = [DRAFT, ACTIVE, INACTIVE]

    @classmethod
    def default_status(cls):
        return cls.DRAFT

    notify_on_change = deferred(
        db.Column(db.Boolean, default=False, nullable=False), 'Workflow')
    notify_custom_message = deferred(
        db.Column(db.Text, nullable=False, default=u""), 'Workflow')

    object_approval = deferred(
        db.Column(db.Boolean, default=False, nullable=False), 'Workflow')

    recurrences = db.Column(db.Boolean, default=False, nullable=False)

    task_groups = db.relationship('TaskGroup',
                                  backref='_workflow',
                                  cascade='all, delete-orphan')

    cycles = db.relationship('Cycle',
                             backref='_workflow',
                             cascade='all, delete-orphan')

    next_cycle_start_date = db.Column(db.Date, nullable=True)

    non_adjusted_next_cycle_start_date = db.Column(db.Date, nullable=True)

    # this is an indicator if the workflow exists from before the change where
    # we deleted cycle objects, which changed how the cycle is created and
    # how objects are mapped to the cycle tasks
    is_old_workflow = deferred(
        db.Column(db.Boolean, default=False, nullable=True), 'Workflow')

    IS_VERIFICATION_NEEDED_DEFAULT = True
    is_verification_needed = db.Column(db.Boolean,
                                       default=IS_VERIFICATION_NEEDED_DEFAULT,
                                       nullable=False)

    repeat_every = deferred(db.Column(db.Integer, nullable=True, default=None),
                            'Workflow')
    DAY_UNIT = 'day'
    WEEK_UNIT = 'week'
    MONTH_UNIT = 'month'
    VALID_UNITS = (DAY_UNIT, WEEK_UNIT, MONTH_UNIT)
    unit = deferred(
        db.Column(db.Enum(*VALID_UNITS), nullable=True, default=None),
        'Workflow')
    repeat_multiplier = deferred(
        db.Column(db.Integer, nullable=False, default=0), 'Workflow')

    UNIT_FREQ_MAPPING = {
        None: "one_time",
        DAY_UNIT: "daily",
        WEEK_UNIT: "weekly",
        MONTH_UNIT: "monthly"
    }

    # pylint: disable=unnecessary-lambda
    REPEAT_MAPPING = {
        None: lambda px, sx: "off",
        DAY_UNIT: lambda px, sx: "every {}weekday{}".format(px, sx),
        WEEK_UNIT: lambda px, sx: "every {}week{}".format(px, sx),
        MONTH_UNIT: lambda px, sx: "every {}month{}".format(px, sx)
    }
    REPEAT_ORDER_MAPPING = {None: 0, DAY_UNIT: 1, WEEK_UNIT: 2, MONTH_UNIT: 3}

    @hybrid.hybrid_property
    def frequency(self):
        """Hybrid property for SearchAPI filtering backward compatibility"""
        return self.UNIT_FREQ_MAPPING[self.unit]

    @frequency.expression
    def frequency(self):
        """Hybrid property for SearchAPI filtering backward compatibility"""
        return case([
            (self.unit.is_(None), self.UNIT_FREQ_MAPPING[None]),
            (self.unit == self.DAY_UNIT,
             self.UNIT_FREQ_MAPPING[self.DAY_UNIT]),
            (self.unit == self.WEEK_UNIT,
             self.UNIT_FREQ_MAPPING[self.WEEK_UNIT]),
            (self.unit == self.MONTH_UNIT,
             self.UNIT_FREQ_MAPPING[self.MONTH_UNIT]),
        ])

    @classmethod
    def _get_repeat(cls, unit, repeat_every):
        """Return repeat field representation for QueryAPI"""
        if repeat_every is None or repeat_every == 1:
            prefix, suffix = "", ""
        else:
            prefix, suffix = "{} ".format(repeat_every), "s"

        func = cls.REPEAT_MAPPING[unit]
        return func(prefix, suffix)

    @hybrid.hybrid_property
    def repeat(self):
        """Hybrid property for filtering in QueryAPI"""
        return self._get_repeat(self.unit, self.repeat_every)

    @repeat.expression
    def repeat(self):
        """Hybrid property for filtering in QueryAPI"""
        case_ = [(self.unit.is_(None), self.REPEAT_MAPPING[None](None, None))]
        case_.extend(
            ((self.unit == unit) & (self.repeat_every == repeat_every),
             self._get_repeat(unit, repeat_every)) for unit in self.VALID_UNITS
            for repeat_every in xrange(1, 31))

        return case(case_)

    @property
    def repeat_order(self):
        """Property for ordering in QueryAPI"""
        unit_map = self.REPEAT_ORDER_MAPPING[self.unit]
        repeat_every_map = self.repeat_every or 0

        return u"{:0>4}_{:0>4}".format(unit_map, repeat_every_map)

    @builder.simple_property
    def can_start_cycle(self):
        """Can start cycle.

    Boolean property, returns True if all task groups have at least one
    task group task, False otherwise.
    """
        return not any(tg
                       for tg in self.task_groups if not tg.task_group_tasks)

    @property
    def tasks(self):
        return list(
            itertools.chain(*[t.task_group_tasks for t in self.task_groups]))

    @property
    def min_task_start_date(self):
        """Fetches non adjusted setup cycle start date based on TGT user's setup.

    Args:
        self: Workflow instance.

    Returns:
        Date when first cycle should be started based on user's setup.
    """
        tasks = self.tasks
        min_date = None
        for task in tasks:
            min_date = min(task.start_date, min_date or task.start_date)
        return min_date

    WORK_WEEK_LEN = 5

    @classmethod
    def first_work_day(cls, day):
        """Get first work day."""
        while day.isoweekday() > cls.WORK_WEEK_LEN:
            day -= relativedelta.relativedelta(days=1)
        return day

    def calc_next_adjusted_date(self, setup_date):
        """Calculates adjusted date which are expected in next cycle.

    Args:
        setup_date: Date which was setup by user.

    Returns:
        Adjusted date which are expected to be in next Workflow cycle.
    """
        if self.repeat_every is None or self.unit is None:
            return self.first_work_day(setup_date)
        try:
            key = {
                self.WEEK_UNIT: "weeks",
                self.MONTH_UNIT: "months",
                self.DAY_UNIT: "days",
            }[self.unit]
        except KeyError:
            raise ValueError("Invalid Workflow unit")
        repeater = self.repeat_every * self.repeat_multiplier
        if self.unit == self.DAY_UNIT:
            weeks = repeater / self.WORK_WEEK_LEN
            days = repeater % self.WORK_WEEK_LEN
            # append weekends if it's needed
            days += ((setup_date.isoweekday() + days) > self.WORK_WEEK_LEN) * 2
            return setup_date + relativedelta.relativedelta(
                setup_date, weeks=weeks, days=days)
        calc_date = setup_date + relativedelta.relativedelta(
            setup_date, **{key: repeater})
        if self.unit == self.MONTH_UNIT:
            # check if setup date is the last day of the month
            # and if it is then calc_date should be the last day of hte month too
            setup_day = calendar.monthrange(setup_date.year,
                                            setup_date.month)[1]
            if setup_day == setup_date.day:
                calc_date = datetime.date(
                    calc_date.year, calc_date.month,
                    calendar.monthrange(calc_date.year, calc_date.month)[1])
        return self.first_work_day(calc_date)

    @orm.validates('repeat_every')
    def validate_repeat_every(self, _, value):
        """Validate repeat_every field for Workflow.

    repeat_every shouldn't have 0 value.
    """
        if value is not None and not isinstance(value, (int, long)):
            raise ValueError("'repeat_every' should be integer or 'null'")
        if value is not None and value <= 0:
            raise ValueError(
                "'repeat_every' should be strictly greater than 0")
        return value

    @orm.validates('unit')
    def validate_unit(self, _, value):
        """Validate unit field for Workflow.

    Unit should have one of the value from VALID_UNITS list or None.
    """
        if value is not None and value not in self.VALID_UNITS:
            raise ValueError("'unit' field should be one of the "
                             "value: null, {}".format(", ".join(
                                 self.VALID_UNITS)))
        return value

    @orm.validates('is_verification_needed')
    def validate_is_verification_needed(self, _, value):
        # pylint: disable=unused-argument
        """Validate is_verification_needed field for Workflow.

    It's not allowed to change is_verification_needed flag after creation.
    If is_verification_needed doesn't send,
    then is_verification_needed flag is True.
    """
        if self.is_verification_needed is None:
            return self.IS_VERIFICATION_NEEDED_DEFAULT if value is None else value
        if value is None:
            return self.is_verification_needed
        if self.status != self.DRAFT and value != self.is_verification_needed:
            raise ValueError("is_verification_needed value isn't changeble "
                             "on workflow with '{}' status".format(
                                 self.status))
        return value

    @builder.simple_property
    def workflow_state(self):
        return WorkflowState.get_workflow_state(self.cycles)

    @property
    def workflow_archived(self):
        """Determines whether workflow is archived."""
        return bool(self.unit and not self.recurrences
                    and self.next_cycle_start_date)

    _sanitize_html = [
        'notify_custom_message',
    ]

    _fulltext_attrs = [
        attributes.CustomOrderingFullTextAttr('repeat',
                                              'repeat',
                                              order_prop_getter='repeat_order')
    ]

    _api_attrs = reflection.ApiAttributes(
        'task_groups', 'notify_on_change', 'notify_custom_message', 'cycles',
        'recurrences', 'is_verification_needed', 'repeat_every', 'unit',
        reflection.Attribute('object_approval', update=False),
        reflection.Attribute('next_cycle_start_date',
                             create=False,
                             update=False),
        reflection.Attribute('can_start_cycle', create=False, update=False),
        reflection.Attribute('non_adjusted_next_cycle_start_date',
                             create=False,
                             update=False),
        reflection.Attribute('workflow_state', create=False, update=False),
        reflection.Attribute('repeat', create=False, update=False))

    _aliases = {
        "repeat_every": {
            "display_name":
            "Repeat Every",
            "description":
            "'Repeat Every' value\nmust fall into\nthe range 1~30"
            "\nor '-' for None",
        },
        "unit": {
            "display_name":
            "Unit",
            "description":
            "Allowed values for\n'Unit' are:\n{}"
            "\nor '-' for None".format("\n".join(VALID_UNITS)),
        },
        "is_verification_needed": {
            "display_name":
            "Need Verification",
            "mandatory":
            True,
            "description":
            "This field is not changeable\nafter "
            "workflow activation.",
        },
        "notify_custom_message": "Custom email message",
        "notify_on_change": {
            "display_name": "Force real-time email updates",
            "mandatory": False,
        },
        "status": None,
        "start_date": None,
        "end_date": None,
    }

    def copy(self, _other=None, **kwargs):
        """Create a partial copy of the current workflow.
    """
        columns = [
            'title', 'description', 'notify_on_change',
            'notify_custom_message', 'end_date', 'start_date', 'repeat_every',
            'unit', 'is_verification_needed'
        ]
        if kwargs.get('clone_people', False):
            access_control_list = [{
                "ac_role_id": acl.ac_role.id,
                "person": {
                    "id": person.id
                }
            } for person, acl in self.access_control_list]
        else:
            role_id = {
                name: ind
                for (ind,
                     name) in role.get_custom_roles_for(self.type).iteritems()
            }['Admin']
            access_control_list = [{
                "ac_role_id": role_id,
                "person": {
                    "id": get_current_user().id
                }
            }]
        target = self.copy_into(_other,
                                columns,
                                access_control_list=access_control_list,
                                **kwargs)
        return target

    def copy_task_groups(self, target, **kwargs):
        """Copy all task groups and tasks mapped to this workflow.
    """
        for task_group in self.task_groups:
            obj = task_group.copy(
                workflow=target,
                context=target.context,
                clone_people=kwargs.get("clone_people", False),
                clone_objects=kwargs.get("clone_objects", False),
                modified_by=get_current_user(),
            )
            target.task_groups.append(obj)

            if kwargs.get("clone_tasks"):
                task_group.copy_tasks(
                    obj,
                    clone_people=kwargs.get("clone_people", False),
                    clone_objects=kwargs.get("clone_objects", True))

        return target

    @classmethod
    def eager_query(cls, **kwargs):
        return super(Workflow, cls).eager_query(**kwargs).options(
            orm.subqueryload('cycles').undefer_group('Cycle_complete').
            subqueryload("cycle_task_group_object_tasks").undefer_group(
                "CycleTaskGroupObjectTask_complete"),
            orm.subqueryload('task_groups').undefer_group(
                'TaskGroup_complete'),
            orm.subqueryload('task_groups').subqueryload(
                "task_group_tasks").undefer_group('TaskGroupTask_complete'),
        )

    @classmethod
    def indexed_query(cls):
        return super(Workflow, cls).indexed_query().options(
            orm.Load(cls).undefer_group("Workflow_complete", ), )
Esempio n. 50
0
 def secondary_contact(cls):  # pylint: disable=no-self-argument
   return db.relationship(
       'Person',
       uselist=False,
       foreign_keys='{}.secondary_contact_id'.format(cls.__name__))
Esempio n. 51
0
class AccessControlRole(Indexed, attributevalidator.AttributeValidator,
                        mixins.Base, db.Model):
    """Access Control Role

  Model holds all roles in the application. These roles can be added
  by the users.
  """
    __tablename__ = 'access_control_roles'

    name = db.Column(db.String, nullable=False)
    object_type = db.Column(db.String)
    tooltip = db.Column(db.String)

    read = db.Column(db.Boolean, nullable=False, default=True)
    update = db.Column(db.Boolean, nullable=False, default=True)
    delete = db.Column(db.Boolean, nullable=False, default=True)
    my_work = db.Column(db.Boolean, nullable=False, default=True)
    mandatory = db.Column(db.Boolean, nullable=False, default=False)

    access_control_list = db.relationship('AccessControlList',
                                          backref='ac_role',
                                          cascade='all, delete-orphan')

    _reserved_names = {}

    @staticmethod
    def _extra_table_args(_):
        return (db.UniqueConstraint('name', 'object_type'), )

    @classmethod
    def eager_query(cls):
        return super(AccessControlRole, cls).eager_query()

    _publish_attrs = [
        "name",
        "object_type",
        "tooltip",
        "read",
        "update",
        "delete",
        "my_work",
    ]

    @sa.orm.validates("name", "object_type")
    def validates_name(self, key, value):  # pylint: disable=no-self-use
        """Validate Custom Role name uniquness.

    Custom Role names need to follow 2 uniqueness rules:
      1) Names must not match any attribute name on any existing object.
      2) Object level CAD names must not match any global CAD name.

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

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

    Args:
      value: access control role name

    Returns:
      value if the name passes all uniqueness checks.
    """
        value = value.strip()
        if key == "name" and self.object_type:
            name = value
            object_type = self.object_type
        elif key == "object_type" and self.name:
            name = self.name.strip()
            object_type = value
        else:
            return value

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

        if self._get_global_cad_names(object_type).get(name) is not None:
            raise ValueError(
                u"Global custom attribute '{}' "
                u"already exists for this object type".format(name))
        return value
Esempio n. 52
0
 def context(cls):
   return db.relationship('Context', uselist=False)
Esempio n. 53
0
class Workflow(mixins.CustomAttributable, HasOwnContext, mixins.Timeboxed,
               mixins.Described, mixins.Titled, mixins.Notifiable,
               mixins.Stateful, mixins.Slugged, Indexed, db.Model):
  """Basic Workflow first class object.
  """
  __tablename__ = 'workflows'
  _title_uniqueness = False

  VALID_STATES = [u"Draft", u"Active", u"Inactive"]

  VALID_FREQUENCIES = [
      "one_time",
      "weekly",
      "monthly",
      "quarterly",
      "annually"
  ]

  @classmethod
  def default_frequency(cls):
    return 'one_time'

  @orm.validates('frequency')
  def validate_frequency(self, _, value):
    """Make sure that value is listed in valid frequencies.

    Args:
      value: A string value for requested frequency

    Returns:
      default_frequency which is 'one_time' if the value is None, or the value
      itself.

    Raises:
      Value error, if the value is not None or in the VALID_FREQUENCIES array.
    """
    if value is None:
      value = self.default_frequency()
    if value not in self.VALID_FREQUENCIES:
      message = u"Invalid state '{}'".format(value)
      raise ValueError(message)
    return value

  notify_on_change = deferred(
      db.Column(db.Boolean, default=False, nullable=False), 'Workflow')
  notify_custom_message = deferred(
      db.Column(db.Text, nullable=True), 'Workflow')

  frequency = deferred(
      db.Column(db.String, nullable=True, default=default_frequency),
      'Workflow'
  )

  object_approval = deferred(
      db.Column(db.Boolean, default=False, nullable=False), 'Workflow')

  recurrences = db.Column(db.Boolean, default=False, nullable=False)

  workflow_people = db.relationship(
      'WorkflowPerson', backref='workflow', cascade='all, delete-orphan')
  people = association_proxy(
      'workflow_people', 'person', 'WorkflowPerson')

  task_groups = db.relationship(
      'TaskGroup', backref='workflow', cascade='all, delete-orphan')

  cycles = db.relationship(
      'Cycle', backref='workflow', cascade='all, delete-orphan')

  next_cycle_start_date = db.Column(db.Date, nullable=True)

  non_adjusted_next_cycle_start_date = db.Column(db.Date, nullable=True)

  # this is an indicator if the workflow exists from before the change where
  # we deleted cycle objects, which changed how the cycle is created and
  # how objects are mapped to the cycle tasks
  is_old_workflow = deferred(
      db.Column(db.Boolean, default=False, nullable=True), 'Workflow')

  # This column needs to be deferred because one of the migrations
  # uses Workflow as a model and breaks since at that point in time
  # there is no 'kind' column yet
  kind = deferred(
      db.Column(db.String, default=None, nullable=True), 'Workflow')

  @builder.simple_property
  def workflow_state(self):
    return WorkflowState.get_workflow_state(self.cycles)

  _sanitize_html = [
      'notify_custom_message',
  ]

  _publish_attrs = [
      'workflow_people',
      reflection.PublishOnly('people'),
      'task_groups',
      'frequency',
      'notify_on_change',
      'notify_custom_message',
      'cycles',
      'object_approval',
      'recurrences',
      reflection.PublishOnly('next_cycle_start_date'),
      reflection.PublishOnly('non_adjusted_next_cycle_start_date'),
      reflection.PublishOnly('workflow_state'),
      reflection.PublishOnly('kind')
  ]

  _aliases = {
      "frequency": {
          "display_name": "Frequency",
          "mandatory": True,
      },
      "notify_custom_message": "Custom email message",
      "notify_on_change": "Force real-time email updates",
      "workflow_owner": {
          "display_name": "Manager",
          "type": reflection.AttributeInfo.Type.USER_ROLE,
          "mandatory": True,
          "filter_by": "_filter_by_workflow_owner",
      },
      "workflow_member": {
          "display_name": "Member",
          "type": reflection.AttributeInfo.Type.USER_ROLE,
          "filter_by": "_filter_by_workflow_member",
      },
      "status": None,
      "start_date": None,
      "end_date": None,
  }

  @classmethod
  def _filter_by_workflow_owner(cls, predicate):
    return cls._filter_by_role("WorkflowOwner", predicate)

  @classmethod
  def _filter_by_workflow_member(cls, predicate):
    return cls._filter_by_role("WorkflowMember", predicate)

  def copy(self, _other=None, **kwargs):
    """Create a partial copy of the current workflow.
    """
    columns = [
        'title', 'description', 'notify_on_change', 'notify_custom_message',
        'frequency', 'end_date', 'start_date'
    ]
    target = self.copy_into(_other, columns, **kwargs)
    return target

  def copy_task_groups(self, target, **kwargs):
    """Copy all task groups and tasks mapped to this workflow.
    """
    for task_group in self.task_groups:
      obj = task_group.copy(
          workflow=target,
          context=target.context,
          clone_people=kwargs.get("clone_people", False),
          clone_objects=kwargs.get("clone_objects", False),
          modified_by=get_current_user(),
      )
      target.task_groups.append(obj)

      if kwargs.get("clone_tasks"):
        task_group.copy_tasks(
            obj,
            clone_people=kwargs.get("clone_people", False),
            clone_objects=kwargs.get("clone_objects", True)
        )

    return target

  @classmethod
  def eager_query(cls):
    return super(Workflow, cls).eager_query().options(
        orm.subqueryload('cycles').undefer_group('Cycle_complete')
           .subqueryload("cycle_task_group_object_tasks")
           .undefer_group("CycleTaskGroupObjectTask_complete"),
        orm.subqueryload('task_groups'),
        orm.subqueryload('workflow_people'),
    )

  @classmethod
  def ensure_backlog_workflow_exists(cls):
    """Ensures there is at least one backlog workflow with an active cycle.
    If such workflow does not exist it creates one."""

    def any_active_cycle(workflows):
      """Checks if any active cycle exists from given workflows"""
      for workflow in workflows:
        for cur_cycle in workflow.cycles:
          if cur_cycle.is_current:
            return True
      return False

    # Check if backlog workflow already exists
    backlog_workflows = Workflow.query\
                                .filter(and_
                                        (Workflow.kind == "Backlog",
                                         Workflow.frequency == "one_time"))\
                                .all()

    if len(backlog_workflows) > 0 and any_active_cycle(backlog_workflows):
      return "At least one backlog workflow already exists"
    # Create a backlog workflow
    backlog_workflow = Workflow(description="Backlog workflow",
                                title="Backlog (one time)",
                                frequency="one_time",
                                status="Active",
                                recurrences=0,
                                kind="Backlog")

    # create wf context
    wf_ctx = backlog_workflow.get_or_create_object_context(context=1)
    backlog_workflow.context = wf_ctx
    db.session.flush(backlog_workflow)
    # create a cycle
    backlog_cycle = cycle.Cycle(description="Backlog workflow",
                                title="Backlog (one time)",
                                is_current=1,
                                status="Assigned",
                                start_date=None,
                                end_date=None,
                                context=backlog_workflow
                                .get_or_create_object_context(),
                                workflow=backlog_workflow)

    # create a cycletaskgroup
    backlog_ctg = cycle_task_group\
        .CycleTaskGroup(description="Backlog workflow taskgroup",
                        title="Backlog TaskGroup",
                        cycle=backlog_cycle,
                        status="InProgress",
                        start_date=None,
                        end_date=None,
                        context=backlog_workflow
                        .get_or_create_object_context())

    db.session.add_all([backlog_workflow, backlog_cycle, backlog_ctg])
    db.session.flush()

    # add fulltext entries
    indexer = get_indexer()
    indexer.create_record(indexer.fts_record_for(backlog_workflow))
    return "Backlog workflow created"
Esempio n. 54
0
class CycleTaskGroupObjectTask(
        WithContact, Stateful, Timeboxed, Relatable, Notifiable,
        Described, Titled, Slugged, Base, Indexed, db.Model):
  """Cycle task model
  """
  __tablename__ = 'cycle_task_group_object_tasks'

  readable_name_alias = 'cycle task'

  _title_uniqueness = False

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

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

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

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

  PROPERTY_TEMPLATE = u"task {}"

  _fulltext_attrs = [
      DateFullTextAttr("end_date", 'end_date',),
      FullTextAttr("assignee", 'contact', ['name', 'email']),
      FullTextAttr("group title", 'cycle_task_group', ['title'], False),
      FullTextAttr("cycle title", 'cycle', ['title'], False),
      FullTextAttr("group assignee",
                   lambda x: x.cycle_task_group.contact,
                   ['email', 'name'], False),
      FullTextAttr("cycle assignee",
                   lambda x: x.cycle.contact,
                   ['email', 'name'], False),
      DateFullTextAttr("group due date",
                       lambda x: x.cycle_task_group.next_due_date,
                       with_template=False),
      DateFullTextAttr("cycle due date",
                       lambda x: x.cycle.next_due_date,
                       with_template=False),
      MultipleSubpropertyFullTextAttr("comments",
                                      "cycle_task_entries",
                                      ["description"]),
  ]

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

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

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

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

  object_approval = association_proxy('cycle', 'workflow.object_approval')
  object_approval.publish_raw = True

  @property
  def cycle_task_objects_for_cache(self):
    """Changing task state must invalidate `workflow_state` on objects
    """
    return [(object_.__class__.__name__, object_.id) for object_ in
            self.related_objects]  # pylint: disable=not-an-iterable

  _publish_attrs = [
      'cycle',
      'cycle_task_group',
      'task_group_task',
      'cycle_task_entries',
      'sort_index',
      'task_type',
      'response_options',
      'selected_response_options',
      PublishOnly('object_approval'),
      PublishOnly('finished_date'),
      PublishOnly('verified_date')
  ]

  default_description = "<ol>"\
                        + "<li>Expand the object review task.</li>"\
                        + "<li>Click on the Object to be reviewed.</li>"\
                        + "<li>Review the object in the Info tab.</li>"\
                        + "<li>Click \"Approve\" to approve the object.</li>"\
                        + "<li>Click \"Decline\" to decline the object.</li>"\
                        + "</ol>"

  _aliases = {
      "title": "Summary",
      "description": "Task Details",
      "contact": {
          "display_name": "Assignee",
          "mandatory": True,
      },
      "secondary_contact": None,
      "finished_date": "Actual Finish Date",
      "verified_date": "Actual Verified Date",
      "cycle": {
          "display_name": "Cycle",
          "filter_by": "_filter_by_cycle",
      },
      "cycle_task_group": {
          "display_name": "Task Group",
          "mandatory": True,
          "filter_by": "_filter_by_cycle_task_group",
      },
      "task_type": {
          "display_name": "Task Type",
          "mandatory": True,
      },
      "status": {
          "display_name": "State",
          "mandatory": False,
          "description": "Options are:\n{}".
                          format('\n'.join((item for item in VALID_STATES
                                            if item)))
      },
      "end_date": "Due Date",
      "start_date": "Start Date",
  }

  @computed_property
  def related_objects(self):
    """Compute and return a list of all the objects related to this cycle task.

    Related objects are those that are found either on the "source" side, or
    on the "destination" side of any of the instance's relations.

    Returns:
      (list) All objects related to the instance.
    """
    # pylint: disable=not-an-iterable
    sources = [r.source for r in self.related_sources]
    destinations = [r.destination for r in self.related_destinations]
    return sources + destinations

  @classmethod
  def _filter_by_cycle(cls, predicate):
    """Get query that filters cycle tasks by related cycles.

    Args:
      predicate: lambda function that excepts a single parameter and returns
      true of false.

    Returns:
      An sqlalchemy query that evaluates to true or false and can be used in
      filtering cycle tasks by related cycles.
    """
    return Cycle.query.filter(
        (Cycle.id == cls.cycle_id) &
        (predicate(Cycle.slug) | predicate(Cycle.title))
    ).exists()

  @classmethod
  def _filter_by_cycle_task_group(cls, predicate):
    """Get query that filters cycle tasks by related cycle task groups.

    Args:
      predicate: lambda function that excepts a single parameter and returns
      true of false.

    Returns:
      An sqlalchemy query that evaluates to true or false and can be used in
      filtering cycle tasks by related cycle task groups.
    """
    return CycleTaskGroup.query.filter(
        (CycleTaskGroup.id == cls.cycle_id) &
        (predicate(CycleTaskGroup.slug) | predicate(CycleTaskGroup.title))
    ).exists()

  @classmethod
  def eager_query(cls):
    """Add cycle task entries to cycle task eager query

    This function adds cycle_task_entries as a join option when fetching cycles
    tasks, and makes sure that with one query we fetch all cycle task related
    data needed for generating cycle taks json for a response.

    Returns:
      a query object with cycle_task_entries added to joined load options.
    """
    query = super(CycleTaskGroupObjectTask, cls).eager_query()
    return query.options(
        orm.joinedload('cycle')
           .joinedload('workflow')
           .undefer_group('Workflow_complete'),
        orm.joinedload('cycle_task_entries'),
    )

  @classmethod
  def indexed_query(cls):
    return super(CycleTaskGroupObjectTask, cls).indexed_query().options(
        orm.Load(cls).load_only(
            "end_date",
            "start_date",
            "created_at",
            "updated_at"
        ),
        orm.Load(cls).joinedload("cycle_task_group").load_only(
            "id",
            "title",
            "end_date",
            "next_due_date",
        ),
        orm.Load(cls).joinedload("cycle").load_only(
            "id",
            "title",
            "next_due_date"
        ),
        orm.Load(cls).joinedload("cycle_task_group").joinedload(
            "contact"
        ).load_only(
            "email",
            "name",
            "id"
        ),
        orm.Load(cls).joinedload("cycle").joinedload(
            "contact"
        ).load_only(
            "email",
            "name",
            "id"
        ),
        orm.Load(cls).subqueryload("cycle_task_entries").load_only(
            "description",
            "id"
        ),
        orm.Load(cls).joinedload("contact").load_only(
            "email",
            "name",
            "id"
        ),
    )
Esempio n. 55
0
class AccessControlList(base.ContextRBAC, mixins.Base, db.Model):
    """Access Control List

  Model is a mapping between a role and an object. It creates a base for
  permissions of the role for mapping a person to this permission.
  """
    __tablename__ = 'access_control_list'
    _api_attrs = reflection.ApiAttributes("ac_role_id")

    ac_role_id = db.Column(db.Integer,
                           db.ForeignKey('access_control_roles.id'),
                           nullable=False)
    object_id = db.Column(db.Integer, nullable=False)
    object_type = db.Column(db.String, nullable=False)

    # Base id always points to the top most parent of the acl propagation chain
    # or to itself if there are no parents. This field is used to optimize
    # permission queries by making sure a single extra join is needed to get to
    # the base ACL entry (one without parents) to which access control people are
    # mapped.
    base_id = db.Column(
        db.Integer,
        db.ForeignKey('access_control_list.id', ondelete='CASCADE'),
        nullable=True,
    )

    # This field is a copy of parent_id but set to not nullable, so it can be
    # used in a unique constraint. Uniqueness check will always pass if there is
    # a NULL in the set.
    parent_id_nn = db.Column(
        db.Integer,
        nullable=False,
        default=0,
    )

    # Parent id field is just to keep the information about the entire chain of
    # acl propagation. This field is only needed for acl deletion. So unmapping
    # will remove the entire subtree of propagated acl entries.
    parent_id = db.Column(
        db.Integer,
        db.ForeignKey('access_control_list.id', ondelete='CASCADE'),
        nullable=True,
    )

    parent = db.relationship(
        lambda: AccessControlList,  # pylint: disable=undefined-variable
        foreign_keys=lambda: AccessControlList.parent_id,
        remote_side=lambda: AccessControlList.id,
    )

    access_control_people = db.relationship(
        'AccessControlPerson',
        foreign_keys='AccessControlPerson.ac_list_id',
        backref='ac_list',
        lazy='subquery',
        cascade='all, delete-orphan',
    )

    @property
    def object_attr(self):
        return '{0}_object'.format(self.object_type)

    @property
    def object(self):
        return getattr(self, self.object_attr)

    @object.setter
    def object(self, value):
        self.object_id = getattr(value, 'id', None)
        self.object_type = getattr(value, 'type', None)
        return setattr(self, self.object_attr, value)

    @staticmethod
    def _extra_table_args(_):
        return (
            db.UniqueConstraint(
                'ac_role_id',
                'object_id',
                'object_type',
                'parent_id_nn',
            ),
            db.Index('idx_object_type_object_idx', 'object_type', 'object_id'),
            db.Index('ix_role_object', 'ac_role_id', 'object_type',
                     'object_id'),
            db.Index(
                'idx_object_type_object_id_parent_id_nn',
                'object_type',
                'object_id',
                'parent_id_nn',
            ),
        )

    def _remove_people(self, obsolete_people):
        """Remove people from the current acl."""
        if not obsolete_people:
            return
        people_acp_map = {
            acp.person: acp
            for acp in self.access_control_people
        }
        for person in obsolete_people:
            self.access_control_people.remove(people_acp_map[person])

    def _add_people(self, additional_people):
        """Add people to the current acl."""
        for person in additional_people:
            people.AccessControlPerson(ac_list=self, person=person)

    def add_person(self, additional_person):
        """Add a single person to current ACL entry.

    Args:
      additional_person: new person model that will be added.
    """
        self.add_people({additional_person})

    def add_people(self, additional_people):
        """Ensure that people are linked to the current ACL entry.

    Args:
      additional_people: set of people objects that will be added.
    """
        existing_people = {acp.person for acp in self.access_control_people}
        self._add_people(additional_people - existing_people)

    def remove_person(self, obsolete_person):
        self.remove_people({obsolete_person})

    def remove_people(self, obsolete_people):
        """Remove the given people from the current ACL entry.

    Args:
      obsolete_people: set of people models that will be removed.
    """
        existing_people = {acp.person for acp in self.access_control_people}
        self._remove_people(obsolete_people & existing_people)

    def update_people(self, new_people):
        """Update the list of current acl people to match new_people.

    Args:
      new_people: set of people objects. Any existing person missing from that
        set will be removed. Any new people will be added.
    """
        existing_people = {acp.person for acp in self.access_control_people}
        self._remove_people(existing_people - new_people)
        self._add_people(new_people - existing_people)
Esempio n. 56
0
 def children(cls):  # pylint: disable=no-self-argument
   return db.relationship(
       cls.__name__,
       backref=db.backref(
           'parent', remote_side='{0}.id'.format(cls.__name__)),
   )
Esempio n. 57
0
class Comment(Relatable, Described, Ownable, Notifiable, Base, 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
    _publish_attrs = [
        "assignee_type",
        "custom_attribute_revision",
    ]

    _update_attrs = [
        "assignee_type",
        "custom_attribute_revision_upd",
    ]

    _sanitize_html = [
        "description",
    ]

    @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'),
        )

    @computed_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
Esempio n. 58
0
 def secondary_contact(cls):
   return db.relationship(
       'Person',
       uselist=False,
       foreign_keys='{}.secondary_contact_id'.format(cls.__name__))
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: "",
        }

        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 ValueError(
                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 existsfor 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
Esempio n. 60
0
class Directive(HasObjectState, LastDeprecatedTimeboxed, Commentable,
                TestPlanned, BusinessObject, db.Model):
    __tablename__ = 'directives'

    version = deferred(db.Column(db.String), 'Directive')
    organization = deferred(db.Column(db.String), 'Directive')
    scope = deferred(db.Column(db.Text), 'Directive')
    kind_id = deferred(db.Column(db.Integer), 'Directive')
    audit_start_date = deferred(db.Column(db.DateTime), 'Directive')
    audit_frequency_id = deferred(db.Column(db.Integer), 'Directive')
    audit_duration_id = deferred(db.Column(db.Integer), 'Directive')
    meta_kind = db.Column(db.String)
    kind = deferred(db.Column(db.String), 'Directive')

    # TODO: FIX jost!
    # sections = db.relationship(
    #     'Section', backref='directive',
    #     order_by='Section.slug', cascade='all, delete-orphan')
    controls = db.relationship('Control',
                               backref='directive',
                               order_by='Control.slug')
    audit_frequency = db.relationship(
        'Option',
        primaryjoin='and_(foreign(Directive.audit_frequency_id) == Option.id, '
        'Option.role == "audit_frequency")',
        uselist=False,
    )
    audit_duration = db.relationship(
        'Option',
        primaryjoin='and_(foreign(Directive.audit_duration_id) == Option.id, '
        'Option.role == "audit_duration")',
        uselist=False,
    )

    __mapper_args__ = {'polymorphic_on': meta_kind}

    _api_attrs = reflection.ApiAttributes(
        'audit_start_date',
        'audit_frequency',
        'audit_duration',
        'controls',
        'kind',
        'organization',
        'scope',
        'version',
    )

    _fulltext_attrs = [
        'audit_start_date',
        'audit_frequency',
        'audit_duration',
        'controls',
        'kind',
        'organization',
        'scope',
        'version',
    ]

    @classmethod
    def indexed_query(cls):
        return super(Directive, cls).indexed_query().options(
            orm.Load(cls).joinedload('audit_frequency'),
            orm.Load(cls).joinedload('audit_duration'),
            orm.Load(cls).subqueryload('controls'),
            orm.Load(cls).load_only(
                'audit_start_date',
                'kind',
                'organization',
                'scope',
                'version',
            ),
        )

    _sanitize_html = [
        'organization',
        'scope',
        'version',
    ]

    _include_links = []

    _aliases = {
        'kind': "Kind/Type",
        "document_url": None,
        "document_evidence": None,
    }

    @validates('kind')
    def validate_kind(self, key, value):
        if not value:
            return None
        if value not in self.VALID_KINDS:
            message = "Invalid value '{}' for attribute {}.{}.".format(
                value, self.__class__.__name__, key)
            raise ValueError(message)
        return value

    @validates('audit_duration', 'audit_frequency')
    def validate_directive_options(self, key, option):
        return validate_option(self.__class__.__name__, key, option, key)

    @classmethod
    def eager_query(cls):
        query = super(Directive, cls).eager_query()
        return cls.eager_inclusions(query, Directive._include_links).options(
            orm.joinedload('audit_frequency'),
            orm.joinedload('audit_duration'), orm.subqueryload('controls'))

    @staticmethod
    def _extra_table_args(cls):
        return (db.Index('ix_{}_meta_kind'.format(cls.__tablename__),
                         'meta_kind'), )