Esempio n. 1
0
class Path(db.Model):
    id = db.Column(db.UUID(as_uuid=True),
                   primary_key=True,
                   server_default=db.text('gen_random_uuid()'))
    lot_id = db.Column(db.UUID(as_uuid=True),
                       db.ForeignKey(Lot.id),
                       nullable=False)
    lot = db.relationship(Lot,
                          backref=db.backref('paths',
                                             lazy=True,
                                             collection_class=set,
                                             cascade=CASCADE_OWN),
                          primaryjoin=Lot.id == lot_id)
    path = db.Column(LtreeType, nullable=False)
    created = db.Column(db.TIMESTAMP(timezone=True),
                        server_default=db.text('CURRENT_TIMESTAMP'))
    created.comment = """
            When Devicehub created this.
        """

    __table_args__ = (
        # dag.delete_edge needs to disable internally/temporarily the unique constraint
        db.UniqueConstraint(path,
                            name='path_unique',
                            deferrable=True,
                            initially='immediate'),
        db.Index('path_gist', path, postgresql_using='gist'),
        db.Index('path_btree', path, postgresql_using='btree'),
        db.Index('lot_id_index', lot_id, postgresql_using='hash'))

    def __init__(self, lot: Lot) -> None:
        super().__init__(lot=lot)
        self.path = UUIDLtree(lot.id)

    @classmethod
    def add(cls, parent_id: uuid.UUID, child_id: uuid.UUID):
        """Creates an edge between parent and child."""
        db.session.execute(db.func.add_edge(str(parent_id), str(child_id)))

    @classmethod
    def delete(cls, parent_id: uuid.UUID, child_id: uuid.UUID):
        """Deletes the edge between parent and child."""
        db.session.execute(db.func.delete_edge(str(parent_id), str(child_id)))

    @classmethod
    def has_lot(cls, parent_id: uuid.UUID, child_id: uuid.UUID) -> bool:
        parent_id = UUIDLtree.convert(parent_id)
        child_id = UUIDLtree.convert(child_id)
        return bool(
            db.session.execute(
                "SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(
                    parent_id, child_id)).first())
Esempio n. 2
0
class Manufacturer(db.Model):
    """The normalized information about a manufacturer.

    Ideally users should use the names from this list when submitting
    devices.
    """
    CSV_DELIMITER = csv.get_dialect('excel').delimiter

    name = db.Column(CIText(), primary_key=True)
    name.comment = """The normalized name of the manufacturer."""
    url = db.Column(URL(), unique=True)
    url.comment = """An URL to a page describing the manufacturer."""
    logo = db.Column(URL())
    logo.comment = """An URL pointing to the logo of the manufacturer."""

    __table_args__ = (
        # from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
        db.Index('name_index',
                 text('name gin_trgm_ops'),
                 postgresql_using='gin'),
        {
            'schema': 'common'
        })

    @classmethod
    def add_all_to_session(cls, session: db.Session):
        """Adds all manufacturers to session."""
        cursor = session.connection().connection.cursor()
        #: Dialect used to write the CSV

        with pathlib.Path(__file__).parent.joinpath(
                'manufacturers.csv').open() as f:
            cursor.copy_expert(
                'COPY common.manufacturer FROM STDIN (FORMAT csv)', f)
Esempio n. 3
0
class Agent(Thing):
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
    type = Column(Unicode, nullable=False)
    name = Column(CIText())
    name.comment = """
        The name of the organization or person.
    """
    tax_id = Column(Unicode(length=STR_SM_SIZE), check_lower('tax_id'))
    tax_id.comment = """
        The Tax / Fiscal ID of the organization, 
        e.g. the TIN in the US or the CIF/NIF in Spain.
    """
    country = Column(DBEnum(enums.Country))
    country.comment = """
        Country issuing the tax_id number.
    """
    telephone = Column(PhoneNumberType())
    email = Column(EmailType, unique=True)

    __table_args__ = (UniqueConstraint(
        tax_id, country, name='Registration Number per country.'),
                      UniqueConstraint(tax_id,
                                       name,
                                       name='One tax ID with one name.'),
                      db.Index('agent_type', type, postgresql_using='hash'))

    @declared_attr
    def __mapper_args__(cls):
        """
        Defines inheritance.

        From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
        extensions/declarative/api.html
        #sqlalchemy.ext.declarative.declared_attr>`_
        """
        args = {POLYMORPHIC_ID: cls.t}
        if cls.t == 'Agent':
            args[POLYMORPHIC_ON] = cls.type
        if JoinedTableMixin in cls.mro():
            args[INHERIT_COND] = cls.id == Agent.id
        return args

    @property
    def events(self) -> list:
        # todo test
        return sorted(chain(self.events_agent, self.events_to),
                      key=attrgetter('created'))

    @validates('name')
    def does_not_contain_slash(self, _, value: str):
        if '/' in value:
            raise ValidationError('Name cannot contain slash \'')
        return value

    def __repr__(self) -> str:
        return '<{0.t} {0.name}>'.format(self)
Esempio n. 4
0
class Inventory(Thing):
    id = db.Column(db.Unicode(), primary_key=True)
    id.comment = """The name of the inventory as in the URL and schema."""
    name = db.Column(db.CIText(), nullable=False, unique=True)
    name.comment = """The human name of the inventory."""
    tag_provider = db.Column(db.URL(), nullable=False)
    tag_token = db.Column(db.UUID(as_uuid=True), unique=True, nullable=False)
    tag_token.comment = """The token to access a Tag service."""
    # todo no validation that UUID is from an existing organization
    org_id = db.Column(db.UUID(as_uuid=True), nullable=False)

    __table_args__ = (db.Index('id_hash', id, postgresql_using='hash'), {
        'schema': 'common'
    })

    @classproperty
    def current(cls) -> 'Inventory':
        """The inventory of the current_app."""
        return Inventory.query.filter_by(id=current_app.id).one()
Esempio n. 5
0
class Component(Device):
    """A device that can be inside another device."""
    id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)

    parent_id = Column(BigInteger, ForeignKey(Computer.id))
    parent = relationship(Computer,
                          backref=backref('components',
                                          lazy=True,
                                          cascade=CASCADE_DEL,
                                          order_by=lambda: Component.id,
                                          collection_class=OrderedSet),
                          primaryjoin=parent_id == Computer.id)

    __table_args__ = (db.Index('parent_index',
                               parent_id,
                               postgresql_using='hash'), )

    def similar_one(self, parent: Computer,
                    blacklist: Set[int]) -> 'Component':
        """
        Gets a component that:
        - has the same parent.
        - Doesn't generate HID.
        - Has same physical properties.
        :param parent:
        :param blacklist: A set of components to not to consider
                          when looking for similar ones.
        """
        assert self.hid is None, 'Don\'t use this method with a component that has HID'
        component = self.__class__.query \
            .filter_by(parent=parent, hid=None, **self.physical_properties) \
            .filter(~Component.id.in_(blacklist)) \
            .first()
        if not component:
            raise ResourceNotFound(self.type)
        return component

    @property
    def events(self) -> list:
        return sorted(chain(super().events, self.events_components),
                      key=self.EVENT_SORT_KEY)
Esempio n. 6
0
class DeviceSearch(db.Model):
    """Temporary table that stores full-text device documents.

    It provides methods to auto-run
    """
    device_id = db.Column(db.BigInteger,
                          db.ForeignKey(Device.id, ondelete='CASCADE'),
                          primary_key=True)
    device = db.relationship(Device, primaryjoin=Device.id == device_id)

    properties = db.Column(TSVECTOR, nullable=False)
    tags = db.Column(TSVECTOR)
    devicehub_ids = db.Column(TSVECTOR)

    __table_args__ = (
        # todo to add concurrency this should be commited separately
        #   see https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#indexes-with-concurrently
        db.Index('properties gist', properties, postgresql_using='gist'),
        db.Index('tags gist', tags, postgresql_using='gist'),
        db.Index('devicehub_ids gist', devicehub_ids, postgresql_using='gist'),
        {
            'prefixes': ['UNLOGGED']
            # Only for temporal tables, can cause table to empty on turn on
        })

    @classmethod
    def update_modified_devices(cls, session: db.Session):
        """Updates the documents of the devices that are part of a
        modified action, or tag in the passed-in session.

        This method is registered as a SQLAlchemy listener in the
        Devicehub class.
        """
        devices_to_update = set()
        for model in chain(session.new, session.dirty):
            if isinstance(model, Action):
                if isinstance(model, ActionWithMultipleDevices):
                    devices_to_update |= model.devices
                elif isinstance(model, ActionWithOneDevice):
                    devices_to_update.add(model.device)
                if model.parent:
                    devices_to_update.add(model.parent)
                devices_to_update |= model.components
            elif isinstance(model, Tag) and model.device:
                devices_to_update.add(model.device)

        # this flush is controversial:
        # see https://groups.google.com/forum/#!topic/sqlalchemy/hBzfypgPfYo
        # todo probably should replace it with what the solution says
        session.flush()
        for device in (d for d in devices_to_update
                       if not isinstance(d, Component)):
            cls.set_device_tokens(session, device)

    @classmethod
    def set_all_devices_tokens_if_empty(cls, session: db.Session):
        """Generates the search docs if the table is empty.

        This can happen if Postgres' shut down unexpectedly, as
        it deletes unlogged tables as ours.
        """
        if not DeviceSearch.query.first():
            cls.regenerate_search_table(session)

    @classmethod
    def regenerate_search_table(cls, session: db.Session):
        """Deletes and re-computes all the search table."""
        DeviceSearch.query.delete()
        for device in Device.query:
            if not isinstance(device, Component):
                cls.set_device_tokens(session, device)

    @classmethod
    def set_device_tokens(cls, session: db.Session, device: Device):
        """(Re)Generates the device search tokens."""
        assert not isinstance(device, Component)

        tokens = [(str(device.id), search.Weight.A),
                  (inflection.humanize(device.type), search.Weight.B),
                  (Device.model, search.Weight.B),
                  (Device.manufacturer, search.Weight.C),
                  (Device.serial_number, search.Weight.A)]

        if device.manufacturer:
            # todo this has to be done using a dictionary
            manufacturer = device.manufacturer.lower()
            if 'asus' in manufacturer:
                tokens.append(('asus', search.Weight.B))
            if 'hewlett' in manufacturer or 'hp' in manufacturer or 'h.p' in manufacturer:
                tokens.append(('hp', search.Weight.B))
                tokens.append(('h.p', search.Weight.C))
                tokens.append(('hewlett', search.Weight.C))
                tokens.append(('packard', search.Weight.C))

        if isinstance(device, Computer):
            # Aggregate the values of all the components of pc
            Comp = aliased(Component)
            tokens.extend((
                (db.func.string_agg(db.cast(Comp.id, db.TEXT),
                                    ' '), search.Weight.D),
                (db.func.string_agg(Comp.model, ' '), search.Weight.C),
                (db.func.string_agg(Comp.manufacturer, ' '), search.Weight.D),
                (db.func.string_agg(Comp.serial_number, ' '), search.Weight.B),
                (db.func.string_agg(Comp.type, ' '), search.Weight.B),
                ('Computer', search.Weight.C),
                ('PC', search.Weight.C),
            ))

        properties = session \
            .query(search.Search.vectorize(*tokens)) \
            .filter(Device.id == device.id)

        if isinstance(device, Computer):
            # Join to components
            properties = properties \
                .outerjoin(Comp, Computer.components) \
                .group_by(Device.id)

        tags = session.query(
            search.Search.vectorize(
                (db.func.string_agg(Tag.id, ' '), search.Weight.A),
                (db.func.string_agg(Tag.secondary, ' '), search.Weight.A),
                (db.func.string_agg(Organization.name, ' '),
                 search.Weight.B))).filter(Tag.device_id == device.id).join(
                     Tag.org)

        devicehub_ids = session.query(
            search.Search.vectorize((db.func.string_agg(
                Device.devicehub_id, ' '), search.Weight.A), )).filter(
                    Device.devicehub_id == device.devicehub_id)

        # Note that commit flushes later
        # todo see how to get rid of the one_or_none() by embedding those as subqueries
        # I don't like this but I want the 'on_conflict_on_update' thingie
        device_document = dict(properties=properties.one_or_none(),
                               tags=tags.one_or_none(),
                               devicehub_ids=devicehub_ids.one_or_none())
        insert = postgresql.insert(DeviceSearch.__table__) \
            .values(device_id=device.id, **device_document) \
            .on_conflict_do_update(constraint='device_search_pkey', set_=device_document)
        session.execute(insert)
Esempio n. 7
0
class Tag(Thing):
    id = Column(db.CIText(), primary_key=True)
    id.comment = """The ID of the tag."""
    org_id = Column(UUID(as_uuid=True),
                    ForeignKey(Organization.id),
                    primary_key=True,
                    # If we link with the Organization object this instance
                    # will be set as persistent and added to session
                    # which is something we don't want to enforce by default
                    default=lambda: Organization.get_default_org_id())
    org = relationship(Organization,
                       backref=backref('tags', lazy=True),
                       primaryjoin=Organization.id == org_id,
                       collection_class=set)
    """The organization that issued the tag."""
    provider = Column(URL())
    provider.comment = """
        The tag provider URL. If None, the provider is this Devicehub.
    """
    device_id = Column(BigInteger,
                       # We don't want to delete the tag on device deletion, only set to null
                       ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL))
    device = relationship(Device,
                          backref=backref('tags', lazy=True, collection_class=Tags),
                          primaryjoin=Device.id == device_id)
    """The device linked to this tag."""
    secondary = Column(db.CIText(), index=True)
    secondary.comment = """
        A secondary identifier for this tag. It has the same
        constraints as the main one. Only needed in special cases.
    """

    __table_args__ = (
        db.Index('device_id_index', device_id, postgresql_using='hash'),
    )

    def __init__(self, id: str, **kwargs) -> None:
        super().__init__(id=id, **kwargs)

    def like_etag(self):
        """Checks if the tag conforms to the `eTag spec <http:
        //devicehub.ereuse.org/tags.html#etags>`_.
        """
        with suppress(ValueError):
            provider, id = self.id.split('-')
            if len(provider) == 2 and 5 <= len(id) <= 10:
                return True
        return False

    @classmethod
    def from_an_id(cls, id: str) -> Query:
        """Query to look for a tag from a possible identifier."""
        return cls.query.filter((cls.id == id) | (cls.secondary == id))

    @validates('id', 'secondary')
    def does_not_contain_slash(self, _, value: str):
        if '/' in value:
            raise ValidationError('Tags cannot contain slashes (/).')
        return value

    @validates('provider')
    def use_only_domain(self, _, url: URL):
        if url.path:
            raise ValidationError('Provider can only contain scheme and host',
                                  field_names=['provider'])
        return url

    __table_args__ = (
        UniqueConstraint(id, org_id, name='one tag id per organization'),
        UniqueConstraint(secondary, org_id, name='one secondary tag per organization')
    )

    @property
    def type(self) -> str:
        return self.__class__.__name__

    @property
    def url(self) -> urlutils.URL:
        """The URL where to GET this device."""
        # todo this url only works for printable internal tags
        return urlutils.URL(url_for_resource(Tag, item_id=self.id))

    @property
    def printable(self) -> bool:
        """Can the tag be printed by the user?

        Only tags that are from the default organization can be
        printed by the user.
        """
        return self.org_id == Organization.get_default_org_id()

    @classmethod
    def is_printable_q(cls):
        """Return a SQLAlchemy filter expression for printable queries"""
        return cls.org_id == Organization.get_default_org_id()

    def __repr__(self) -> str:
        return '<Tag {0.id} org:{0.org_id} device:{0.device_id}>'.format(self)

    def __str__(self) -> str:
        return '{0.id} org: {0.org.name} device: {0.device}'.format(self)

    def __format__(self, format_spec: str) -> str:
        return '{0.org.name} {0.id}'.format(self)
Esempio n. 8
0
class Device(Thing):
    """Base class for any type of physical object that can be identified.

    Device partly extends `Schema's IndividualProduct <https
    ://schema.org/IndividualProduct>`_, adapting it to our
    use case.

    A device requires an identification method, ideally a serial number,
    although it can be identified only with tags too. More ideally
    both methods are used.

    Devices can contain ``Components``, which are just a type of device
    (it is a recursive relationship).
    """
    EVENT_SORT_KEY = attrgetter('created')

    id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
    id.comment = """
        The identifier of the device for this database. Used only
        internally for software; users should not use this.
    """
    type = Column(Unicode(STR_SM_SIZE), nullable=False)
    hid = Column(Unicode(), check_lower('hid'), unique=True)
    hid.comment = """
        The Hardware ID (HID) is the unique ID traceability systems 
        use to ID a device globally. This field is auto-generated
        from Devicehub using literal identifiers from the device,
        so it can re-generated *offline*.
        
    """ + HID_CONVERSION_DOC
    model = Column(Unicode, check_lower('model'))
    model.comment = """The model of the device in lower case.
    
    The model is the unambiguous, as technical as possible, denomination
    for the product. This field, among others, is used to identify 
    the product.
    """
    manufacturer = Column(Unicode(), check_lower('manufacturer'))
    manufacturer.comment = """The normalized name of the manufacturer,
    in lower case.
    
    Although as of now Devicehub does not enforce normalization,
    users can choose a list of normalized manufacturer names
    from the own ``/manufacturers`` REST endpoint.
    """
    serial_number = Column(Unicode(), check_lower('serial_number'))
    serial_number.comment = """The serial number of the device in lower case."""
    brand = db.Column(CIText())
    brand.comment = """A naming for consumers. This field can represent
    several models, so it can be ambiguous, and it is not used to 
    identify the product.
    """
    generation = db.Column(db.SmallInteger, check_range('generation', 0))
    generation.comment = """The generation of the device."""
    weight = Column(Float(decimal_return_scale=3),
                    check_range('weight', 0.1, 5))
    weight.comment = """
        The weight of the device.
    """
    width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5))
    width.comment = """
        The width of the device in meters.
    """
    height = Column(Float(decimal_return_scale=3),
                    check_range('height', 0.1, 5))
    height.comment = """
        The height of the device in meters.
    """
    depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5))
    depth.comment = """
        The depth of the device in meters.
    """
    color = Column(ColorType)
    color.comment = """The predominant color of the device."""
    production_date = Column(db.DateTime)
    production_date.comment = """The date of production of the device. 
    This is timezone naive, as Workbench cannot report this data
    with timezone information.
    """
    variant = Column(Unicode)
    variant.comment = """A variant or sub-model of the device."""

    _NON_PHYSICAL_PROPS = {
        'id',
        'type',
        'created',
        'updated',
        'parent_id',
        'hid',
        'production_date',
        'color',  # these are only user-input thus volatile
        'width',
        'height',
        'depth',
        'weight',
        'brand',
        'generation',
        'production_date',
        'variant'
    }

    __table_args__ = (db.Index('device_id', id, postgresql_using='hash'),
                      db.Index('type_index', type, postgresql_using='hash'))

    def __init__(self, **kw) -> None:
        super().__init__(**kw)
        with suppress(TypeError):
            self.hid = Naming.hid(self.type, self.manufacturer, self.model,
                                  self.serial_number)

    @property
    def events(self) -> list:
        """
        All the events where the device participated, including:

        1. Events performed directly to the device.
        2. Events performed to a component.
        3. Events performed to a parent device.

        Events are returned by descending ``created`` time.
        """
        return sorted(chain(self.events_multiple, self.events_one),
                      key=self.EVENT_SORT_KEY)

    @property
    def problems(self):
        """Current events with severity.Warning or higher.

        There can be up to 3 events: current Snapshot,
        current Physical event, current Trading event.
        """
        from ereuse_devicehub.resources.device import states
        from ereuse_devicehub.resources.event.models import Snapshot
        events = set()
        with suppress(LookupError, ValueError):
            events.add(self.last_event_of(Snapshot))
        with suppress(LookupError, ValueError):
            events.add(self.last_event_of(*states.Physical.events()))
        with suppress(LookupError, ValueError):
            events.add(self.last_event_of(*states.Trading.events()))
        return self._warning_events(events)

    @property
    def physical_properties(self) -> Dict[str, object or None]:
        """
        Fields that describe the physical properties of a device.

        :return A generator where each value is a tuple with tho fields:
                - Column.
                - Actual value of the column or None.
        """
        # todo ensure to remove materialized values when start using them
        # todo or self.__table__.columns if inspect fails
        return {
            c.key: getattr(self, c.key, None)
            for c in inspect(self.__class__).attrs
            if isinstance(c, ColumnProperty)
            and not getattr(c, 'foreign_keys', None)
            and c.key not in self._NON_PHYSICAL_PROPS
        }

    @property
    def url(self) -> urlutils.URL:
        """The URL where to GET this device."""
        return urlutils.URL(url_for_resource(Device, item_id=self.id))

    @property
    def rate(self):
        """The last AggregateRate of the device."""
        with suppress(LookupError, ValueError):
            from ereuse_devicehub.resources.event.models import AggregateRate
            return self.last_event_of(AggregateRate)

    @property
    def price(self):
        """The actual Price of the device, or None if no price has
        ever been set."""
        with suppress(LookupError, ValueError):
            from ereuse_devicehub.resources.event.models import Price
            return self.last_event_of(Price)

    @property
    def trading(self):
        """The actual trading state, or None if no Trade event has
        ever been performed to this device."""
        from ereuse_devicehub.resources.device import states
        with suppress(LookupError, ValueError):
            event = self.last_event_of(*states.Trading.events())
            return states.Trading(event.__class__)

    @property
    def physical(self):
        """The actual physical state, None otherwise."""
        from ereuse_devicehub.resources.device import states
        with suppress(LookupError, ValueError):
            event = self.last_event_of(*states.Physical.events())
            return states.Physical(event.__class__)

    @property
    def physical_possessor(self):
        """The actual physical possessor or None.

        The physical possessor is the Agent that has physically
        the device. It differs from legal owners, usufructuarees
        or reserves in that the physical possessor does not have
        a legal relation per se with the device, but it is the one
        that has it physically. As an example, a transporter could
        be a physical possessor of a device although it does not
        own it legally.

        Note that there can only be one physical possessor per device,
        and :class:`ereuse_devicehub.resources.event.models.Receive`
        changes it.
        """
        from ereuse_devicehub.resources.event.models import Receive
        with suppress(LookupError):
            event = self.last_event_of(Receive)
            return event.agent

    @property
    def working(self):
        """A list of the current tests with warning or errors. A
        device is working if the list is empty.

        This property returns, for the last test performed of each type,
        the one with the worst ``severity`` of them, or ``None`` if no
        test has been executed.
        """
        from ereuse_devicehub.resources.event.models import Test
        current_tests = unique_everseen(
            (e for e in reversed(self.events) if isinstance(e, Test)),
            key=attrgetter('type'))  # last test of each type
        return self._warning_events(current_tests)

    @declared_attr
    def __mapper_args__(cls):
        """
        Defines inheritance.

        From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
        extensions/declarative/api.html
        #sqlalchemy.ext.declarative.declared_attr>`_
        """
        args = {POLYMORPHIC_ID: cls.t}
        if cls.t == 'Device':
            args[POLYMORPHIC_ON] = cls.type
        return args

    def last_event_of(self, *types):
        """Gets the last event of the given types.

        :raise LookupError: Device has not an event of the given type.
        """
        try:
            # noinspection PyTypeHints
            return next(e for e in reversed(self.events)
                        if isinstance(e, types))
        except StopIteration:
            raise LookupError(
                '{!r} does not contain events of types {}.'.format(
                    self, types))

    def _warning_events(self, events):
        return sorted((ev for ev in events if ev.severity >= Severity.Warning),
                      key=self.EVENT_SORT_KEY)

    def __lt__(self, other):
        return self.id < other.id

    def __str__(self) -> str:
        return '{0.t} {0.id}: model {0.model}, S/N {0.serial_number}'.format(
            self)

    def __format__(self, format_spec):
        if not format_spec:
            return super().__format__(format_spec)
        v = ''
        if 't' in format_spec:
            v += '{0.t} {0.model}'.format(self)
        if 's' in format_spec:
            v += '({0.manufacturer})'.format(self)
            if self.serial_number:
                v += ' S/N ' + self.serial_number.upper()
        return v
Esempio n. 9
0
class TradeDocument(Thing):
    """This represent a document involved in a trade action.
    Every document is added to a lot.
    When this lot is converted in one trade, the action trade is added to the document
    and the action trade need to be confirmed for the both users of the trade.
    This confirmation can be revoked and this revoked need to be ConfirmRevoke for have
    some efect.

    This documents can be invoices or list of devices or certificates of erasure of
    one disk.

    Like a Devices one document have actions and is possible add or delete of one lot
    if this lot don't have a trade

    The document is saved in the database

    """

    id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
    id.comment = """The identifier of the device for this database. Used only
    internally for software; users should not use this.
    """
    # type = Column(Unicode(STR_SM_SIZE), nullable=False)
    date = Column(db.DateTime)
    date.comment = """The date of document, some documents need to have one date
    """
    id_document = Column(CIText())
    id_document.comment = """The id of one document like invoice so they can be linked."""
    description = Column(db.CIText())
    description.comment = """A description of document."""
    owner_id = db.Column(UUID(as_uuid=True),
                         db.ForeignKey(User.id),
                         nullable=False,
                         default=lambda: g.user.id)
    owner = db.relationship(User, primaryjoin=owner_id == User.id)
    lot_id = db.Column(UUID(as_uuid=True),
                       db.ForeignKey('lot.id'),
                       nullable=False)
    lot = db.relationship('Lot',
                          backref=backref('documents',
                                          lazy=True,
                                          cascade=CASCADE_OWN,
                                          **_sorted_documents),
                          primaryjoin='TradeDocument.lot_id == Lot.id')
    lot.comment = """Lot to which the document is associated"""
    file_name = Column(db.CIText())
    file_name.comment = """This is the name of the file when user up the document."""
    file_hash = Column(db.CIText())
    file_hash.comment = """This is the hash of the file produced from frontend."""
    url = db.Column(URL())
    url.comment = """This is the url where resides the document."""
    weight = db.Column(db.Float(nullable=True))
    weight.comment = """This is the weight of one container than this document express."""

    __table_args__ = (
        db.Index('document_id', id, postgresql_using='hash'),
        # db.Index('type_doc', type, postgresql_using='hash')
    )

    @property
    def actions(self) -> list:
        """All the actions where the device participated, including:

        1. Actions performed directly to the device.
        2. Actions performed to a component.
        3. Actions performed to a parent device.

        Actions are returned by descending ``created`` time.
        """
        return sorted(self.actions_docs, key=lambda x: x.created)

    @property
    def trading(self):
        """The trading state, or None if no Trade action has
        ever been performed to this device. This extract the posibilities for to do"""

        confirm = 'Confirm'
        need_confirm = 'Need Confirmation'
        double_confirm = 'Document Confirmed'
        revoke = 'Revoke'
        revoke_pending = 'Revoke Pending'
        confirm_revoke = 'Document Revoked'
        ac = self.last_action_trading()
        if not ac:
            return

        if ac.type == 'ConfirmRevokeDocument':
            # can to do revoke_confirmed
            return confirm_revoke

        if ac.type == 'RevokeDocument':
            if ac.user == g.user:
                # can todo revoke_pending
                return revoke_pending
            else:
                # can to do confirm_revoke
                return revoke

        if ac.type == 'ConfirmDocument':
            if ac.user == self.owner:
                if self.owner == g.user:
                    # can to do revoke
                    return confirm
                else:
                    # can to do confirm
                    return need_confirm
            else:
                # can to do revoke
                return double_confirm

    @property
    def total_weight(self):
        """Return all weight than this container have."""
        weight = self.weight or 0
        for x in self.actions:
            if not x.type == 'MoveOnDocument' or not x.weight:
                continue
            if self == x.container_from:
                continue
            weight += x.weight

        return weight

    def last_action_trading(self):
        """which is the last action trading"""
        with suppress(StopIteration, ValueError):
            actions = copy.copy(self.actions)
            actions.sort(key=lambda x: x.created)
            t_trades = [
                'Trade', 'Confirm', 'ConfirmRevokeDocument', 'RevokeDocument',
                'ConfirmDocument'
            ]
            return next(e for e in reversed(actions) if e.t in t_trades)

    def _warning_actions(self, actions):
        """Show warning actions"""
        return sorted(ev for ev in actions if ev.severity >= Severity.Warning)

    def __lt__(self, other):
        return self.id < other.id

    def __str__(self) -> str:
        return '{0.file_name}'.format(self)