コード例 #1
0
ファイル: mysql.py プロジェクト: IanMadlenya/ggrc-core
class MysqlRecordProperty(db.Model):
    __tablename__ = 'fulltext_record_properties'

    key = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.String(64), primary_key=True)
    context_id = db.Column(db.Integer)
    tags = db.Column(db.String)
    property = db.Column(db.String(64), primary_key=True)
    subproperty = db.Column(db.String(64), primary_key=True)
    content = db.Column(db.Text)

    @declared_attr
    def __table_args__(self):
        return (
            # NOTE
            # This is here to prevent Alembic from wanting to drop the index, but
            # the DDL below or a similar Alembic migration should be used to create
            # the index.
            db.Index('{}_text_idx'.format(self.__tablename__), 'content'),
            # These are real indexes
            db.Index('ix_{}_key'.format(self.__tablename__), 'key'),
            db.Index('ix_{}_type'.format(self.__tablename__), 'type'),
            db.Index('ix_{}_tags'.format(self.__tablename__), 'tags'),
            db.Index('ix_{}_context_id'.format(self.__tablename__),
                     'context_id'),
            # Only MyISAM supports fulltext indexes until newer MySQL/MariaDB
            {
                'mysql_engine': 'myisam'
            },
        )
コード例 #2
0
class MysqlRecordProperty(db.Model):
    __tablename__ = 'fulltext_record_properties'
    __table_args__ = {'mysql_engine': 'myisam'}

    key = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.String(64), primary_key=True)
    tags = db.Column(db.String)
    property = db.Column(db.String(64), primary_key=True)
    content = db.Column(db.Text)
コード例 #3
0
class Role(base.ContextRBAC, Base, Described, db.Model):
    """A user role. All roles have a unique name. This name could be a simple
  string, an email address, or some other form of string identifier.

  Example:

  ..  code-block:: python

      {
        'create': ['Program', 'Control'],
        'read': ['Program', 'Control'],
        'update': ['Program', 'Control'],
        'delete': ['Program'],
      }

  """
    __tablename__ = 'roles'

    name = db.Column(db.String(128), nullable=False)
    permissions_json = db.Column(db.Text(), nullable=False)
    scope = db.Column(db.String(64), nullable=True)
    role_order = db.Column(db.Integer(), nullable=True)

    @simple_property
    def permissions(self):
        if self.permissions_json == DECLARED_ROLE:
            declared_role = get_declared_role(self.name)
            permissions = declared_role.permissions
        else:
            permissions = json.loads(self.permissions_json) or {}
        # make sure not to omit actions
        for action in ['create', 'read', 'update', 'delete']:
            if action not in permissions:
                permissions[action] = []
        return permissions

    @permissions.setter
    def permissions(self, value):
        self.permissions_json = json.dumps(value)

    _api_attrs = reflection.ApiAttributes(
        'name',
        'permissions',
        'scope',
        'role_order',
    )

    def _display_name(self):
        return self.name
コード例 #4
0
class Context(Base, db.Model):
  """Context class. Sign permissions object for specific user."""
  __tablename__ = 'contexts'

  # This block and the 'description' attrs are taken from the Described mixin
  #  which we do not use for Context because indexing Context descriptions
  #  for fulltext search leads to undesirable results
  @declared_attr
  def description(cls):  # pylint: disable=no-self-argument
    return deferred(db.Column(db.Text, nullable=False, default=u""),
                    cls.__name__)

  name = deferred(db.Column(db.String(128), nullable=True), 'Context')
  related_object_id = deferred(
      db.Column(db.Integer(), nullable=True), 'Context')
  related_object_type = deferred(
      db.Column(db.String(128), nullable=True), 'Context')

  @property
  def related_object_attr(self):
    return '{0}_related_object'.format(self.related_object_type)

  @property
  def related_object(self):
    return getattr(self, self.related_object_attr)

  @related_object.setter
  def related_object(self, value):
    self.related_object_id = value.id if value is not None else None
    self.related_object_type = value.__class__.__name__ if value is not None \
        else None
    return setattr(self, self.related_object_attr, value)

  @staticmethod
  def _extra_table_args(_):
    return (
        db.Index(
            'ix_context_related_object',
            'related_object_type', 'related_object_id'),
    )

  _api_attrs = reflection.ApiAttributes('name',
                                        'related_object',
                                        'description')

  _sanitize_html = ['name', 'description']
  _include_links = []
コード例 #5
0
ファイル: mysql.py プロジェクト: zdqf/ggrc-core
class MysqlRecordProperty(db.Model):
    """ Db model for collect fulltext index records"""
    __tablename__ = 'fulltext_record_properties'

    key = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.String(64), primary_key=True)
    tags = db.Column(db.String)
    property = db.Column(db.String(250), primary_key=True)
    subproperty = db.Column(db.String(64), primary_key=True)
    content = db.Column(db.Text, nullable=False, default=u"")

    @declared_attr
    def __table_args__(cls):  # pylint: disable=no-self-argument
        return (
            db.Index('ix_{}_tags'.format(cls.__tablename__), 'tags'),
            db.Index('ix_{}_key'.format(cls.__tablename__), 'key'),
            db.Index('ix_{}_type'.format(cls.__tablename__), 'type'),
        )
コード例 #6
0
class Context(Base, db.Model):
  __tablename__ = 'contexts'

  # This block and the 'description' attrs are taken from the Described mixin
  #  which we do not use for Context because indexing Context descriptions
  #  for fulltext search leads to undesirable results
  @declared_attr
  def description(cls):
    return deferred(db.Column(db.Text), cls.__name__)

  name = deferred(db.Column(db.String(128), nullable=True), 'Context')
  related_object_id = deferred(
      db.Column(db.Integer(), nullable=True), 'Context')
  related_object_type = deferred(
      db.Column(db.String(128), nullable=True), 'Context')

  @property
  def related_object_attr(self):
    return '{0}_related_object'.format(self.related_object_type)

  @property
  def related_object(self):
    return getattr(self, self.related_object_attr)

  @related_object.setter
  def related_object(self, value):
    self.related_object_id = value.id if value is not None else None
    self.related_object_type = value.__class__.__name__ if value is not None \
        else None
    return setattr(self, self.related_object_attr, value)

  @staticmethod
  def _extra_table_args(cls):
    return (
        db.Index(
            'ix_context_related_object',
            'related_object_type', 'related_object_id'),
    )

  _publish_attrs = ['name', 'related_object', 'description']
  _sanitize_html = ['name', 'description']
  _include_links = []
コード例 #7
0
class MysqlRecordProperty(db.Model):
    """ Db model for collect fulltext index records"""
    __tablename__ = 'fulltext_record_properties'

    key = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.String(64), primary_key=True)
    context_id = db.Column(db.Integer)
    tags = db.Column(db.String)
    property = db.Column(db.String(250), primary_key=True)
    subproperty = db.Column(db.String(64), primary_key=True)
    content = db.Column(db.Text)

    @declared_attr
    def __table_args__(self):
        return (
            db.Index('ix_{}_tags'.format(self.__tablename__), 'tags'),
            db.Index('ix_{}_key'.format(self.__tablename__), 'key'),
            db.Index('ix_{}_type'.format(self.__tablename__), 'type'),
            db.Index('ix_{}_context_id'.format(self.__tablename__),
                     'context_id'),
        )
コード例 #8
0
ファイル: task_group.py プロジェクト: valchanin/ggrc-core
class TaskGroup(WithContact, Timeboxed, Described, Titled, Slugged, Indexed,
                db.Model):
    """Workflow TaskGroup model."""

    __tablename__ = 'task_groups'
    _title_uniqueness = False

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

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

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

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

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

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

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

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

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

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

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

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

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

        return target

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

        return target

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

        return target

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

    @classmethod
    def _filter_by_objects(cls, predicate):
        parts = []
        for model_name in all_models.__all__:
            model = getattr(all_models, model_name)
            query = getattr(model, "query", None)
            field = getattr(model, "slug", getattr(model, "email", None))
            if query is None or field is None or not hasattr(model, "id"):
                continue
            parts.append(
                query.filter((TaskGroupObject.object_type == model_name)
                             & (model.id == TaskGroupObject.object_id)
                             & predicate(field)).exists())
        return TaskGroupObject.query.filter(
            (TaskGroupObject.task_group_id == cls.id) & or_(*parts)).exists()
コード例 #9
0
class CycleTaskGroupObjectTask(WithContact, Stateful, Timeboxed, Relatable,
                               Notifiable, Described, Titled, Slugged, Base,
                               Indexed, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'
    _title_uniqueness = False

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

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

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

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

    PROPERTY_TEMPLATE = u"task {}"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @classmethod
    def indexed_query(cls):
        return super(CycleTaskGroupObjectTask, cls).indexed_query().options(
            orm.Load(cls).load_only("end_date", "start_date", "created_at",
                                    "updated_at"),
            orm.Load(cls).joinedload("cycle_task_group").load_only(
                "id",
                "title",
                "end_date",
                "next_due_date",
            ),
            orm.Load(cls).joinedload("cycle").load_only(
                "id", "title", "next_due_date"),
            orm.Load(cls).joinedload("cycle_task_group").joinedload(
                "contact").load_only("email", "name", "id"),
            orm.Load(cls).joinedload("cycle").joinedload("contact").load_only(
                "email", "name", "id"),
            orm.Load(cls).subqueryload("cycle_task_entries").load_only(
                "description", "id"),
            orm.Load(cls).joinedload("contact").load_only(
                "email", "name", "id"),
        )
コード例 #10
0
class TaskGroup(roleable.Roleable, relationship.Relatable, WithContact,
                Timeboxed, Described, Titled, base.ContextRBAC, Slugged,
                Indexed, db.Model):
    """Workflow TaskGroup model."""

    __tablename__ = 'task_groups'
    _title_uniqueness = False

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

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

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

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

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

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

    _api_attrs = reflection.ApiAttributes(
        'workflow',
        'task_group_objects',
        reflection.Attribute('objects', create=False, update=False),
        'task_group_tasks',
        'lock_task_order',
        'sort_index',
        # Intentionally do not include `cycle_task_groups`
        # 'cycle_task_groups',
    )

    _aliases = {
        "title": "Summary",
        "description": "Details",
        "contact": {
            "display_name":
            "Assignee",
            "mandatory":
            True,
            "description": ("One person could be added "
                            "as a Task Group assignee")
        },
        "secondary_contact": None,
        "start_date": None,
        "end_date": None,
        "workflow": {
            "display_name": "Workflow",
            "mandatory": True,
            "filter_by": "_filter_by_workflow",
        },
        "task_group_objects": {
            "display_name": "Objects",
            "type": AttributeInfo.Type.SPECIAL_MAPPING,
            "filter_by": "_filter_by_objects",
        },
    }

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

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

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

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

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

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

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

        target.ensure_assignee_is_workflow_member()

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

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

        return target

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

        return target

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

        return target

    @classmethod
    def eager_query(cls):
        query = super(TaskGroup, cls).eager_query()
        return query.options(
            orm.Load(cls).subqueryload('task_group_objects'),
            orm.Load(cls).subqueryload('task_group_tasks'))

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

    @classmethod
    def _filter_by_objects(cls, predicate):
        parts = []
        for model_name in all_models.__all__:
            model = getattr(all_models, model_name)
            query = getattr(model, "query", None)
            field = getattr(model, "slug", getattr(model, "email", None))
            if query is None or field is None or not hasattr(model, "id"):
                continue
            parts.append(
                query.filter((TaskGroupObject.object_type == model_name)
                             & (model.id == TaskGroupObject.object_id)
                             & predicate(field)).exists())
        return TaskGroupObject.query.filter(
            (TaskGroupObject.task_group_id == cls.id) & or_(*parts)).exists()
コード例 #11
0
ファイル: audit.py プロジェクト: nanohaikaros/ggrc-core
class Audit(Snapshotable, clonable.Clonable, PublicDocumentable,
            CustomAttributable, Personable, HasOwnContext, Relatable,
            Timeboxed, WithContact, BusinessObject, Indexed, db.Model):
    """Audit model."""

    __tablename__ = 'audits'
    _slug_uniqueness = False

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

    CLONEABLE_CHILDREN = {"AssessmentTemplate"}

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

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

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

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

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

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

    _include_links = []

    _aliases = {
        "program": {
            "display_name": "Program",
            "filter_by": "_filter_by_program",
            "mandatory": True,
        },
        "user_role:Auditor": {
            "display_name": "Auditors",
            "type": AttributeInfo.Type.USER_ROLE,
            "filter_by": "_filter_by_auditor",
        },
        "start_date": "Planned Start Date",
        "end_date": "Planned End Date",
        "report_start_date": "Planned Report Period from",
        "report_end_date": "Planned Report Period to",
        "contact": {
            "display_name": "Audit Captain",
            "mandatory": True,
        },
        "secondary_contact": None,
        "notes": None,
        "reference_url": None,
        "archived": {
            "display_name": "Archived",
            "mandatory": False
        },
        "status": {
            "display_name": "Status",
            "mandatory": True,
            "description": "Options are:\n{}".format('\n'.join(VALID_STATES))
        }
    }

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

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

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

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

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

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

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

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

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

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

    Children that can be cloned should be specified in CLONEABLE_CHILDREN.

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

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

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

            for obj in related_children:
                obj.clone(self)

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

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

    @classmethod
    def eager_query(cls):
        query = super(Audit, cls).eager_query()
        return query.options(
            orm.joinedload('program'),
            orm.subqueryload('object_people').joinedload('person'),
            orm.subqueryload('audit_objects'),
        )
コード例 #12
0
class Audit(Snapshotable,
            clonable.SingleClonable,
            WithEvidence,
            mixins.CustomAttributable,
            Personable,
            HasOwnContext,
            Relatable,
            Roleable,
            issue_tracker_mixins.IssueTrackedWithConfig,
            issue_tracker_mixins.IssueTrackedWithPeopleSync,
            WithLastDeprecatedDate,
            mixins.Timeboxed,
            base.ContextRBAC,
            mixins.BusinessObject,
            mixins.Folderable,
            rest_handable_mixins.WithDeleteHandable,
            Indexed,
            db.Model):
  """Audit model."""

  __tablename__ = 'audits'
  _slug_uniqueness = False

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

  CLONEABLE_CHILDREN = {"AssessmentTemplate"}

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

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

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

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

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

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

  _include_links = []

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

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

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

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

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

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

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

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

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

    Children that can be cloned should be specified in CLONEABLE_CHILDREN.

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

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

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

      for obj in related_children:
        obj.clone(self)

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

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

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

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

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

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

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

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

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

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

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

  def handle_delete(self):
    """Handle model_deleted signals."""
    self._check_no_assessments()
コード例 #13
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')
    )
コード例 #14
0
class IssuetrackerIssue(base.ContextRBAC, Base, db.Model):
    """Class representing IssuetrackerIssue."""

    __tablename__ = 'issuetracker_issues'

    object_id = db.Column(db.Integer, nullable=False)
    object_type = db.Column(db.String(250), nullable=False)
    enabled = db.Column(db.Boolean, nullable=False, default=False)

    title = db.Column(db.String(250), nullable=True)
    component_id = db.Column(db.String(50), nullable=True)
    hotlist_id = db.Column(db.String(50), nullable=True)
    issue_type = db.Column(db.String(50), nullable=True)
    issue_priority = db.Column(db.String(50), nullable=True)
    issue_severity = db.Column(db.String(50), nullable=True)
    assignee = db.Column(db.String(250), nullable=True)
    cc_list = db.Column(db.Text, nullable=False, default="")
    due_date = db.Column(db.Date, nullable=True)

    issue_id = db.Column(db.String(50), nullable=True)
    issue_url = db.Column(db.String(250), nullable=True)

    issue_tracked_obj = utils.PolymorphicRelationship("object_id",
                                                      "object_type",
                                                      "{}_issue_tracked")

    @classmethod
    def get_issue(cls, object_type, object_id):
        """Returns an issue object by given type and ID or None.

    Args:
      object_type: A string representing a model.
      object_id: An integer identifier of model's instance.

    Returns:
      An instance of IssuetrackerIssue or None.
    """
        return cls.query.filter(cls.object_type == object_type,
                                cls.object_id == object_id).first()

    def to_dict(self, include_issue=False, include_private=False):
        """Returns representation of object as a dict.

    Args:
      include_issue: A boolean whether to include issue related properties.
      include_private: A boolean whether to include private properties.

    Returns:
      A dict representing an instance of IssuetrackerIssue.
    """
        res = {
            'enabled': self.enabled,
            'component_id': self.component_id,
            'hotlist_id': self.hotlist_id,
            'issue_type': self.issue_type,
            'issue_priority': self.issue_priority,
            'issue_severity': self.issue_severity,
        }

        if include_issue:
            res['issue_id'] = self.issue_id
            res['issue_url'] = self.issue_url
            res['title'] = self.title

        if include_private:
            res['object_id'] = self.object_id
            res['object_type'] = self.object_type
            res['assignee'] = self.assignee
            res['cc_list'] = self.cc_list.split(',') if self.cc_list else []

        return res

    @classmethod
    def create_or_update_from_dict(cls, obj, info):
        """Creates or updates issue with given parameters.

    Args:
      obj: An object which is an IssueTracked instance.
      info: A dict with issue properties.

    Returns:
      An instance of IssuetrackerIssue.
    """
        if not info:
            raise ValueError('Issue tracker info cannot be empty.')

        issue_obj = cls.get_issue(obj.type, obj.id)

        info = dict(info, issue_tracked_obj=obj)
        if issue_obj is not None:
            issue_obj.update_from_dict(info)
        else:
            issue_obj = cls.create_from_dict(info)
            db.session.add(issue_obj)

        return issue_obj

    @classmethod
    def create_from_dict(cls, info):
        """Creates issue with given parameters.

    Args:
      info: A dict with issue properties.

    Returns:
      An instance of IssuetrackerIssue.
    """

        cc_list = info.get('cc_list')
        if cc_list is not None:
            cc_list = ','.join(cc_list)

        return cls(
            issue_tracked_obj=info['issue_tracked_obj'],
            enabled=bool(info.get('enabled')),
            title=info.get('title'),
            component_id=info.get('component_id'),
            hotlist_id=info.get('hotlist_id'),
            issue_type=info.get('issue_type'),
            issue_priority=info.get('issue_priority'),
            issue_severity=info.get('issue_severity'),
            assignee=info.get('assignee'),
            cc_list=cc_list,
            issue_id=info.get('issue_id'),
            issue_url=info.get('issue_url'),
        )

    def update_from_dict(self, info):
        """Updates issue with given parameters.

    Args:
      info: A dict with issue properties.

    Returns:
      An instance of IssuetrackerIssue.
    """
        cc_list = info.pop('cc_list', None)

        info = dict(self.to_dict(include_issue=True, include_private=True),
                    **info)

        if cc_list is not None:
            info['cc_list'] = cc_list

        if info['cc_list'] is not None:
            info['cc_list'] = ','.join(info['cc_list'])

        self.object_type = info['object_type']
        self.object_id = info['object_id']
        self.enabled = info['enabled']
        self.title = info['title']
        self.component_id = info['component_id']

        self.hotlist_id = info['hotlist_id']

        self.issue_type = info['issue_type']
        self.issue_priority = info['issue_priority']
        self.issue_severity = info['issue_severity']
        self.assignee = info['assignee']
        self.cc_list = info['cc_list']

        self.issue_id = info['issue_id']
        self.issue_url = info['issue_url']

        if info.get('due_date'):
            self.due_date = info.get('due_date')

    @staticmethod
    def get_issuetracker_issue_stub():
        """Returns dict with all Issue Tracker fields with empty values."""
        return {
            'enabled': False,
            'component_id': None,
            'hotlist_id': None,
            'issue_type': None,
            'issue_priority': None,
            'issue_severity': None,
            'title': None,
            'issue_id': None,
            'issue_url': None
        }
コード例 #15
0
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
コード例 #16
0
ファイル: audit.py プロジェクト: zhaoshiling1017/ggrc-core
class Audit(Snapshotable, clonable.SingleClonable, PublicDocumentable,
            mixins.CustomAttributable, Personable, HasOwnContext, Relatable,
            Roleable, issuetracker_issue.IssueTracked, WithLastDeprecatedDate,
            mixins.Timeboxed, mixins.BusinessObject, mixins.Folderable,
            Indexed, db.Model):
    """Audit model."""

    __tablename__ = 'audits'
    _slug_uniqueness = False

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

    CLONEABLE_CHILDREN = {"AssessmentTemplate"}

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

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

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

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

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

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

    _include_links = []

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

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

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

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

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

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

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

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

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

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

    Children that can be cloned should be specified in CLONEABLE_CHILDREN.

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

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

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

            for obj in related_children:
                obj.clone(self)

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

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

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

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

    @classmethod
    def eager_query(cls):
        query = super(Audit, cls).eager_query()
        return query.options(
            orm.joinedload('program'),
            orm.subqueryload('object_people').joinedload('person'),
            orm.subqueryload('audit_objects'),
        )
コード例 #17
0
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
コード例 #18
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'))
コード例 #19
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"),
        )
コード例 #20
0
class TaskGroupTask(WithContact, Titled, Described, RelativeTimeboxed, Slugged,
                    Indexed, db.Model):
    """Workflow TaskGroupTask model."""

    __tablename__ = 'task_group_tasks'
    _extra_table_args = (schema.CheckConstraint('start_date <= end_date'), )
    _title_uniqueness = False
    _start_changed = False

    @classmethod
    def default_task_type(cls):
        return "text"

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

    task_group_id = db.Column(
        db.Integer,
        db.ForeignKey('task_groups.id', ondelete="CASCADE"),
        nullable=False,
    )
    sort_index = db.Column(db.String(length=250), default="", nullable=False)

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

    task_type = db.Column(db.String(length=250),
                          default=default_task_type,
                          nullable=False)

    response_options = db.Column(JsonType(), nullable=False, default=[])

    VALID_TASK_TYPES = ['text', 'menu', 'checkbox']

    @orm.validates('task_type')
    def validate_task_type(self, key, value):
        # pylint: disable=unused-argument
        if value is None:
            value = self.default_task_type()
        if value not in self.VALID_TASK_TYPES:
            raise ValueError(u"Invalid type '{}'".format(value))
        return value

    def validate_date(self, value):
        if isinstance(value, datetime):
            value = value.date()
        if value is not None and value.year <= 1900:
            current_century = date.today().year / 100 * 100
            year = current_century + value.year % 100
            return date(year, value.month, value.day)
        return value

    @orm.validates("start_date", "end_date")
    def validate_end_date(self, key, value):
        value = self.validate_date(value)
        if key == "start_date":
            self._start_changed = True
        if key == "end_date" and self._start_changed and self.start_date > value:
            self._start_changed = False
            raise ValueError("Start date can not be after end date.")
        return value

    _publish_attrs = [
        'task_group', 'sort_index', 'relative_start_month',
        'relative_start_day', 'relative_end_month', 'relative_end_day',
        'object_approval', 'task_type', 'response_options'
    ]
    _sanitize_html = []
    _aliases = {
        "title": "Summary",
        "description": {
            "display_name": "Task Description",
            "handler_key": "task_description",
        },
        "contact": {
            "display_name": "Assignee",
            "mandatory": True,
        },
        "secondary_contact": None,
        "start_date": None,
        "end_date": None,
        "task_group": {
            "display_name": "Task Group",
            "mandatory": True,
            "filter_by": "_filter_by_task_group",
        },
        "relative_start_date": {
            "display_name":
            "Start",
            "mandatory":
            True,
            "description":
            ("Enter the task start date in the following format:\n"
             "'mm/dd/yyyy' for one time workflows\n"
             "'#' for weekly workflows (where # represents day "
             "of the week & Monday = day 1)\n"
             "'dd' for monthly workflows\n"
             "'mmm/mmm/mmm/mmm dd' for monthly workflows "
             "e.g. feb/may/aug/nov 17\n"
             "'mm/dd' for yearly workflows"),
        },
        "relative_end_date": {
            "display_name":
            "End",
            "mandatory":
            True,
            "description":
            ("Enter the task end date in the following format:\n"
             "'mm/dd/yyyy' for one time workflows\n"
             "'#' for weekly workflows (where # represents day "
             "of the week & Monday = day 1)\n"
             "'dd' for monthly workflows\n"
             "'mmm/mmm/mmm/mmm dd' for monthly workflows "
             "e.g. feb/may/aug/nov 17\n"
             "'mm/dd' for yearly workflows"),
        },
        "task_type": {
            "display_name":
            "Task Type",
            "mandatory":
            True,
            "description": ("Accepted values are:"
                            "\n'Rich Text'\n'Dropdown'\n'Checkbox'"),
        }
    }

    @classmethod
    def _filter_by_task_group(cls, predicate):
        return TaskGroup.query.filter((TaskGroup.id == cls.task_group_id) & (
            predicate(TaskGroup.slug) | predicate(TaskGroup.title))).exists()

    @classmethod
    def eager_query(cls):
        query = super(TaskGroupTask, cls).eager_query()
        return query.options(orm.subqueryload('task_group'), )

    def _display_name(self):
        return self.title + '<->' + self.task_group.display_name

    def copy(self, _other=None, **kwargs):
        columns = [
            'title',
            'description',
            'task_group',
            'sort_index',
            'relative_start_month',
            'relative_start_day',
            'relative_end_month',
            'relative_end_day',
            'start_date',
            'end_date',
            'contact',
            'modified_by',
            'task_type',
            'response_options',
        ]

        contact = None
        if kwargs.get('clone_people', False):
            contact = self.contact
        else:
            contact = get_current_user()

        kwargs['modified_by'] = get_current_user()

        target = self.copy_into(_other, columns, contact=contact, **kwargs)
        return target
コード例 #21
0
class CycleTaskGroup(WithContact, Stateful, Slugged, Timeboxed, Described,
                     Titled, Base, db.Model):
  """Cycle Task Group model.
  """
  __tablename__ = 'cycle_task_groups'
  _title_uniqueness = False

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

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

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

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

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

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

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

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

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

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

    Returns:
      a query object with cycle_task_group_tasks and cycle_task_group_objects
      added to joined load options.
    """
    query = super(CycleTaskGroup, cls).eager_query()
    return query.options(
        orm.joinedload('cycle_task_group_tasks')
    )
コード例 #22
0
class IssuetrackerIssue(Base, db.Model):
  """Class representing IssuetrackerIssue."""

  __tablename__ = 'issuetracker_issues'

  object_id = db.Column(db.Integer, nullable=False)
  object_type = db.Column(db.String(250), nullable=False)
  enabled = db.Column(db.Boolean, nullable=False, default=False)

  title = db.Column(db.String(250), nullable=True)
  component_id = db.Column(db.String(50), nullable=True)
  hotlist_id = db.Column(db.String(50), nullable=True)
  issue_type = db.Column(db.String(50), nullable=True)
  issue_priority = db.Column(db.String(50), nullable=True)
  issue_severity = db.Column(db.String(50), nullable=True)
  assignee = db.Column(db.String(250), nullable=True)
  cc_list = db.Column(db.Text, nullable=True)

  issue_id = db.Column(db.String(50), nullable=True)
  issue_url = db.Column(db.String(250), nullable=True)

  _MANDATORY_ATTRS = (
      'object_type', 'object_id',
  )

  @classmethod
  def get_issue(cls, object_type, object_id):
    """Returns an issue object by given type and ID or None.

    Args:
      object_type: A string representing a model.
      object_id: An integer identifier of model's instance.

    Returns:
      An instance of IssuetrackerIssue or None.
    """
    return cls.query.filter(
        cls.object_type == object_type,
        cls.object_id == object_id).first()

  @classmethod
  def _validate_info(cls, info):
    """Validates issue info to have all required properties."""
    missing_attrs = [
        attr
        for attr in cls._MANDATORY_ATTRS
        if attr not in info
    ]
    if missing_attrs:
      raise ValueError(
          'Issue tracker info is missing mandatory attributes: %s' % (
              ', '.join(missing_attrs)))

  def to_dict(self, include_issue=False, include_private=False):
    """Returns representation of object as a dict.

    Args:
      include_issue: A boolean whether to include issue related properties.
      include_private: A boolean whether to include private properties.

    Returns:
      A dict representing an instance of IssuetrackerIssue.
    """
    res = {
        'enabled': self.enabled,
        'component_id': self.component_id,
        'hotlist_id': self.hotlist_id,
        'issue_type': self.issue_type,
        'issue_priority': self.issue_priority,
        'issue_severity': self.issue_severity,
    }

    if include_issue:
      res['issue_id'] = self.issue_id
      res['issue_url'] = self.issue_url
      res['title'] = self.title

    if include_private:
      res['object_id'] = self.object_id
      res['object_type'] = self.object_type
      res['assignee'] = self.assignee
      res['cc_list'] = self.cc_list.split(',') if self.cc_list else []

    return res

  @classmethod
  def create_or_update_from_dict(cls, object_type, object_id, info):
    """Creates or updates issue with given parameters.

    Args:
      object_type: A string representing a model.
      object_id: An integer identifier of model's instance.
      info: A dict with issue properties.

    Returns:
      An instance of IssuetrackerIssue.
    """
    if not info:
      raise ValueError('Issue tracker info cannot be empty.')

    issue_obj = cls.get_issue(object_type, object_id)

    info = dict(info, object_type=object_type, object_id=object_id)
    if issue_obj is not None:
      issue_obj.update_from_dict(info)
    else:
      issue_obj = cls.create_from_dict(info)
      db.session.add(issue_obj)

    return issue_obj

  @classmethod
  def create_from_dict(cls, info):
    """Creates issue with given parameters.

    Args:
      info: A dict with issue properties.

    Returns:
      An instance of IssuetrackerIssue.
    """
    cls._validate_info(info)

    cc_list = info.get('cc_list')
    if cc_list is not None:
      cc_list = ','.join(cc_list)

    return cls(
        object_type=info['object_type'],
        object_id=info['object_id'],

        enabled=bool(info.get('enabled')),
        title=info.get('title'),
        component_id=info.get('component_id'),
        hotlist_id=info.get('hotlist_id'),
        issue_type=info.get('issue_type'),
        issue_priority=info.get('issue_priority'),
        issue_severity=info.get('issue_severity'),

        assignee=info.get('assignee'),
        cc_list=cc_list,

        issue_id=info.get('issue_id'),
        issue_url=info.get('issue_url'),
    )

  def update_from_dict(self, info):
    """Updates issue with given parameters.

    Args:
      info: A dict with issue properties.

    Returns:
      An instance of IssuetrackerIssue.
    """
    cc_list = info.pop('cc_list', None)

    info = dict(
        self.to_dict(include_issue=True, include_private=True),
        **info)

    if cc_list is not None:
      info['cc_list'] = cc_list

    if info['cc_list'] is not None:
      info['cc_list'] = ','.join(info['cc_list'])

    self.object_type = info['object_type']
    self.object_id = info['object_id']
    self.enabled = info['enabled']
    self.title = info['title']
    self.component_id = info['component_id']

    self.hotlist_id = info['hotlist_id']

    self.issue_type = info['issue_type']
    self.issue_priority = info['issue_priority']
    self.issue_severity = info['issue_severity']
    self.assignee = info['assignee']
    self.cc_list = info['cc_list']

    self.issue_id = info['issue_id']
    self.issue_url = info['issue_url']
コード例 #23
0
ファイル: task_group_task.py プロジェクト: xuechaos/ggrc-core
class TaskGroupTask(roleable.Roleable,
                    relationship.Relatable,
                    mixins.Titled,
                    mixins.Described,
                    base.ContextRBAC,
                    mixins.Slugged,
                    mixins.Timeboxed,
                    Indexed,
                    db.Model):
  """Workflow TaskGroupTask model."""

  __tablename__ = 'task_group_tasks'
  _extra_table_args = (
      schema.CheckConstraint('start_date <= end_date'),
  )
  _title_uniqueness = False
  _start_changed = False

  @classmethod
  def default_task_type(cls):
    return cls.TEXT

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

  task_group_id = db.Column(
      db.Integer,
      db.ForeignKey('task_groups.id', ondelete="CASCADE"),
      nullable=False,
  )

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

  task_type = db.Column(
      db.String(length=250), default=default_task_type, nullable=False)

  response_options = db.Column(
      JsonType(), nullable=False, default=[])

  relative_start_day = deferred(
      db.Column(db.Integer, default=None), "TaskGroupTask"
  )
  relative_end_day = deferred(
      db.Column(db.Integer, default=None), "TaskGroupTask"
  )

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

  @hybrid.hybrid_property
  def task_group(self):
    """Getter for task group foreign key."""
    return self._task_group

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

  TEXT = 'text'
  MENU = 'menu'
  CHECKBOX = 'checkbox'
  VALID_TASK_TYPES = [TEXT, MENU, CHECKBOX]

  @orm.validates('task_type')
  def validate_task_type(self, key, value):
    # pylint: disable=unused-argument
    if value is None:
      value = self.default_task_type()
    if value not in self.VALID_TASK_TYPES:
      raise ValueError(u"Invalid type '{}'".format(value))
    return value

  # pylint: disable=unused-argument
  @orm.validates("start_date", "end_date")
  def validate_date(self, key, value):
    """Validates date's itself correctness, start_ end_ dates relative to each
    other correctness is checked with 'before_insert' hook
    """
    if value is None:
      return
    if isinstance(value, datetime.datetime):
      value = value.date()
    if value < datetime.date(100, 1, 1):
      current_century = datetime.date.today().year / 100
      return datetime.date(value.year + current_century * 100,
                           value.month,
                           value.day)
    return value

  _api_attrs = reflection.ApiAttributes(
      'task_group',
      'object_approval',
      'task_type',
      'response_options',
      reflection.Attribute('view_start_date', update=False, create=False),
      reflection.Attribute('view_end_date', update=False, create=False),
  )
  _sanitize_html = []
  _aliases = {
      "title": "Summary",
      "description": {
          "display_name": "Task Description",
          "handler_key": "task_description",
      },
      "start_date": {
          "display_name": "Start Date",
          "mandatory": True,
          "description": (
              "Enter the task start date\nin the following format:\n"
              "'mm/dd/yyyy'"
          ),
      },
      "end_date": {
          "display_name": "End Date",
          "mandatory": True,
          "description": (
              "Enter the task end date\nin the following format:\n"
              "'mm/dd/yyyy'"
          ),
      },
      "task_group": {
          "display_name": "Task Group",
          "mandatory": True,
          "filter_by": "_filter_by_task_group",
      },
      "task_type": {
          "display_name": "Task Type",
          "mandatory": True,
          "description": ("Accepted values are:"
                          "\n'Rich Text'\n'Dropdown'\n'Checkbox'"),
      }
  }

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

  @classmethod
  def _filter_by_task_group(cls, predicate):
    return TaskGroup.query.filter(
        (TaskGroup.id == cls.task_group_id) &
        (predicate(TaskGroup.slug) | predicate(TaskGroup.title))
    ).exists()

  def _get_view_date(self, date):
    if date and self.task_group and self.task_group.workflow:
      return self.task_group.workflow.calc_next_adjusted_date(date)

  @simple_property
  def view_start_date(self):
    return self._get_view_date(self.start_date)

  @simple_property
  def view_end_date(self):
    return self._get_view_date(self.end_date)

  @classmethod
  def _populate_query(cls, query):
    return query.options(
        orm.Load(cls).joinedload("task_group")
                     .undefer_group("TaskGroup_complete"),
        orm.Load(cls).joinedload("task_group")
                     .joinedload("workflow")
                     .undefer_group("Workflow_complete"),
    )

  @classmethod
  def eager_query(cls, **kwargs):
    return cls._populate_query(super(TaskGroupTask, cls).eager_query(**kwargs))

  def _display_name(self):
    return self.title + '<->' + self.task_group.display_name

  def copy(self, _other=None, **kwargs):
    columns = ['title',
               'description',
               'task_group',
               'start_date',
               'end_date',
               'access_control_list',
               'modified_by',
               'task_type',
               'response_options']

    if kwargs.get('clone_people', False):
      access_control_list = [
          {"ac_role_id": acl.ac_role_id, "person": {"id": person.id}}
          for person, acl in self.access_control_list
      ]
    else:
      role_id = {
          v: k for (k, v) in
          role.get_custom_roles_for(self.type).iteritems()
      }['Task Assignees']
      access_control_list = [
          {"ac_role_id": role_id, "person": {"id": get_current_user().id}}
      ]
    kwargs['modified_by'] = get_current_user()
    return self.copy_into(_other,
                          columns,
                          access_control_list=access_control_list,
                          **kwargs)
コード例 #24
0
class CycleTaskGroupObjectTask(WithContact, Stateful, Slugged, Timeboxed,
                               Relatable, Described, Titled, Base, db.Model):
    """Cycle task model
  """
    __tablename__ = 'cycle_task_group_object_tasks'
    _title_uniqueness = False

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

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

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

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

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

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

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

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

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

    _aliases = {
        "title": "Summary",
        "description": "Task Details",
        "contact": {
            "display_name": "Assignee",
            "mandatory": True,
            "filter_by": "_filter_by_contact",
        },
        "secondary_contact": None,
        "start_date": "Start Date",
        "end_date": "End Date",
        "finished_date": "Actual Finish Date",
        "verified_date": "Actual Verified Date",
        "cycle": {
            "display_name": "Cycle",
            "filter_by": "_filter_by_cycle",
        },
        "cycle_task_group": {
            "display_name": "Task Group",
            "mandatory": True,
            "filter_by": "_filter_by_cycle_task_group",
        },
        "task_type": {
            "display_name": "Task Type",
            "mandatory": True,
        },
    }

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

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

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

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

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

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

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

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

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

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

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

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