Exemple #1
0
class Ownable(object):

  AUTO_REINDEX_RULES = [
      ReindexRule(
          "ObjectOwner",
          lambda x: [x.ownable] if isinstance(x.ownable, Indexed) else []
      )
  ]

  @declared_attr
  def object_owners(cls):  # pylint: disable=no-self-argument
    cls.owners = association_proxy(
        'object_owners', 'person',
        creator=lambda person: ObjectOwner(
            person=person,
            ownable_type=cls.__name__,
        )
    )
    joinstr = 'and_(foreign(ObjectOwner.ownable_id) == {type}.id, '\
        'foreign(ObjectOwner.ownable_type) == "{type}")'
    joinstr = joinstr.format(type=cls.__name__)
    return db.relationship(
        'ObjectOwner',
        primaryjoin=joinstr,
        backref='{0}_ownable'.format(cls.__name__),
        cascade='all, delete-orphan',
    )

  _publish_attrs = [
      'owners',
      PublishOnly('object_owners'),
  ]
  _include_links = []
  _fulltext_attrs = [
      MultipleSubpropertyFullTextAttr('owners', 'owners',
                                      ['user_name', 'email', 'name'])
  ]
  _aliases = {
      "owners": {
          "display_name": "Admin",
          "mandatory": True,
      }
  }

  @classmethod
  def indexed_query(cls):
    return super(Ownable, cls).indexed_query().options(
        orm.Load(cls).joinedload(
            "object_owners"
        )
    )

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

    query = super(Ownable, cls).eager_query()
    return cls.eager_inclusions(query, Ownable._include_links).options(
        orm.subqueryload('object_owners'))
Exemple #2
0
class ExternalCommentable(object):
  """Mixin for external commentable objects.

  This is a mixin for adding external comments (comments that can be
  created only by sync service) to the object.
  """
  _fulltext_attrs = [
      MultipleSubpropertyFullTextAttr("comment", "comments", ["description"]),
  ]

  @classmethod
  def indexed_query(cls):
    """Indexed query for ExternalCommentable mixin."""
    return super(ExternalCommentable, cls).indexed_query().options(
        orm.Load(cls).subqueryload("comments").load_only("id", "description")
    )

  @classmethod
  def eager_query(cls):
    """Eager query for ExternalCommentable mixin."""
    query = super(ExternalCommentable, cls).eager_query()
    return query.options(orm.subqueryload("comments"))

  @declared_attr
  def comments(cls):  # pylint: disable=no-self-argument
    """ExternalComment related to self via Relationship table."""
    return db.relationship(
        ExternalComment,
        primaryjoin=lambda: sa.or_(
            sa.and_(
                cls.id == Relationship.source_id,
                Relationship.source_type == cls.__name__,
                Relationship.destination_type == ExternalComment.__name__,
            ),
            sa.and_(
                cls.id == Relationship.destination_id,
                Relationship.destination_type == cls.__name__,
                Relationship.source_type == ExternalComment.__name__,
            )
        ),
        secondary=Relationship.__table__,
        secondaryjoin=lambda: sa.or_(
            sa.and_(
                ExternalComment.id == Relationship.source_id,
                Relationship.source_type == ExternalComment.__name__,
            ),
            sa.and_(
                ExternalComment.id == Relationship.destination_id,
                Relationship.destination_type == ExternalComment.__name__,
            )
        ),
        viewonly=True,
    )
Exemple #3
0
class Labeled(object):
    """Mixin to add label in required model."""

    _update_raw = _include_links = [
        'labels',
    ]
    _api_attrs = reflection.ApiAttributes(*_include_links)
    _aliases = {'labels': 'Labels'}

    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr("label", "labels", ["name"]),
    ]

    @declared_attr
    def _object_labels(cls):  # pylint: disable=no-self-argument
        """Object labels property"""
        # pylint: disable=attribute-defined-outside-init
        cls._labels = association_proxy(
            '_object_labels',
            'label',
            creator=lambda label: ObjectLabel(
                label=label,  # noqa
                object_type=cls.__name__))
        return db.relationship(
            ObjectLabel,
            primaryjoin=lambda: and_(cls.id == ObjectLabel.object_id, cls.
                                     __name__ == ObjectLabel.object_type),
            foreign_keys=ObjectLabel.object_id,
            backref='{}_labeled'.format(cls.__name__),
            cascade='all, delete-orphan')

    @hybrid_property
    def labels(self):
        return self._labels

    @labels.setter
    def labels(self, values):
        """Setter function for labeled.

    Args:
      values: List of labels in json. Labels mapped on labeled
    and not represented in values will be unmapped from labeled.
    label is represented as a dict {"id": <label id>, "name": <label name>}
    Currently, FE sends the following info:
      for a newly created label: {"id": None, "name": <label name>}
      for old label: {"id": <label_id>, "name": <label name>}
      for being mapped label: {"id": <label id>}
    """
        if values is None:
            return

        for value in values:
            if 'name' in value:
                value['name'] = value['name'].strip()

        if values:
            new_ids = {value['id'] for value in values if value['id']}
            new_names = {value['name'] for value in values if 'name' in value}
            # precache labels
            filter_group = []
            if new_ids:
                filter_group.append(Label.id.in_(new_ids))
            if new_names:
                filter_group.append(Label.name.in_(new_names))
            cached_labels = Label.query.filter(
                and_(or_(*filter_group),
                     Label.object_type == self.__class__.__name__)).all()
        else:
            new_ids = set()
            new_names = set()
            cached_labels = []

        old_ids = {label.id for label in self.labels}
        self._unmap_labels(old_ids - new_ids, new_names)
        self._map_labels(new_ids - old_ids, cached_labels)
        # label comparison has to be case insensitive
        if new_names:
            self._add_labels_by_name(new_names, new_ids | old_ids,
                                     cached_labels)

    def _map_labels(self, ids, cached_labels):
        """Attach new labels to current object."""
        labels_dict = {label.id: label for label in cached_labels}
        for id_ in ids:
            self._labels.append(labels_dict[id_])

    def _add_labels_by_name(self, names, ids, cached_labels):
        """Creates new labels and map them to current object"""
        labels_dict = {label.name.lower(): label for label in cached_labels}
        for name in names:
            if name.lower() in labels_dict:
                if labels_dict[name.lower()].id in ids:
                    continue
                label = labels_dict[name.lower()]
            else:
                label = Label(name=name, object_type=self.__class__.__name__)
            self._labels.append(label)

    def _unmap_labels(self, ids, names):
        """Remove labels from current object."""
        values_map = {
            label.id: label
            for label in self._labels  # noqa pylint: disable=not-an-iterable
        }
        lower_names = [name.lower() for name in names]
        for id_ in ids:
            if values_map[id_].name.lower() not in lower_names:
                self._labels.remove(values_map[id_])

    def log_json(self):
        """Log label values."""
        # pylint: disable=not-an-iterable
        res = super(Labeled, self).log_json()
        res["labels"] = [value.log_json() for value in self.labels]
        return res

    @classmethod
    def eager_query(cls, **kwargs):
        """Eager query classmethod."""
        return super(Labeled, cls).eager_query(**kwargs).options(
            orm.subqueryload('_object_labels'))

    @classmethod
    def indexed_query(cls):
        return super(Labeled, cls).indexed_query().options(
            orm.subqueryload("_object_labels"))
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"),
        )
Exemple #5
0
class Commentable(object):
    """Mixin for commentable objects.

  This is a mixin for adding default options to objects on which people can
  comment.

  recipients is used for setting who gets notified (Verifer, Requester, ...).
  send_by_default should be used for setting the "send notification" flag in
  the comment modal.

  """
    # pylint: disable=too-few-public-methods

    VALID_RECIPIENTS = frozenset([
        "Assignees",
        "Creators",
        "Verifiers",
        "Admin",
        "Primary Contacts",
        "Secondary Contacts",
    ])

    @validates("recipients")
    def validate_recipients(self, key, value):
        """
      Validate recipients list

      Args:
        value (string): Can be either empty, or
                        list of comma separated `VALID_RECIPIENTS`
    """
        # pylint: disable=unused-argument
        if value:
            value = set(name for name in value.split(",") if name)

        if value and value.issubset(self.VALID_RECIPIENTS):
            # The validator is a bit more smart and also makes some filtering of the
            # given data - this is intended.
            return ",".join(value)
        elif not value:
            return ""
        else:
            raise ValueError(
                value, 'Value should be either empty ' +
                'or comma separated list of ' +
                ', '.join(sorted(self.VALID_RECIPIENTS)))

    recipients = db.Column(db.String,
                           nullable=True,
                           default=u"Assignees,Creators,Verifiers")

    send_by_default = db.Column(db.Boolean, nullable=True, default=True)

    _api_attrs = reflection.ApiAttributes("recipients", "send_by_default")

    _aliases = {
        "recipients": "Recipients",
        "send_by_default": "Send by default",
        "comments": {
            "display_name": "Comments",
            "description": 'DELIMITER=";;" double semi-colon separated values',
        },
    }
    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr("comment", "comments",
                                        ["description"]),
    ]

    @classmethod
    def indexed_query(cls):
        return super(Commentable, cls).indexed_query().options(
            orm.Load(cls).subqueryload("comments").load_only(
                "id", "description"))

    @classmethod
    def eager_query(cls):
        """Eager Query"""
        query = super(Commentable, cls).eager_query()
        return query.options(orm.subqueryload('comments'))

    @declared_attr
    def comments(cls):  # pylint: disable=no-self-argument
        """Comments related to self via Relationship table."""
        return db.relationship(
            Comment,
            primaryjoin=lambda: sa.or_(
                sa.and_(
                    cls.id == Relationship.source_id,
                    Relationship.source_type == cls.__name__,
                    Relationship.destination_type == "Comment",
                ),
                sa.and_(
                    cls.id == Relationship.destination_id,
                    Relationship.destination_type == cls.__name__,
                    Relationship.source_type == "Comment",
                )),
            secondary=Relationship.__table__,
            secondaryjoin=lambda: sa.or_(
                sa.and_(
                    Comment.id == Relationship.source_id,
                    Relationship.source_type == "Comment",
                ),
                sa.and_(
                    Comment.id == Relationship.destination_id,
                    Relationship.destination_type == "Comment",
                )),
            viewonly=True,
        )
Exemple #6
0
class Assessment(statusable.Statusable, AuditRelationship,
                 AutoStatusChangeable, Assignable, HasObjectState, TestPlanned,
                 CustomAttributable, EvidenceURL, Commentable, Personable,
                 reminderable.Reminderable, Timeboxed, Relatable,
                 WithSimilarityScore, FinishedDate, VerifiedDate,
                 ValidateOnComplete, Notifiable, BusinessObject, Indexed,
                 db.Model):
    """Class representing Assessment.

  Assessment is an object representing an individual assessment performed on
  a specific object during an audit to ascertain whether or not
  certain conditions were met for that object.
  """

    __tablename__ = 'assessments'
    _title_uniqueness = False

    ASSIGNEE_TYPES = (u"Creator", u"Assessor", u"Verifier")

    REMINDERABLE_HANDLERS = {
        "statusToPerson": {
            "handler":
            reminderable.Reminderable.handle_state_to_person_reminder,
            "data": {
                statusable.Statusable.START_STATE: "Assessor",
                "In Progress": "Assessor"
            },
            "reminders": {
                "assessment_assessor_reminder",
            }
        }
    }

    design = deferred(db.Column(db.String), "Assessment")
    operationally = deferred(db.Column(db.String), "Assessment")
    audit_id = deferred(
        db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False),
        'Assessment')

    @declared_attr
    def object_level_definitions(self):
        """Set up a backref so that we can create an object level custom
       attribute definition without the need to do a flush to get the
       assessment id.

      This is used in the relate_ca method in hooks/assessment.py.
    """
        return db.relationship('CustomAttributeDefinition',
                               primaryjoin=lambda: and_(
                                   remote(CustomAttributeDefinition.
                                          definition_id) == Assessment.id,
                                   remote(CustomAttributeDefinition.
                                          definition_type) == "assessment"),
                               foreign_keys=[
                                   CustomAttributeDefinition.definition_id,
                                   CustomAttributeDefinition.definition_type
                               ],
                               backref='assessment_definition',
                               cascade='all, delete-orphan')

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

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

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

    _fulltext_attrs = [
        'design',
        'operationally',
        MultipleSubpropertyFullTextAttr('related_assessors', 'assessors',
                                        ['user_name', 'email', 'name']),
        MultipleSubpropertyFullTextAttr('related_creators', 'creators',
                                        ['user_name', 'email', 'name']),
        MultipleSubpropertyFullTextAttr('related_verifiers', 'verifiers',
                                        ['user_name', 'email', 'name']),
        MultipleSubpropertyFullTextAttr('document_evidence',
                                        'document_evidence',
                                        ['title', 'link']),
        MultipleSubpropertyFullTextAttr('document_url', 'document_url',
                                        ['link']),
    ]

    _tracked_attrs = {
        'contact_id', 'description', 'design', 'notes', 'operationally',
        'reference_url', 'secondary_contact_id', 'test_plan', 'title', 'url',
        'start_date', 'end_date'
    }

    _aliases = {
        "owners": None,
        "assessment_template": {
            "display_name": "Template",
            "ignore_on_update": True,
            "filter_by": "_ignore_filter",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "url": "Assessment URL",
        "design": "Conclusion: Design",
        "operationally": "Conclusion: Operation",
        "related_creators": {
            "display_name": "Creators",
            "mandatory": True,
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_assessors": {
            "display_name": "Assignees",
            "mandatory": True,
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_verifiers": {
            "display_name": "Verifiers",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
    }

    AUTO_REINDEX_RULES = [
        ReindexRule("RelationshipAttr", reindex_by_relationship_attr),
        ReindexRule("Relationship", reindex_by_relationship)
    ]

    similarity_options = similarity_options_module.ASSESSMENT

    @property
    def assessors(self):
        """Get the list of assessor assignees"""
        return self.assignees_by_type.get("Assessor", [])

    @property
    def creators(self):
        """Get the list of creator assignees"""
        return self.assignees_by_type.get("Creator", [])

    @property
    def verifiers(self):
        """Get the list of verifier assignees"""
        return self.assignees_by_type.get("Verifier", [])

    @property
    def document_evidence(self):
        return self.documents_by_type("document_evidence")

    @property
    def document_url(self):
        return self.documents_by_type("document_url")

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

    @validates("operationally")
    def validate_opperationally(self, key, value):
        # pylint: disable=unused-argument
        return self.validate_conclusion(value)

    @validates("design")
    def validate_design(self, key, value):
        # pylint: disable=unused-argument
        return self.validate_conclusion(value)

    @classmethod
    def _ignore_filter(cls, _):
        return None
Exemple #7
0
class Assessment(Roleable, statusable.Statusable, AuditRelationship,
                 AutoStatusChangeable, Assignable, HasObjectState, TestPlanned,
                 CustomAttributable, PublicDocumentable, Commentable,
                 Personable, reminderable.Reminderable, Timeboxed, Relatable,
                 WithSimilarityScore, FinishedDate, VerifiedDate,
                 ValidateOnComplete, Notifiable, WithAction, BusinessObject,
                 Indexed, db.Model):
    """Class representing Assessment.

  Assessment is an object representing an individual assessment performed on
  a specific object during an audit to ascertain whether or not
  certain conditions were met for that object.
  """

    __tablename__ = 'assessments'
    _title_uniqueness = False

    ASSIGNEE_TYPES = (u"Creator", u"Assessor", u"Verifier")

    REMINDERABLE_HANDLERS = {
        "statusToPerson": {
            "handler":
            reminderable.Reminderable.handle_state_to_person_reminder,
            "data": {
                statusable.Statusable.START_STATE: "Assessor",
                "In Progress": "Assessor"
            },
            "reminders": {
                "assessment_assessor_reminder",
            }
        }
    }

    design = deferred(db.Column(db.String), "Assessment")
    operationally = deferred(db.Column(db.String), "Assessment")
    audit_id = deferred(
        db.Column(db.Integer, db.ForeignKey('audits.id'), nullable=False),
        'Assessment')
    assessment_type = deferred(
        db.Column(db.String, nullable=False, server_default="Control"),
        "Assessment")

    @declared_attr
    def object_level_definitions(cls):  # pylint: disable=no-self-argument
        """Set up a backref so that we can create an object level custom
       attribute definition without the need to do a flush to get the
       assessment id.

      This is used in the relate_ca method in hooks/assessment.py.
    """
        return db.relationship(
            'CustomAttributeDefinition',
            primaryjoin=lambda: and_(
                remote(CustomAttributeDefinition.definition_id) == cls.id,
                remote(CustomAttributeDefinition.definition_type) ==
                "assessment"),
            foreign_keys=[
                CustomAttributeDefinition.definition_id,
                CustomAttributeDefinition.definition_type
            ],
            backref='assessment_definition',
            cascade='all, delete-orphan')

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

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

    # REST properties
    _api_attrs = reflection.ApiAttributes(
        'design',
        'operationally',
        'audit',
        'assessment_type',
        reflection.Attribute('archived', create=False, update=False),
        reflection.Attribute('object', create=False, update=False),
    )

    _fulltext_attrs = [
        'archived',
        'design',
        'operationally',
        MultipleSubpropertyFullTextAttr('related_assessors', 'assessors',
                                        ['user_name', 'email', 'name']),
        MultipleSubpropertyFullTextAttr('related_creators', 'creators',
                                        ['user_name', 'email', 'name']),
        MultipleSubpropertyFullTextAttr('related_verifiers', 'verifiers',
                                        ['user_name', 'email', 'name']),
    ]

    @classmethod
    def indexed_query(cls):
        query = super(Assessment, cls).indexed_query()
        return query.options(
            orm.Load(cls).undefer_group("Assessment_complete", ),
            orm.Load(cls).joinedload("audit").undefer_group(
                "Audit_complete", ),
        )

    _tracked_attrs = {
        'description', 'design', 'notes', 'operationally', 'test_plan',
        'title', 'start_date', 'end_date'
    }

    _aliases = {
        "owners": None,
        "assessment_template": {
            "display_name": "Template",
            "ignore_on_update": True,
            "filter_by": "_ignore_filter",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "assessment_type": {
            "display_name": "Assessment Type",
            "mandatory": False,
        },
        "design": "Conclusion: Design",
        "operationally": "Conclusion: Operation",
        "related_creators": {
            "display_name": "Creators",
            "mandatory": True,
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_assessors": {
            "display_name": "Assignees",
            "mandatory": True,
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "related_verifiers": {
            "display_name": "Verifiers",
            "type": reflection.AttributeInfo.Type.MAPPING,
        },
        "archived": {
            "display_name": "Archived",
            "mandatory": False,
            "ignore_on_update": True,
            "view_only": True,
        },
        "test_plan": "Assessment Procedure",
    }

    AUTO_REINDEX_RULES = [
        ReindexRule("RelationshipAttr", reindex_by_relationship_attr)
    ]

    similarity_options = {
        "relevant_types": {
            "Objective": {
                "weight": 2
            },
            "Control": {
                "weight": 2
            },
        },
        "threshold": 1,
    }

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

    @property
    def assessors(self):
        """Get the list of assessor assignees"""
        return self.assignees_by_type.get("Assessor", [])

    @property
    def creators(self):
        """Get the list of creator assignees"""
        return self.assignees_by_type.get("Creator", [])

    @property
    def verifiers(self):
        """Get the list of verifier assignees"""
        return self.assignees_by_type.get("Verifier", [])

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

    @validates("operationally")
    def validate_opperationally(self, key, value):
        # pylint: disable=unused-argument
        return self.validate_conclusion(value)

    @validates("design")
    def validate_design(self, key, value):
        # pylint: disable=unused-argument
        return self.validate_conclusion(value)

    @validates("assessment_type")
    def validate_assessment_type(self, key, value):
        """Validate assessment type to be the same as existing model name"""
        # pylint: disable=unused-argument
        # pylint: disable=no-self-use
        from ggrc.snapshotter.rules import Types
        if value and value not in Types.all:
            raise ValueError(
                "Assessment type '{}' is not snapshotable".format(value))
        return value

    @classmethod
    def _ignore_filter(cls, _):
        return None
Exemple #8
0
class Documentable(object):
    """Documentable mixin."""

    _include_links = []

    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr('document_evidence',
                                        'document_evidence',
                                        ['title', 'link']),
        MultipleSubpropertyFullTextAttr('document_url', 'document_url',
                                        ['link']),
    ]

    @classmethod
    def documents(cls, document_type):
        """Return documents releated for that instance and sent docuemtn type."""
        document_id = case([
            (
                Relationship.destination_type == "Document",
                Relationship.destination_id,
            ),
            (
                Relationship.source_type == "Document",
                Relationship.source_id,
            ),
        ],
                           else_=literal(False))
        documentable_id = case([
            (Relationship.destination_type
             == "Document", Relationship.source_id),
            (
                Relationship.source_type == "Document",
                Relationship.destination_id,
            ),
        ],
                               else_=literal(False))
        return db.relationship(
            Document,
            # at first we check is documentable_id not False (it return id in fact)
            # after that we can compare values.
            # this is required for saving logic consistancy
            # case return 2 types of values BOOL(false) and INT(id) not Null
            primaryjoin=lambda: and_(documentable_id, cls.id == documentable_id
                                     ),
            secondary=Relationship.__table__,
            # at first we check is document_id not False (it return id in fact)
            # after that we can compare values.
            # this is required for saving logic consistancy
            # case return 2 types of values BOOL(false) and INT(id) not Null
            secondaryjoin=lambda:
            and_(document_id, Document.id == document_id, Document.
                 document_type == document_type),
            viewonly=True,
        )

    @declared_attr
    def document_url(cls):  # pylint: disable=no-self-argument
        return cls.documents(Document.URL)

    @declared_attr
    def document_evidence(cls):  # pylint: disable=no-self-argument
        return cls.documents(Document.ATTACHMENT)

    @classmethod
    def eager_query(cls):
        """Eager query classmethod."""
        query = super(Documentable, cls).eager_query()
        document_fields = [
            "id", "title", "link", "description", "document_type"
        ]
        return cls.eager_inclusions(
            query, Documentable._include_links).options(
                orm.subqueryload('document_url').load_only(*document_fields),
                orm.subqueryload('document_evidence').load_only(
                    *document_fields),
            )

    @staticmethod
    def _log_docs(documents):
        return [create_stub(d) for d in documents if d]

    def log_json(self):
        """Serialize to JSON"""
        out_json = super(Documentable, self).log_json()
        if hasattr(self, "urls"):
            out_json["urls"] = self._log_docs(self.urls)
        if hasattr(self, "attachments"):
            out_json["attachments"] = self._log_docs(self.urls)
        return out_json

    @classmethod
    def indexed_query(cls):
        return super(Documentable, cls).indexed_query().options(
            orm.subqueryload("document_url").load_only("id", "title", "link"),
            orm.subqueryload("document_evidence").load_only("id", "link"),
        )
class Documentable(object):
    """Documentable mixin."""

    _include_links = []

    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr('document_evidence',
                                        'document_evidence',
                                        ['title', 'link']),
        MultipleSubpropertyFullTextAttr('document_url', 'document_url',
                                        ['link']),
        MultipleSubpropertyFullTextAttr('reference_url', 'reference_url',
                                        ['link']),
    ]

    @declared_attr
    def documents(cls):
        """Return documents releated for that instance."""
        document_id = case([
            (
                Relationship.destination_type == "Document",
                Relationship.destination_id,
            ),
            (
                Relationship.source_type == "Document",
                Relationship.source_id,
            ),
        ],
                           else_=literal(False))
        documentable_id = case([
            (Relationship.destination_type
             == "Document", Relationship.source_id),
            (
                Relationship.source_type == "Document",
                Relationship.destination_id,
            ),
        ],
                               else_=literal(False))
        documentable_type = case([
            (Relationship.destination_type
             == "Document", Relationship.source_type),
            (
                Relationship.source_type == "Document",
                Relationship.destination_type,
            ),
        ], )
        return db.relationship(
            Document,
            # at first we check is documentable_id not False (it return id in fact)
            # after that we can compare values.
            # this is required for saving logic consistancy
            # case return 2 types of values BOOL(false) and INT(id) not Null
            primaryjoin=lambda: and_(documentable_id, cls.id == documentable_id
                                     ),
            secondary=Relationship.__table__,
            # at first we check is document_id not False (it return id in fact)
            # after that we can compare values.
            # this is required for saving logic consistancy
            # case return 2 types of values BOOL(false) and INT(id) not Null
            secondaryjoin=lambda: and_(document_id, Document.id == document_id,
                                       documentable_type == cls.__name__),
            viewonly=True,
        )

    @property
    def document_url(self):  # pylint: disable=no-self-argument
        # pylint: disable=not-an-iterable
        return [d for d in self.documents if Document.URL == d.document_type]

    @property
    def document_evidence(self):  # pylint: disable=no-self-argument
        # pylint: disable=not-an-iterable
        return [
            d for d in self.documents if Document.ATTACHMENT == d.document_type
        ]

    @property
    def reference_url(self):  # pylint: disable=no-self-argument
        # pylint: disable=not-an-iterable
        return [
            d for d in self.documents
            if Document.REFERENCE_URL == d.document_type
        ]

    @classmethod
    def eager_query(cls):
        """Eager query classmethod."""
        return cls.eager_inclusions(
            super(Documentable, cls).eager_query(),
            Documentable._include_links,
        ).options(
            orm.subqueryload('documents', ).undefer_group(
                "Document_complete", ), )

    @staticmethod
    def _log_docs(documents):
        """Returns serialization of the given docs"""
        return [d.log_json() for d in documents if d]

    def log_json(self):
        """Serialize to JSON"""
        # This query is required to refresh related documents collection after
        # they were mapped to an object. Otherwise python uses cached value,
        # which might not contain newly created documents.
        out_json = super(Documentable, self).log_json()
        out_json["document_url"] = self._log_docs(self.document_url)
        out_json["document_evidence"] = self._log_docs(self.document_evidence)
        out_json["reference_url"] = self._log_docs(self.reference_url)
        return out_json

    @classmethod
    def indexed_query(cls):
        return super(Documentable, cls).indexed_query().options(
            orm.subqueryload("documents").undefer_group("Document_complete"), )
Exemple #10
0
class WithEvidence(object):
    """WithEvidence mixin."""

    _include_links = []

    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr('evidences_file', 'evidences_file',
                                        ['title', 'link']),
        MultipleSubpropertyFullTextAttr('evidences_url', 'evidences_url',
                                        ['link'])
    ]

    _aliases = {
        "evidences_url": {
            "display_name": "Evidence URL",
            "type": reflection.AttributeInfo.Type.SPECIAL_MAPPING,
            "description": "New line separated list of URLs.",
        },
        "evidences_file": {
            "display_name":
            "Evidence File",
            "type":
            reflection.AttributeInfo.Type.SPECIAL_MAPPING,
            "description": ("New line separated list of evidence links and "
                            "titles.\nExample:\n\nhttp://my.gdrive.link/file "
                            "Title of the evidence link"),
        }
    }

    @declared_attr
    def evidences(cls):  # pylint: disable=no-self-argument
        """Return evidences related for that instance."""
        return db.relationship(
            Evidence,
            primaryjoin=lambda: sa.or_(
                sa.and_(
                    cls.id == Relationship.source_id,
                    Relationship.source_type == cls.__name__,
                    Relationship.destination_type == "Evidence",
                ),
                sa.and_(
                    cls.id == Relationship.destination_id,
                    Relationship.destination_type == cls.__name__,
                    Relationship.source_type == "Evidence",
                )),
            secondary=Relationship.__table__,
            secondaryjoin=lambda: sa.or_(
                sa.and_(
                    Evidence.id == Relationship.source_id,
                    Relationship.source_type == "Evidence",
                ),
                sa.and_(
                    Evidence.id == Relationship.destination_id,
                    Relationship.destination_type == "Evidence",
                )),
            viewonly=True,
        )

    def get_evidences_by_kind(self, kind):
        return [e for e in self.evidences if e.kind == kind]

    @property
    def evidences_url(self):
        return self.get_evidences_by_kind(Evidence.URL)

    @property
    def evidences_file(self):
        return self.get_evidences_by_kind(Evidence.FILE)

    @classmethod
    def eager_query(cls):
        """Eager query classmethod."""
        return cls.eager_inclusions(
            super(WithEvidence, cls).eager_query(),
            WithEvidence._include_links,
        ).options(
            sa.orm.subqueryload('evidences', ).undefer_group(
                'Evidence_complete', ), )

    @staticmethod
    def _log_evidences(evidences):
        """Returns serialization of the given docs"""
        return [e.log_json() for e in evidences if e]

    def log_json(self):
        """Serialize to JSON"""
        # This query is required to refresh related documents collection after
        # they were mapped to an object. Otherwise python uses cached value,
        # which might not contain newly created documents.
        out_json = super(WithEvidence, self).log_json()
        out_json['evidences_url'] = self._log_evidences(self.evidences_url)
        out_json['evidences_file'] = self._log_evidences(self.evidences_file)
        return out_json

    @classmethod
    def indexed_query(cls):
        return super(WithEvidence, cls).indexed_query().options(
            sa.orm.subqueryload("evidences").load_only(
                "kind",
                "title",
                "link",
            ), )
Exemple #11
0
class Commentable(object):
    """Mixin for commentable objects.

  This is a mixin for adding default options to objects on which people can
  comment.

  recipients is used for setting who gets notified (Verifer, Requester, ...).
  send_by_default should be used for setting the "send notification" flag in
  the comment modal.

  """
    # pylint: disable=too-few-public-methods

    VALID_RECIPIENTS = frozenset([
        "Assessor",
        "Assignee",
        "Creator",
        "Verifier",
    ])

    @validates("recipients")
    def validate_recipients(self, key, value):
        """
      Validate recipients list

      Args:
        value (string): Can be either empty, or
                        list of comma separated `VALID_RECIPIENTS`
    """
        # pylint: disable=unused-argument
        if value:
            value = set(name for name in value.split(",") if name)

        if value and value.issubset(self.VALID_RECIPIENTS):
            # The validator is a bit more smart and also makes some filtering of the
            # given data - this is intended.
            return ",".join(value)
        elif not value:
            return None
        else:
            raise ValueError(
                value, 'Value should be either empty ' +
                'or comma separated list of ' +
                ', '.join(sorted(self.VALID_RECIPIENTS)))

    recipients = db.Column(db.String,
                           nullable=True,
                           default=u"Assessor,Creator,Verifier")

    send_by_default = db.Column(db.Boolean, nullable=True, default=True)

    _publish_attrs = [
        "recipients",
        "send_by_default",
    ]
    _aliases = {
        "recipients": "Recipients",
        "send_by_default": "Send by default",
    }
    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr("comment", "comments",
                                        ["description"]),
    ]

    @classmethod
    def indexed_query(cls):
        return super(Commentable, cls).indexed_query().options(
            orm.Load(cls).subqueryload("comments").load_only(
                "id", "description"))

    @declared_attr
    def comments(self):
        """Comments related to self via Relationship table."""
        comment_id = case(
            [(Relationship.destination_type
              == "Comment", Relationship.destination_id)],
            else_=Relationship.source_id,
        )
        commentable_id = case(
            [(Relationship.destination_type
              == "Comment", Relationship.source_id)],
            else_=Relationship.destination_id,
        )

        return db.relationship(
            Comment,
            primaryjoin=lambda: self.id == commentable_id,
            secondary=Relationship.__table__,
            secondaryjoin=lambda: Comment.id == comment_id,
            viewonly=True,
        )
Exemple #12
0
class Cycle(WithContact, Stateful, Timeboxed, Described, Titled, Slugged,
            Notifiable, Indexed, db.Model):
    """Workflow Cycle model
  """
    __tablename__ = 'cycles'
    _title_uniqueness = False

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

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

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

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

    PROPERTY_TEMPLATE = u"cycle {}"

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

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

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

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

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

    Returns:
      a query object with cycle_task_groups added to joined load options.
    """
        query = super(Cycle, cls).eager_query()
        return query.options(orm.joinedload('cycle_task_groups'), )
class CycleTaskGroup(WithContact, Stateful, Slugged, Timeboxed, Described,
                     Titled, Indexed, 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",
        },
    }

    PROPERTY_TEMPLATE = u"group {}"

    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr("task title", 'cycle_task_group_tasks',
                                        ["title"], False),
        MultipleSubpropertyFullTextAttr(
            "task assignee", lambda instance:
            [t.contact for t in instance.cycle_task_group_tasks],
            ["name", "email"], False),
        MultipleSubpropertyFullTextAttr("task due date",
                                        "cycle_task_group_tasks", ["end_date"],
                                        False),
        FullTextAttr(
            "due date",
            'next_due_date',
        ),
        FullTextAttr("assignee", "contact", ['name', 'email']),
        FullTextAttr("cycle title", 'cycle', ['title'], False),
        FullTextAttr("cycle assignee", lambda x: x.cycle.contact,
                     ['email', 'name'], False),
        FullTextAttr("cycle due date",
                     lambda x: x.cycle.next_due_date,
                     with_template=False),
    ]

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

    @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 added to joined load options.
    """
        query = super(CycleTaskGroup, cls).eager_query()
        return query.options(orm.joinedload('cycle_task_group_tasks'))
Exemple #14
0
class Documentable(object):
    """Documentable mixin."""

    _include_links = []

    _fulltext_attrs = [
        MultipleSubpropertyFullTextAttr('documents_file', 'documents_file',
                                        ['title', 'link']),
        MultipleSubpropertyFullTextAttr('documents_reference_url',
                                        'documents_reference_url', ['link']),
    ]

    @declared_attr
    def documents(cls):  # pylint: disable=no-self-argument
        """Return documents related for that instance."""
        return db.relationship(
            Document,
            primaryjoin=lambda: sa.or_(
                sa.and_(
                    cls.id == Relationship.source_id,
                    Relationship.source_type == cls.__name__,
                    Relationship.destination_type == "Document",
                ),
                sa.and_(
                    cls.id == Relationship.destination_id,
                    Relationship.destination_type == cls.__name__,
                    Relationship.source_type == "Document",
                )),
            secondary=Relationship.__table__,
            secondaryjoin=lambda: sa.or_(
                sa.and_(
                    Document.id == Relationship.source_id,
                    Relationship.source_type == "Document",
                ),
                sa.and_(
                    Document.id == Relationship.destination_id,
                    Relationship.destination_type == "Document",
                )),
            viewonly=True,
        )

    def get_documents_by_kind(self, kind):
        return [e for e in self.documents if e.kind == kind]

    @property
    def documents_file(self):
        """List of documents FILE type"""
        return self.get_documents_by_kind(Document.FILE)

    @property
    def documents_reference_url(self):
        """List of documents REFERENCE_URL type"""
        return self.get_documents_by_kind(Document.REFERENCE_URL)

    @classmethod
    def eager_query(cls):
        """Eager query classmethod."""
        return cls.eager_inclusions(
            super(Documentable, cls).eager_query(),
            Documentable._include_links,
        ).options(
            sa.orm.subqueryload('documents', ).undefer_group(
                'Document_complete', ), )

    @staticmethod
    def _log_docs(documents):
        """Returns serialization of the given docs"""
        return [d.log_json() for d in documents if d]

    def log_json(self):
        """Serialize to JSON"""
        # This query is required to refresh related documents collection after
        # they were mapped to an object. Otherwise python uses cached value,
        # which might not contain newly created documents.
        out_json = super(Documentable, self).log_json()
        out_json["documents_file"] = self._log_docs(self.documents_file)
        out_json["documents_reference_url"] = self._log_docs(
            self.documents_reference_url)
        return out_json

    @classmethod
    def indexed_query(cls):
        return super(Documentable, cls).indexed_query().options(
            sa.orm.subqueryload("documents").load_only(
                "title",
                "link",
                "kind",
            ), )