Exemple #1
0
class WithContact(object):
    """Mixin that defines `contact` and `secondary_contact` fields."""
    @declared_attr
    def contact_id(cls):  # pylint: disable=no-self-argument
        return deferred(db.Column(db.Integer, db.ForeignKey('people.id')),
                        cls.__name__)

    @declared_attr
    def secondary_contact_id(cls):  # pylint: disable=no-self-argument
        return deferred(db.Column(db.Integer, db.ForeignKey('people.id')),
                        cls.__name__)

    @declared_attr
    def contact(cls):  # pylint: disable=no-self-argument
        return db.relationship('Person',
                               uselist=False,
                               foreign_keys='{}.contact_id'.format(
                                   cls.__name__))

    @declared_attr
    def secondary_contact(cls):  # pylint: disable=no-self-argument
        return db.relationship('Person',
                               uselist=False,
                               foreign_keys='{}.secondary_contact_id'.format(
                                   cls.__name__))

    @staticmethod
    def _extra_table_args(model):
        return (
            db.Index('fk_{}_contact'.format(model.__tablename__),
                     'contact_id'),
            db.Index('fk_{}_secondary_contact'.format(model.__tablename__),
                     'secondary_contact_id'),
        )

    _api_attrs = reflection.ApiAttributes('contact', 'secondary_contact')
    _fulltext_attrs = [
        attributes.FullTextAttr("contact", "contact", ["email", "name"]),
        attributes.FullTextAttr('secondary_contact', 'secondary_contact',
                                ["email", "name"]),
    ]

    @classmethod
    def indexed_query(cls):
        return super(WithContact, cls).indexed_query().options(
            orm.Load(cls).joinedload("contact").load_only(
                "name", "email", "id"),
            orm.Load(cls).joinedload("secondary_contact").load_only(
                "name", "email", "id"),
        )

    _aliases = {
        "contact": "Primary Contact",
        "secondary_contact": "Secondary Contact",
    }
class WithLastComment(attributable.Attributable):
  """Defines logic to get last comment for object."""
  # pylint: disable=too-few-public-methods

  _api_attrs = reflection.ApiAttributes(
      reflection.Attribute("last_comment", create=False, update=False),
      reflection.Attribute("last_comment_id", create=False, update=False),
  )

  _aliases = {
      "last_comment": {
          "display_name": "Last Comment",
          "view_only": True,
      },
  }

  _fulltext_attrs = [attributes.FullTextAttr("last_comment", "last_comment")]

  @simple_property
  def last_comment(self):
    lc_attr = self.attributes.get("last_comment")
    return lc_attr.value_string if lc_attr else None

  @simple_property
  def last_comment_id(self):
    lc_attr = self.attributes.get("last_comment")
    return lc_attr.source_id if lc_attr else None
Exemple #3
0
class ScopeObject(BusinessObject):
    """Mixin that re-name status attribute"""

    _fulltext_attrs = [attributes.FullTextAttr("Launch Status", "status")]
    _aliases = {
        "status": {
            "display_name":
            "Launch Status",
            "mandatory":
            False,
            "description":
            "Options are:\n{}".format("\n".join(BusinessObject.VALID_STATES))
        }
    }
Exemple #4
0
def person_relation_factory(relation_name, fulltext_attr=None, api_attr=None):
  """Factory that will generate person  """

  def field_declaration(cls):  # pylint: disable=no-self-argument
    return deferred(db.Column(db.Integer,
                              db.ForeignKey('people.id'),
                              nullable=True),
                    cls.__name__)

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

  gen_fulltext_attr = (
      fulltext_attr or attributes.FullTextAttr(relation_name,
                                               relation_name,
                                               ["email", "name"]))
  api_attr = api_attr or reflection.Attribute(relation_name)

  # pylint: disable=too-few-public-methods,missing-docstring
  class DecoratedClass(object):

    _api_attrs = reflection.ApiAttributes(api_attr)
    fulltext_attr = [gen_fulltext_attr]

    @classmethod
    def indexed_query(cls):
      return super(DecoratedClass, cls).indexed_query().options(
          orm.Load(cls).joinedload(
              relation_name
          ).load_only(
              "name", "email", "id"
          ),
      )

  return type(
      "{}_mixin".format(relation_name),
      (DecoratedClass, ),
      {
          "{}_id".format(relation_name): declared_attr(field_declaration),
          relation_name: declared_attr(attr_declaration),
      })
Exemple #5
0
class Person(CustomAttributable, CustomAttributeMapable, HasOwnContext,
             Relatable, base.ContextRBAC, Base, Indexed, db.Model):
    """Person model definition."""
    def __init__(self, *args, **kwargs):
        """Initialize profile relationship while creating Person instance"""
        super(Person, self).__init__(*args, **kwargs)
        self.profile = PersonProfile()

        self.build_object_context(context=1,
                                  name='Personal Context',
                                  description='')

        self.profile.add_person_with_role_name(self, "Admin")

    __tablename__ = 'people'

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

    object_people = db.relationship('ObjectPerson',
                                    backref='person',
                                    cascade='all, delete-orphan')
    language = db.relationship(
        'Option',
        primaryjoin='and_(foreign(Person.language_id) == Option.id, '
        'Option.role == "person_language")',
        uselist=False,
    )
    saved_searches = db.relationship(
        "SavedSearch",
        lazy="dynamic",
        cascade="all, delete-orphan",
    )
    profile = db.relationship(
        "PersonProfile",
        foreign_keys='PersonProfile.person_id',
        uselist=False,
        backref="person",
        cascade='all, delete-orphan',
    )
    access_control_people = db.relationship(
        'AccessControlPerson',
        foreign_keys='AccessControlPerson.person_id',
        backref="person",
    )

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

    _fulltext_attrs = [
        'company',
        'email',
        'name',
        attributes.FullTextAttr("Authorizations", "system_wide_role"),
    ]
    _api_attrs = reflection.ApiAttributes(
        'company',
        'email',
        'language',
        'name',
        reflection.Attribute('profile', create=False, update=False),
        reflection.Attribute('object_people', create=False, update=False),
        reflection.Attribute('system_wide_role', create=False, update=False),
    )
    _sanitize_html = [
        'company',
        'name',
    ]
    _include_links = []
    ROLE_OPTIONS = ("No Access", "Creator", "Reader", "Editor",
                    "Administrator")
    _aliases = {
        "name": "Name",
        "email": {
            "display_name": "Email",
            "unique": True,
        },
        "company": "Company",
        "user_role": {
            "display_name": "Role",
            "type": "user_role",
            "filter_by": "_filter_by_user_role",
            "description":
            "Allowed values are\n{}".format("\n".join(ROLE_OPTIONS))
        },
    }

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

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

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

    @property
    def title(self):
        return self.name or self.email

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

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

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

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

    @validates('email')
    def validate_email(self, _, email):
        """Email property validator."""
        if not Person.is_valid_email(email):
            message = "Email address '{}' is invalid. Valid email must be provided"
            raise ValidationError(message.format(email))
        return email

    @validates('name')
    def validate_name(self, _, name):
        """Name property validator."""
        if not name:
            raise ValidationError("Name is empty")
        return name

    @staticmethod
    def is_valid_email(val):
        """Check for valid email.

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

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

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

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

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

    def _display_name(self):
        return self.email

    @builder.simple_property
    def system_wide_role(self):
        """For choosing the role string to show to the user; of all the roles in
    the system-wide context, it shows the highest ranked one (if there are
    multiple) or "No Access" if there are none.
    """

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

        from ggrc.utils.user_generator import is_app_2_app_user_email
        if is_app_2_app_user_email(self.email):
            return SystemWideRoles.SUPERUSER

        role_hierarchy = {
            SystemWideRoles.ADMINISTRATOR: 0,
            SystemWideRoles.EDITOR: 1,
            SystemWideRoles.READER: 2,
            SystemWideRoles.CREATOR: 3,
        }
        unique_roles = set([
            user_role.role.name for user_role in self.user_roles
            if user_role.role.name in role_hierarchy
        ])
        if not unique_roles:
            return u"No Access"

        # -1 as default to make items not in this list appear on top
        # and thus shown to the user
        sorted_roles = sorted(unique_roles,
                              key=lambda x: role_hierarchy.get(x, -1))
        return sorted_roles[0]
Exemple #6
0
class ChangeTracked(object):

  """A model with fields to tracked the last user to modify the model, the
  creation time of the model, and the last time the model was updated.
  """
  @declared_attr
  def modified_by_id(cls):  # pylint: disable=no-self-argument
    """Id of user who did the last modification of the object."""
    return db.Column(db.Integer)

  @declared_attr
  def created_at(cls):  # pylint: disable=no-self-argument
    """Date of creation. Set to current time on object creation."""
    column = db.Column(
        db.DateTime,
        nullable=False,
        default=db.text('current_timestamp'),
    )
    return column

  @declared_attr
  def updated_at(cls):  # pylint: disable=no-self-argument
    """Date of last update. Set to current time on object creation/update."""
    column = db.Column(
        db.DateTime,
        nullable=False,
        default=db.text('current_timestamp'),
        onupdate=db.text('current_timestamp'),
    )
    return column

  @declared_attr
  def modified_by(cls):  # pylint: disable=no-self-argument
    """Relationship to user referenced by modified_by_id."""
    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,
    )

  @staticmethod
  def _extra_table_args(model):
    """Apply extra table args (like indexes) to model definition."""
    return (
        db.Index('ix_{}_updated_at'.format(model.__tablename__), 'updated_at'),
    )

  # TODO Add a transaction id, this will be handy for generating etags
  # and for tracking the changes made to several resources together.
  # transaction_id = db.Column(db.Integer)

  # REST properties
  _api_attrs = reflection.ApiAttributes(
      reflection.Attribute('modified_by', create=False, update=False),
      reflection.Attribute('created_at', create=False, update=False),
      reflection.Attribute('updated_at', create=False, update=False),
  )
  _fulltext_attrs = [
      attributes.DatetimeFullTextAttr('created_at', 'created_at'),
      attributes.DatetimeFullTextAttr('updated_at', 'updated_at'),
      attributes.FullTextAttr(
          "modified_by", "modified_by", ["email", "name"]
      ),
  ]

  _aliases = {
      "updated_at": "Last Updated",
      "created_at": "Created Date",
      "modified_by": "Last Updated By",
  }

  @classmethod
  def indexed_query(cls):
    return super(ChangeTracked, cls).indexed_query().options(
        orm.Load(cls).load_only("created_at", "updated_at"),
        orm.Load(cls).joinedload(
            "modified_by"
        ).load_only(
            "name", "email", "id"
        ),
    )
Exemple #7
0
class CycleTaskGroup(mixins.WithContact,
                     mixins.Stateful,
                     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"

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

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

  _publish_attrs = [
      'cycle',
      'task_group',
      'cycle_task_group_tasks',
      'sort_index',
      'next_due_date'
  ]

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

  PROPERTY_TEMPLATE = u"group {}"

  _fulltext_attrs = [
      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')
    )
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
Exemple #9
0
class Risk(synchronizable.Synchronizable,
           synchronizable.RoleableSynchronizable,
           mixins.ExternalCustomAttributable, Relatable, PublicDocumentable,
           comment.ExternalCommentable, mixins.TestPlanned,
           mixins.LastDeprecatedTimeboxed, mixins.base.ContextRBAC,
           mixins.BusinessObject, mixins.Folderable, Indexed, db.Model):
    """Basic Risk model."""
    __tablename__ = 'risks'

    # GGRCQ attributes
    external_id = db.Column(db.Integer, nullable=False)
    due_date = db.Column(db.Date, nullable=True)
    created_by_id = db.Column(db.Integer, nullable=False)
    review_status = deferred(db.Column(db.String, nullable=True), "Risk")
    review_status_display_name = deferred(db.Column(db.String, nullable=True),
                                          "Risk")

    # pylint: disable=no-self-argument
    @declared_attr
    def created_by(cls):
        """Relationship to user referenced by created_by_id."""
        return utils.person_relationship(cls.__name__, "created_by_id")

    last_submitted_at = db.Column(db.DateTime, nullable=True)
    last_submitted_by_id = db.Column(db.Integer, nullable=True)

    @declared_attr
    def last_submitted_by(cls):
        """Relationship to user referenced by last_submitted_by_id."""
        return utils.person_relationship(cls.__name__, "last_submitted_by_id")

    last_verified_at = db.Column(db.DateTime, nullable=True)
    last_verified_by_id = db.Column(db.Integer, nullable=True)

    @declared_attr
    def last_verified_by(cls):
        """Relationship to user referenced by last_verified_by_id."""
        return utils.person_relationship(cls.__name__, "last_verified_by_id")

    # Overriding mixin to make mandatory
    @declared_attr
    def description(cls):
        #  pylint: disable=no-self-argument
        return deferred(db.Column(db.Text, nullable=False, default=u""),
                        cls.__name__)

    risk_type = db.Column(db.Text, nullable=True)
    threat_source = db.Column(db.Text, nullable=True)
    threat_event = db.Column(db.Text, nullable=True)
    vulnerability = db.Column(db.Text, nullable=True)

    @validates('review_status')
    def validate_review_status(self, _, value):
        """Add explicit non-nullable validation."""
        # pylint: disable=no-self-use

        if value is None:
            raise exceptions.ValidationError(
                "Review status for the object is not specified")

        return value

    @validates('review_status_display_name')
    def validate_review_status_display_name(self, _, value):
        """Add explicit non-nullable validation."""
        # pylint: disable=no-self-use
        # pylint: disable=invalid-name

        if value is None:
            raise exceptions.ValidationError(
                "Review status display for the object is not specified")

        return value

    _sanitize_html = [
        'risk_type', 'threat_source', 'threat_event', 'vulnerability'
    ]

    _fulltext_attrs = [
        'risk_type', 'threat_source', 'threat_event', 'vulnerability',
        'review_status_display_name',
        attributes.DateFullTextAttr('due_date', 'due_date'),
        attributes.DatetimeFullTextAttr('last_submitted_at',
                                        'last_submitted_at'),
        attributes.DatetimeFullTextAttr('last_verified_at',
                                        'last_verified_at'),
        attributes.FullTextAttr("created_by", "created_by", ["email", "name"]),
        attributes.FullTextAttr("last_submitted_by", "last_submitted_by",
                                ["email", "name"]),
        attributes.FullTextAttr("last_verified_by", "last_verified_by",
                                ["email", "name"])
    ]

    _custom_publish = {
        'created_by': ggrc_utils.created_by_stub,
        'last_submitted_by': ggrc_utils.last_submitted_by_stub,
        'last_verified_by': ggrc_utils.last_verified_by_stub,
    }

    _api_attrs = reflection.ApiAttributes(
        'risk_type',
        'threat_source',
        'threat_event',
        'vulnerability',
        'external_id',
        'due_date',
        reflection.ExternalUserAttribute('created_by', force_create=True),
        reflection.ExternalUserAttribute('last_submitted_by',
                                         force_create=True),
        reflection.ExternalUserAttribute('last_verified_by',
                                         force_create=True),
        'last_submitted_at',
        'last_verified_at',
        'external_slug',
        'review_status',
        'review_status_display_name',
    )

    _aliases = {
        "description": {
            "display_name": "Description",
            "mandatory": True
        },
        "risk_type": {
            "display_name": "Risk Type",
            "mandatory": False
        },
        "threat_source": {
            "display_name": "Threat Source",
            "mandatory": False
        },
        "threat_event": {
            "display_name": "Threat Event",
            "mandatory": False
        },
        "vulnerability": {
            "display_name": "Vulnerability",
            "mandatory": False
        },
        "documents_file": None,
        "status": {
            "display_name":
            "State",
            "mandatory":
            False,
            "description":
            "Options are: \n {}".format('\n'.join(
                mixins.BusinessObject.VALID_STATES))
        },
        "review_status": {
            "display_name": "Review State",
            "mandatory": False,
            "filter_only": True,
        },
        "review_status_display_name": {
            "display_name": "Review Status",
            "mandatory": False,
        },
        "due_date": {
            "display_name": "Due Date",
            "mandatory": False,
        },
        "created_by": {
            "display_name": "Created By",
            "mandatory": False,
        },
        "last_submitted_at": {
            "display_name": "Last Owner Reviewed Date",
            "mandatory": False,
        },
        "last_submitted_by": {
            "display_name": "Last Owner Reviewed By",
            "mandatory": False,
        },
        "last_verified_at": {
            "display_name": "Last Compliance Reviewed Date",
            "mandatory": False,
        },
        "last_verified_by": {
            "display_name": "Last Compliance Reviewed By",
            "mandatory": False,
        },
    }

    def log_json(self):
        res = super(Risk, self).log_json()
        res["created_by"] = ggrc_utils.created_by_stub(self)
        res["last_submitted_by"] = ggrc_utils.last_submitted_by_stub(self)
        res["last_verified_by"] = ggrc_utils.last_verified_by_stub(self)
        return res
Exemple #10
0
class ChangeTracked(object):
    """A model with fields to tracked the last user to modify the model, the
  creation time of the model, and the last time the model was updated.
  """
    @declared_attr
    def modified_by_id(cls):
        """Id of user who did the last modification of the object."""
        return deferred(db.Column(db.Integer), cls.__name__)

    @declared_attr
    def created_at(cls):
        """Date of creation. Set to current time on object creation."""
        column = db.Column(
            db.DateTime,
            nullable=False,
            default=db.text('current_timestamp'),
        )
        return deferred(column, cls.__name__)

    @declared_attr
    def updated_at(cls):
        """Date of last update. Set to current time on object creation/update."""
        column = db.Column(
            db.DateTime,
            nullable=False,
            default=db.text('current_timestamp'),
            onupdate=db.text('current_timestamp'),
        )
        return deferred(column, cls.__name__)

    @declared_attr
    def modified_by(cls):
        """Relationship to user referenced by modified_by_id."""
        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,
        )

    @staticmethod
    def _extra_table_args(model):
        """Apply extra table args (like indexes) to model definition."""
        return (db.Index('ix_{}_updated_at'.format(model.__tablename__),
                         'updated_at'), )

    # TODO Add a transaction id, this will be handy for generating etags
    # and for tracking the changes made to several resources together.
    # transaction_id = db.Column(db.Integer)

    # REST properties
    _publish_attrs = [
        'modified_by',
        'created_at',
        'updated_at',
    ]
    _fulltext_attrs = [
        attributes.DatetimeFullTextAttr('created_at', 'created_at'),
        attributes.DatetimeFullTextAttr('updated_at', 'updated_at'),
        attributes.FullTextAttr("modified_by", "modified_by",
                                ["name", "email"]),
    ]

    _update_attrs = []
    _aliases = {
        "updated_at": {
            "display_name": "Last Updated",
            "filter_only": True,
        },
        "created_at": {
            "display_name": "Created Date",
            "filter_only": True,
        },
    }
Exemple #11
0
class WithContact(object):
    """Mixin that defines `contact` and `secondary_contact` fields."""
    @declared_attr
    def contact_id(cls):
        return deferred(db.Column(db.Integer, db.ForeignKey('people.id')),
                        cls.__name__)

    @declared_attr
    def secondary_contact_id(cls):
        return deferred(db.Column(db.Integer, db.ForeignKey('people.id')),
                        cls.__name__)

    @declared_attr
    def contact(cls):
        return db.relationship('Person',
                               uselist=False,
                               foreign_keys='{}.contact_id'.format(
                                   cls.__name__))

    @declared_attr
    def secondary_contact(cls):
        return db.relationship('Person',
                               uselist=False,
                               foreign_keys='{}.secondary_contact_id'.format(
                                   cls.__name__))

    @staticmethod
    def _extra_table_args(model):
        return (
            db.Index('fk_{}_contact'.format(model.__tablename__),
                     'contact_id'),
            db.Index('fk_{}_secondary_contact'.format(model.__tablename__),
                     'secondary_contact_id'),
        )

    _publish_attrs = ['contact', 'secondary_contact']
    _fulltext_attrs = [
        attributes.FullTextAttr("contact", "contact", ["name", "email"]),
        attributes.FullTextAttr('secondary_contact', 'secondary_contact',
                                ["name", "email"]),
    ]
    _aliases = {
        "contact": {
            "display_name": "Primary Contact",
            "filter_by": "_filter_by_contact",
        },
        "secondary_contact": {
            "display_name": "Secondary Contact",
            "filter_by": "_filter_by_secondary_contact",
        },
    }

    @classmethod
    def _filter_by_contact(cls, predicate):
        # dependency cycle mixins.py <~> person.py
        from ggrc.models.person import Person
        return Person.query.filter((Person.id == cls.contact_id)
                                   & (predicate(Person.name)
                                      | predicate(Person.email))).exists()

    @classmethod
    def _filter_by_secondary_contact(cls, predicate):
        # dependency cycle mixins.py <~> person.py
        from ggrc.models.person import Person
        return Person.query.filter((Person.id == cls.secondary_contact_id)
                                   & (predicate(Person.name)
                                      | predicate(Person.email))).exists()
Exemple #12
0
class CycleTaskGroupObjectTask(mixins.WithContact,
                               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',
        'contact',
    )

    @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("assignee", 'contact', ['name', 'email']),
        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"]),
    ]

    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

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

    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,
        },
        "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."""
        current_user_id = login.get_current_user_id()

        # pylint: disable=not-an-iterable
        return (current_user_id == self.contact_id
                or current_user_id in [ur.person_id for ur in self.wfo_roles])

    @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("contact").load_only(
                "email", "name", "id"),
        )
Exemple #13
0
class Control(WithLastAssessmentDate, HasObjectState, Relatable,
              CustomAttributable, Personable, ControlCategorized,
              AssertionCategorized, Hierarchical, Timeboxed, Ownable,
              Auditable, TestPlanned, BusinessObject, Indexed, db.Model):
  __tablename__ = 'controls'

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

  principal_assessor = db.relationship(
      'Person', uselist=False, foreign_keys='Control.principal_assessor_id')
  secondary_assessor = db.relationship(
      'Person', uselist=False, foreign_keys='Control.secondary_assessor_id')

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

  @staticmethod
  def _extra_table_args(_):
    return (
        db.Index('ix_controls_principal_assessor', 'principal_assessor_id'),
        db.Index('ix_controls_secondary_assessor', 'secondary_assessor_id'),
    )

  # REST properties
  _publish_attrs = [
      'active',
      'company_control',
      'directive',
      'documentation_description',
      'fraud_related',
      'key_control',
      'kind',
      'means',
      'verify_frequency',
      'version',
      'principal_assessor',
      'secondary_assessor',
  ]

  _fulltext_attrs = [
      'active',
      'company_control',
      'directive',
      'documentation_description',
      'fraud_related',
      'key_control',
      'kind',
      'means',
      'verify_frequency',
      'version',
      attributes.FullTextAttr(
          "principal_assessor",
          "principal_assessor",
          ["name", "email"]
      ),
      attributes.FullTextAttr(
          'secondary_assessor',
          'secondary_assessor',
          ["name", "email"]),
  ]

  _sanitize_html = [
      'documentation_description',
      'version',
  ]

  _include_links = []

  _aliases = {
      "url": "Control URL",
      "kind": {
          "display_name": "Kind/Nature",
          "filter_by": "_filter_by_kind",
      },
      "means": {
          "display_name": "Type/Means",
          "filter_by": "_filter_by_means",
      },
      "verify_frequency": {
          "display_name": "Frequency",
          "filter_by": "_filter_by_verify_frequency",
      },
      "fraud_related": "Fraud Related",
      "principal_assessor": {
          "display_name": "Principal Assignee",
      },
      "secondary_assessor": {
          "display_name": "Secondary Assignee",
      },
      "key_control": {
          "display_name": "Significance",
          "description": "Allowed values are:\nkey\nnon-key\n---",
      }
  }

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

  @classmethod
  def _filter_by_kind(cls, predicate):
    return Option.query.filter(
        (Option.id == cls.kind_id) & predicate(Option.title)
    ).exists()

  @classmethod
  def _filter_by_means(cls, predicate):
    return Option.query.filter(
        (Option.id == cls.means_id) & predicate(Option.title)
    ).exists()

  @classmethod
  def _filter_by_verify_frequency(cls, predicate):
    return Option.query.filter(
        (Option.id == cls.verify_frequency_id) & predicate(Option.title)
    ).exists()

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

  def log_json(self):
    out_json = super(Control, self).log_json()
    # so that event log can refer to deleted directive
    if self.directive:
      out_json["mapped_directive"] = self.directive.display_name
    return out_json
Exemple #14
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'
  )
  next_due_date = db.Column(db.Date)

  _api_attrs = reflection.ApiAttributes(
      'cycle',
      'task_group',
      'cycle_task_group_tasks',
      '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),
      attributes.MultipleSubpropertyFullTextAttr(
          "task title",
          "cycle_task_group_tasks",
          ["title"],
          False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task assignees",
          "_task_assignees",
          ["email", "name"],
          False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task state",
          "cycle_task_group_tasks",
          ["status"],
          False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task secondary assignees",
          "_task_secondary_assignees",
          ["email", "name"],
          False),
      attributes.DateMultipleSubpropertyFullTextAttr(
          "task due date",
          "cycle_task_group_tasks",
          ["end_date"],
          False),
      attributes.MultipleSubpropertyFullTextAttr(
          "task comment",
          lambda instance: itertools.chain(*[
              t.comments for t in instance.cycle_task_group_tasks
          ]),
          ["description"],
          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(
          "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",
            "status",
        ),
        orm.Load(cls).subqueryload("cycle_task_group_tasks").subqueryload(
            "_access_control_list",
        ).load_only(
            "ac_role_id",
        ).subqueryload(
            "access_control_people",
        ).load_only(
            "person_id",
        ),
        orm.Load(cls).subqueryload("cycle_task_group_tasks"),
        orm.Load(cls).joinedload("cycle").load_only(
            "id",
            "title",
            "next_due_date",
        ),
        orm.Load(cls).joinedload("cycle").joinedload(
            "contact"
        ).load_only(
            "name",
            "email",
            "id",
        ),
        orm.Load(cls).joinedload("contact").load_only(
            "name",
            "email",
            "id",
        ),
    )

  @classmethod
  def eager_query(cls, **kwargs):
    """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(**kwargs)
    return query.options(
        orm.subqueryload("cycle_task_group_tasks"),
        orm.joinedload("cycle").undefer_group("Cycle_complete"),
        orm.joinedload("cycle").joinedload("contact")
    )
Exemple #15
0
class Control(WithLastAssessmentDate, HasObjectState, Roleable, Relatable,
              CustomAttributable, Personable, ControlCategorized,
              PublicDocumentable, AssertionCategorized, Hierarchical,
              LastDeprecatedTimeboxed, Auditable, TestPlanned, BusinessObject,
              Indexed, db.Model):
    __tablename__ = 'controls'

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

    principal_assessor = db.relationship(
        'Person', uselist=False, foreign_keys='Control.principal_assessor_id')
    secondary_assessor = db.relationship(
        'Person', uselist=False, foreign_keys='Control.secondary_assessor_id')

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

    @staticmethod
    def _extra_table_args(_):
        return (
            db.Index('ix_controls_principal_assessor',
                     'principal_assessor_id'),
            db.Index('ix_controls_secondary_assessor',
                     'secondary_assessor_id'),
        )

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

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

    _sanitize_html = [
        'documentation_description',
        'version',
    ]

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

    _include_links = []

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

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

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

    def log_json(self):
        out_json = super(Control, self).log_json()
        # so that event log can refer to deleted directive
        if self.directive:
            out_json["mapped_directive"] = self.directive.display_name
        return out_json
class CycleTaskGroupObjectTask(
        roleable.Roleable, wf_mixins.CycleTaskStatusValidatedMixin,
        wf_mixins.WorkflowCommentable, mixins.WithLastDeprecatedDate,
        mixins.Timeboxed, relationship.Relatable, mixins.Notifiable,
        mixins.Described, mixins.Titled, mixins.Slugged, mixins.Base,
        base.ContextRBAC, ft_mixin.Indexed, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'

    readable_name_alias = 'cycle task'

    _title_uniqueness = False

    IMPORTABLE_FIELDS = (
        'slug',
        'title',
        'description',
        'start_date',
        'end_date',
        'finished_date',
        'verified_date',
        'status',
        '__acl__:Task Assignees',
        '__acl__:Task Secondary Assignees',
    )

    @classmethod
    def generate_slug_prefix(cls):
        return "CYCLETASK"

    # Note: this statuses are used in utils/query_helpers to filter out the tasks
    # that should be visible on My Tasks pages.

    PROPERTY_TEMPLATE = u"task {}"

    _fulltext_attrs = [
        ft_attributes.DateFullTextAttr(
            "end_date",
            'end_date',
        ),
        ft_attributes.FullTextAttr("group title", 'cycle_task_group',
                                   ['title'], False),
        ft_attributes.FullTextAttr("object_approval",
                                   'object_approval',
                                   with_template=False),
        ft_attributes.FullTextAttr("cycle title", 'cycle', ['title'], False),
        ft_attributes.FullTextAttr("group assignee",
                                   lambda x: x.cycle_task_group.contact,
                                   ['email', 'name'], False),
        ft_attributes.FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                                   ['email', 'name'], False),
        ft_attributes.DateFullTextAttr(
            "group due date",
            lambda x: x.cycle_task_group.next_due_date,
            with_template=False),
        ft_attributes.DateFullTextAttr("cycle due date",
                                       lambda x: x.cycle.next_due_date,
                                       with_template=False),
        ft_attributes.MultipleSubpropertyFullTextAttr("comments",
                                                      "cycle_task_entries",
                                                      ["description"]),
        ft_attributes.BooleanFullTextAttr("needs verification",
                                          "is_verification_needed",
                                          with_template=False,
                                          true_value="Yes",
                                          false_value="No"),
        "folder",
    ]

    # The app should not pass to the json representation of
    # relationships to the internal models
    IGNORED_RELATED_TYPES = ["CalendarEvent"]

    _custom_publish = {
        "related_sources":
        lambda obj: [
            rel.log_json() for rel in obj.related_sources
            if rel.source_type not in obj.IGNORED_RELATED_TYPES
        ],
        "related_destinations":
        lambda obj: [
            rel.log_json() for rel in obj.related_destinations
            if rel.destination_type not in obj.IGNORED_RELATED_TYPES
        ]
    }

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

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

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

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

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

    @hybrid.hybrid_property
    def cycle_task_group(self):
        """Getter for cycle task group foreign key."""
        return self._cycle_task_group

    @cycle_task_group.setter
    def cycle_task_group(self, cycle_task_group):
        """Setter for cycle task group foreign key."""
        if not self._cycle_task_group and cycle_task_group:
            relationship.Relationship(source=cycle_task_group,
                                      destination=self)
        self._cycle_task_group = cycle_task_group

    @hybrid.hybrid_property
    def object_approval(self):
        return self.cycle.workflow.object_approval

    @object_approval.expression
    def object_approval(cls):  # pylint: disable=no-self-argument
        return sa.select([
            Workflow.object_approval,
        ]).where(
            sa.and_(
                (Cycle.id == cls.cycle_id),
                (Cycle.workflow_id == Workflow.id))).label('object_approval')

    @builder.simple_property
    def folder(self):
        """Simple property for cycle folder."""
        if self.cycle:
            return self.cycle.folder
        return ""

    @builder.simple_property
    def is_in_history(self):
        """Used on UI to disable editing finished CycleTask which is in history"""
        return not self.cycle.is_current

    @property
    def cycle_task_objects_for_cache(self):
        """Get all related objects for this CycleTaskGroupObjectTask

    Returns:
      List of tuples with (related_object_type, related_object_id)
    """
        return [(object_.__class__.__name__, object_.id)
                for object_ in self.related_objects()]

    _api_attrs = reflection.ApiAttributes(
        'cycle',
        'cycle_task_group',
        'task_group_task',
        'cycle_task_entries',
        'sort_index',
        'task_type',
        'response_options',
        'selected_response_options',
        reflection.Attribute('related_sources', create=False, update=False),
        reflection.Attribute('related_destinations',
                             create=False,
                             update=False),
        reflection.Attribute('object_approval', create=False, update=False),
        reflection.Attribute('finished_date', create=False, update=False),
        reflection.Attribute('verified_date', create=False, update=False),
        reflection.Attribute('allow_change_state', create=False, update=False),
        reflection.Attribute('folder', create=False, update=False),
        reflection.Attribute('workflow', create=False, update=False),
        reflection.Attribute('workflow_title', create=False, update=False),
        reflection.Attribute('cycle_task_group_title',
                             create=False,
                             update=False),
        reflection.Attribute('is_in_history', create=False, update=False),
    )

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

    _aliases = {
        "title": "Summary",
        "description": "Task Details",
        "finished_date": {
            "display_name":
            "Actual Finish Date",
            "description": ("Make sure that 'Actual Finish Date' isn't set, "
                            "if cycle task state is <'Assigned' / "
                            "'In Progress' / 'Declined' / 'Deprecated'>. "
                            "Type double dash '--' into "
                            "'Actual Finish Date' cell to remove it.")
        },
        "verified_date": {
            "display_name":
            "Actual Verified Date",
            "description": ("Make sure that 'Actual Verified Date' isn't set, "
                            "if cycle task state is <'Assigned' / "
                            "'In Progress' / 'Declined' / 'Deprecated' / "
                            "'Finished'>. Type double dash '--' into "
                            "'Actual Verified Date' cell to remove it.")
        },
        "cycle": {
            "display_name": "Cycle",
            "filter_by": "_filter_by_cycle",
        },
        "cycle_task_group": {
            "display_name": "Task Group",
            "mandatory": True,
            "filter_by": "_filter_by_cycle_task_group",
        },
        "task_type": {
            "display_name": "Task Type",
            "mandatory": True,
        },
        "end_date": "Due Date",
        "start_date": "Start Date",
    }

    @builder.simple_property
    def cycle_task_group_title(self):
        """Property. Returns parent CycleTaskGroup title."""
        return self.cycle_task_group.title

    @builder.simple_property
    def workflow_title(self):
        """Property. Returns parent Workflow's title."""
        return self.workflow.title

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

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

    def current_user_wfa_or_assignee(self):
        """Current user is WF Admin, Assignee or Secondary Assignee for self."""
        wfa_ids = self.workflow.get_person_ids_for_rolename("Admin")
        ta_ids = self.get_person_ids_for_rolename("Task Assignees")
        tsa_ids = self.get_person_ids_for_rolename("Task Secondary Assignees")
        return login.get_current_user_id() in set().union(
            wfa_ids, ta_ids, tsa_ids)

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

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

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

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

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

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

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

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

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

    @classmethod
    def indexed_query(cls):
        return super(CycleTaskGroupObjectTask, cls).indexed_query().options(
            orm.Load(cls).load_only("end_date", "start_date", "created_at",
                                    "updated_at"),
            orm.Load(cls).joinedload("cycle_task_group").load_only(
                "id",
                "title",
                "end_date",
                "next_due_date",
            ),
            orm.Load(cls).joinedload("cycle").load_only(
                "id",
                "title",
                "next_due_date",
                "is_verification_needed",
            ),
            orm.Load(cls).joinedload("cycle_task_group").joinedload(
                "contact").load_only("email", "name", "id"),
            orm.Load(cls).joinedload("cycle").joinedload("contact").load_only(
                "email", "name", "id"),
            orm.Load(cls).subqueryload("cycle_task_entries").load_only(
                "description", "id"),
            orm.Load(cls).joinedload("cycle").joinedload(
                "workflow").undefer_group("Workflow_complete"),
        )

    def log_json(self):
        out_json = super(CycleTaskGroupObjectTask, self).log_json()
        out_json["folder"] = self.folder
        return out_json

    @classmethod
    def bulk_update(cls, src):
        """Update statuses for bunch of tasks in a bulk.

    Args:
        src: input json with next structure:
          [{"status": "Assigned", "id": 1}, {"status": "In Progress", "id": 2}]

    Returns:
        list of updated_instances
    """
        new_prv_state_map = {
            cls.DEPRECATED: (cls.ASSIGNED, cls.IN_PROGRESS, cls.FINISHED,
                             cls.VERIFIED, cls.DECLINED),
            cls.IN_PROGRESS: (cls.ASSIGNED, ),
            cls.FINISHED: (cls.IN_PROGRESS, cls.DECLINED),
            cls.VERIFIED: (cls.FINISHED, ),
            cls.DECLINED: (cls.FINISHED, ),
            cls.ASSIGNED: (),
        }
        uniq_states = set([item['state'] for item in src])
        if len(list(uniq_states)) != 1:
            raise BadRequest("Request's JSON contains multiple statuses for "
                             "CycleTasks")
        new_state = uniq_states.pop()
        LOGGER.info("Do bulk update CycleTasks with '%s' status", new_state)
        if new_state not in cls.VALID_STATES:
            raise BadRequest("Request's JSON contains invalid statuses for "
                             "CycleTasks")
        prv_states = new_prv_state_map[new_state]
        all_ids = {item['id'] for item in src}
        # Eagerly loading is needed to get user permissions for CycleTask faster
        updatable_objects = cls.eager_query().filter(
            cls.id.in_(list(all_ids)), cls.status.in_(prv_states))
        if new_state in (cls.VERIFIED, cls.DECLINED):
            updatable_objects = [
                obj for obj in updatable_objects
                if obj.cycle.is_verification_needed
            ]
        # Bulk update works only on MyTasks page. Don't need to check for
        # WorkflowMembers' permissions here. User should update only his own tasks.
        updatable_objects = [
            obj for obj in updatable_objects
            if obj.current_user_wfa_or_assignee()
        ]
        # Queries count is constant because we are using eager query for objects.
        for obj in updatable_objects:
            obj.status = new_state
            obj.modified_by_id = login.get_current_user_id()
        return updatable_objects
Exemple #17
0
class CycleTaskGroup(roleable.Roleable, 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),
    ]

    @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.joinedload('cycle_task_group_tasks'))