<commit_before># Copyright (C) 2016 Google Inc.
# Licensed under http://www.apache.org/licenses/LICENSE-2.0 <see LICENSE file>

"""Module containing custom attributable mixin."""

import collections

from flask import current_app
from sqlalchemy import and_
from sqlalchemy import orm
from sqlalchemy import or_
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import foreign
from sqlalchemy.orm import relationship
from werkzeug.exceptions import BadRequest

from ggrc import db
from ggrc import utils
from ggrc.models.computed_property import computed_property
from ggrc.models.reflection import AttributeInfo

# pylint: disable=attribute-defined-outside-init; CustomAttributable is a mixin
class CustomAttributable(object):
  """Custom Attributable mixin."""

  _publish_attrs = [
      'custom_attribute_values',
      'custom_attribute_definitions',
      'preconditions_failed',
  ]
  _update_attrs = ['custom_attribute_values', 'custom_attributes']
  _include_links = ['custom_attribute_values', 'custom_attribute_definitions']
  _update_raw = ['custom_attribute_values']

  @declared_attr
  def custom_attribute_definitions(self):
    """Load custom attribute definitions"""
    from ggrc.models.custom_attribute_definition\
        import CustomAttributeDefinition

    def join_function():
      """Object and CAD join function."""
      definition_id = foreign(CustomAttributeDefinition.definition_id)
      definition_type = foreign(CustomAttributeDefinition.definition_type)
      return and_(or_(definition_id == self.id, definition_id.is_(None)),
                  definition_type == self._inflector.table_singular)

    return relationship(
        "CustomAttributeDefinition",
        primaryjoin=join_function,
        backref='{0}_custom_attributable_definition'.format(self.__name__),
        viewonly=True,
    )

  @declared_attr
  def _custom_attributes_deletion(self):
    """This declared attribute is used only for handling cascade deletions
       for CustomAttributes. This is done in order not to try to delete
       "global" custom attributes that don't have any definition_id related.
       Attempt to delete custom attributes with definition_id=None causes the
       IntegrityError as we shouldn't be able to delete global attributes along
       side with any other object (e.g. Assessments).
    """
    from ggrc.models.custom_attribute_definition import (
        CustomAttributeDefinition
    )

    def join_function():
      """Join condition used for deletion"""
      definition_id = foreign(CustomAttributeDefinition.definition_id)
      definition_type = foreign(CustomAttributeDefinition.definition_type)
      return and_(definition_id == self.id,
                  definition_type == self._inflector.table_singular)

    return relationship(
        "CustomAttributeDefinition",
        primaryjoin=join_function,
        cascade='all, delete-orphan',
        order_by="CustomAttributeDefinition.id"
    )

  @declared_attr
  def _custom_attribute_values(self):
    """Load custom attribute values"""
    from ggrc.models.custom_attribute_value import CustomAttributeValue

    def join_function():
      return and_(
          foreign(CustomAttributeValue.attributable_id) == self.id,
          foreign(CustomAttributeValue.attributable_type) == self.__name__)
    return relationship(
        "CustomAttributeValue",
        primaryjoin=join_function,
        backref='{0}_custom_attributable'.format(self.__name__),
        cascade='all, delete-orphan',
    )

  @hybrid_property
  def custom_attribute_values(self):
    return self._custom_attribute_values

  @custom_attribute_values.setter
  def custom_attribute_values(self, values):
    """Setter function for custom attribute values.

    This setter function accepts 2 kinds of values:
      - list of custom attributes. This is used on the back-end by developers.
      - list of dictionaries containing custom attribute values. This is to
        have a clean API where the front-end can put the custom attribute
        values into the custom_attribute_values property and the json builder
        can then handle the attributes just by setting them.

    Args:
      value: List of custom attribute values or dicts containing json
        representation of custom attribute values.
    """
    if not values:
      return

    self._values_map = {
        value.custom_attribute_id or value.custom_attribute.id: value
        for value in self.custom_attribute_values
    }

    if isinstance(values[0], dict):
      self._add_ca_value_dicts(values)
    else:
      self._add_ca_values(values)

  def _add_ca_values(self, values):
    """Add CA value objects to _custom_attributes_values property.

    Args:
      values: list of CustomAttributeValue models
    """
    for new_value in values:
      existing_value = self._values_map.get(new_value.custom_attribute.id)
      if existing_value:
        existing_value.attribute_value = new_value.attribute_value
        existing_value.attribute_object_id = new_value.attribute_object_id
      else:
        new_value.attributable = self
        # new_value is automatically appended to self._custom_attribute_values
        # on new_value.attributable = self

  def _add_ca_value_dicts(self, values):
    """Add CA dict representations to _custom_attributes_values property.

    This adds or updates the _custom_attribute_values with the values in the
    custom attribute values serialized dictionary.

    Args:
      values: List of dictionaries that represent custom attribute values.
    """
    from ggrc.models.custom_attribute_value import CustomAttributeValue

    for value in values:
      attr = self._values_map.get(value.get("custom_attribute_id"))
      if attr:
        attr.attributable = self
        attr.attribute_value = value.get("attribute_value")
        attr.attribute_object_id = value.get("attribute_object_id")
      elif "custom_attribute_id" in value:
        # this is automatically appended to self._custom_attribute_values
        # on attributable=self
        CustomAttributeValue(
            attributable=self,
            custom_attribute_id=value.get("custom_attribute_id"),
            attribute_value=value.get("attribute_value"),
            attribute_object_id=value.get("attribute_object_id"),
        )
      elif "href" in value:
        # Ignore setting of custom attribute stubs. Getting here means that the
        # front-end is not using the API correctly and needs to be updated.
        current_app.logger.info("Ignoring post/put of custom attribute stubs.")
      else:
        raise BadRequest("Bad custom attribute value inserted")

  def insert_definition(self, definition):
    """Insert a new custom attribute definition into database

    Args:
      definition: dictionary with field_name: value
    """
    from ggrc.models.custom_attribute_definition \
        import CustomAttributeDefinition
    field_names = AttributeInfo.gather_create_attrs(
        CustomAttributeDefinition)

    data = {fname: definition.get(fname) for fname in field_names}
    data["definition_type"] = self._inflector.table_singular
    cad = CustomAttributeDefinition(**data)
    db.session.add(cad)

  def process_definitions(self, definitions):
    """
    Process custom attribute definitions

    If present, delete all related custom attribute definition and insert new
    custom attribute definitions in the order provided.

    Args:
      definitions: Ordered list of custom attribute definitions
    """
    from ggrc.models.custom_attribute_definition \
        import CustomAttributeDefinition as CADef

    if not hasattr(self, "PER_OBJECT_CUSTOM_ATTRIBUTABLE"):
      return

    if self.id is not None:
      db.session.query(CADef).filter(
          CADef.definition_id == self.id,
          CADef.definition_type == self._inflector.table_singular
      ).delete()
      db.session.commit()

    for definition in definitions:
      if "_pending_delete" in definition and definition["_pending_delete"]:
        continue
      self.insert_definition(definition)

  def custom_attributes(self, src):
    """Legacy setter for custom attribute values and definitions.

    This code should only be used for custom attribute definitions until
    setter for that is updated.
    """
    from ggrc.fulltext.mysql import MysqlRecordProperty
    from ggrc.models.custom_attribute_value import CustomAttributeValue
    from ggrc.services import signals

    ca_values = src.get("custom_attribute_values")
    if ca_values and "attribute_value" in ca_values[0]:
      # This indicates that the new CA API is being used and the legacy API
      # should be ignored. If we need to use the legacy API the
      # custom_attribute_values property should contain stubs instead of entire
      # objects.
      return

    definitions = src.get("custom_attribute_definitions")
    if definitions:
      self.process_definitions(definitions)

    attributes = src.get("custom_attributes")
    if not attributes:
      return

    old_values = collections.defaultdict(list)
    last_values = dict()

    # attributes looks like this:
    #    [ {<id of attribute definition> : attribute value, ... }, ... ]

    # 1) Get all custom attribute values for the CustomAttributable instance
    attr_values = db.session.query(CustomAttributeValue).filter(and_(
        CustomAttributeValue.attributable_type == self.__class__.__name__,
        CustomAttributeValue.attributable_id == self.id)).all()

    attr_value_ids = [value.id for value in attr_values]
    ftrp_properties = [
        "attribute_value_{id}".format(id=_id) for _id in attr_value_ids]

    # Save previous value of custom attribute. This is a bit complicated by
    # the fact that imports can save multiple values at the time of writing.
    # old_values holds all previous values of attribute, last_values holds
    # chronologically last value.
    for value in attr_values:
      old_values[value.custom_attribute_id].append(
          (value.created_at, value.attribute_value))

    last_values = {str(key): max(old_vals,
                                 key=lambda (created_at, _): created_at)
                   for key, old_vals in old_values.iteritems()}

    # 2) Delete all fulltext_record_properties for the list of values
    if len(attr_value_ids) > 0:
      db.session.query(MysqlRecordProperty)\
          .filter(
              and_(
                  MysqlRecordProperty.type == self.__class__.__name__,
                  MysqlRecordProperty.property.in_(ftrp_properties)))\
          .delete(synchronize_session='fetch')

      # 3) Delete the list of custom attribute values
      db.session.query(CustomAttributeValue)\
          .filter(CustomAttributeValue.id.in_(attr_value_ids))\
          .delete(synchronize_session='fetch')

      db.session.commit()

    # 4) Instantiate custom attribute values for each of the definitions
    #    passed in (keys)
    # pylint: disable=not-an-iterable
    definitions = {d.id: d for d in self.get_custom_attribute_definitions()}
    for ad_id in attributes.keys():
      obj_type = self.__class__.__name__
      obj_id = self.id
      new_value = CustomAttributeValue(
          custom_attribute_id=ad_id,
          attributable=self,
          attribute_value=attributes[ad_id],
      )
      if definitions[int(ad_id)].attribute_type.startswith("Map:"):
        obj_type, obj_id = new_value.attribute_value.split(":")
        new_value.attribute_value = obj_type
        new_value.attribute_object_id = obj_id
      # 5) Set the context_id for each custom attribute value to the context id
      #    of the custom attributable.
      # TODO: We are ignoring contexts for now
      # new_value.context_id = cls.context_id
      self.custom_attribute_values.append(new_value)
      if ad_id in last_values:
        _, previous_value = last_values[ad_id]
        if previous_value != attributes[ad_id]:
          signals.Signals.custom_attribute_changed.send(
              self.__class__,
              obj=self,
              src={
                  "type": obj_type,
                  "id": obj_id,
                  "operation": "UPDATE",
                  "value": new_value,
                  "old": previous_value
              }, service=self.__class__.__name__)
      else:
        signals.Signals.custom_attribute_changed.send(
            self.__class__,
            obj=self,
            src={
                "type": obj_type,
                "id": obj_id,
                "operation": "INSERT",
                "value": new_value,
            }, service=self.__class__.__name__)

  @classmethod
  def get_custom_attribute_definitions(cls):
    """Get all applicable CA definitions (even ones without a value yet)."""
    from ggrc.models.custom_attribute_definition import \
        CustomAttributeDefinition as cad
    if cls.__name__ == "Assessment":
      return cad.query.filter(or_(
          cad.definition_type == utils.underscore_from_camelcase(cls.__name__),
          cad.definition_type == "assessment_template",
      )).all()
    else:
      return cad.query.filter(
          cad.definition_type == utils.underscore_from_camelcase(cls.__name__)
      ).all()

  @classmethod
  def eager_query(cls):
    """Define fields to be loaded eagerly to lower the count of DB queries."""
    query = super(CustomAttributable, cls).eager_query()
    query = query.options(
        orm.subqueryload('custom_attribute_definitions')
           .undefer_group('CustomAttributeDefinition_complete'),
        orm.subqueryload('_custom_attribute_values')
           .undefer_group('CustomAttributeValue_complete'),
    )
    if hasattr(cls, 'comments'):
      # only for Commentable classess
      query = query.options(
          orm.subqueryload('comments'),
      )
    return query

  def log_json(self):
    """Log custom attribute values."""
    # pylint: disable=not-an-iterable
    from ggrc.models.custom_attribute_definition import \
        CustomAttributeDefinition
    # to integrate with Base mixin without order dependencies
    res = getattr(super(CustomAttributable, self), "log_json", lambda: {})()

    if self.custom_attribute_values:
      res["custom_attributes"] = [value.log_json()
                                  for value in self.custom_attribute_values]
      # fetch definitions form database because `self.custom_attribute`
      # may not be populated
      defs = CustomAttributeDefinition.query.filter(
          CustomAttributeDefinition.definition_type == self.type,
          CustomAttributeDefinition.id.in_([
              value.custom_attribute_id
              for value in self.custom_attribute_values
          ])
      )
      # also log definitions to freeze field names in time
      res["custom_attribute_definitions"] = [definition.log_json()
                                             for definition in defs]
    else:
      res["custom_attribute_definitions"] = []
      res["custom_attributes"] = []

    return res

  def validate_custom_attributes(self):
    # pylint: disable=not-an-iterable; we can iterate over relationships
    map_ = {d.id: d for d in self.custom_attribute_definitions}
    for value in self._custom_attribute_values:
      if not value.custom_attribute and value.custom_attribute_id:
        value.custom_attribute = map_.get(int(value.custom_attribute_id))
      value.validate()

  @computed_property
  def preconditions_failed(self):
    """Returns True if any mandatory CAV, comment or evidence is missing."""
    values_map = {
        cav.custom_attribute_id or cav.custom_attribute.id: cav
        for cav in self.custom_attribute_values
    }
    # pylint: disable=not-an-iterable; we can iterate over relationships
    for cad in self.custom_attribute_definitions:
      if cad.mandatory:
        cav = values_map.get(cad.id)
        if not cav or not cav.attribute_value:
          return True

    return any(cav.preconditions_failed
               for cav in self.custom_attribute_values)
<commit_msg>Fix storing Map:Person CA values<commit_after># Copyright (C) 2016 Google Inc.
# Licensed under http://www.apache.org/licenses/LICENSE-2.0 <see LICENSE file>

"""Module containing custom attributable mixin."""

import collections

from flask import current_app
from sqlalchemy import and_
from sqlalchemy import orm
from sqlalchemy import or_
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import foreign
from sqlalchemy.orm import relationship
from werkzeug.exceptions import BadRequest

from ggrc import db
from ggrc import utils
from ggrc.models.computed_property import computed_property
from ggrc.models.reflection import AttributeInfo

# pylint: disable=attribute-defined-outside-init; CustomAttributable is a mixin
class CustomAttributable(object):
  """Custom Attributable mixin."""

  _publish_attrs = [
      'custom_attribute_values',
      'custom_attribute_definitions',
      'preconditions_failed',
  ]
  _update_attrs = ['custom_attribute_values', 'custom_attributes']
  _include_links = ['custom_attribute_values', 'custom_attribute_definitions']
  _update_raw = ['custom_attribute_values']

  @declared_attr
  def custom_attribute_definitions(self):
    """Load custom attribute definitions"""
    from ggrc.models.custom_attribute_definition\
        import CustomAttributeDefinition

    def join_function():
      """Object and CAD join function."""
      definition_id = foreign(CustomAttributeDefinition.definition_id)
      definition_type = foreign(CustomAttributeDefinition.definition_type)
      return and_(or_(definition_id == self.id, definition_id.is_(None)),
                  definition_type == self._inflector.table_singular)

    return relationship(
        "CustomAttributeDefinition",
        primaryjoin=join_function,
        backref='{0}_custom_attributable_definition'.format(self.__name__),
        viewonly=True,
    )

  @declared_attr
  def _custom_attributes_deletion(self):
    """This declared attribute is used only for handling cascade deletions
       for CustomAttributes. This is done in order not to try to delete
       "global" custom attributes that don't have any definition_id related.
       Attempt to delete custom attributes with definition_id=None causes the
       IntegrityError as we shouldn't be able to delete global attributes along
       side with any other object (e.g. Assessments).
    """
    from ggrc.models.custom_attribute_definition import (
        CustomAttributeDefinition
    )

    def join_function():
      """Join condition used for deletion"""
      definition_id = foreign(CustomAttributeDefinition.definition_id)
      definition_type = foreign(CustomAttributeDefinition.definition_type)
      return and_(definition_id == self.id,
                  definition_type == self._inflector.table_singular)

    return relationship(
        "CustomAttributeDefinition",
        primaryjoin=join_function,
        cascade='all, delete-orphan',
        order_by="CustomAttributeDefinition.id"
    )

  @declared_attr
  def _custom_attribute_values(self):
    """Load custom attribute values"""
    from ggrc.models.custom_attribute_value import CustomAttributeValue

    def join_function():
      return and_(
          foreign(CustomAttributeValue.attributable_id) == self.id,
          foreign(CustomAttributeValue.attributable_type) == self.__name__)
    return relationship(
        "CustomAttributeValue",
        primaryjoin=join_function,
        backref='{0}_custom_attributable'.format(self.__name__),
        cascade='all, delete-orphan',
    )

  @hybrid_property
  def custom_attribute_values(self):
    return self._custom_attribute_values

  @custom_attribute_values.setter
  def custom_attribute_values(self, values):
    """Setter function for custom attribute values.

    This setter function accepts 2 kinds of values:
      - list of custom attributes. This is used on the back-end by developers.
      - list of dictionaries containing custom attribute values. This is to
        have a clean API where the front-end can put the custom attribute
        values into the custom_attribute_values property and the json builder
        can then handle the attributes just by setting them.

    Args:
      value: List of custom attribute values or dicts containing json
        representation of custom attribute values.
    """
    if not values:
      return

    self._values_map = {
        value.custom_attribute_id or value.custom_attribute.id: value
        for value in self.custom_attribute_values
    }

    if isinstance(values[0], dict):
      self._add_ca_value_dicts(values)
    else:
      self._add_ca_values(values)

  def _add_ca_values(self, values):
    """Add CA value objects to _custom_attributes_values property.

    Args:
      values: list of CustomAttributeValue models
    """
    for new_value in values:
      existing_value = self._values_map.get(new_value.custom_attribute.id)
      if existing_value:
        existing_value.attribute_value = new_value.attribute_value
        existing_value.attribute_object_id = new_value.attribute_object_id
      else:
        new_value.attributable = self
        # new_value is automatically appended to self._custom_attribute_values
        # on new_value.attributable = self

  def _add_ca_value_dicts(self, values):
    """Add CA dict representations to _custom_attributes_values property.

    This adds or updates the _custom_attribute_values with the values in the
    custom attribute values serialized dictionary.

    Args:
      values: List of dictionaries that represent custom attribute values.
    """
    from ggrc.models.custom_attribute_value import CustomAttributeValue

    for value in values:
      if not value.get("attribute_object_id"):
        # value.get("attribute_object", {}).get("id") won't help because
        # value["attribute_object"] can be None
        value["attribute_object_id"] = (value["attribute_object"].get("id") if
                                        value.get("attribute_object") else
                                        None)
      attr = self._values_map.get(value.get("custom_attribute_id"))
      if attr:
        attr.attributable = self
        attr.attribute_value = value.get("attribute_value")
        attr.attribute_object_id = value.get("attribute_object_id")
      elif "custom_attribute_id" in value:
        # this is automatically appended to self._custom_attribute_values
        # on attributable=self
        CustomAttributeValue(
            attributable=self,
            custom_attribute_id=value.get("custom_attribute_id"),
            attribute_value=value.get("attribute_value"),
            attribute_object_id=value.get("attribute_object_id"),
        )
      elif "href" in value:
        # Ignore setting of custom attribute stubs. Getting here means that the
        # front-end is not using the API correctly and needs to be updated.
        current_app.logger.info("Ignoring post/put of custom attribute stubs.")
      else:
        raise BadRequest("Bad custom attribute value inserted")

  def insert_definition(self, definition):
    """Insert a new custom attribute definition into database

    Args:
      definition: dictionary with field_name: value
    """
    from ggrc.models.custom_attribute_definition \
        import CustomAttributeDefinition
    field_names = AttributeInfo.gather_create_attrs(
        CustomAttributeDefinition)

    data = {fname: definition.get(fname) for fname in field_names}
    data["definition_type"] = self._inflector.table_singular
    cad = CustomAttributeDefinition(**data)
    db.session.add(cad)

  def process_definitions(self, definitions):
    """
    Process custom attribute definitions

    If present, delete all related custom attribute definition and insert new
    custom attribute definitions in the order provided.

    Args:
      definitions: Ordered list of custom attribute definitions
    """
    from ggrc.models.custom_attribute_definition \
        import CustomAttributeDefinition as CADef

    if not hasattr(self, "PER_OBJECT_CUSTOM_ATTRIBUTABLE"):
      return

    if self.id is not None:
      db.session.query(CADef).filter(
          CADef.definition_id == self.id,
          CADef.definition_type == self._inflector.table_singular
      ).delete()
      db.session.commit()

    for definition in definitions:
      if "_pending_delete" in definition and definition["_pending_delete"]:
        continue
      self.insert_definition(definition)

  def custom_attributes(self, src):
    """Legacy setter for custom attribute values and definitions.

    This code should only be used for custom attribute definitions until
    setter for that is updated.
    """
    from ggrc.fulltext.mysql import MysqlRecordProperty
    from ggrc.models.custom_attribute_value import CustomAttributeValue
    from ggrc.services import signals

    ca_values = src.get("custom_attribute_values")
    if ca_values and "attribute_value" in ca_values[0]:
      # This indicates that the new CA API is being used and the legacy API
      # should be ignored. If we need to use the legacy API the
      # custom_attribute_values property should contain stubs instead of entire
      # objects.
      return

    definitions = src.get("custom_attribute_definitions")
    if definitions:
      self.process_definitions(definitions)

    attributes = src.get("custom_attributes")
    if not attributes:
      return

    old_values = collections.defaultdict(list)
    last_values = dict()

    # attributes looks like this:
    #    [ {<id of attribute definition> : attribute value, ... }, ... ]

    # 1) Get all custom attribute values for the CustomAttributable instance
    attr_values = db.session.query(CustomAttributeValue).filter(and_(
        CustomAttributeValue.attributable_type == self.__class__.__name__,
        CustomAttributeValue.attributable_id == self.id)).all()

    attr_value_ids = [value.id for value in attr_values]
    ftrp_properties = [
        "attribute_value_{id}".format(id=_id) for _id in attr_value_ids]

    # Save previous value of custom attribute. This is a bit complicated by
    # the fact that imports can save multiple values at the time of writing.
    # old_values holds all previous values of attribute, last_values holds
    # chronologically last value.
    for value in attr_values:
      old_values[value.custom_attribute_id].append(
          (value.created_at, value.attribute_value))

    last_values = {str(key): max(old_vals,
                                 key=lambda (created_at, _): created_at)
                   for key, old_vals in old_values.iteritems()}

    # 2) Delete all fulltext_record_properties for the list of values
    if len(attr_value_ids) > 0:
      db.session.query(MysqlRecordProperty)\
          .filter(
              and_(
                  MysqlRecordProperty.type == self.__class__.__name__,
                  MysqlRecordProperty.property.in_(ftrp_properties)))\
          .delete(synchronize_session='fetch')

      # 3) Delete the list of custom attribute values
      db.session.query(CustomAttributeValue)\
          .filter(CustomAttributeValue.id.in_(attr_value_ids))\
          .delete(synchronize_session='fetch')

      db.session.commit()

    # 4) Instantiate custom attribute values for each of the definitions
    #    passed in (keys)
    # pylint: disable=not-an-iterable
    definitions = {d.id: d for d in self.get_custom_attribute_definitions()}
    for ad_id in attributes.keys():
      obj_type = self.__class__.__name__
      obj_id = self.id
      new_value = CustomAttributeValue(
          custom_attribute_id=ad_id,
          attributable=self,
          attribute_value=attributes[ad_id],
      )
      if definitions[int(ad_id)].attribute_type.startswith("Map:"):
        obj_type, obj_id = new_value.attribute_value.split(":")
        new_value.attribute_value = obj_type
        new_value.attribute_object_id = obj_id
      # 5) Set the context_id for each custom attribute value to the context id
      #    of the custom attributable.
      # TODO: We are ignoring contexts for now
      # new_value.context_id = cls.context_id
      self.custom_attribute_values.append(new_value)
      if ad_id in last_values:
        _, previous_value = last_values[ad_id]
        if previous_value != attributes[ad_id]:
          signals.Signals.custom_attribute_changed.send(
              self.__class__,
              obj=self,
              src={
                  "type": obj_type,
                  "id": obj_id,
                  "operation": "UPDATE",
                  "value": new_value,
                  "old": previous_value
              }, service=self.__class__.__name__)
      else:
        signals.Signals.custom_attribute_changed.send(
            self.__class__,
            obj=self,
            src={
                "type": obj_type,
                "id": obj_id,
                "operation": "INSERT",
                "value": new_value,
            }, service=self.__class__.__name__)

  @classmethod
  def get_custom_attribute_definitions(cls):
    """Get all applicable CA definitions (even ones without a value yet)."""
    from ggrc.models.custom_attribute_definition import \
        CustomAttributeDefinition as cad
    if cls.__name__ == "Assessment":
      return cad.query.filter(or_(
          cad.definition_type == utils.underscore_from_camelcase(cls.__name__),
          cad.definition_type == "assessment_template",
      )).all()
    else:
      return cad.query.filter(
          cad.definition_type == utils.underscore_from_camelcase(cls.__name__)
      ).all()

  @classmethod
  def eager_query(cls):
    """Define fields to be loaded eagerly to lower the count of DB queries."""
    query = super(CustomAttributable, cls).eager_query()
    query = query.options(
        orm.subqueryload('custom_attribute_definitions')
           .undefer_group('CustomAttributeDefinition_complete'),
        orm.subqueryload('_custom_attribute_values')
           .undefer_group('CustomAttributeValue_complete'),
    )
    if hasattr(cls, 'comments'):
      # only for Commentable classess
      query = query.options(
          orm.subqueryload('comments'),
      )
    return query

  def log_json(self):
    """Log custom attribute values."""
    # pylint: disable=not-an-iterable
    from ggrc.models.custom_attribute_definition import \
        CustomAttributeDefinition
    # to integrate with Base mixin without order dependencies
    res = getattr(super(CustomAttributable, self), "log_json", lambda: {})()

    if self.custom_attribute_values:
      res["custom_attributes"] = [value.log_json()
                                  for value in self.custom_attribute_values]
      # fetch definitions form database because `self.custom_attribute`
      # may not be populated
      defs = CustomAttributeDefinition.query.filter(
          CustomAttributeDefinition.definition_type == self.type,
          CustomAttributeDefinition.id.in_([
              value.custom_attribute_id
              for value in self.custom_attribute_values
          ])
      )
      # also log definitions to freeze field names in time
      res["custom_attribute_definitions"] = [definition.log_json()
                                             for definition in defs]
    else:
      res["custom_attribute_definitions"] = []
      res["custom_attributes"] = []

    return res

  def validate_custom_attributes(self):
    # pylint: disable=not-an-iterable; we can iterate over relationships
    map_ = {d.id: d for d in self.custom_attribute_definitions}
    for value in self._custom_attribute_values:
      if not value.custom_attribute and value.custom_attribute_id:
        value.custom_attribute = map_.get(int(value.custom_attribute_id))
      value.validate()

  @computed_property
  def preconditions_failed(self):
    """Returns True if any mandatory CAV, comment or evidence is missing."""
    values_map = {
        cav.custom_attribute_id or cav.custom_attribute.id: cav
        for cav in self.custom_attribute_values
    }
    # pylint: disable=not-an-iterable; we can iterate over relationships
    for cad in self.custom_attribute_definitions:
      if cad.mandatory:
        cav = values_map.get(cad.id)
        if not cav or not cav.attribute_value:
          return True

    return any(cav.preconditions_failed
               for cav in self.custom_attribute_values)
<|endoftext|>


<commit_before>#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
A python script to calculate E96 resistor values
... and do other useful things with resistors, e.g.
connect them in parallel and serial fashions.

Originally published at techoverflow.net
"""
import itertools
import numpy as np
from UliEngineering.EngineerIO import normalize_numeric
from UliEngineering.Units import Unit

__all__ = ["e96", "e48", "e24", "e12", "resistor_range",
           "standard_resistors", "parallel_resistors",
           "current_through_resistor",
           "power_dissipated_in_resistor_by_current",
           "power_dissipated_in_resistor_by_voltage",
           "voltage_across_resistor",
           "series_resistors", "nearest_resistor",
           "resistor_by_voltage_and_current"]

# Standard resistor sequences
e96 = np.asarray([
       1.00, 1.02, 1.05, 1.07, 1.10, 1.13, 1.15, 1.18, 1.21, 1.24, 1.27, 1.30, 1.33, 1.37, 1.40,
       1.43, 1.47, 1.50, 1.54, 1.58, 1.62, 1.65, 1.69, 1.74, 1.78, 1.82, 1.87, 1.91, 1.96, 2.00,
       2.05, 2.10, 2.15, 2.21, 2.26, 2.32, 2.37, 2.43, 2.49, 2.55, 2.61, 2.67, 2.74, 2.80, 2.87,
       2.94, 3.01, 3.09, 3.16, 3.24, 3.32, 3.40, 3.48, 3.57, 3.65, 3.74, 3.83, 3.92, 4.02, 4.12,
       4.22, 4.32, 4.42, 4.53, 4.64, 4.75, 4.87, 4.99, 5.11, 5.23, 5.36, 5.49, 5.62, 5.76, 5.90,
       6.04, 6.19, 6.34, 6.49, 6.65, 6.81, 6.98, 7.15, 7.32, 7.50, 7.68, 7.87, 8.06, 8.25, 8.45,
       8.66, 8.87, 9.09, 9.31, 9.53, 9.76])
e48 = np.asarray([
       1.00, 1.05, 1.10, 1.15, 1.21, 1.27, 1.33, 1.40, 1.47, 1.54, 1.62, 1.69,
       1.78, 1.87, 1.96, 2.05, 2.15, 2.26, 2.37, 2.49, 2.61, 2.74, 2.87, 3.01,
       3.16, 3.32, 3.48, 3.65, 3.83, 4.02, 4.22, 4.42, 4.64, 4.87, 5.11, 5.36,
       5.62, 5.90, 6.19, 6.49, 6.81, 7.15, 7.50, 7.87, 8.25, 8.66, 9.09, 9.53])
e24 = np.asarray([1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0,
                  3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1])
e12 = np.asarray([1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2])

def current_through_resistor(resistor, voltage) -> Unit("A"):
    """
    Compute the current that flows through a resistor
    using ohms law.

    Parameters
    ----------
    resistor : float or Engineer string
        The resistor in Ohms
    voltage : float or Engineer string
        The voltage across the resistor
    """
    resistor = normalize_numeric(resistor)
    voltage = normalize_numeric(voltage)
    return voltage / resistor

def voltage_across_resistor(resistor, current) -> Unit("A"):
    """
    Compute the voltage that is dropped across
    a resistor using ohms law.

    Parameters
    ----------
    resistor : float or Engineer string
        The resistor in Ohms
    current : float or Engineer string
        The current flowing through the resistor
    """
    resistor = normalize_numeric(resistor)
    current = normalize_numeric(current)
    return resistor * current

def power_dissipated_in_resistor_by_current(resistor, current) -> Unit("W"):
    """
    Compute the power that is dissipated in
    a resistor using P=I²R given
    its resistance and the current flowing through it

    Parameters
    ----------
    resistor : float or Engineer string
        The resistor in Ohms
    current : float or Engineer string
        The current flowing through the resistor
    """
    resistor = normalize_numeric(resistor)
    current = normalize_numeric(current)
    return np.abs(resistor * current * current)

def power_dissipated_in_resistor_by_voltage(resistor, voltage) -> Unit("W"):
    """
    Compute the power that is dissipated in
    a resistor using P=VI given
    its resistance and the current flowing through it

    Parameters
    ----------
    resistor : float or Engineer string
        The resistor in Ohms
    current : float or Engineer string
        The current flowing through the resistor
    """
    resistor = normalize_numeric(resistor)
    voltage = normalize_numeric(voltage)
    current = current_through_resistor(resistor, voltage)
    return np.abs(current * voltage)

def resistor_range(multiplicator, sequence=e96):
    """
    Get a single range of resistors of a given sequence,
    e.g. for 1k to <10k use multiplicator = 1000.
    """
    return sequence * multiplicator

def standard_resistors(minExp=-1, maxExp=9, sequence=e96):
    """
    Get a list of all standard resistor values from 100mOhm up to 976 MΩ in Ω"""
    exponents = itertools.islice(itertools.count(minExp, 1), 0, maxExp - minExp)
    multiplicators = [10 ** x for x in exponents]
    return itertools.chain(*(resistor_range(r, sequence=sequence) for r in multiplicators))

def nearest_resistor(value, sequence=e96) -> Unit("Ω"):
    """
    Find the standard reistor value with the minimal difference to the given value
    """
    return min(standard_resistors(sequence=sequence), key=lambda r: abs(value - r))

def parallel_resistors(*args) -> Unit("Ω"):
    """
    Compute the total resistance of n parallel resistors and return
    the value in Ohms.
    """
    resistors = np.asarray(list(map(normalize_numeric, args)))
    # Check for zeros
    if len(np.nonzero(resistors)) != len(resistors):
        return 0.0
    return 1.0 / np.sum(np.reciprocal(resistors))

def series_resistors(*args) -> Unit("Ω"):
    """
    Compute the total resistance of n parallel resistors and return
    the value in Ohms.
    """
    resistors = list(map(normalize_numeric, args))
    return sum(resistors)

def resistor_by_voltage_and_current(voltage, current) -> Unit("Ω"):
    """
    Compute the resistance value in Ohms that draws the given amount of
    current if the given voltage is across it.
    """
    voltage = normalize_numeric(voltage)
    current = normalize_numeric(current)
    return voltage / current<commit_msg>Fix parallel_resistors bug due to zero handling<commit_after>#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
A python script to calculate E96 resistor values
... and do other useful things with resistors, e.g.
connect them in parallel and serial fashions.

Originally published at techoverflow.net
"""
import itertools
import numpy as np
from UliEngineering.EngineerIO import normalize_numeric
from UliEngineering.Units import Unit

__all__ = ["e96", "e48", "e24", "e12", "resistor_range",
           "standard_resistors", "parallel_resistors",
           "current_through_resistor",
           "power_dissipated_in_resistor_by_current",
           "power_dissipated_in_resistor_by_voltage",
           "voltage_across_resistor",
           "series_resistors", "nearest_resistor",
           "resistor_by_voltage_and_current"]

# Standard resistor sequences
e96 = np.asarray([
       1.00, 1.02, 1.05, 1.07, 1.10, 1.13, 1.15, 1.18, 1.21, 1.24, 1.27, 1.30, 1.33, 1.37, 1.40,
       1.43, 1.47, 1.50, 1.54, 1.58, 1.62, 1.65, 1.69, 1.74, 1.78, 1.82, 1.87, 1.91, 1.96, 2.00,
       2.05, 2.10, 2.15, 2.21, 2.26, 2.32, 2.37, 2.43, 2.49, 2.55, 2.61, 2.67, 2.74, 2.80, 2.87,
       2.94, 3.01, 3.09, 3.16, 3.24, 3.32, 3.40, 3.48, 3.57, 3.65, 3.74, 3.83, 3.92, 4.02, 4.12,
       4.22, 4.32, 4.42, 4.53, 4.64, 4.75, 4.87, 4.99, 5.11, 5.23, 5.36, 5.49, 5.62, 5.76, 5.90,
       6.04, 6.19, 6.34, 6.49, 6.65, 6.81, 6.98, 7.15, 7.32, 7.50, 7.68, 7.87, 8.06, 8.25, 8.45,
       8.66, 8.87, 9.09, 9.31, 9.53, 9.76])
e48 = np.asarray([
       1.00, 1.05, 1.10, 1.15, 1.21, 1.27, 1.33, 1.40, 1.47, 1.54, 1.62, 1.69,
       1.78, 1.87, 1.96, 2.05, 2.15, 2.26, 2.37, 2.49, 2.61, 2.74, 2.87, 3.01,
       3.16, 3.32, 3.48, 3.65, 3.83, 4.02, 4.22, 4.42, 4.64, 4.87, 5.11, 5.36,
       5.62, 5.90, 6.19, 6.49, 6.81, 7.15, 7.50, 7.87, 8.25, 8.66, 9.09, 9.53])
e24 = np.asarray([1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0,
                  3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1])
e12 = np.asarray([1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2])

def current_through_resistor(resistor, voltage) -> Unit("A"):
    """
    Compute the current that flows through a resistor
    using ohms law.

    Parameters
    ----------
    resistor : float or Engineer string
        The resistor in Ohms
    voltage : float or Engineer string
        The voltage across the resistor
    """
    resistor = normalize_numeric(resistor)
    voltage = normalize_numeric(voltage)
    return voltage / resistor

def voltage_across_resistor(resistor, current) -> Unit("A"):
    """
    Compute the voltage that is dropped across
    a resistor using ohms law.

    Parameters
    ----------
    resistor : float or Engineer string
        The resistor in Ohms
    current : float or Engineer string
        The current flowing through the resistor
    """
    resistor = normalize_numeric(resistor)
    current = normalize_numeric(current)
    return resistor * current

def power_dissipated_in_resistor_by_current(resistor, current) -> Unit("W"):
    """
    Compute the power that is dissipated in
    a resistor using P=I²R given
    its resistance and the current flowing through it

    Parameters
    ----------
    resistor : float or Engineer string
        The resistor in Ohms
    current : float or Engineer string
        The current flowing through the resistor
    """
    resistor = normalize_numeric(resistor)
    current = normalize_numeric(current)
    return np.abs(resistor * current * current)

def power_dissipated_in_resistor_by_voltage(resistor, voltage) -> Unit("W"):
    """
    Compute the power that is dissipated in
    a resistor using P=VI given
    its resistance and the current flowing through it

    Parameters
    ----------
    resistor : float or Engineer string
        The resistor in Ohms
    current : float or Engineer string
        The current flowing through the resistor
    """
    resistor = normalize_numeric(resistor)
    voltage = normalize_numeric(voltage)
    current = current_through_resistor(resistor, voltage)
    return np.abs(current * voltage)

def resistor_range(multiplicator, sequence=e96):
    """
    Get a single range of resistors of a given sequence,
    e.g. for 1k to <10k use multiplicator = 1000.
    """
    return sequence * multiplicator

def standard_resistors(minExp=-1, maxExp=9, sequence=e96):
    """
    Get a list of all standard resistor values from 100mOhm up to 976 MΩ in Ω"""
    exponents = itertools.islice(itertools.count(minExp, 1), 0, maxExp - minExp)
    multiplicators = [10 ** x for x in exponents]
    return itertools.chain(*(resistor_range(r, sequence=sequence) for r in multiplicators))

def nearest_resistor(value, sequence=e96) -> Unit("Ω"):
    """
    Find the standard reistor value with the minimal difference to the given value
    """
    return min(standard_resistors(sequence=sequence), key=lambda r: abs(value - r))

def parallel_resistors(*args) -> Unit("Ω"):
    """
    Compute the total resistance of n parallel resistors and return
    the value in Ohms.
    """
    resistors = np.asarray(list(map(normalize_numeric, args)))
    # Check for zeros
    if len(np.nonzero(resistors)[0]) != len(resistors):
        return 0.0
    return 1.0 / np.sum(np.reciprocal(resistors))

def series_resistors(*args) -> Unit("Ω"):
    """
    Compute the total resistance of n parallel resistors and return
    the value in Ohms.
    """
    resistors = list(map(normalize_numeric, args))
    return sum(resistors)

def resistor_by_voltage_and_current(voltage, current) -> Unit("Ω"):
    """
    Compute the resistance value in Ohms that draws the given amount of
    current if the given voltage is across it.
    """
    voltage = normalize_numeric(voltage)
    current = normalize_numeric(current)
    return voltage / current<|endoftext|>


<commit_before>"""Tests for CNF detection in expression trees."""

import unittest

from tt.trees import BooleanExpressionTree as bet

class TestExpressionTreeIsCnf(unittest.TestCase):

    def test_is_cnf_single_operand(self):
        """Test cnf determination for single operand trees."""
        for postfix_tokens in (['0'], ['1'], ['token']):
            self.assertTrue(bet(postfix_tokens).is_cnf)

    def test_is_cnf_only_unary_operators(self):
        """Test cnf determination for trees with only unary operators."""
        self.assertTrue(bet(['A', 'not']).is_cnf)
        self.assertFalse(bet(['A', 'not', 'not']).is_cnf)
        self.assertFalse(bet(['A', 'not', 'not', 'not']).is_cnf)

    def test_is_cnf_single_clause(self):
        """Test that a single clause of ORed operands is in cnf form."""
        self.assertTrue(bet(['op1', 'op2', 'or']).is_cnf)
        self.assertTrue(bet(['A', 'B', 'C', 'D', 'or', 'or', 'or']).is_cnf)
        self.assertTrue(bet(['A', '~', 'B', 'C', '~', 'or', 'or']).is_cnf)

    def test_is_cnf_clauses_of_single_operands(self):
        """Test several ANDed clauses of single operands is in cnf form."""
        self.assertTrue(bet(['op1', 'op2', 'and']).is_cnf)
        self.assertTrue(bet(['A', 'B', 'C', 'D', 'and', 'and', 'and']).is_cnf)
        self.assertTrue(bet(['A', 'B', 'not', 'C', 'and', 'and']).is_cnf)

    def test_is_cnf_contains_non_primitive_operator_in_clause(self):
        """Test cases where non-primitive operator is in a clause."""
        self.assertFalse(bet(['A', 'B', 'or',
                              'C', 'D', 'or',
                              'D', 'E', 'nand',
                              'and', 'and']).is_cnf)
        self.assertFalse(bet(['A', 'B', 'C', 'xor', 'or',
                              'A', 'B', 'or',
                              'and']).is_cnf)

    def test_is_cnf_contains_non_primitive_operator_joining_clauses(self):
        """Test cases where non-primitive operator joins clauses."""
        self.assertFalse(bet(['A', 'B', '<->']).is_cnf)
        self.assertFalse(bet(['A', 'B', 'or',
                              'B', 'C', 'or',
                              'C', 'E', 'or',
                              '->', 'and']).is_cnf)

    def test_is_cnf_contains_notted_clause(self):
        """Test cases where an entire clause is notted."""
        self.assertFalse(bet(['A', 'B', 'or', 'not',
                              'C', 'D', 'or',
                              'and']).is_cnf)

    def test_is_cnf_multiple_clauses(self):
        """Test cnf determination for mulitple clauses."""
        self.assertTrue(bet(['A', 'C', 'or',
                             'B', 'C', 'or',
                             'E', 'F', 'G', 'or', 'or',
                             'and', 'and']).is_cnf)
<commit_msg>Refactor tree is_cnf tests to use basic assert_* methods<commit_after>"""Tests for CNF detection in expression trees."""

import unittest

from tt.trees import BooleanExpressionTree

class TestExpressionTreeIsCnf(unittest.TestCase):

    def assert_is_cnf(self, postfix_tokens):
        """Assert the passed tokens are in conjunctive normal form."""
        self.assertTrue(BooleanExpressionTree(postfix_tokens).is_cnf)

    def assert_not_cnf(self, postfix_tokens):
        """Assert the passed tokens are not in conjunctive normal form."""
        self.assertFalse(BooleanExpressionTree(postfix_tokens).is_cnf)

    def test_is_cnf_single_operand(self):
        """Test cnf determination for single operand trees."""
        for postfix_tokens in (['0'], ['1'], ['token']):
            self.assert_is_cnf(postfix_tokens)

    def test_is_cnf_only_unary_operators(self):
        """Test cnf determination for trees with only unary operators."""
        self.assert_is_cnf(['A', 'not'])
        self.assert_not_cnf(['A', 'not', 'not'])
        self.assert_not_cnf(['A', 'not', 'not', 'not'])

    def test_is_cnf_single_clause(self):
        """Test that a single clause of ORed operands is in cnf form."""
        self.assert_is_cnf(['op1', 'op2', 'or'])
        self.assert_is_cnf(['A', 'B', 'C', 'D', 'or', 'or', 'or'])
        self.assert_is_cnf(['A', '~', 'B', 'C', '~', 'or', 'or'])

    def test_is_cnf_clauses_of_single_operands(self):
        """Test several ANDed clauses of single operands is in cnf form."""
        self.assert_is_cnf(['op1', 'op2', 'and'])
        self.assert_is_cnf(['A', 'B', 'C', 'D', 'and', 'and', 'and'])
        self.assert_is_cnf(['A', 'B', 'not', 'C', 'and', 'and'])

    def test_is_cnf_contains_non_primitive_operator_in_clause(self):
        """Test cases where non-primitive operator is in a clause."""
        self.assert_not_cnf(['A', 'B', 'or',
                             'C', 'D', 'or',
                             'D', 'E', 'nand',
                             'and', 'and'])
        self.assert_not_cnf(['A', 'B', 'C', 'xor', 'or',
                             'A', 'B', 'or',
                             'and'])

    def test_is_cnf_contains_non_primitive_operator_joining_clauses(self):
        """Test cases where non-primitive operator joins clauses."""
        self.assert_not_cnf(['A', 'B', '<->'])
        self.assert_not_cnf(['A', 'B', 'or',
                             'B', 'C', 'or',
                             'C', 'E', 'or',
                             '->', 'and'])

    def test_is_cnf_contains_notted_clause(self):
        """Test cases where an entire clause is notted."""
        self.assert_not_cnf(['A', 'B', 'or', 'not',
                             'C', 'D', 'or',
                             'and'])

    def test_is_cnf_multiple_clauses(self):
        """Test CNF determination for multiple clauses."""
        self.assert_is_cnf(['A', 'C', 'or',
                            'B', 'C', 'or',
                            'E', 'F', 'G', 'or', 'or',
                            'and', 'and'])

    def test_is_cnf_for_dnf_expression(self):
        """Test CNF determination for an expression in DNF."""
        self.assert_not_cnf(['A', 'B', 'C', 'and', 'and',
                             'B', 'C', 'not', 'and',
                             'D', 'E', 'and',
                             'or', 'or'])
<|endoftext|>


<commit_before>"""Code for working with display lists"""

from collections import namedtuple

import sysinfo
import datafilters

class DisplayListCommand:
    def __init__(self, command_name, contents="", css_classes=[], subcommands=[]):
        self.name = command_name
        self.subcommands = subcommands
        self.contents = contents
        self.css_classes = css_classes

class DisplayList(list):
    def __init__(self, display_list_path):
        processed_list = self._get_display_list(display_list_path)
        list.__init__(self, processed_list)

    def _get_display_list(self, display_list_path):
        commands = self._parse_display_list(display_list_path)
        return self._process_list(commands)

    def _parse_display_list(self, display_list_path):
        """
        Reads in a display list file and parses its contents.

        Config file format:
        Every line begins with a command, usually a display object such as
        a label or a widget. Following the command are parameters to the
        object on the same line.

        Some lines make up blocks of commands, which start with one command and
        are followed by indented lines, similar to a Python block. Much like
        Python, each line must be at the same indentation as the others in the
        block, with the block ending once a line at a previous indentation is
        reached.

        This format is still in the early stages and may undergo significant
        changes in the future. Hopefully Dave has not been an idiot and
        remembered to update this information.
        """

        ParsedCommand = namedtuple('ParsedCommand', ['command', 'arguments', 'subcommands'])
        IndentStackItem = namedtuple('IndentStackItem', ['indent_level', 'block'])

        list_file = open(display_list_path, 'r')

        #TODO: assumes first line has no indent, will break if not true
        root_block = current_block = []
        indent_stack = [ IndentStackItem(0, current_block) ]

        for line in list_file:
            # Ignore comments
            if line.lstrip().startswith("#"):
                continue
            # Ignore blank lines
            if not line.strip():
                continue

            indent_level = len(line) - len(line.lstrip())
            last_indent = indent_stack[-1].indent_level
            if indent_level > last_indent:
                current_block = indent_stack[-1].block[-1].subcommands
                indent_stack.append( IndentStackItem(indent_level, current_block) )
            elif indent_level < last_indent:
                while True:
                    indent_stack.pop()
                    if indent_level == indent_stack[-1].indent_level:
                        current_block = indent_stack[-1].block
                        break
                #TODO: exception is raised here if a matching indent level is
                # not found. This is a malformed config file, catch and show error

            command, *data = line.strip().split(None, 1)
            if data:
                parameters = self._parse_command_arguments(data[0])
            else:
                parameters = None

            current_block.append( ParsedCommand(command, parameters, []) )

        return root_block

    def _parse_command_arguments(self, argumentstring):
        """Parsed the argments given to a command"""
        ParsedArgument = namedtuple('ParsedArgument', ['type', 'value', 'funcargs', 'filter'])
        arguments = []

        rawargs = argumentstring.split(',')
        for full_arg in rawargs:
            arg, *arg_filter = full_arg.split('|')
            arg_filter = arg_filter[0] if arg_filter != [] else None
            if '(' in arg:
                #TODO: handle multiple arguments to functions
                funcargs = argumentstring.split('(')[1].split(')')[0]
                arguments.append(ParsedArgument('function', 'VolumeFreeSpace', [funcargs], arg_filter))
            else:
                arguments.append(ParsedArgument('string', arg, [], arg_filter))

        return arguments

    def _process_list(self, parsed_config):
        current_list = []
        for command, arguments, subcommands in parsed_config:
            # Preprocess function arguments
            if arguments:
                arg = arguments[0]
                processed_args = []
                if arg.type == 'function':
                    value = sysinfo.displaylist_functions[arg.value](arg.funcargs[0])
                else:
                    value = arg.value

                if arg.filter:
                    value = datafilters.output_filters[arg.filter](value)

                processed_args.append(value)
            # Process command
            command_name = command
            contents = ""
            classes = []
            processed_subcommands = []
            if command == 'text':
                contents = processed_args[0]
            elif command == 'inline':
                processed_subcommands = self._process_list(subcommands)
                classes = ['inlinecontents']
            else:
                #TODO: Only do this if a debug variable is set, otherwise raise an error
                #TODO: Add formatting to errors in output
                command_name = 'error'
                contents = "ERROR: unknown command {}".format(command)
            processed_command = DisplayListCommand(command_name,
                                                   contents=contents,
                                                   css_classes=classes,
                                                   subcommands=processed_subcommands)
            current_list.append(processed_command)
        return current_list
<commit_msg>No longer hardcodes a function name<commit_after>"""Code for working with display lists"""

from collections import namedtuple

import sysinfo
import datafilters

class DisplayListCommand:
    def __init__(self, command_name, contents="", css_classes=[], subcommands=[]):
        self.name = command_name
        self.subcommands = subcommands
        self.contents = contents
        self.css_classes = css_classes

class DisplayList(list):
    def __init__(self, display_list_path):
        processed_list = self._get_display_list(display_list_path)
        list.__init__(self, processed_list)

    def _get_display_list(self, display_list_path):
        commands = self._parse_display_list(display_list_path)
        return self._process_list(commands)

    def _parse_display_list(self, display_list_path):
        """
        Reads in a display list file and parses its contents.

        Config file format:
        Every line begins with a command, usually a display object such as
        a label or a widget. Following the command are parameters to the
        object on the same line.

        Some lines make up blocks of commands, which start with one command and
        are followed by indented lines, similar to a Python block. Much like
        Python, each line must be at the same indentation as the others in the
        block, with the block ending once a line at a previous indentation is
        reached.

        This format is still in the early stages and may undergo significant
        changes in the future. Hopefully Dave has not been an idiot and
        remembered to update this information.
        """

        ParsedCommand = namedtuple('ParsedCommand', ['command', 'arguments', 'subcommands'])
        IndentStackItem = namedtuple('IndentStackItem', ['indent_level', 'block'])

        list_file = open(display_list_path, 'r')

        #TODO: assumes first line has no indent, will break if not true
        root_block = current_block = []
        indent_stack = [ IndentStackItem(0, current_block) ]

        for line in list_file:
            # Ignore comments
            if line.lstrip().startswith("#"):
                continue
            # Ignore blank lines
            if not line.strip():
                continue

            indent_level = len(line) - len(line.lstrip())
            last_indent = indent_stack[-1].indent_level
            if indent_level > last_indent:
                current_block = indent_stack[-1].block[-1].subcommands
                indent_stack.append( IndentStackItem(indent_level, current_block) )
            elif indent_level < last_indent:
                while True:
                    indent_stack.pop()
                    if indent_level == indent_stack[-1].indent_level:
                        current_block = indent_stack[-1].block
                        break
                #TODO: exception is raised here if a matching indent level is
                # not found. This is a malformed config file, catch and show error

            command, *data = line.strip().split(None, 1)
            if data:
                parameters = self._parse_command_arguments(data[0])
            else:
                parameters = None

            current_block.append( ParsedCommand(command, parameters, []) )

        return root_block

    def _parse_command_arguments(self, argumentstring):
        """Parsed the argments given to a command"""
        ParsedArgument = namedtuple('ParsedArgument', ['type', 'value', 'funcargs', 'filter'])
        arguments = []

        rawargs = argumentstring.split(',')
        for full_arg in rawargs:
            arg, *arg_filter = full_arg.split('|')
            arg_filter = arg_filter[0] if arg_filter != [] else None
            # If argument is a function
            if '(' in arg:
                #TODO: handle multiple arguments to functions
                funcname = argumentstring.split('(')[0].strip()
                funcargs = argumentstring.split('(')[1].split(')')[0]
                arguments.append(ParsedArgument('function', funcname, [funcargs], arg_filter))
            else:
                arguments.append(ParsedArgument('string', arg, [], arg_filter))

        return arguments

    def _process_list(self, parsed_config):
        current_list = []
        for command, arguments, subcommands in parsed_config:
            # Preprocess function arguments
            if arguments:
                arg = arguments[0]
                processed_args = []
                if arg.type == 'function':
                    value = sysinfo.displaylist_functions[arg.value](arg.funcargs[0])
                else:
                    value = arg.value

                if arg.filter:
                    value = datafilters.output_filters[arg.filter](value)

                processed_args.append(value)
            # Process command
            command_name = command
            contents = ""
            classes = []
            processed_subcommands = []
            if command == 'text':
                contents = processed_args[0]
            elif command == 'inline':
                processed_subcommands = self._process_list(subcommands)
                classes = ['inlinecontents']
            else:
                #TODO: Only do this if a debug variable is set, otherwise raise an error
                #TODO: Add formatting to errors in output
                command_name = 'error'
                contents = "ERROR: unknown command {}".format(command)
            processed_command = DisplayListCommand(command_name,
                                                   contents=contents,
                                                   css_classes=classes,
                                                   subcommands=processed_subcommands)
            current_list.append(processed_command)
        return current_list
<|endoftext|>


<commit_before>##
# Copyright (c) 2012 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

"""
Tests for L{txdav.common.datastore.sql}.
"""

from twext.enterprise.dal.syntax import Select
from txdav.xml import element as davxml

from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.task import Clock
from twisted.trial.unittest import TestCase

from txdav.common.datastore.sql import log, CommonStoreTransactionMonitor,\
    CommonHome, CommonHomeChild, ECALENDARTYPE
from txdav.common.datastore.sql_tables import schema, CALENDAR_BIND_TABLE,\
    CALENDAR_OBJECT_REVISIONS_TABLE
from txdav.common.datastore.test.util import CommonCommonTests, buildStore
from txdav.common.icommondatastore import AllRetriesFailed
from twext.enterprise.dal.syntax import Insert
from txdav.common.datastore.sql import fixUUIDNormalization

class CommonSQLStoreTests(CommonCommonTests, TestCase):
    """
    Tests for shared functionality in L{txdav.common.datastore.sql}.
    """

    @inlineCallbacks
    def setUp(self):
        """
        Set up two stores to migrate between.
        """
        yield super(CommonSQLStoreTests, self).setUp()
        self._sqlStore = yield buildStore(self, self.notifierFactory)

    def storeUnderTest(self):
        """
        Return a store for testing.
        """
        return self._sqlStore

    @inlineCallbacks
    def test_logging(self):
        """
        txn.execSQL works with all logging options on.
        """

        # Patch config to turn on logging then rebuild the store
        self.patch(self._sqlStore, "logLabels", True)
        self.patch(self._sqlStore, "logStats", True)
        self.patch(self._sqlStore, "logSQL", True)

        txn = self.transactionUnderTest()
        cs = schema.CALENDARSERVER
        version = (yield Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(txn))
        self.assertNotEqual(version, None)
        self.assertEqual(len(version), 1)
        self.assertEqual(len(version[0]), 1)

    def test_logWaits(self):
        """
        CommonStoreTransactionMonitor logs waiting transactions.
        """

        c = Clock()
        self.patch(CommonStoreTransactionMonitor, "callLater", c.callLater)

        # Patch config to turn on log waits then rebuild the store
        self.patch(self._sqlStore, "logTransactionWaits", 1)

        ctr = [0]
        def counter(_ignore):
            ctr[0] += 1
        self.patch(log, "error", counter)

        txn = self.transactionUnderTest()

        c.advance(2)
        self.assertNotEqual(ctr[0], 0)
        txn.abort()

    def test_txnTimeout(self):
        """
        CommonStoreTransactionMonitor terminates long transactions.
        """

        c = Clock()
        self.patch(CommonStoreTransactionMonitor, "callLater", c.callLater)

        # Patch config to turn on transaction timeouts then rebuild the store
        self.patch(self._sqlStore, "timeoutTransactions", 1)

        ctr = [0]
        def counter(_ignore):
            ctr[0] += 1
        self.patch(log, "error", counter)

        txn = self.transactionUnderTest()

        c.advance(2)
        self.assertNotEqual(ctr[0], 0)
        self.assertTrue(txn._sqlTxn._completed)

    def test_logWaitsAndTxnTimeout(self):
        """
        CommonStoreTransactionMonitor logs waiting transactions and terminates long transactions.
        """

        c = Clock()
        self.patch(CommonStoreTransactionMonitor, "callLater", c.callLater)

        # Patch config to turn on log waits then rebuild the store
        self.patch(self._sqlStore, "logTransactionWaits", 1)
        self.patch(self._sqlStore, "timeoutTransactions", 2)

        ctr = [0, 0]
        def counter(logStr):
            if "wait" in logStr:
                ctr[0] += 1
            elif "abort" in logStr:
                ctr[1] += 1
        self.patch(log, "error", counter)

        txn = self.transactionUnderTest()

        c.advance(2)
        self.assertNotEqual(ctr[0], 0)
        self.assertNotEqual(ctr[1], 0)
        self.assertTrue(txn._sqlTxn._completed)

    @inlineCallbacks
    def test_subtransactionOK(self):
        """
        txn.subtransaction runs loop once.
        """

        txn = self.transactionUnderTest()
        ctr = [0]

        def _test(subtxn):
            ctr[0] += 1
            cs = schema.CALENDARSERVER
            return Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(subtxn)

        (yield txn.subtransaction(_test, retries=0))[0][0]
        self.assertEqual(ctr[0], 1)

    @inlineCallbacks
    def test_subtransactionOKAfterRetry(self):
        """
        txn.subtransaction runs loop twice when one failure.
        """

        txn = self.transactionUnderTest()
        ctr = [0]

        def _test(subtxn):
            ctr[0] += 1
            if ctr[0] == 1:
                raise ValueError
            cs = schema.CALENDARSERVER
            return Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(subtxn)

        (yield txn.subtransaction(_test, retries=1))[0][0]
        self.assertEqual(ctr[0], 2)

    @inlineCallbacks
    def test_subtransactionFailNoRetry(self):
        """
        txn.subtransaction runs loop once when one failure and no retries.
        """

        txn = self.transactionUnderTest()
        ctr = [0]

        def _test(subtxn):
            ctr[0] += 1
            raise ValueError
            cs = schema.CALENDARSERVER
            return Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(subtxn)

        try:
            (yield txn.subtransaction(_test, retries=0))[0][0]
        except AllRetriesFailed:
            pass
        else:
            self.fail("AllRetriesFailed not raised")
        self.assertEqual(ctr[0], 1)

    @inlineCallbacks
    def test_subtransactionFailSomeRetries(self):
        """
        txn.subtransaction runs loop three times when all fail and two retries requested.
        """

        txn = self.transactionUnderTest()
        ctr = [0]

        def _test(subtxn):
            ctr[0] += 1
            raise ValueError
            cs = schema.CALENDARSERVER
            return Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(subtxn)

        try:
            (yield txn.subtransaction(_test, retries=2))[0][0]
        except AllRetriesFailed:
            pass
        else:
            self.fail("AllRetriesFailed not raised")
        self.assertEqual(ctr[0], 3)

    @inlineCallbacks
    def test_changeRevision(self):
        """
        CommonHomeChild._changeRevision actions.
        """

        class TestCommonHome(CommonHome):
            _bindTable = CALENDAR_BIND_TABLE
            _revisionsTable = CALENDAR_OBJECT_REVISIONS_TABLE

        class TestCommonHomeChild(CommonHomeChild):
            _homeChildSchema = schema.CALENDAR
            _homeChildMetaDataSchema = schema.CALENDAR_METADATA
            _bindSchema = schema.CALENDAR_BIND
            _revisionsSchema = schema.CALENDAR_OBJECT_REVISIONS
            _bindTable = CALENDAR_BIND_TABLE
            _revisionsTable = CALENDAR_OBJECT_REVISIONS_TABLE

            def resourceType(self):
                return davxml.ResourceType.calendar

        txn = self.transactionUnderTest()
        home = yield txn.homeWithUID(ECALENDARTYPE, "uid", create=True)
        homeChild = yield TestCommonHomeChild.create(home, "B")

        # insert test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("insert", "C")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, (["C"], [],))

        # update test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("update", "C")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, (["C"], [],))

        # delete test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("delete", "C")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, ([], ["C"],))

        # missing update test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("update", "D")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, (["D"], [],))

        # missing delete test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("delete", "E")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, ([], [],))

        yield txn.abort()

    @inlineCallbacks
    def test_normalizeColumnUUIDs(self):
        """
        L{_normalizeColumnUUIDs} upper-cases only UUIDs in a given column.
        """
        rp = schema.RESOURCE_PROPERTY
        txn = self.transactionUnderTest()
        # setup
        yield Insert({rp.RESOURCE_ID: 1,
                      rp.NAME: "asdf",
                      rp.VALUE: "property-value",
                      rp.VIEWER_UID: "not-a-uuid"}).on(txn)
        yield Insert({rp.RESOURCE_ID: 2,
                      rp.NAME: "fdsa",
                      rp.VALUE: "another-value",
                      rp.VIEWER_UID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
                    ).on(txn)
        # test
        from txdav.common.datastore.sql import _normalizeColumnUUIDs
        yield _normalizeColumnUUIDs(txn, rp.VIEWER_UID)
        self.assertEqual(
            (yield Select([rp.RESOURCE_ID, rp.NAME,
                           rp.VALUE, rp.VIEWER_UID], From=rp).on(txn)),
            [[1, "asdf", "property-value", "not-a-uuid"],
             [2, "fdsa", "another-value",
              "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"]]
        )

    @inlineCallbacks
    def allHomeUIDs(self, table=schema.CALENDAR_HOME):
        """
        Get a listing of all UIDs in the current store.
        """
        results = yield (Select([table.OWNER_UID], From=table)
                         .on(self.transactionUnderTest()))
        yield self.commit()
        returnValue(results)

    @inlineCallbacks
    def test_fixUUIDNormalization_lowerToUpper(self):
        """
        L{fixUUIDNormalization} will fix the normalization of UUIDs.  If a home
        is found with the wrong case but no duplicate, it will simply be
        upper-cased.
        """
        t1 = self.transactionUnderTest()
        yield t1.calendarHomeWithUID(denormalizedUID, create=True)
        yield self.commit()
        yield fixUUIDNormalization(self.storeUnderTest())
        self.assertEqual((yield self.allHomeUIDs()), [[normalizedUID]])

    @inlineCallbacks
    def test_fixUUIDNormalization_lowerToUpper_notification(self):
        """
        L{fixUUIDNormalization} will fix the normalization of UUIDs.  If a home
        is found with the wrong case but no duplicate, it will simply be
        upper-cased.
        """
        t1 = self.transactionUnderTest()
        yield t1.notificationsWithUID(denormalizedUID, create=True)
        yield self.commit()
        yield fixUUIDNormalization(self.storeUnderTest())
        self.assertEqual((yield self.allHomeUIDs(schema.NOTIFICATION_HOME)),
                         [[normalizedUID]])

    @inlineCallbacks
    def test_fixUUIDNormalization_lowerToUpper_addressbook(self):
        """
        L{fixUUIDNormalization} will fix the normalization of UUIDs.  If a home
        is found with the wrong case but no duplicate, it will simply be
        upper-cased.
        """
        t1 = self.transactionUnderTest()
        yield t1.addressbookHomeWithUID(denormalizedUID, create=True)
        yield self.commit()
        yield fixUUIDNormalization(self.storeUnderTest())
        self.assertEqual((yield self.allHomeUIDs(schema.ADDRESSBOOK_HOME)),
                         [[normalizedUID]])

from uuid import UUID
exampleUID = UUID("a"*32)
denormalizedUID = str(exampleUID)
normalizedUID = denormalizedUID.upper()

<commit_msg>test values in SQL really always need to specify an ordering.<commit_after>##
# Copyright (c) 2012 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

"""
Tests for L{txdav.common.datastore.sql}.
"""

from twext.enterprise.dal.syntax import Select
from txdav.xml import element as davxml

from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.task import Clock
from twisted.trial.unittest import TestCase

from txdav.common.datastore.sql import log, CommonStoreTransactionMonitor,\
    CommonHome, CommonHomeChild, ECALENDARTYPE
from txdav.common.datastore.sql_tables import schema, CALENDAR_BIND_TABLE,\
    CALENDAR_OBJECT_REVISIONS_TABLE
from txdav.common.datastore.test.util import CommonCommonTests, buildStore
from txdav.common.icommondatastore import AllRetriesFailed
from twext.enterprise.dal.syntax import Insert
from txdav.common.datastore.sql import fixUUIDNormalization

class CommonSQLStoreTests(CommonCommonTests, TestCase):
    """
    Tests for shared functionality in L{txdav.common.datastore.sql}.
    """

    @inlineCallbacks
    def setUp(self):
        """
        Set up two stores to migrate between.
        """
        yield super(CommonSQLStoreTests, self).setUp()
        self._sqlStore = yield buildStore(self, self.notifierFactory)

    def storeUnderTest(self):
        """
        Return a store for testing.
        """
        return self._sqlStore

    @inlineCallbacks
    def test_logging(self):
        """
        txn.execSQL works with all logging options on.
        """

        # Patch config to turn on logging then rebuild the store
        self.patch(self._sqlStore, "logLabels", True)
        self.patch(self._sqlStore, "logStats", True)
        self.patch(self._sqlStore, "logSQL", True)

        txn = self.transactionUnderTest()
        cs = schema.CALENDARSERVER
        version = (yield Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(txn))
        self.assertNotEqual(version, None)
        self.assertEqual(len(version), 1)
        self.assertEqual(len(version[0]), 1)

    def test_logWaits(self):
        """
        CommonStoreTransactionMonitor logs waiting transactions.
        """

        c = Clock()
        self.patch(CommonStoreTransactionMonitor, "callLater", c.callLater)

        # Patch config to turn on log waits then rebuild the store
        self.patch(self._sqlStore, "logTransactionWaits", 1)

        ctr = [0]
        def counter(_ignore):
            ctr[0] += 1
        self.patch(log, "error", counter)

        txn = self.transactionUnderTest()

        c.advance(2)
        self.assertNotEqual(ctr[0], 0)
        txn.abort()

    def test_txnTimeout(self):
        """
        CommonStoreTransactionMonitor terminates long transactions.
        """

        c = Clock()
        self.patch(CommonStoreTransactionMonitor, "callLater", c.callLater)

        # Patch config to turn on transaction timeouts then rebuild the store
        self.patch(self._sqlStore, "timeoutTransactions", 1)

        ctr = [0]
        def counter(_ignore):
            ctr[0] += 1
        self.patch(log, "error", counter)

        txn = self.transactionUnderTest()

        c.advance(2)
        self.assertNotEqual(ctr[0], 0)
        self.assertTrue(txn._sqlTxn._completed)

    def test_logWaitsAndTxnTimeout(self):
        """
        CommonStoreTransactionMonitor logs waiting transactions and terminates long transactions.
        """

        c = Clock()
        self.patch(CommonStoreTransactionMonitor, "callLater", c.callLater)

        # Patch config to turn on log waits then rebuild the store
        self.patch(self._sqlStore, "logTransactionWaits", 1)
        self.patch(self._sqlStore, "timeoutTransactions", 2)

        ctr = [0, 0]
        def counter(logStr):
            if "wait" in logStr:
                ctr[0] += 1
            elif "abort" in logStr:
                ctr[1] += 1
        self.patch(log, "error", counter)

        txn = self.transactionUnderTest()

        c.advance(2)
        self.assertNotEqual(ctr[0], 0)
        self.assertNotEqual(ctr[1], 0)
        self.assertTrue(txn._sqlTxn._completed)

    @inlineCallbacks
    def test_subtransactionOK(self):
        """
        txn.subtransaction runs loop once.
        """

        txn = self.transactionUnderTest()
        ctr = [0]

        def _test(subtxn):
            ctr[0] += 1
            cs = schema.CALENDARSERVER
            return Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(subtxn)

        (yield txn.subtransaction(_test, retries=0))[0][0]
        self.assertEqual(ctr[0], 1)

    @inlineCallbacks
    def test_subtransactionOKAfterRetry(self):
        """
        txn.subtransaction runs loop twice when one failure.
        """

        txn = self.transactionUnderTest()
        ctr = [0]

        def _test(subtxn):
            ctr[0] += 1
            if ctr[0] == 1:
                raise ValueError
            cs = schema.CALENDARSERVER
            return Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(subtxn)

        (yield txn.subtransaction(_test, retries=1))[0][0]
        self.assertEqual(ctr[0], 2)

    @inlineCallbacks
    def test_subtransactionFailNoRetry(self):
        """
        txn.subtransaction runs loop once when one failure and no retries.
        """

        txn = self.transactionUnderTest()
        ctr = [0]

        def _test(subtxn):
            ctr[0] += 1
            raise ValueError
            cs = schema.CALENDARSERVER
            return Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(subtxn)

        try:
            (yield txn.subtransaction(_test, retries=0))[0][0]
        except AllRetriesFailed:
            pass
        else:
            self.fail("AllRetriesFailed not raised")
        self.assertEqual(ctr[0], 1)

    @inlineCallbacks
    def test_subtransactionFailSomeRetries(self):
        """
        txn.subtransaction runs loop three times when all fail and two retries requested.
        """

        txn = self.transactionUnderTest()
        ctr = [0]

        def _test(subtxn):
            ctr[0] += 1
            raise ValueError
            cs = schema.CALENDARSERVER
            return Select(
                [cs.VALUE,],
                From=cs,
                Where=cs.NAME == 'VERSION',
            ).on(subtxn)

        try:
            (yield txn.subtransaction(_test, retries=2))[0][0]
        except AllRetriesFailed:
            pass
        else:
            self.fail("AllRetriesFailed not raised")
        self.assertEqual(ctr[0], 3)

    @inlineCallbacks
    def test_changeRevision(self):
        """
        CommonHomeChild._changeRevision actions.
        """

        class TestCommonHome(CommonHome):
            _bindTable = CALENDAR_BIND_TABLE
            _revisionsTable = CALENDAR_OBJECT_REVISIONS_TABLE

        class TestCommonHomeChild(CommonHomeChild):
            _homeChildSchema = schema.CALENDAR
            _homeChildMetaDataSchema = schema.CALENDAR_METADATA
            _bindSchema = schema.CALENDAR_BIND
            _revisionsSchema = schema.CALENDAR_OBJECT_REVISIONS
            _bindTable = CALENDAR_BIND_TABLE
            _revisionsTable = CALENDAR_OBJECT_REVISIONS_TABLE

            def resourceType(self):
                return davxml.ResourceType.calendar

        txn = self.transactionUnderTest()
        home = yield txn.homeWithUID(ECALENDARTYPE, "uid", create=True)
        homeChild = yield TestCommonHomeChild.create(home, "B")

        # insert test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("insert", "C")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, (["C"], [],))

        # update test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("update", "C")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, (["C"], [],))

        # delete test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("delete", "C")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, ([], ["C"],))

        # missing update test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("update", "D")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, (["D"], [],))

        # missing delete test
        token = yield homeChild.syncToken()
        yield homeChild._changeRevision("delete", "E")
        changed = yield homeChild.resourceNamesSinceToken(token)
        self.assertEqual(changed, ([], [],))

        yield txn.abort()

    @inlineCallbacks
    def test_normalizeColumnUUIDs(self):
        """
        L{_normalizeColumnUUIDs} upper-cases only UUIDs in a given column.
        """
        rp = schema.RESOURCE_PROPERTY
        txn = self.transactionUnderTest()
        # setup
        yield Insert({rp.RESOURCE_ID: 1,
                      rp.NAME: "asdf",
                      rp.VALUE: "property-value",
                      rp.VIEWER_UID: "not-a-uuid"}).on(txn)
        yield Insert({rp.RESOURCE_ID: 2,
                      rp.NAME: "fdsa",
                      rp.VALUE: "another-value",
                      rp.VIEWER_UID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
                    ).on(txn)
        # test
        from txdav.common.datastore.sql import _normalizeColumnUUIDs
        yield _normalizeColumnUUIDs(txn, rp.VIEWER_UID)
        self.assertEqual(
            (yield Select([rp.RESOURCE_ID, rp.NAME,
                           rp.VALUE, rp.VIEWER_UID], From=rp,
                           OrderBy=rp.RESOURCE_ID, Ascending=True,
                           ).on(txn)),
            [[1, "asdf", "property-value", "not-a-uuid"],
             [2, "fdsa", "another-value",
              "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"]]
        )

    @inlineCallbacks
    def allHomeUIDs(self, table=schema.CALENDAR_HOME):
        """
        Get a listing of all UIDs in the current store.
        """
        results = yield (Select([table.OWNER_UID], From=table)
                         .on(self.transactionUnderTest()))
        yield self.commit()
        returnValue(results)

    @inlineCallbacks
    def test_fixUUIDNormalization_lowerToUpper(self):
        """
        L{fixUUIDNormalization} will fix the normalization of UUIDs.  If a home
        is found with the wrong case but no duplicate, it will simply be
        upper-cased.
        """
        t1 = self.transactionUnderTest()
        yield t1.calendarHomeWithUID(denormalizedUID, create=True)
        yield self.commit()
        yield fixUUIDNormalization(self.storeUnderTest())
        self.assertEqual((yield self.allHomeUIDs()), [[normalizedUID]])

    @inlineCallbacks
    def test_fixUUIDNormalization_lowerToUpper_notification(self):
        """
        L{fixUUIDNormalization} will fix the normalization of UUIDs.  If a home
        is found with the wrong case but no duplicate, it will simply be
        upper-cased.
        """
        t1 = self.transactionUnderTest()
        yield t1.notificationsWithUID(denormalizedUID, create=True)
        yield self.commit()
        yield fixUUIDNormalization(self.storeUnderTest())
        self.assertEqual((yield self.allHomeUIDs(schema.NOTIFICATION_HOME)),
                         [[normalizedUID]])

    @inlineCallbacks
    def test_fixUUIDNormalization_lowerToUpper_addressbook(self):
        """
        L{fixUUIDNormalization} will fix the normalization of UUIDs.  If a home
        is found with the wrong case but no duplicate, it will simply be
        upper-cased.
        """
        t1 = self.transactionUnderTest()
        yield t1.addressbookHomeWithUID(denormalizedUID, create=True)
        yield self.commit()
        yield fixUUIDNormalization(self.storeUnderTest())
        self.assertEqual((yield self.allHomeUIDs(schema.ADDRESSBOOK_HOME)),
                         [[normalizedUID]])

from uuid import UUID
exampleUID = UUID("a"*32)
denormalizedUID = str(exampleUID)
normalizedUID = denormalizedUID.upper()

<|endoftext|>