Merge branch 'master' into reports
# Conflicts: # ereuse_devicehub/resources/device/views.py # tests/test_rate_workbench_v1.py # tests/test_workbench.py
This commit is contained in:
commit
208814ecf2
10
README.md
10
README.md
|
@ -23,9 +23,8 @@ Devicehub is built with [Teal](https://github.com/bustawin/teal) and
|
|||
The requirements are:
|
||||
|
||||
- Python 3.5.3 or higher. In debian 9 is `# apt install python3-pip`.
|
||||
- PostgreSQL 9.6 or higher with pgcrypto and ltree.
|
||||
In debian 9 is `# apt install postgresql-contrib`
|
||||
- passlib. In debian 9 is `# apt install python3-passlib`.
|
||||
- [PostgreSQL 11 or higher](https://www.postgresql.org/download/).
|
||||
- Weasyprint [dependencies](http://weasyprint.readthedocs.io/en/stable/install.html).
|
||||
|
||||
Install Devicehub with *pip*: `pip3 install ereuse-devicehub -U --pre`.
|
||||
|
||||
|
@ -57,11 +56,14 @@ $ flask run
|
|||
```
|
||||
|
||||
The error `flask: command not found` can happen when you are not in a
|
||||
*virtual environment*. Try executing then `python3 -m flask`.
|
||||
*virtual environment*. Try executing then `python3 -m flask`.
|
||||
|
||||
See the [Flask quickstart](http://flask.pocoo.org/docs/1.0/quickstart/)
|
||||
for more info.
|
||||
|
||||
The error 'bdist_wheel' can happen when you works with *virtual environment*.
|
||||
To fix it, install in the *virtual environment* wheel package. `pip3 install wheel`
|
||||
|
||||
## Administrating
|
||||
Devicehub has many commands that allows you to administrate it. You
|
||||
can, for example, create a dummy database of devices with ``flask dummy``
|
||||
|
|
238
docs/actions.rst
238
docs/actions.rst
|
@ -1,6 +1,9 @@
|
|||
Actions and states
|
||||
##################
|
||||
|
||||
Actions
|
||||
*******
|
||||
|
||||
Actions are events performed to devices, changing their **state**.
|
||||
Actions can have attributes defining
|
||||
**where** it happened, **who** performed them, **when**, etc.
|
||||
|
@ -8,13 +11,6 @@ Actions are stored in a log for each device. An exemplifying action
|
|||
can be ``Repair``, which dictates that a device has been repaired,
|
||||
after this action, the device is in the ``repaired`` state.
|
||||
|
||||
Actions and states affect devices in different ways or **dimensions**.
|
||||
For example, ``Repair`` affects the **physical** dimension of a device,
|
||||
and ``Sell`` the **political** dimension of a device. A device
|
||||
can be in several states at the same time, one per dimension; ie. a
|
||||
device can be ``repaired`` (physical) and ``reserved`` (political),
|
||||
but not ``repaired`` and ``disposed`` at the same time.
|
||||
|
||||
Devicehub actions inherit from `schema actions
|
||||
<http://schema.org/Action>`_, are written in Pascal case and using
|
||||
a verb in infinitive. Some verbs represent the willingness or
|
||||
|
@ -23,208 +19,50 @@ is going to be / must be repaired, whereas ``Repair`` states
|
|||
that the reparation happened. The former actions have the preposition
|
||||
*To* prefixing the verb.
|
||||
|
||||
In the following section we define the actions and states.
|
||||
To see how to perform actions to the Devicehub API head
|
||||
to the `Swagger docs
|
||||
<https://app.swaggerhub.com/apis/ereuse/devicehub/0.2>`_.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
actions
|
||||
|
||||
.. uml:: actions.puml
|
||||
Actions and states affect devices in different ways or **dimensions**.
|
||||
For example, ``Repair`` affects the **physical** dimension of a device,
|
||||
and ``Sell`` the **political** dimension of a device. A device
|
||||
can be in several states at the same time, one per dimension; ie. a
|
||||
device can be ``repaired`` (physical) and ``reserved`` (political),
|
||||
but not ``repaired`` and ``disposed`` at the same time:
|
||||
|
||||
|
||||
Physical Actions
|
||||
****************
|
||||
The following actions describe and react on the
|
||||
:class:`ereuse_devicehub.resources.device.states.Physical` condition
|
||||
of the devices.
|
||||
- Physical actions: The following actions describe and react on the
|
||||
Physical condition of the devices.
|
||||
|
||||
ToPrepare, Prepare
|
||||
==================
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Prepare
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.ToPrepare
|
||||
- ToPrepare and prepare.
|
||||
- ToRepair, Repair
|
||||
- ReadyToUse
|
||||
- Live
|
||||
- DisposeWaste, Recover
|
||||
|
||||
ToRepair, Repair
|
||||
================
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Repair
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.ToRepair
|
||||
- Association actions: Actions that change the associations users have with devices;
|
||||
ie. the **owners**, **usufructuarees**, **reservees**,
|
||||
and **physical possessors**.
|
||||
|
||||
ReadyToUse
|
||||
==========
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.ReadyToUse
|
||||
- Trade
|
||||
- Transfer
|
||||
- Organize
|
||||
|
||||
Live
|
||||
====
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Live
|
||||
- Internal state actions: Actions providing metadata about devices that don't usually change
|
||||
their state.
|
||||
|
||||
DisposeWaste, Recover
|
||||
=====================
|
||||
``RecyclingCenter`` users have two extra special events:
|
||||
- ``DisposeWaste``: The device has been disposed in an unspecified
|
||||
manner.
|
||||
- ``Recover``: The device has been scrapped and its materials have
|
||||
been recovered under a new product.
|
||||
|
||||
See `ToDisposeProduct, DisposeProduct`_.
|
||||
|
||||
.. todo:: Events not developed yet.
|
||||
|
||||
Association actions
|
||||
*******************
|
||||
Actions that change the associations users have with devices;
|
||||
ie. the **owners**, **usufructuarees**, **reservees**,
|
||||
and **physical possessors**.
|
||||
|
||||
There are three sub-dimensions: **trade**, **transfer**,
|
||||
and **organize** actions.
|
||||
|
||||
.. uml:: association-events.puml
|
||||
|
||||
Trade actions
|
||||
=============
|
||||
Not fully developed.
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Trade
|
||||
|
||||
Sell
|
||||
----
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Sell
|
||||
|
||||
Donate
|
||||
------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Donate
|
||||
|
||||
Rent
|
||||
----
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Rent
|
||||
|
||||
CancelTrade
|
||||
-----------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.CancelTrade
|
||||
|
||||
ToDisposeProduct, DisposeProduct
|
||||
--------------------------------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.DisposeProduct
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.ToDisposeProduct
|
||||
|
||||
Transfer actions
|
||||
================
|
||||
The act of transferring/moving devices from one place to another.
|
||||
|
||||
Receive
|
||||
-------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Receive
|
||||
.. autoclass:: ereuse_devicehub.resources.enums.ReceiverRole
|
||||
:members:
|
||||
:undoc-members:
|
||||
.. autoattribute:: ereuse_devicehub.resources.device.models.Device.physical_possessor
|
||||
|
||||
Organize actions
|
||||
================
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Organize
|
||||
|
||||
Reserve, CancelReservation
|
||||
-------------------------
|
||||
Not fully developed.
|
||||
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Reserve
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.CancelReservation
|
||||
|
||||
Assign, Accept, Reject
|
||||
----------------------
|
||||
Not developed.
|
||||
|
||||
``Assign`` allocates devices to an user. The purpose or meaning
|
||||
of the association is defined by the users.
|
||||
|
||||
``Accept`` and ``Reject`` allow users to accept and reject the
|
||||
assignments.
|
||||
|
||||
.. todo:: shall we add ``Deassign`` or make ``Assign``
|
||||
always define all active users?
|
||||
Assign won't be developed until further notice.
|
||||
|
||||
Internal state actions
|
||||
**********************
|
||||
Actions providing metadata about devices that don't usually change
|
||||
their state.
|
||||
|
||||
Snapshot
|
||||
========
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Snapshot
|
||||
- Snapshot
|
||||
- Add, remove
|
||||
- Erase
|
||||
- Install
|
||||
- Test
|
||||
- Benchmark
|
||||
- Rate
|
||||
- Price
|
||||
|
||||
|
||||
Add, Remove
|
||||
===========
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Add
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Remove
|
||||
The following index has all the actions (please note we are moving from calling them
|
||||
``Event`` to call them ``Action``):
|
||||
|
||||
EraseBasic, EraseSectors
|
||||
========================
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.EraseBasic
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.EraseSectors
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.ErasePhysical
|
||||
.. dhlist::
|
||||
:module: ereuse_devicehub.resources.event.schemas
|
||||
|
||||
Install
|
||||
=======
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Install
|
||||
|
||||
Test
|
||||
====
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Test
|
||||
|
||||
TestDataStorage
|
||||
---------------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.TestDataStorage
|
||||
|
||||
StressTest
|
||||
----------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.StressTest
|
||||
|
||||
Benchmark
|
||||
=========
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Benchmark
|
||||
|
||||
|
||||
BenchmarkDataStorage
|
||||
--------------------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkDataStorage
|
||||
|
||||
|
||||
BenchmarkWithRate
|
||||
-----------------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkWithRate
|
||||
|
||||
|
||||
BenchmarkProcessor
|
||||
------------------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkProcessor
|
||||
|
||||
|
||||
BenchmarkProcessorSysbench
|
||||
--------------------------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkProcessorSysbench
|
||||
|
||||
|
||||
BenchmarkRamSysbench
|
||||
--------------------
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkRamSysbench
|
||||
|
||||
Rate
|
||||
====
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Rate
|
||||
|
||||
Price
|
||||
=====
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Price
|
||||
|
||||
Migrate
|
||||
=======
|
||||
Not done.
|
||||
|
||||
.. autoclass:: ereuse_devicehub.resources.event.models.Migrate
|
||||
|
||||
States
|
||||
******
|
||||
|
@ -233,8 +71,4 @@ States
|
|||
.. uml:: states.puml
|
||||
|
||||
.. autoclass:: ereuse_devicehub.resources.device.states.Trading
|
||||
:members:
|
||||
:undoc-members:
|
||||
.. autoclass:: ereuse_devicehub.resources.device.states.Physical
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
Using the API
|
||||
#############
|
||||
|
||||
Devicehub is a REST API on the web that partially extends Schema.org's
|
||||
ontology and it is formatted in JSON.
|
||||
|
||||
The main resource are devices. However, you do not perform operations
|
||||
directly against them (there is no ``POST /device``),
|
||||
as you use an Action / Event to do so (you only ``GET /devices``).
|
||||
For example, to upload information of devices with tests, erasures, etcetera, use
|
||||
the action/event ``POST /snapshot`` (:ref:`devices-snapshot`).
|
||||
|
||||
Login
|
||||
*****
|
||||
To use the API, you need first to log in with an existing account from the DeviceHub.
|
||||
Perform ``POST /users/login/`` with the email and password fields filled::
|
||||
|
||||
POST /users/login/
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
{
|
||||
"email": "user@dhub.com",
|
||||
"password: "1234"
|
||||
}
|
||||
|
||||
Upon success, you are answered with the account object, containing a Token field::
|
||||
|
||||
{
|
||||
"id": "...",
|
||||
"token: "A base 64 codified token",
|
||||
"type": "User",
|
||||
"inventories": [{"type": "Inventory", id: "db1", ...}, ...],
|
||||
...
|
||||
}
|
||||
|
||||
From this moment, any other following operation against
|
||||
the API requires the following HTTP Header:
|
||||
``Authorization: Basic token``. This is, the word **Basic**
|
||||
followed with a **space** and then the **token**,
|
||||
obtained from the account object above, **exactly as it is**.
|
||||
|
||||
.. _authenticate-requests:
|
||||
|
||||
|
||||
Authenticate requests
|
||||
---------------------
|
||||
To explain how to operate with resources like events or devices, we
|
||||
use one as an example: obtaining devices. The template of
|
||||
a request is::
|
||||
|
||||
GET <inventory>/devices/
|
||||
Accept: application/json
|
||||
Authorization: Basic <token>
|
||||
|
||||
And an example is::
|
||||
|
||||
GET acme/devices/
|
||||
Accept: application/json
|
||||
Authorization: Basic myTokenInBase64
|
||||
|
||||
Let's go through the variables:
|
||||
|
||||
- ``<inventory>`` is the name of the inventory where you operate.
|
||||
You get this value from the ``User`` object returned from the login.
|
||||
The ``inventories`` field contains a set of databases the account
|
||||
can operate with, being the first inventory the default one.
|
||||
- ``<token>`` is the token of the account.
|
||||
|
||||
See :ref:`devices:devices` for more information on how to query
|
||||
devices.
|
133
docs/conf.py
133
docs/conf.py
|
@ -18,6 +18,19 @@
|
|||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
import importlib
|
||||
import inspect
|
||||
from typing import Union
|
||||
|
||||
from docutils.parsers.rst import Directive, directives
|
||||
from docutils.statemachine import StringList, string2lines
|
||||
from marshmallow.fields import DateTime, Field
|
||||
from marshmallow.schema import SchemaMeta
|
||||
from teal.enums import Country, Currency, Layouts, Subdivision
|
||||
from teal.marshmallow import EnumField
|
||||
|
||||
from ereuse_devicehub.marshmallow import NestedOn
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
|
||||
project = 'Devicehub'
|
||||
copyright = '2018, eReuse.org team'
|
||||
|
@ -176,3 +189,123 @@ html_favicon = 'img/favicon.ico'
|
|||
# autosectionlabel
|
||||
autosectionlabel_prefix_document = True
|
||||
autodoc_member_order = 'bysource'
|
||||
|
||||
import docutils.nodes as n
|
||||
|
||||
|
||||
class DhlistDirective(Directive):
|
||||
"""Generates documentation from Devicehub Schema.
|
||||
|
||||
This requires :py:class:`ereuse_devicehub.resources.schemas.SchemaMeta`.
|
||||
You will find in that module more information.
|
||||
"""
|
||||
has_content = False
|
||||
|
||||
# Definition of passed-in options
|
||||
option_spec = {'module': directives.unchanged}
|
||||
|
||||
def _import(self, module):
|
||||
for obj in vars(module).values():
|
||||
if inspect.isclass(obj):
|
||||
if isinstance(obj, SchemaMeta) and hasattr(obj, '_base_class'):
|
||||
yield obj
|
||||
|
||||
def run(self):
|
||||
env = self.state.document.settings.env
|
||||
module = importlib.import_module(self.options['module'])
|
||||
things = tuple(self._import(module))
|
||||
|
||||
sections = []
|
||||
sections.append(self.links(things)) # Make index
|
||||
for thng in things: # type: Thing
|
||||
# Generate a section for each class, with a title,
|
||||
# fields description and a paragraph
|
||||
section = n.section(ids=[self._id(thng)])
|
||||
section += n.title(thng.__name__, thng.__name__)
|
||||
section += self.parse('*Extends {}*'.format(thng._base_class))
|
||||
if thng.__doc__:
|
||||
section += self.parse(thng.__doc__)
|
||||
fields = n.field_list()
|
||||
for key, f in thng._own:
|
||||
name = n.field_name(text=f.data_key or key)
|
||||
body = [
|
||||
self.parse('{} {}'.format(self.type(f), f.metadata.get('description', '')))
|
||||
]
|
||||
if isinstance(f, EnumField):
|
||||
body.append(self._parse_enum_field(f))
|
||||
attrs = n.field_list()
|
||||
if f.dump_only:
|
||||
attrs += self.field('Submit', 'No.')
|
||||
if f.required:
|
||||
attrs += self.field('Required', f.required)
|
||||
fields += n.field('', name, n.field_body('', *body, attrs))
|
||||
section += fields
|
||||
sections.append(section)
|
||||
return sections
|
||||
|
||||
def _parse_enum_field(self, f):
|
||||
from ereuse_devicehub.resources.device import states
|
||||
if issubclass(f.enum, (Subdivision, Currency, Country, Layouts, states.State)):
|
||||
return self.parse(f.enum.__doc__)
|
||||
else:
|
||||
enum_fields = n.field_list()
|
||||
for el in f.enum:
|
||||
enum_fields += self.field(el.name, el.value)
|
||||
return enum_fields
|
||||
|
||||
def field(self, name: str, body: Union[str, bool]):
|
||||
"""Generates a field node with a name and a paragraph body."""
|
||||
if isinstance(body, bool):
|
||||
body = 'Yes.' if body else 'No.'
|
||||
body = str(body) if body else ''
|
||||
return n.field('', n.field_name(text=name), n.field_body('', self.parse(body)))
|
||||
|
||||
def type(self, field: Field):
|
||||
"""Parses the type field."""
|
||||
if isinstance(field, NestedOn):
|
||||
t = ''
|
||||
if field.many:
|
||||
t = 'List of '
|
||||
t = t + str(field.schema.t)
|
||||
elif isinstance(field, EnumField):
|
||||
t = field.enum.__name__
|
||||
elif isinstance(field, DateTime):
|
||||
t = 'Date time (ISO 8601 with timezone)'
|
||||
else:
|
||||
t = field.__class__.__name__
|
||||
if 'str' in t.lower():
|
||||
t = 'Text'
|
||||
if 'unit' in field.metadata:
|
||||
t = t + ' ({})'.format(field.metadata['unit'])
|
||||
return t + '.'
|
||||
|
||||
def links(self, things, parent='Schema'):
|
||||
"""Generates an index of things with inheritance awareness."""
|
||||
l = n.bullet_list('')
|
||||
for child in (c for c in things if c._base_class == parent):
|
||||
ref = n.reference(text=child.__name__)
|
||||
ref['refuri'] = '#{}'.format(self._id(child))
|
||||
p = n.paragraph()
|
||||
p += ref
|
||||
l += n.list_item('', p)
|
||||
sub_list = self.links(things, parent=child.__name__)
|
||||
if sub_list:
|
||||
l += sub_list
|
||||
return l
|
||||
|
||||
def _id(self, thing):
|
||||
"""Generate an id to use as html anchors."""
|
||||
return n.make_id('dh-{}'.format(thing.__name__))
|
||||
|
||||
def parse(self, text) -> n.container:
|
||||
"""Parses text possibly containing ReST stuff and adds it in
|
||||
a node."""
|
||||
p = n.container('')
|
||||
self.state.nested_parse(StringList(string2lines(inspect.cleandoc(text))), 0, p)
|
||||
return p
|
||||
# return publish_doctree(text).children
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_directive('dhlist', DhlistDirective)
|
||||
return {'version': '0.1'}
|
||||
|
|
|
@ -37,19 +37,14 @@ Result
|
|||
******
|
||||
The result is a JSON object with the following fields:
|
||||
|
||||
- **devices**: A list of devices.
|
||||
- **groups**: A list of groups.
|
||||
- **widgets**: A dictionary of widgets.
|
||||
- **pagination**: Pagination information:
|
||||
|
||||
- **items**: A list of devices.
|
||||
- **pagination**:
|
||||
- **page**: The page you requested in the ``page`` param of the query,
|
||||
or ``1``.
|
||||
- **perPage**: How many devices are in every page, fixed to ``30``.
|
||||
- **total**: How many total devices passed the filters.
|
||||
- **next**: The number of the next page, if any.
|
||||
- **last**: The number of the last page, if any.
|
||||
|
||||
Models
|
||||
******
|
||||
|
||||
.. automodule:: ereuse_devicehub.resources.device.models
|
||||
:members:
|
||||
:member-order: bysource
|
||||
.. dhlist::
|
||||
:module: ereuse_devicehub.resources.device.schemas
|
||||
|
|
|
@ -5,16 +5,35 @@
|
|||
:alt: DeviceHub logo
|
||||
|
||||
|
||||
This is the documentation and API of the `eReuse.org Devicehub
|
||||
This is the documentation of the `eReuse.org Devicehub
|
||||
<https://github.com/ereuse/devicehub-teal>`_.
|
||||
|
||||
Devicehub is a distributed IT Asset Management System focused in
|
||||
reusing devices, created under the project
|
||||
`eReuse.org <https://www.ereuse.org>`_.
|
||||
|
||||
Our main objectives are:
|
||||
|
||||
- To offer a common IT Asset Management for donors, receivers and IT
|
||||
professionals so they can manage devices and exchange them.
|
||||
This is, reusing –and ultimately recycling.
|
||||
- To automatically recollect, analyse, process and share
|
||||
(controlling privacy) metadata about devices with other tools of the
|
||||
eReuse ecosystem to guarantee traceability, and to provide inputs for
|
||||
the indicators which measure circularity.
|
||||
- To highly integrate with existing IT Asset Management Systems.
|
||||
- To be decentralized.
|
||||
|
||||
Devicehub is built with `Teal <https://github.com/bustawin/teal>`_ and
|
||||
`Flask <http://flask.pocoo.org>`_.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
actions
|
||||
agents
|
||||
api
|
||||
devices
|
||||
actions
|
||||
tags
|
||||
lots
|
||||
|
||||
|
|
|
@ -0,0 +1,542 @@
|
|||
Processes
|
||||
#########
|
||||
|
||||
This is a unclosed list of processes that you can do in Devicehub.
|
||||
Use them as a reference.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
processes
|
||||
|
||||
|
||||
Registration and refurbish
|
||||
**************************
|
||||
|
||||
Tag provisioning
|
||||
================
|
||||
Please refer to :ref:`tags:Use case`.
|
||||
|
||||
Processing a device with Workbench
|
||||
==================================
|
||||
Processing a device with the `eReuse.org Workbench
|
||||
<https://github.com/ereuse/workbench>`_ means creating a hardware
|
||||
report of the device (including serial numbers and other metadata),
|
||||
linking the device with tags, and registering it to a Devicehub.
|
||||
|
||||
This is the first step when dealing with a new device with
|
||||
the eReuse.org tools, as it registers the device with the database,
|
||||
or updates its information if the device existed before. So any
|
||||
other process, unless stated contrary, requires this one to be
|
||||
performed to a device before.
|
||||
|
||||
For generic devices, the process is as follows:
|
||||
|
||||
1. The user opens the eReuse.org Android App (App) and selects
|
||||
*add snapshot*.
|
||||
2. The user sticks and scans the tags of the device, including the
|
||||
:ref:`tags:eTags`, manufacturer tags (like serial numbers), and
|
||||
tags provided by third-parties like donors.
|
||||
3. The user manually introduces other information, like ratings,
|
||||
finally submitting the information to Devicehub.
|
||||
|
||||
For a computer, `This video <https://vimeo.com/250253019>`_ explains
|
||||
the process, and it is as follows:
|
||||
|
||||
1. The user connects the computers to process to an eReuse.org Box
|
||||
running the Workbench Server software using a local network.
|
||||
2. Computers boot and automatically execute the eReuse.org Workbench
|
||||
software, generating information from the computer and its components,
|
||||
erasing the data storage components, testing the machine, etc.
|
||||
3. During the process, the user opens the Android App and selects
|
||||
the *Workbench* option, which connects the App to a running
|
||||
Workbench Server in the local network.
|
||||
4. From now on, like in step 2. from the generic device, the user
|
||||
sticks and scans the tags from the device, specifically the
|
||||
:ref:`tags:eTags` and the ones provided by third-parties. The
|
||||
manufacturer tags are not required as such information is taken
|
||||
by the Workbench automatically.
|
||||
5. Android App and Workbench embed the information into a report
|
||||
that is submitted to Devicehub.
|
||||
|
||||
.. _prepare:
|
||||
|
||||
Preparing a device for use
|
||||
==========================
|
||||
Users, like refurbishers, ready the devices so they are suitable
|
||||
for trading. This process implies repairing, cleaning, etc.
|
||||
|
||||
1. The user scans the tag of the device with the Android App or searches it from the
|
||||
website and selects *actions* > :ref:`actions:ToPrepare`,
|
||||
which informs Devicehub that a device has to be prepared for trading.
|
||||
2. The user prepares the device. Upon success, it performs the action
|
||||
:ref:`actions:Prepare` in the similar way that
|
||||
did in 1.
|
||||
3. A prepared device might still not be ready for trading. For example,
|
||||
a seller still might want to clean a device once a trade has been
|
||||
confirmed, for example because the device gathered dust between
|
||||
the preparation and trading. To denote a final "this device is
|
||||
ready to be used or shipped", the user performs
|
||||
the action :ref:`actions:ReadyToUse` in the same way it did in 1.
|
||||
|
||||
If the device is broken or it breaks, the user performs the action
|
||||
:ref:`actions:ToRepair` denoting that the device has to be repaired,
|
||||
and :ref:`actions:Repair` upon success.
|
||||
|
||||
Broken devices that are not going to be fixed are set to
|
||||
`Dispose a device`_.
|
||||
|
||||
Track a device
|
||||
==============
|
||||
`processing a device with workbench`_ registers into Devicehub
|
||||
the required metadata from a device to identify it: a digital
|
||||
passport for the device (information submitted in a Devicehub),
|
||||
plus a physical passport (a tag that links the device with the digital
|
||||
passport). If the physical passport is an :ref:`tags:eTags` then
|
||||
it is unforgeable.
|
||||
|
||||
The rest of the traceability is based in keeping track of the events
|
||||
occurring on the device, for example when it changes location or
|
||||
it is traded. eReuse.org allows recording these actions, providing
|
||||
mechanisms to ease them or ensure them. Please refer to the specific
|
||||
use cases for more information.
|
||||
|
||||
.. _share:
|
||||
|
||||
Share device information
|
||||
========================
|
||||
Users can generate public links to share with external users, like
|
||||
retailers or donors, so they can see a subset of the metadata. Thanks
|
||||
to this, external users can audit the devices (donors, consumers), take
|
||||
confident and faster decisions when requesting devices (retailers,
|
||||
consumers).
|
||||
|
||||
This information includes hardware information, device rates,
|
||||
device price (both guessed and manually set), and a public part of
|
||||
the traceability log.
|
||||
|
||||
To share devices:
|
||||
|
||||
1. The user scan the tags of the devices it wants to share with the
|
||||
Android App or searches the devices through the website.
|
||||
2. The user select *generate sharing links*, which gives it a list of
|
||||
public links of the devices.
|
||||
3. Users send those links to their contacts using their preferred
|
||||
method, like e-mail.
|
||||
4. External users visit those links in order to see a web page
|
||||
containing the public information of the device.
|
||||
|
||||
Public information of a device is always accessible when an user
|
||||
scans the QR of the tag through its smartphone, as the QR contains
|
||||
a public link of the device. This way people in physical contact with the
|
||||
device, like consumers, can always check information about the device.
|
||||
|
||||
|
||||
.. _public-webpage:
|
||||
|
||||
The public webpage
|
||||
------------------
|
||||
The public webpage of a device includes:
|
||||
|
||||
- A description of the device, including specifications,
|
||||
public identifiers, and associated tags.
|
||||
- Instructions in how to challenge the Photochromic tag of the
|
||||
device for `checking device authenticity`_.
|
||||
- Traceability log of the device.
|
||||
- :ref:`The public custody chain for present devices <public-custody>`.
|
||||
- Certificates like erasures.
|
||||
|
||||
Checking device authenticity
|
||||
============================
|
||||
Any user can check the authenticity of a device registered in a
|
||||
Devicehub, even if the user is not registered, like a customer.
|
||||
|
||||
If the device has an :ref:`tags:eTags` or a regular tag generated by
|
||||
a Devicehub (stuck on the `Processing a device with Workbench`_),
|
||||
the process is as follows:
|
||||
|
||||
1. The user scans the QR code with a smartphone using a generic QR
|
||||
code scanner.
|
||||
2. The scanner opens the browser and takes the user to
|
||||
`the public webpage`_ containing public information of the
|
||||
device, like identifiers and instructions in how to challenge the
|
||||
photochromic tag.
|
||||
3. The user tests the photochromic tag by touching the flash bulb of
|
||||
the smartphone with the tag for, at least, 6 seconds, checking
|
||||
that the tag changes color temporarily.
|
||||
|
||||
Other ways of checking device authenticity are:
|
||||
|
||||
- Scanning the QR code stuck and comparing the serial numbers of the
|
||||
device with the ones of the public webpage.
|
||||
- Directly applying the photochromic challenge.
|
||||
|
||||
Workbench and Devicehub detect changes in computer components. Certain
|
||||
scenarios where the computer passed by untrusted users require
|
||||
ensuring that no component has been taken or replaced.
|
||||
A deeper verification process is re-processing the computer with
|
||||
Workbench, generating a new report that updates the information of
|
||||
the computer in the Devicehub, ultimately showing the differences
|
||||
in removed and added components.
|
||||
|
||||
Finally, the eReuse.org team is developing, using the platform Evrythng,
|
||||
a global record of devices, which takes non-private IDs of the devices
|
||||
of participating Devicehubs and records the most important life
|
||||
events of the devices. This database is publicly available, so
|
||||
users can search on it an ID of a device, for example the S/N or the one
|
||||
written in a tag, like an :ref:`tags:eTags`, and know which Devicehub is
|
||||
registered in, ultimately accessing the public information of the device.
|
||||
|
||||
Recover a lost device
|
||||
=====================
|
||||
Users can recover a lost device found in a waste dump by following the
|
||||
process of `checking device authenticity`_.
|
||||
|
||||
A Devicehub participating in the global record of devices (explained
|
||||
in `checking device authenticity`_) automatically uploads public
|
||||
device information into Evrythng. If the device was previously
|
||||
registered in another Devicehub and there is no record of trading
|
||||
between Devicehubs, Evrythng warns both systems. Note that this
|
||||
functionality is in development.
|
||||
|
||||
Rating a device
|
||||
===============
|
||||
Rating a device is the act of grading the appearance, performance,
|
||||
and functionality of a device. This results in a :ref:`actions:Rate`
|
||||
action, which includes a guessed **price** for the device.
|
||||
|
||||
There are two ways of rating a device:
|
||||
|
||||
1. When processing a computer with Workbench and the Android App.
|
||||
|
||||
1. While Workbench is processing the machine, the user
|
||||
links the tag with the computer. In this process, as it requires the
|
||||
user to scan the tag with the App, the app allows the user to introduce
|
||||
more information, including the appearance and functionality.
|
||||
2. The App embeds the rate with the device report generated by the
|
||||
Workbench.
|
||||
3. The Workbench uploads the report to Devicehub.
|
||||
2. Anytime with the Android App or website.
|
||||
|
||||
- The user scans the tag of the device with the Android App.
|
||||
After scanning it, the App allows the user to rate the
|
||||
appearance and functionality.
|
||||
- Through the website, the user searches the device and then
|
||||
selects to perform a new :ref:`actions:ManualRate`, rating
|
||||
the appearance and functionality.
|
||||
|
||||
In any case, when Devicehub receives the ratings, it computes a final
|
||||
global :ref:`actions:Rate`, embedding a guessed price for the device.
|
||||
|
||||
Refer to :ref:`actions:Rate` for technical details.
|
||||
|
||||
.. _storing:
|
||||
|
||||
Storing devices
|
||||
===============
|
||||
Devices are stored in places like warehouses.
|
||||
|
||||
:ref:`lots:`, :ref:`actions:Locate`, :ref:`actions:Receive`,
|
||||
and :ref:`actions:Live`, actions help locating devices,
|
||||
from a global scale to inside places.
|
||||
|
||||
The :ref:`actions:Locate`, :ref:`actions:Receive`,
|
||||
and :ref:`actions:Live` embed approximated city or province level
|
||||
information, and the user can write a location, name, or address
|
||||
in Locate and Receive. This location can be as detailed as required,
|
||||
like shelves in a building. Users can create actions by scanning
|
||||
a tag with the App or searching a device through the website,
|
||||
and then selecting *create an action*.
|
||||
|
||||
Lots are more versatile than actions, and they do not pollute the
|
||||
traceability log, which is unneeded when placing devices in temporal
|
||||
places like warehouses. Lots act like folders in an Operative System,
|
||||
so the user is free to choose what each lot represents —for example
|
||||
physical locations:
|
||||
|
||||
- Lot company ACME
|
||||
|
||||
- Lot Warehouse 1 of ACME
|
||||
|
||||
- Lot Zone A
|
||||
|
||||
- Computer 1
|
||||
- Monitor 2
|
||||
|
||||
To create a lot the user uses the webiste or App, selecting *create lot*
|
||||
and giving it a name.
|
||||
|
||||
To place devices inside a lot through the website, the user selects
|
||||
the devices, it presses *add to lot*, and writes the name of the lot.
|
||||
To place them through the App, the user scans the tags of the devices,
|
||||
it presses *add to lot*, and writes the name of the lot.
|
||||
|
||||
To look for devices the user reduces the area to look for them by
|
||||
checking to which lot the device is. This is done through the website
|
||||
or App by searching the device and checking to which lots is inside,
|
||||
or searching the lot and checking which devices are inside. Once the
|
||||
user is in the place, it picks up the correct device by reading
|
||||
its tag.
|
||||
|
||||
Erasing data and obtaining a certificate
|
||||
========================================
|
||||
|
||||
When `Processing a device with Workbench`_ user can order Workbench
|
||||
to erase the data stroage units. In the configuration users parametrize
|
||||
the erasure to follow their desired erasure standard (involving
|
||||
customizing erasure steps).
|
||||
|
||||
Once the Workbench uploads the report to a Devicehub, the user gets
|
||||
the erasure certificate of the (data storage units of the) computer.
|
||||
|
||||
A logged-in user with access to the device can scan the tag with
|
||||
the App or search the device through the web app and select
|
||||
*certificates*, then *erasure certificate*, to view an on-line
|
||||
version of the certificate and download a PDF.
|
||||
|
||||
An external user can access `The public webpage`_ of the device
|
||||
to download the erasure certificate.
|
||||
|
||||
Please refer to :ref:`actions:Erase` for detailed information about
|
||||
how erasures work and which information they take.
|
||||
|
||||
.. _delivery:
|
||||
|
||||
Delivery
|
||||
========
|
||||
:ref:`actions:Receive` is the act of physically taking delivery of a
|
||||
device. When an user performs a Receive, it means that another user took
|
||||
the device physically, confirming reception.
|
||||
|
||||
To perform this action the user scans the tag of the devices with the App,
|
||||
or search it through the website, and selects *actions* > *Receive*,
|
||||
filling information about the receiver and delivery.
|
||||
|
||||
An exemplifying case is delivering a device from the warehouse to
|
||||
a customer through a transporter:
|
||||
|
||||
1. Warehouse employees look in the website devices that are sold,
|
||||
donated, rented (:ref:`actions:Trade`) that are still in
|
||||
the warehouse and ready to be used.
|
||||
2. They :ref:`store devices <storing>` in the warehouse.
|
||||
3. Once the devices are located the employees give them to the
|
||||
transporter. To acknowledge this to the system, they scan the
|
||||
tags of those devices with the App and perform the action
|
||||
:ref:`actions:Receive`, stating that the transporter received the
|
||||
devices.
|
||||
4. The transporter takes the devices to the customer, performing the
|
||||
same :ref:`actions:Receive` again, this time stating that the
|
||||
customer received the devices.
|
||||
|
||||
The last :ref:`actions:Receive` of a delivery, the one referring
|
||||
to the final customer, can :ref:`activate the warranty <warranty>`.
|
||||
|
||||
Value (price) devices
|
||||
=====================
|
||||
Devicehub guesses automatically a price after each new rate, explained
|
||||
in `Rating a device`_, and manually by performing the action
|
||||
:ref:`actions:Price`. By doing manually it, the user can set any
|
||||
price.
|
||||
|
||||
To perform a manual price the user scans the tags of the devices
|
||||
with the App, or searches them through the website, and selects
|
||||
*actions* > *price*.
|
||||
|
||||
The user has still a chance to set the final trading price when
|
||||
performing :ref:`actions:Trade`. If the user does not set any price,
|
||||
and the trade is not a :ref:`actions:Donation` or similar, Devicehub
|
||||
assumes that the last known price is the one which the device is
|
||||
sold.
|
||||
|
||||
Refer to :ref:`actions:Price` to know the technical details in how
|
||||
Devicehub guesses the price.
|
||||
|
||||
Manage sale with buyer (reserve, outgoing lots, sell, receive)
|
||||
==============================================================
|
||||
We exemplify the use of lots and actions to manage sales with
|
||||
a buyer.
|
||||
|
||||
1. The first step on sales is for a seller to showcase the devices
|
||||
to potential customers by :ref:`sharing them <share>`.
|
||||
2. A customer inquires about the devices, for example through e-mail.
|
||||
3. This can imply a reservation process.
|
||||
In such case, the seller can perform the action :ref:`actions:Reserve`,
|
||||
which reserves the selected devices for the customer.
|
||||
To perform that action, the user scan the tags of the devices
|
||||
with the App or search them through the website, select them,
|
||||
and click *actions* > *Reserve*.
|
||||
2. Reservations can be cancelled but not modified nor deleted. To cancel a
|
||||
reservation the user uses the App or the web to select the devices,
|
||||
and look for their reservation to cancel it.
|
||||
3. A reservation is fulfilled once the customer buys, gets through, or rents
|
||||
a device; for example by an e-commerce or through a confirmation e-mail.
|
||||
To perform any of those actions in Devicehub,
|
||||
a seller selects the devices and clicks
|
||||
*actions* > *Sell*, *Donate*, or *Rent*. It can perform those actions
|
||||
over devices that are not reserved, or mix reserved devices with
|
||||
non-reserved devices. Refer to :ref:`actions:Trade`.
|
||||
4. Lots help sellers in keeping an order in sales. A good ordering is
|
||||
creating a lot called ``Sales``, and then, inside that lot,
|
||||
a lot for each sales, and/or a lot for each customer.
|
||||
5. The seller gets confirmation from the warehouse or refurbisher
|
||||
that the devices have :ref:`been prepared for use <prepare>`.
|
||||
6. Devices are :ref:`delivered <delivery>` to the customer.
|
||||
|
||||
Verify refurbishment of a device through the tag
|
||||
================================================
|
||||
|
||||
.. todo called Verify refurbishment of end-user's device
|
||||
|
||||
Devicehub and eReuse.org allows usage of the :ref:`tags:Photochromic tags`
|
||||
to visually assist users, at-a-glance, in verifying correctly non-fraudulent
|
||||
refurbishing of a device.
|
||||
|
||||
Users like refurbishers stick the tags on the devices.
|
||||
|
||||
On the end-user side:
|
||||
|
||||
1. The end-user wants to verify refurbishment from a device of a retailer.
|
||||
2. The End-user sees a QR in a tag, like the the :ref:`tags:eTags`,
|
||||
which scans with its smartphone's QR reader app, taking the
|
||||
user to the :ref:`Share device information <public-webpage>`.
|
||||
3. The public web page contains, along information about the device,
|
||||
instructions on how to check the validity of the Photochromic tag
|
||||
— consisting on illuminating the tag with the smartphone's lantern
|
||||
during a minimum of 6 seconds.
|
||||
|
||||
Delivery or pickup from buyer after use
|
||||
=======================================
|
||||
After customer usage devices can be picked-up so they are prepared
|
||||
for re-use or recycle.
|
||||
|
||||
.. todo what happens if the device is from another inventory?
|
||||
|
||||
Once the customer agrees for the devices to be taken, a transporter
|
||||
or the same customer takes the device to the warehouse, and an
|
||||
employee performs a :ref:`actions:Receive` to state that a device has been
|
||||
physically received, and a :ref:`actions:Trade` to state the change of
|
||||
property. These actions can be performed by scanning the tag with
|
||||
the App or by manually searching the device through the website.
|
||||
|
||||
.. _dispose:
|
||||
|
||||
Dispose a device
|
||||
================
|
||||
Users can manage the disposal of devices in Devicehub. A disposal
|
||||
in Devicehub means two things: 1) trading devices to a company that
|
||||
manages its 2) final destruction or recovery.
|
||||
|
||||
The first case is managed by the actions
|
||||
:ref:`actions:ToDisposeProduct, DisposeProduct`:
|
||||
|
||||
1. An user marks a device to be disposed by scanning the tag of the
|
||||
device or searching it through the website and selecting
|
||||
*actions* > *ToDisposeProduct*.
|
||||
2. When the organization in charge of the disposition takes the device
|
||||
the user performs *actions* > *DisposeProduct*.
|
||||
|
||||
.. todo when takes the devices (receive?) or when agreed (trade)?
|
||||
|
||||
The latter case is managed by the actions
|
||||
:ref:`actions:DisposeWaste, Recover`. The user performs the action
|
||||
*DisposeWaste* when the product has been destroyed and put into waste,
|
||||
and *Recover* when the product has been recycled.
|
||||
|
||||
Retail and distribution
|
||||
***********************
|
||||
|
||||
Make devices available for sale to final users
|
||||
==============================================
|
||||
Once the devices are registered in the Devicehub, users can share
|
||||
the devices to potential customers. Please refer to
|
||||
:ref:`share devices information <share>`.
|
||||
|
||||
Manage purchase of devices with refurbisher / ITAD
|
||||
==================================================
|
||||
Please refer to `Manage sale with buyer (reserve, outgoing lots, sell, receive)`_.
|
||||
|
||||
Distribution of devices
|
||||
=======================
|
||||
Please refer to `Delivery or pickup from buyer after use`_.
|
||||
|
||||
Transport between service providers and buyers
|
||||
==============================================
|
||||
Please refer to `Delivery or pickup from buyer after use`_.
|
||||
|
||||
Estimate selling price
|
||||
======================
|
||||
Please refer to `Value (price) devices`_.
|
||||
|
||||
Manage donations and interactions with donors
|
||||
=============================================
|
||||
(Nope)
|
||||
|
||||
Post-sale channel support
|
||||
*************************
|
||||
|
||||
Customer service for hardware issues
|
||||
====================================
|
||||
Devicehub allows introducing contact information in the
|
||||
:ref:`public webpage <public-webpage>` of the device,
|
||||
including an e-mail and phone number.
|
||||
|
||||
This information is based on the default organization, which an
|
||||
administrator sets when installing Devicehub.
|
||||
|
||||
.. todo program this
|
||||
|
||||
.. _warranty:
|
||||
|
||||
Provide hardware warranty
|
||||
=========================
|
||||
Devicehub helps in recording the day the warranty is activated by
|
||||
saving in the traceability log the `Delivery`_ of the device to the final
|
||||
user. Specifically, an user can check the last :ref:`actions:Receive`
|
||||
(step 4. of `Delivery`_) to be the one that activates the warranty.
|
||||
|
||||
Recyclers
|
||||
*********
|
||||
|
||||
Get the certification for recycling
|
||||
===================================
|
||||
Recyclers can obtain a certificate after performing a
|
||||
:ref:`Dispose a device <dispose>` to devices.
|
||||
|
||||
To obtain the certificate, the user scans the tags of the devices with
|
||||
the Android App or searches them through the web, and then selects
|
||||
*certificate* > *recycling*.
|
||||
|
||||
.. todo defined but not programmed
|
||||
|
||||
Device reuse management
|
||||
***********************
|
||||
|
||||
Pick-up at donor
|
||||
================
|
||||
Please see `Manage donations and interactions with donors`_.
|
||||
|
||||
Transfer donations to refurbishers
|
||||
==================================
|
||||
(Nope)
|
||||
|
||||
Get internal custody chain report for donation
|
||||
==============================================
|
||||
Users can obtain the internal custody chain report for donation
|
||||
as an comma separated value spreadsheet.
|
||||
|
||||
To obtain the document, the user scans the tags of the devices with
|
||||
the Android App or searches them through the web, and then selects
|
||||
*certificate* > *Internal custody chain report for donation*.
|
||||
|
||||
Users can see part of this information too by selecting *Actions*
|
||||
after selecting a device, resulting in a web view of the traceability
|
||||
log of the device.
|
||||
|
||||
.. todo defined but not programmed
|
||||
|
||||
.. _public-custody:
|
||||
|
||||
View public custody chain for present devices
|
||||
=============================================
|
||||
The public custody chain of a device is part of the public information
|
||||
of the device that users can :ref:`share device information <share>`.
|
|
@ -107,6 +107,14 @@ Tags and migrations
|
|||
Tags travel with the devices they are linked when migrating them. Future
|
||||
implementations can parameterize this.
|
||||
|
||||
Photochromic tags
|
||||
*****************
|
||||
The photochromic Reversible Tag helps the end-user to identify a
|
||||
legitimate device that has correctly refurbished by an eReuse.org
|
||||
authorized refurbisher, without the hassle to read the QR code.
|
||||
|
||||
Only eReuse.org authorized organizations can use the Photochromic tags.
|
||||
|
||||
Use-case with eTags
|
||||
*******************
|
||||
We explain the use-case of tagging a device with an :ref:`tags:eTags`,
|
||||
|
@ -165,8 +173,8 @@ Use case
|
|||
|
||||
1. By using the `eReuse.org Android App <https://github.com/eReuse/eReuseAndroidApp>`_
|
||||
the user can scan the QR code or the NFC of the eTag.
|
||||
2. If the *user* is processing devices with the `eReuse.org
|
||||
Workbench <https://github.com/ereuse/workbench>`_, Workbench
|
||||
2. If the *user* is processing devices with the
|
||||
`eReuse.org Workbench <https://github.com/ereuse/workbench>`_, Workbench
|
||||
automatically attaches hardware information like serial numbers,
|
||||
otherwise the *user* can add that information through the app.
|
||||
3. These softwares communicate with the Devicehub of the user and
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import os
|
||||
|
||||
import click.testing
|
||||
import ereuse_utils
|
||||
import flask.cli
|
||||
|
||||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
|
||||
|
||||
class DevicehubGroup(flask.cli.FlaskGroup):
|
||||
# todo users cannot make cli to use a custom db this way!
|
||||
CONFIG = DevicehubConfig
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
# todo this should be taken as an argument for the cli
|
||||
inventory = os.environ.get('dhi')
|
||||
if not inventory:
|
||||
raise ValueError('Please do "export dhi={inventory}"')
|
||||
self.create_app = self.create_app_factory(inventory)
|
||||
return super().main(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def create_app_factory(cls, inventory):
|
||||
return lambda: Devicehub(inventory, config=cls.CONFIG())
|
||||
|
||||
|
||||
def get_version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
click.echo('Devicehub {}'.format(ereuse_utils.version('ereuse-devicehub')), color=ctx.color)
|
||||
flask.cli.get_version(ctx, param, value)
|
||||
|
||||
|
||||
@click.option('--version',
|
||||
help='Devicehub version.',
|
||||
expose_value=False,
|
||||
callback=get_version,
|
||||
is_flag=True,
|
||||
is_eager=True)
|
||||
@click.group(cls=DevicehubGroup,
|
||||
context_settings=Devicehub.cli_context_settings,
|
||||
add_version_option=False,
|
||||
help="""
|
||||
Manages the Devicehub of the inventory {}.
|
||||
|
||||
Use 'export dhi=xx' to set the inventory that this CLI
|
||||
manages. For example 'export dhi=db1' and then executing
|
||||
'dh tag add' adds a tag in the db1 database. Operations
|
||||
that affect the common database (like creating an user)
|
||||
are not affected by this.
|
||||
""".format(os.environ.get('dhi')))
|
||||
def cli():
|
||||
pass
|
|
@ -106,7 +106,7 @@ class Client(TealClient):
|
|||
def login(self, email: str, password: str):
|
||||
assert isinstance(email, str)
|
||||
assert isinstance(password, str)
|
||||
return self.post({'email': email, 'password': password}, '/users/login', status=200)
|
||||
return self.post({'email': email, 'password': password}, '/users/login/', status=200)
|
||||
|
||||
def get_many(self,
|
||||
res: ResourceLike,
|
||||
|
|
|
@ -7,8 +7,9 @@ from teal.config import Config
|
|||
from teal.enums import Currency
|
||||
from teal.utils import import_resource
|
||||
|
||||
from ereuse_devicehub.resources import agent, event, lot, tag, user
|
||||
from ereuse_devicehub.resources import agent, event, inventory, lot, tag, user
|
||||
from ereuse_devicehub.resources.device import definitions
|
||||
from ereuse_devicehub.resources.documents import documents
|
||||
from ereuse_devicehub.resources.enums import PriceSoftware, RatingSoftware
|
||||
|
||||
|
||||
|
@ -18,22 +19,17 @@ class DevicehubConfig(Config):
|
|||
import_resource(user),
|
||||
import_resource(tag),
|
||||
import_resource(agent),
|
||||
import_resource(lot)))
|
||||
import_resource(lot),
|
||||
import_resource(documents),
|
||||
import_resource(inventory)),
|
||||
)
|
||||
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/devicehub' # type: str
|
||||
SCHEMA = 'dhub'
|
||||
MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion
|
||||
"""
|
||||
the minimum version of ereuse.org workbench that this devicehub
|
||||
accepts. we recommend not changing this value.
|
||||
"""
|
||||
ORGANIZATION_NAME = None # type: str
|
||||
ORGANIZATION_TAX_ID = None # type: str
|
||||
"""
|
||||
The organization using this Devicehub.
|
||||
|
||||
It is used by default, for example, when creating tags.
|
||||
"""
|
||||
API_DOC_CONFIG_TITLE = 'Devicehub'
|
||||
API_DOC_CONFIG_VERSION = '0.2'
|
||||
API_DOC_CONFIG_COMPONENTS = {
|
||||
|
@ -56,8 +52,3 @@ class DevicehubConfig(Config):
|
|||
"""
|
||||
Official versions
|
||||
"""
|
||||
|
||||
def __init__(self, db: str = None) -> None:
|
||||
if not self.ORGANIZATION_NAME or not self.ORGANIZATION_TAX_ID:
|
||||
raise ValueError('You need to set the main organization parameters.')
|
||||
super().__init__(db)
|
||||
|
|
|
@ -1,8 +1,29 @@
|
|||
import citext
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy_utils import view
|
||||
from teal.db import SchemaSQLAlchemy
|
||||
from teal.db import SchemaSQLAlchemy, SchemaSession
|
||||
|
||||
|
||||
class DhSession(SchemaSession):
|
||||
def final_flush(self):
|
||||
"""A regular flush that performs expensive final operations
|
||||
through Devicehub (like saving searches), so it is thought
|
||||
to be used once in each request, at the very end before
|
||||
a commit.
|
||||
"""
|
||||
# This was done before with an ``before_commit`` sqlalchemy event
|
||||
# however it is too fragile –it does not detect previously-flushed
|
||||
# things
|
||||
# This solution makes this more aware to the user, although
|
||||
# has the same problem. This is not final solution.
|
||||
# todo a solution would be for this session to save, on every
|
||||
# flush, all the new / dirty interesting things in a variable
|
||||
# until DeviceSearch is executed
|
||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||
DeviceSearch.update_modified_devices(session=self)
|
||||
|
||||
|
||||
class SQLAlchemy(SchemaSQLAlchemy):
|
||||
|
@ -11,12 +32,20 @@ class SQLAlchemy(SchemaSQLAlchemy):
|
|||
schema of the database, as it is in the `search_path`
|
||||
defined in teal.
|
||||
"""
|
||||
# todo add here all types of columns used so we don't have to
|
||||
# manually import them all the time
|
||||
UUID = postgresql.UUID
|
||||
CIText = citext.CIText
|
||||
PSQL_INT_MAX = 2147483648
|
||||
|
||||
def drop_all(self, bind='__all__', app=None):
|
||||
def drop_all(self, bind='__all__', app=None, common_schema=True):
|
||||
"""A faster nuke-like option to drop everything."""
|
||||
self.drop_schema()
|
||||
self.drop_schema(schema='common')
|
||||
if common_schema:
|
||||
self.drop_schema(schema='common')
|
||||
|
||||
def create_session(self, options):
|
||||
return sessionmaker(class_=DhSession, db=self, **options)
|
||||
|
||||
|
||||
def create_view(name, selectable):
|
||||
|
@ -37,6 +66,6 @@ def create_view(name, selectable):
|
|||
return table
|
||||
|
||||
|
||||
db = SQLAlchemy(session_options={"autoflush": False})
|
||||
db = SQLAlchemy(session_options={'autoflush': False})
|
||||
f = db.func
|
||||
exp = expression
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
import os
|
||||
import uuid
|
||||
from typing import Type
|
||||
|
||||
import boltons.urlutils
|
||||
import click
|
||||
import click_spinner
|
||||
import ereuse_utils.cli
|
||||
from ereuse_utils.session import DevicehubClient
|
||||
from flask.globals import _app_ctx_stack, g
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import event
|
||||
from teal.config import Config as ConfigClass
|
||||
from teal.teal import Teal
|
||||
|
||||
from ereuse_devicehub.auth import Auth
|
||||
from ereuse_devicehub.client import Client
|
||||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.dummy.dummy import Dummy
|
||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||
from ereuse_devicehub.resources.inventory import Inventory, InventoryDef
|
||||
|
||||
|
||||
class Devicehub(Teal):
|
||||
|
@ -17,7 +25,8 @@ class Devicehub(Teal):
|
|||
Dummy = Dummy
|
||||
|
||||
def __init__(self,
|
||||
config: ConfigClass,
|
||||
inventory: str,
|
||||
config: DevicehubConfig = DevicehubConfig(),
|
||||
db: SQLAlchemy = db,
|
||||
import_name=__name__.split('.')[0],
|
||||
static_url_path=None,
|
||||
|
@ -30,24 +39,102 @@ class Devicehub(Teal):
|
|||
instance_relative_config=False,
|
||||
root_path=None,
|
||||
Auth: Type[Auth] = Auth):
|
||||
super().__init__(config, db, import_name, static_url_path, static_folder, static_host,
|
||||
assert inventory
|
||||
super().__init__(config, db, inventory, import_name, static_url_path, static_folder,
|
||||
static_host,
|
||||
host_matching, subdomain_matching, template_folder, instance_path,
|
||||
instance_relative_config, root_path, Auth)
|
||||
instance_relative_config, root_path, False, Auth)
|
||||
self.id = inventory
|
||||
"""The Inventory ID of this instance. In Teal is the app.schema."""
|
||||
self.dummy = Dummy(self)
|
||||
self.before_request(self.register_db_events_listeners)
|
||||
self.cli.command('regenerate-search')(self.regenerate_search)
|
||||
|
||||
def register_db_events_listeners(self):
|
||||
"""Registers the SQLAlchemy event listeners."""
|
||||
# todo can I make it with a global Session only?
|
||||
event.listen(db.session, 'before_commit', DeviceSearch.update_modified_devices)
|
||||
@self.cli.group(short_help='Inventory management.',
|
||||
help='Manages the inventory {}.'.format(os.environ.get('dhi')))
|
||||
def inv():
|
||||
pass
|
||||
|
||||
def _init_db(self):
|
||||
super()._init_db()
|
||||
DeviceSearch.set_all_devices_tokens_if_empty(self.db.session)
|
||||
inv.command('add')(self.init_db)
|
||||
inv.command('del')(self.delete_inventory)
|
||||
inv.command('search')(self.regenerate_search)
|
||||
self.before_request(self._prepare_request)
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
@click.option('--name', '-n',
|
||||
default='Test 1',
|
||||
help='The human name of the inventory.')
|
||||
@click.option('--org-name', '-on',
|
||||
default='My Organization',
|
||||
help='The name of the default organization that owns this inventory.')
|
||||
@click.option('--org-id', '-oi',
|
||||
default='foo-bar',
|
||||
help='The Tax ID of the organization.')
|
||||
@click.option('--tag-url', '-tu',
|
||||
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
|
||||
default='http://example.com',
|
||||
help='The base url (scheme and host) of the tag provider.')
|
||||
@click.option('--tag-token', '-tt',
|
||||
type=click.UUID,
|
||||
default='899c794e-1737-4cea-9232-fdc507ab7106',
|
||||
help='The token provided by the tag provider. It is an UUID.')
|
||||
@click.option('--erase/--no-erase',
|
||||
default=False,
|
||||
help='Delete the schema before? '
|
||||
'If --common is set this includes the common database.')
|
||||
@click.option('--common/--no-common',
|
||||
default=False,
|
||||
help='Creates common databases. Only execute if the database is empty.')
|
||||
def init_db(self, name: str,
|
||||
org_name: str,
|
||||
org_id: str,
|
||||
tag_url: boltons.urlutils.URL,
|
||||
tag_token: uuid.UUID,
|
||||
erase: bool,
|
||||
common: bool):
|
||||
"""Creates an inventory.
|
||||
|
||||
This creates the database and adds the inventory to the
|
||||
inventory tables with the passed-in settings, and does nothing if the
|
||||
inventory already exists.
|
||||
|
||||
After you create the inventory you might want to create an user
|
||||
executing *dh user add*.
|
||||
"""
|
||||
assert _app_ctx_stack.top, 'Use an app context.'
|
||||
print('Initializing database...'.ljust(30), end='')
|
||||
with click_spinner.spinner():
|
||||
if erase:
|
||||
self.db.drop_all(common_schema=common)
|
||||
assert not db.has_schema(self.id), 'Schema {} already exists.'.format(self.id)
|
||||
exclude_schema = 'common' if not common else None
|
||||
self._init_db(exclude_schema=exclude_schema)
|
||||
InventoryDef.set_inventory_config(name, org_name, org_id, tag_url, tag_token)
|
||||
DeviceSearch.set_all_devices_tokens_if_empty(self.db.session)
|
||||
self._init_resources(exclude_schema=exclude_schema)
|
||||
self.db.session.commit()
|
||||
print('done.')
|
||||
|
||||
@click.confirmation_option(prompt='Are you sure you want to delete the inventory {}?'
|
||||
.format(os.environ.get('dhi')))
|
||||
def delete_inventory(self):
|
||||
"""Erases an inventory.
|
||||
|
||||
This removes its private database and its entry in the common
|
||||
inventory.
|
||||
|
||||
This deletes users that have only access to this inventory.
|
||||
"""
|
||||
InventoryDef.delete_inventory()
|
||||
self.db.session.commit()
|
||||
self.db.drop_all(common_schema=False)
|
||||
|
||||
def regenerate_search(self):
|
||||
"""Re-creates from 0 all the search tables."""
|
||||
DeviceSearch.regenerate_search_table(self.db.session)
|
||||
db.session.commit()
|
||||
print('Done.')
|
||||
|
||||
def _prepare_request(self):
|
||||
"""Prepares request stuff."""
|
||||
inv = g.inventory = Inventory.current # type: Inventory
|
||||
g.tag_provider = DevicehubClient(base_url=inv.tag_provider,
|
||||
token=DevicehubClient.encode_token(inv.tag_token))
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
from threading import Lock
|
||||
|
||||
import sqlalchemy as sa
|
||||
import werkzeug.exceptions
|
||||
from werkzeug import wsgi
|
||||
|
||||
import ereuse_devicehub.config
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
|
||||
|
||||
class PathDispatcher:
|
||||
NOT_FOUND = werkzeug.exceptions.NotFound()
|
||||
INV = Inventory
|
||||
|
||||
def __init__(self, config_cls=ereuse_devicehub.config.DevicehubConfig) -> None:
|
||||
self.lock = Lock()
|
||||
self.instances = {}
|
||||
self.CONFIG = config_cls
|
||||
self.engine = sa.create_engine(self.CONFIG.SQLALCHEMY_DATABASE_URI)
|
||||
with self.lock:
|
||||
self.instantiate()
|
||||
if not self.instances:
|
||||
raise ValueError('There are no Devicehub instances! Please, execute `dh init-db`.')
|
||||
self.one_app = next(iter(self.instances.values()))
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
if wsgi.get_path_info(environ).startswith('/users'):
|
||||
# Not nice solution but it works well for now
|
||||
# Return any app, as all apps can handle login
|
||||
return self.call(self.one_app, environ, start_response)
|
||||
inventory = wsgi.pop_path_info(environ)
|
||||
with self.lock:
|
||||
if inventory not in self.instances:
|
||||
self.instantiate()
|
||||
app = self.instances.get(inventory, self.NOT_FOUND)
|
||||
return self.call(app, environ, start_response)
|
||||
|
||||
@staticmethod
|
||||
def call(app, environ, start_response):
|
||||
return app(environ, start_response)
|
||||
|
||||
def instantiate(self):
|
||||
sel = sa.select([self.INV.id]).where(self.INV.id.notin_(self.instances.keys()))
|
||||
for row in self.engine.execute(sel):
|
||||
self.instances[row.id] = Devicehub(inventory=row.id)
|
|
@ -5,6 +5,7 @@ from typing import Set
|
|||
|
||||
import click
|
||||
import click_spinner
|
||||
import ereuse_utils.cli
|
||||
import yaml
|
||||
from ereuse_utils.test import ANY
|
||||
|
||||
|
@ -26,9 +27,11 @@ class Dummy:
|
|||
)
|
||||
"""Tags to create."""
|
||||
ET = (
|
||||
('A0000000000001', 'DT-AAAAA'),
|
||||
('A0000000000002', 'DT-BBBBB'),
|
||||
('A0000000000003', 'DT-CCCCC'),
|
||||
('DT-AAAAA', 'A0000000000001'),
|
||||
('DT-BBBBB', 'A0000000000002'),
|
||||
('DT-CCCCC', 'A0000000000003'),
|
||||
('DT-BRRAB', '04970DA2A15984'),
|
||||
('DT-XXXXX', '04e4bc5af95980')
|
||||
)
|
||||
"""eTags to create."""
|
||||
ORG = 'eReuse.org CAT', '-t', 'G-60437761', '-c', 'ES'
|
||||
|
@ -39,34 +42,42 @@ class Dummy:
|
|||
self.app = app
|
||||
self.app.cli.command('dummy', short_help='Creates dummy devices and users.')(self.run)
|
||||
|
||||
@click.option('--tag-url', '-tu',
|
||||
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
|
||||
default='http://localhost:8081',
|
||||
help='The base url (scheme and host) of the tag provider.')
|
||||
@click.option('--tag-token', '-tt',
|
||||
type=click.UUID,
|
||||
default='899c794e-1737-4cea-9232-fdc507ab7106',
|
||||
help='The token provided by the tag provider. It is an UUID.')
|
||||
@click.confirmation_option(prompt='This command (re)creates the DB from scratch.'
|
||||
'Do you want to continue?')
|
||||
def run(self):
|
||||
def run(self, tag_url, tag_token):
|
||||
runner = self.app.test_cli_runner()
|
||||
self.app.init_db(erase=True)
|
||||
self.app.init_db('Dummy',
|
||||
'ACME',
|
||||
'acme-id',
|
||||
tag_url,
|
||||
tag_token,
|
||||
erase=True,
|
||||
common=True)
|
||||
print('Creating stuff...'.ljust(30), end='')
|
||||
with click_spinner.spinner():
|
||||
out = runner.invoke(args=['create-org', *self.ORG], catch_exceptions=False).output
|
||||
out = runner.invoke('org', 'add', *self.ORG).output
|
||||
org_id = json.loads(out)['id']
|
||||
user = self.user_client('user@dhub.com', '1234')
|
||||
# todo put user's agent into Org
|
||||
for id in self.TAGS:
|
||||
user.post({'id': id}, res=Tag)
|
||||
for id, sec in self.ET:
|
||||
runner.invoke(args=[
|
||||
'create-tag', id,
|
||||
'-p', 'https://t.devicetag.io',
|
||||
'-s', sec,
|
||||
'-o', org_id
|
||||
],
|
||||
catch_exceptions=False)
|
||||
runner.invoke('tag', 'add', id,
|
||||
'-p', 'https://t.devicetag.io',
|
||||
'-s', sec,
|
||||
'-o', org_id)
|
||||
# create tag for pc-laudem
|
||||
runner.invoke(args=[
|
||||
'create-tag', 'tagA',
|
||||
'-p', 'https://t.devicetag.io',
|
||||
'-s', 'tagA-secondary'
|
||||
],
|
||||
catch_exceptions=False)
|
||||
runner.invoke('tag', 'add', 'tagA',
|
||||
'-p', 'https://t.devicetag.io',
|
||||
'-s', 'tagA-secondary')
|
||||
files = tuple(Path(__file__).parent.joinpath('files').iterdir())
|
||||
print('done.')
|
||||
sample_pc = None # We treat this one as a special sample for demonstrations
|
||||
|
@ -80,6 +91,11 @@ class Dummy:
|
|||
sample_pc = s['device']['id']
|
||||
else:
|
||||
pcs.add(s['device']['id'])
|
||||
if s.get('uuid', None) == 'de4f495e-c58b-40e1-a33e-46ab5e84767e': # oreo
|
||||
# Make one hdd ErasePhysical
|
||||
hdd = next(hdd for hdd in s['components'] if hdd['type'] == 'HardDrive')
|
||||
user.post({'type': 'ErasePhysical', 'method': 'Shred', 'device': hdd['id']},
|
||||
res=m.Event)
|
||||
assert sample_pc
|
||||
print('PC sample is', sample_pc)
|
||||
# Link tags and eTags
|
||||
|
@ -119,9 +135,9 @@ class Dummy:
|
|||
assert len(inventory['items'])
|
||||
|
||||
i, _ = user.get(res=Device, query=[('search', 'intel')])
|
||||
assert len(i['items']) == 12
|
||||
assert 12 == len(i['items'])
|
||||
i, _ = user.get(res=Device, query=[('search', 'pc')])
|
||||
assert len(i['items']) == 13
|
||||
assert 14 == len(i['items'])
|
||||
|
||||
# Let's create a set of events for the pc device
|
||||
# Make device Ready
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
# This is a complete Snapshot with benchmarks, tests, erasure
|
||||
# installation, and an eTag (TIS) linked
|
||||
{
|
||||
"closed": true,
|
||||
"components": [
|
||||
{
|
||||
"address": 64,
|
||||
"cores": 1,
|
||||
"events": [
|
||||
{
|
||||
"elapsed": 0,
|
||||
"rate": 6666.22,
|
||||
"type": "BenchmarkProcessor"
|
||||
},
|
||||
{
|
||||
"elapsed": 165,
|
||||
"rate": 165.365,
|
||||
"type": "BenchmarkProcessorSysbench"
|
||||
}
|
||||
],
|
||||
"manufacturer": "Intel Corp.",
|
||||
"model": "Intel Atom CPU N455 @ 1.66GHz",
|
||||
"serialNumber": null,
|
||||
"speed": 1.667,
|
||||
"threads": 2,
|
||||
"type": "Processor"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Qualcomm Atheros",
|
||||
"model": "AR9285 Wireless Network Adapter",
|
||||
"serialNumber": "74:2f:68:8b:fd:c8",
|
||||
"type": "NetworkAdapter",
|
||||
"wireless": true
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Qualcomm Atheros",
|
||||
"model": "AR8152 v2.0 Fast Ethernet",
|
||||
"serialNumber": "14:da:e9:42:f6:7c",
|
||||
"speed": 100,
|
||||
"type": "NetworkAdapter",
|
||||
"wireless": false
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"format": "DIMM",
|
||||
"interface": "DDR2",
|
||||
"manufacturer": null,
|
||||
"model": null,
|
||||
"serialNumber": null,
|
||||
"size": 1024,
|
||||
"speed": 667.0,
|
||||
"type": "RamModule"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Intel Corporation",
|
||||
"model": "NM10/ICH7 Family High Definition Audio Controller",
|
||||
"serialNumber": null,
|
||||
"type": "SoundCard"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Azurewave",
|
||||
"model": "USB 2.0 UVC VGA WebCam",
|
||||
"serialNumber": "0x0001",
|
||||
"type": "SoundCard"
|
||||
},
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"endTime": "2018-11-24T22:00:39.643726+00:00",
|
||||
"severity": "Info",
|
||||
"startTime": "2018-11-24T18:12:42.641985+00:00",
|
||||
"steps": [
|
||||
{
|
||||
"endTime": "2018-11-24T19:28:51.215882+00:00",
|
||||
"severity": "Info",
|
||||
"startTime": "2018-11-24T18:12:42.643104+00:00",
|
||||
"type": "StepZero"
|
||||
},
|
||||
{
|
||||
"endTime": "2018-11-24T22:00:39.642482+00:00",
|
||||
"severity": "Info",
|
||||
"startTime": "2018-11-24T19:28:51.216747+00:00",
|
||||
"type": "StepRandom"
|
||||
}
|
||||
],
|
||||
"type": "EraseSectors"
|
||||
},
|
||||
{
|
||||
"assessment": true,
|
||||
"currentPendingSectorCount": 0,
|
||||
"elapsed": 99,
|
||||
"length": "Short",
|
||||
"lifetime": 1199,
|
||||
"offlineUncorrectable": 0,
|
||||
"powerCycleCount": 2128,
|
||||
"reallocatedSectorCount": 0,
|
||||
"severity": "Info",
|
||||
"status": "Completed without error",
|
||||
"type": "TestDataStorage"
|
||||
},
|
||||
{
|
||||
"type": "Install",
|
||||
"elapsed": 1000,
|
||||
"name": "LinuxMintFSAx32-Eng.fsa",
|
||||
"address": 32
|
||||
},
|
||||
{
|
||||
"elapsed": 16,
|
||||
"readSpeed": 66.1,
|
||||
"type": "BenchmarkDataStorage",
|
||||
"writeSpeed": 21.8
|
||||
}
|
||||
],
|
||||
"interface": "ATA",
|
||||
"manufacturer": "Hitachi",
|
||||
"model": "HTS54322",
|
||||
"serialNumber": "E2024242CV86HJ",
|
||||
"size": 238475,
|
||||
"type": "HardDrive"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Intel Corporation",
|
||||
"memory": 256.0,
|
||||
"model": "Atom Processor D4xx/D5xx/N4xx/N5xx Integrated Graphics Controller",
|
||||
"serialNumber": null,
|
||||
"type": "GraphicCard"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"firewire": 0,
|
||||
"manufacturer": "ASUSTeK Computer INC.",
|
||||
"model": "1001PXD",
|
||||
"pcmcia": 0,
|
||||
"serial": 1,
|
||||
"serialNumber": "Eee0123456789",
|
||||
"slots": 2,
|
||||
"type": "Motherboard",
|
||||
"usb": 5
|
||||
}
|
||||
],
|
||||
"device": {
|
||||
"chassis": "Netbook",
|
||||
"events": [
|
||||
{
|
||||
"elapsed": 16,
|
||||
"rate": 15.9165,
|
||||
"type": "BenchmarkRamSysbench"
|
||||
},
|
||||
{
|
||||
"elapsed": 60,
|
||||
"severity": "Info",
|
||||
"type": "StressTest"
|
||||
},
|
||||
{
|
||||
"appearanceRange": "A",
|
||||
"biosRange": "A",
|
||||
"functionalityRange": "A",
|
||||
"type": "WorkbenchRate"
|
||||
}
|
||||
],
|
||||
"manufacturer": "ASUSTeK Computer INC.",
|
||||
"model": "1001PXD",
|
||||
"serialNumber": "B8OAAS048286",
|
||||
"tags": [
|
||||
{
|
||||
"id": "04e4bc5af95980",
|
||||
"type": "Tag"
|
||||
}
|
||||
],
|
||||
"type": "Laptop"
|
||||
},
|
||||
"elapsed": 14725,
|
||||
"endTime": "2018-11-24T18:06:37.611704+00:00",
|
||||
"expectedEvents": [
|
||||
"Benchmark",
|
||||
"TestDataStorage",
|
||||
"StressTest",
|
||||
"EraseBasic",
|
||||
"Install"
|
||||
],
|
||||
"software": "Workbench",
|
||||
"type": "Snapshot",
|
||||
"uuid": "f6cba71f-0ac1-4aba-8b6a-c1fd56ab483d",
|
||||
"version": "11.0b2"
|
||||
}
|
|
@ -97,7 +97,7 @@
|
|||
"endTime": "2018-07-11T11:42:12.971177"
|
||||
}
|
||||
],
|
||||
"zeros": false,
|
||||
|
||||
"severity": "Info",
|
||||
"type": "EraseBasic",
|
||||
"endTime": "2018-07-11T11:42:12.975358",
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
"events": [
|
||||
{
|
||||
"type": "EraseBasic",
|
||||
"zeros": false,
|
||||
|
||||
"endTime": "2018-07-11T11:56:52.390306",
|
||||
"severity": "Info",
|
||||
"startTime": "2018-07-11T10:49:31.998217",
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
},
|
||||
{
|
||||
"startTime": "2018-07-11T10:32:14.445306",
|
||||
"zeros": false,
|
||||
|
||||
"type": "EraseBasic",
|
||||
"severity": "Info",
|
||||
"endTime": "2018-07-11T10:53:46.442123",
|
||||
|
@ -113,7 +113,7 @@
|
|||
},
|
||||
{
|
||||
"startTime": "2018-07-11T10:53:46.442187",
|
||||
"zeros": false,
|
||||
|
||||
"type": "EraseBasic",
|
||||
"severity": "Info",
|
||||
"endTime": "2018-07-11T11:16:28.469899",
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
{
|
||||
"software": "Workbench",
|
||||
"endTime": "2018-09-22T19:05:47.005552+00:00",
|
||||
"device": {
|
||||
"events": [
|
||||
{
|
||||
"rate": 15.9663,
|
||||
"type": "BenchmarkRamSysbench",
|
||||
"elapsed": 16
|
||||
},
|
||||
{
|
||||
|
||||
"type": "StressTest",
|
||||
"elapsed": 120
|
||||
}
|
||||
],
|
||||
"model": "E627",
|
||||
"chassis": "Netbook",
|
||||
"serialNumber": "LXN650207893942DE21601",
|
||||
"type": "Laptop",
|
||||
"manufacturer": "eMachines"
|
||||
},
|
||||
"elapsed": 451,
|
||||
"expectedEvents": [
|
||||
"Benchmark",
|
||||
"TestDataStorage",
|
||||
"StressTest"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"events": [],
|
||||
"model": "Video WebCam",
|
||||
"serialNumber": "CN0314-SN30-OV035-VA-R05.00.00",
|
||||
"type": "SoundCard",
|
||||
"manufacturer": "SuYin"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"model": "SBx00 Azalia",
|
||||
"serialNumber": null,
|
||||
"type": "SoundCard",
|
||||
"manufacturer": "Advanced Micro Devices, Inc. AMD/ATI"
|
||||
},
|
||||
{
|
||||
"speed": 400.0,
|
||||
"size": 2048,
|
||||
"format": "DIMM",
|
||||
"events": [],
|
||||
"model": "HYMP125S64CP8-S6",
|
||||
"interface": "DDR2",
|
||||
"type": "RamModule",
|
||||
"manufacturer": null,
|
||||
"serialNumber": null
|
||||
},
|
||||
{
|
||||
"speed": 400.0,
|
||||
"size": 2048,
|
||||
"format": "DIMM",
|
||||
"events": [],
|
||||
"model": "HYMP125S64CP8-S6",
|
||||
"interface": "DDR2",
|
||||
"type": "RamModule",
|
||||
"manufacturer": null,
|
||||
"serialNumber": null
|
||||
},
|
||||
{
|
||||
"speed": 0.8,
|
||||
"address": 64,
|
||||
"serialNumber": null,
|
||||
"events": [
|
||||
{
|
||||
"rate": 173.6996,
|
||||
"type": "BenchmarkProcessorSysbench",
|
||||
"elapsed": 174
|
||||
},
|
||||
{
|
||||
"rate": 3191.96,
|
||||
"type": "BenchmarkProcessor",
|
||||
"elapsed": 0
|
||||
}
|
||||
],
|
||||
"model": "AMD Athlon Processor TF-20",
|
||||
"threads": 1,
|
||||
"cores": 1,
|
||||
"type": "Processor",
|
||||
"manufacturer": "Advanced Micro Devices AMD"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"model": "AR9285 Wireless Network Adapter",
|
||||
"serialNumber": "0c:60:76:5f:49:91",
|
||||
"type": "NetworkAdapter",
|
||||
"manufacturer": "Qualcomm Atheros",
|
||||
"wireless": true
|
||||
},
|
||||
{
|
||||
"speed": 100,
|
||||
"events": [],
|
||||
"model": "AR8132 Fast Ethernet",
|
||||
"serialNumber": "00:26:22:59:a1:56",
|
||||
"type": "NetworkAdapter",
|
||||
"manufacturer": "Qualcomm Atheros",
|
||||
"wireless": false
|
||||
},
|
||||
{
|
||||
"size": 152627,
|
||||
"serialNumber": "WD-WX80A8996018",
|
||||
"events": [
|
||||
{
|
||||
"writeSpeed": 17.8,
|
||||
"type": "BenchmarkDataStorage",
|
||||
"elapsed": 20,
|
||||
"readSpeed": 59.8
|
||||
},
|
||||
{
|
||||
"currentPendingSectorCount": 0,
|
||||
"length": "Short",
|
||||
"elapsed": 117,
|
||||
"reallocatedSectorCount": 0,
|
||||
"powerCycleCount": 2872,
|
||||
"offlineUncorrectable": 0,
|
||||
"type": "TestDataStorage",
|
||||
"lifetime": 2775,
|
||||
"assessment": true,
|
||||
"status": "Completed without error"
|
||||
}
|
||||
],
|
||||
"model": "WDC WD1600BEVT-2",
|
||||
"interface": "ATA",
|
||||
"type": "HardDrive",
|
||||
"manufacturer": "Western Digital"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"model": "RS780M Mobility Radeon HD 3200",
|
||||
"serialNumber": null,
|
||||
"type": "GraphicCard",
|
||||
"manufacturer": "Advanced Micro Devices, Inc. AMD/ATI",
|
||||
"memory": 256.0
|
||||
},
|
||||
{
|
||||
"slots": 4,
|
||||
"firewire": 0,
|
||||
"events": [],
|
||||
"model": "E627",
|
||||
"usb": 3,
|
||||
"serialNumber": "LXN650207893942DE21601",
|
||||
"type": "Motherboard",
|
||||
"manufacturer": "eMachines",
|
||||
"serial": 1,
|
||||
"pcmcia": 0
|
||||
}
|
||||
],
|
||||
"uuid": "a01eacdb-db01-43ec-b6fb-a9b8cd21492d",
|
||||
"type": "Snapshot",
|
||||
"version": "11.0a4",
|
||||
"closed": false
|
||||
}
|
|
@ -110,8 +110,7 @@
|
|||
"endTime": "2018-07-11T14:04:04.861590",
|
||||
"severity": "Info"
|
||||
}
|
||||
],
|
||||
"zeros": false
|
||||
]
|
||||
}
|
||||
],
|
||||
"size": 238475,
|
||||
|
|
|
@ -104,7 +104,6 @@
|
|||
"severity": "Info",
|
||||
"endTime": "2018-07-11T11:33:41.531918",
|
||||
"startTime": "2018-07-11T10:30:35.643855",
|
||||
"zeros": false,
|
||||
"type": "EraseBasic",
|
||||
"steps": [
|
||||
{
|
||||
|
|
|
@ -128,6 +128,10 @@
|
|||
{
|
||||
"id": "tagA-secondary",
|
||||
"type": "Tag"
|
||||
},
|
||||
{
|
||||
"id": "DT-BRRAB",
|
||||
"type": "Tag"
|
||||
}
|
||||
],
|
||||
"type": "Desktop"
|
||||
|
|
|
@ -105,7 +105,7 @@
|
|||
],
|
||||
"startTime": "2018-07-03T09:15:22.256074",
|
||||
"severity": "Info",
|
||||
"zeros": false,
|
||||
|
||||
"endTime": "2018-07-03T10:32:11.848455"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from typing import Dict, List
|
||||
|
||||
from flask import Response, jsonify, request
|
||||
from teal.query import NestedQueryFlaskParser
|
||||
from webargs.flaskparser import FlaskParser
|
||||
|
||||
|
@ -10,3 +13,31 @@ class SearchQueryParser(NestedQueryFlaskParser):
|
|||
else:
|
||||
v = super().parse_querystring(req, name, field)
|
||||
return v
|
||||
|
||||
|
||||
def things_response(items: List[Dict],
|
||||
page: int = None,
|
||||
per_page: int = None,
|
||||
total: int = None,
|
||||
previous: int = None,
|
||||
next: int = None,
|
||||
url: str = None,
|
||||
code: int = 200) -> Response:
|
||||
"""Generates a Devicehub API list conformant response for multiple
|
||||
things.
|
||||
"""
|
||||
response = jsonify({
|
||||
'items': items,
|
||||
# todo pagination should be in Header like github
|
||||
# https://developer.github.com/v3/guides/traversing-with-pagination/
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'perPage': per_page,
|
||||
'total': total,
|
||||
'previous': previous,
|
||||
'next': next
|
||||
},
|
||||
'url': url or request.path
|
||||
})
|
||||
response.status_code = code
|
||||
return response
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import json
|
||||
|
||||
import click
|
||||
from flask import current_app as app
|
||||
from teal.db import SQLAlchemy
|
||||
from boltons.typeutils import classproperty
|
||||
from teal.resource import Converters, Resource
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
|
@ -24,7 +23,7 @@ class OrganizationDef(AgentDef):
|
|||
static_url_path=None,
|
||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||
root_path=None):
|
||||
cli_commands = ((self.create_org, 'create-org'),)
|
||||
cli_commands = ((self.create_org, 'add'),)
|
||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||
|
||||
|
@ -46,10 +45,9 @@ class OrganizationDef(AgentDef):
|
|||
print(json.dumps(o, indent=2))
|
||||
return o
|
||||
|
||||
def init_db(self, db: SQLAlchemy):
|
||||
"""Creates the default organization."""
|
||||
org = models.Organization(**app.config.get_namespace('ORGANIZATION_'))
|
||||
db.session.add(org)
|
||||
@classproperty
|
||||
def cli_name(cls):
|
||||
return 'org'
|
||||
|
||||
|
||||
class Membership(Resource):
|
||||
|
|
|
@ -3,17 +3,17 @@ from operator import attrgetter
|
|||
from uuid import uuid4
|
||||
|
||||
from citext import CIText
|
||||
from flask import current_app as app, g
|
||||
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.orm import backref, relationship, validates
|
||||
from sqlalchemy_utils import EmailType, PhoneNumberType
|
||||
from teal import enums
|
||||
from teal.db import DBError, INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, check_lower
|
||||
from teal.db import INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, check_lower
|
||||
from teal.marshmallow import ValidationError
|
||||
from werkzeug.exceptions import NotImplemented, UnprocessableEntity
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
|
||||
|
@ -27,7 +27,7 @@ class JoinedTableMixin:
|
|||
|
||||
class Agent(Thing):
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||
type = Column(Unicode, nullable=False, index=True)
|
||||
type = Column(Unicode, nullable=False)
|
||||
name = Column(CIText())
|
||||
name.comment = """
|
||||
The name of the organization or person.
|
||||
|
@ -46,6 +46,8 @@ class Agent(Thing):
|
|||
|
||||
__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
|
||||
|
@ -80,21 +82,23 @@ class Agent(Thing):
|
|||
|
||||
|
||||
class Organization(JoinedTableMixin, Agent):
|
||||
default_of = db.relationship(Inventory,
|
||||
uselist=False,
|
||||
lazy=True,
|
||||
backref=backref('org', lazy=True),
|
||||
# We need to use this as we cannot do Inventory.foreign -> Org
|
||||
# as foreign keys can only reference to one table
|
||||
# and we have multiple organization table (one per schema)
|
||||
foreign_keys=[Inventory.org_id],
|
||||
primaryjoin=lambda: Organization.id == Inventory.org_id)
|
||||
|
||||
def __init__(self, name: str, **kwargs) -> None:
|
||||
super().__init__(**kwargs, name=name)
|
||||
|
||||
@classmethod
|
||||
def get_default_org_id(cls) -> UUID:
|
||||
"""Retrieves the default organization."""
|
||||
try:
|
||||
return g.setdefault('org_id',
|
||||
Organization.query.filter_by(
|
||||
**app.config.get_namespace('ORGANIZATION_')
|
||||
).one().id)
|
||||
except (DBError, UnprocessableEntity):
|
||||
# todo test how well this works
|
||||
raise NotImplemented('Error in getting the default organization. '
|
||||
'Is the DB initialized?')
|
||||
return cls.query.filter_by(default_of=Inventory.current).one().id
|
||||
|
||||
|
||||
class Individual(JoinedTableMixin, Agent):
|
||||
|
|
|
@ -21,6 +21,7 @@ class Agent(Thing):
|
|||
|
||||
class Organization(Agent):
|
||||
members = NestedOn('Membership')
|
||||
default_of = NestedOn('Inventory')
|
||||
|
||||
|
||||
class Membership(Thing):
|
||||
|
|
|
@ -297,6 +297,7 @@ class ManufacturerDef(Resource):
|
|||
SCHEMA = schemas.Manufacturer
|
||||
AUTH = True
|
||||
|
||||
def init_db(self, db: 'db.SQLAlchemy'):
|
||||
def init_db(self, db: 'db.SQLAlchemy', exclude_schema=None):
|
||||
"""Loads the manufacturers to the database."""
|
||||
Manufacturer.add_all_to_session(db.session)
|
||||
if exclude_schema != 'common':
|
||||
Manufacturer.add_all_to_session(db.session)
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import Dict, List, Set
|
|||
|
||||
from boltons import urlutils
|
||||
from citext import CIText
|
||||
from ereuse_utils.naming import Naming
|
||||
from ereuse_utils.naming import HID_CONVERSION_DOC, Naming
|
||||
from more_itertools import unique_everseen
|
||||
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
||||
Sequence, SmallInteger, Unicode, inspect, text
|
||||
|
@ -29,44 +29,71 @@ from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
|||
|
||||
|
||||
class Device(Thing):
|
||||
"""
|
||||
Base class for any type of physical object that can be identified.
|
||||
"""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.
|
||||
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, index=True)
|
||||
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.
|
||||
"""
|
||||
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 or brand of the device in lower case.
|
||||
|
||||
Devices usually report one of both (model or brand). This value
|
||||
must be consistent through time.
|
||||
"""
|
||||
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."""
|
||||
weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5))
|
||||
weight.comment = """
|
||||
The weight of the device in Kgm.
|
||||
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.
|
||||
The width of the device.
|
||||
"""
|
||||
height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5))
|
||||
height.comment = """
|
||||
The height of the device in meters.
|
||||
The height of the device.
|
||||
"""
|
||||
depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5))
|
||||
depth.comment = """
|
||||
The depth of the device in meters.
|
||||
The depth of the device.
|
||||
"""
|
||||
color = Column(ColorType)
|
||||
color.comment = """The predominant color of the device."""
|
||||
production_date = Column(db.TIMESTAMP(timezone=True))
|
||||
production_date.comment = """The date of production of the item."""
|
||||
production_date.comment = """The date of production of the device."""
|
||||
|
||||
_NON_PHYSICAL_PROPS = {
|
||||
'id',
|
||||
|
@ -76,22 +103,33 @@ class Device(Thing):
|
|||
'parent_id',
|
||||
'hid',
|
||||
'production_date',
|
||||
'color'
|
||||
'color', # these are only user-input thus volatile
|
||||
'width',
|
||||
'height',
|
||||
'depth',
|
||||
'weight'
|
||||
}
|
||||
|
||||
__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.manufacturer, self.serial_number, self.model)
|
||||
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, and 3) events performed to a parent device.
|
||||
All the events where the device participated, including:
|
||||
|
||||
Events are returned by ascending creation time.
|
||||
1. Events performed directly to the device.
|
||||
2. Events performed to a component.
|
||||
3. Events performed to a parent device.
|
||||
|
||||
Events are returned by ascending ``created`` time.
|
||||
"""
|
||||
return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY)
|
||||
|
||||
|
@ -194,7 +232,7 @@ class Device(Thing):
|
|||
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
|
||||
the one with the worst ``severity`` of them, or ``None`` if no
|
||||
test has been executed.
|
||||
"""
|
||||
from ereuse_devicehub.resources.event.models import Test
|
||||
|
@ -288,8 +326,18 @@ class DisplayMixin:
|
|||
|
||||
|
||||
class Computer(Device):
|
||||
"""A chassis with components inside that can be processed
|
||||
automatically with Workbench Computer.
|
||||
|
||||
Computer is broadly extended by ``Desktop``, ``Laptop``, and
|
||||
``Server``. The property ``chassis`` defines it more granularly.
|
||||
"""
|
||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||
chassis = Column(DBEnum(ComputerChassis), nullable=False)
|
||||
chassis.comment = """The physical form of the computer.
|
||||
|
||||
It is a subset of the Linux definition of DMI / DMI decode.
|
||||
"""
|
||||
|
||||
def __init__(self, chassis, **kwargs) -> None:
|
||||
chassis = ComputerChassis(chassis)
|
||||
|
@ -338,8 +386,8 @@ class Computer(Device):
|
|||
|
||||
@property
|
||||
def privacy(self):
|
||||
"""Returns the privacy of all DataStorage components when
|
||||
it is None.
|
||||
"""Returns the privacy of all ``DataStorage`` components when
|
||||
it is not None.
|
||||
"""
|
||||
return set(
|
||||
privacy for privacy in
|
||||
|
@ -391,6 +439,8 @@ class Projector(Monitor):
|
|||
|
||||
|
||||
class Mobile(Device):
|
||||
"""A mobile device consisting of smartphones, tablets, and cellphones."""
|
||||
|
||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||
imei = Column(BigInteger)
|
||||
imei.comment = """
|
||||
|
@ -406,11 +456,13 @@ class Mobile(Device):
|
|||
def validate_imei(self, _, value: int):
|
||||
if not imei.is_valid(str(value)):
|
||||
raise ValidationError('{} is not a valid imei.'.format(value))
|
||||
return value
|
||||
|
||||
@validates('meid')
|
||||
def validate_meid(self, _, value: str):
|
||||
if not meid.is_valid(value):
|
||||
raise ValidationError('{} is not a valid meid.'.format(value))
|
||||
return value
|
||||
|
||||
|
||||
class Smartphone(Mobile):
|
||||
|
@ -426,9 +478,10 @@ class Cellphone(Mobile):
|
|||
|
||||
|
||||
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), index=True)
|
||||
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
||||
parent = relationship(Computer,
|
||||
backref=backref('components',
|
||||
lazy=True,
|
||||
|
@ -437,6 +490,10 @@ class Component(Device):
|
|||
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:
|
||||
|
@ -475,6 +532,7 @@ class GraphicCard(JoinedComponentTableMixin, Component):
|
|||
|
||||
|
||||
class DataStorage(JoinedComponentTableMixin, Component):
|
||||
"""A device that stores information."""
|
||||
size = Column(Integer, check_range('size', min=1, max=10 ** 8))
|
||||
size.comment = """
|
||||
The size of the data-storage in MB.
|
||||
|
@ -483,7 +541,10 @@ class DataStorage(JoinedComponentTableMixin, Component):
|
|||
|
||||
@property
|
||||
def privacy(self):
|
||||
"""Returns the privacy compliance state of the data storage."""
|
||||
"""Returns the privacy compliance state of the data storage.
|
||||
|
||||
This is, the last erasure performed to the data storage.
|
||||
"""
|
||||
from ereuse_devicehub.resources.event.models import EraseBasic
|
||||
try:
|
||||
ev = self.last_event_of(EraseBasic)
|
||||
|
@ -494,7 +555,7 @@ class DataStorage(JoinedComponentTableMixin, Component):
|
|||
def __format__(self, format_spec):
|
||||
v = super().__format__(format_spec)
|
||||
if 's' in format_spec:
|
||||
v += ' – {} GB'.format(self.size // 1000)
|
||||
v += ' – {} GB'.format(self.size // 1000 if self.size else '?')
|
||||
return v
|
||||
|
||||
|
||||
|
@ -539,14 +600,21 @@ class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
|
|||
|
||||
|
||||
class Processor(JoinedComponentTableMixin, Component):
|
||||
"""The CPU."""
|
||||
speed = Column(Float, check_range('speed', 0.1, 15))
|
||||
speed.comment = """The regular CPU speed."""
|
||||
cores = Column(SmallInteger, check_range('cores', 1, 10))
|
||||
cores.comment = """The number of regular cores."""
|
||||
threads = Column(SmallInteger, check_range('threads', 1, 20))
|
||||
threads.comment = """The number of threads per core."""
|
||||
address = Column(SmallInteger, check_range('address', 8, 256))
|
||||
address.comment = """The address of the CPU: 8, 16, 32, 64, 128 or 256 bits."""
|
||||
|
||||
|
||||
class RamModule(JoinedComponentTableMixin, Component):
|
||||
"""A stick of RAM."""
|
||||
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
||||
size.comment = """The capacity of the RAM stick."""
|
||||
speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
|
||||
interface = Column(DBEnum(RamInterface))
|
||||
format = Column(DBEnum(RamFormat))
|
||||
|
@ -559,14 +627,15 @@ class SoundCard(JoinedComponentTableMixin, Component):
|
|||
class Display(JoinedComponentTableMixin, DisplayMixin, Component):
|
||||
"""
|
||||
The display of a device. This is used in all devices that have
|
||||
displays but that it is not their main treat, like laptops,
|
||||
mobiles, smart-watches, and so on; excluding then ComputerMonitor
|
||||
and Television Set.
|
||||
displays but that it is not their main part, like laptops,
|
||||
mobiles, smart-watches, and so on; excluding ``ComputerMonitor``
|
||||
and ``TelevisionSet``.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ComputerAccessory(Device):
|
||||
"""Computer peripherals and similar accessories."""
|
||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||
pass
|
||||
|
||||
|
@ -588,6 +657,7 @@ class MemoryCardReader(ComputerAccessory):
|
|||
|
||||
|
||||
class Networking(NetworkMixin, Device):
|
||||
"""Routers, switches, hubs..."""
|
||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||
|
||||
|
||||
|
@ -632,6 +702,7 @@ class Microphone(Sound):
|
|||
|
||||
|
||||
class Video(Device):
|
||||
"""Devices related to video treatment."""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -644,6 +715,7 @@ class Videoconference(Video):
|
|||
|
||||
|
||||
class Cooking(Device):
|
||||
"""Cooking devices."""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -652,15 +724,25 @@ class Mixer(Cooking):
|
|||
|
||||
|
||||
class Manufacturer(db.Model):
|
||||
__table_args__ = {'schema': 'common'}
|
||||
"""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,
|
||||
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
||||
index=db.Index('name', text('name gin_trgm_ops'), postgresql_using='gin'))
|
||||
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):
|
||||
|
|
|
@ -19,6 +19,7 @@ from ereuse_devicehub.resources.image.models import ImageList
|
|||
from ereuse_devicehub.resources.lot.models import Lot
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
from ereuse_devicehub.resources.tag import Tag
|
||||
from ereuse_devicehub.resources.tag.model import Tags
|
||||
|
||||
|
||||
class Device(Thing):
|
||||
|
@ -55,7 +56,7 @@ class Device(Thing):
|
|||
self.events_multiple = ... # type: Set[e.EventWithMultipleDevices]
|
||||
self.events_one = ... # type: Set[e.EventWithOneDevice]
|
||||
self.images = ... # type: ImageList
|
||||
self.tags = ... # type: Set[Tag]
|
||||
self.tags = ... # type: Tags[Tag]
|
||||
self.lots = ... # type: Set[Lot]
|
||||
self.production_date = ... # type: datetime
|
||||
|
||||
|
@ -286,11 +287,13 @@ class Processor(Component):
|
|||
speed = ... # type: Column
|
||||
cores = ... # type: Column
|
||||
address = ... # type: Column
|
||||
threads = ... # type: Column
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.speed = ... # type: float
|
||||
self.cores = ... # type: int
|
||||
self.threads = ... # type: int
|
||||
self.address = ... # type: int
|
||||
|
||||
|
||||
|
@ -308,6 +311,10 @@ class RamModule(Component):
|
|||
self.format = ... # type: RamFormat
|
||||
|
||||
|
||||
class SoundCard(Component):
|
||||
pass
|
||||
|
||||
|
||||
class Display(DisplayMixin, Component):
|
||||
pass
|
||||
|
||||
|
|
|
@ -15,12 +15,13 @@ from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
|||
|
||||
|
||||
class Device(Thing):
|
||||
__doc__ = m.Device.__doc__
|
||||
id = Integer(description=m.Device.id.comment, dump_only=True)
|
||||
hid = SanitizedStr(lower=True, dump_only=True, description=m.Device.hid.comment)
|
||||
tags = NestedOn('Tag',
|
||||
many=True,
|
||||
collection_class=OrderedSet,
|
||||
description='The set of tags that identify the device.')
|
||||
description='A set of tags that identify the device.')
|
||||
model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE))
|
||||
manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE))
|
||||
serial_number = SanitizedStr(lower=True, data_key='serialNumber')
|
||||
|
@ -75,29 +76,54 @@ class Device(Thing):
|
|||
|
||||
|
||||
class Computer(Device):
|
||||
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet)
|
||||
chassis = EnumField(enums.ComputerChassis, required=True)
|
||||
ram_size = Integer(dump_only=True, data_key='ramSize')
|
||||
data_storage_size = Integer(dump_only=True, data_key='dataStorageSize')
|
||||
processor_model = Str(dump_only=True, data_key='processorModel')
|
||||
graphic_card_model = Str(dump_only=True, data_key='graphicCardModel')
|
||||
network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds')
|
||||
privacy = NestedOn('Event', many=True, dump_only=True, collection_class=set)
|
||||
__doc__ = m.Computer.__doc__
|
||||
components = NestedOn('Component',
|
||||
many=True,
|
||||
dump_only=True,
|
||||
collection_class=OrderedSet,
|
||||
description='The components that are inside this computer.')
|
||||
chassis = EnumField(enums.ComputerChassis,
|
||||
required=True,
|
||||
description=m.Computer.chassis.comment)
|
||||
ram_size = Integer(dump_only=True,
|
||||
data_key='ramSize',
|
||||
description=m.Computer.ram_size.__doc__)
|
||||
data_storage_size = Integer(dump_only=True,
|
||||
data_key='dataStorageSize',
|
||||
description=m.Computer.data_storage_size.__doc__)
|
||||
processor_model = Str(dump_only=True,
|
||||
data_key='processorModel',
|
||||
description=m.Computer.processor_model.__doc__)
|
||||
graphic_card_model = Str(dump_only=True,
|
||||
data_key='graphicCardModel',
|
||||
description=m.Computer.graphic_card_model.__doc__)
|
||||
network_speeds = List(Integer(dump_only=True),
|
||||
dump_only=True,
|
||||
data_key='networkSpeeds',
|
||||
description=m.Computer.network_speeds.__doc__)
|
||||
privacy = NestedOn('Event',
|
||||
many=True,
|
||||
dump_only=True,
|
||||
collection_class=set,
|
||||
description=m.Computer.privacy.__doc__)
|
||||
|
||||
|
||||
class Desktop(Computer):
|
||||
pass
|
||||
__doc__ = m.Desktop.__doc__
|
||||
|
||||
|
||||
class Laptop(Computer):
|
||||
pass
|
||||
layout = EnumField(Layouts, description=m.Laptop.layout.comment)
|
||||
__doc__ = m.Laptop.__doc__
|
||||
|
||||
|
||||
class Server(Computer):
|
||||
pass
|
||||
__doc__ = m.Server.__doc__
|
||||
|
||||
|
||||
class DisplayMixin:
|
||||
__doc__ = m.DisplayMixin.__doc__
|
||||
|
||||
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150))
|
||||
technology = EnumField(enums.DisplayTech,
|
||||
description=m.DisplayMixin.technology.comment)
|
||||
|
@ -113,6 +139,8 @@ class DisplayMixin:
|
|||
|
||||
|
||||
class NetworkMixin:
|
||||
__doc__ = m.NetworkMixin.__doc__
|
||||
|
||||
speed = Integer(validate=Range(min=10, max=10000),
|
||||
unit=UnitCodes.mbps,
|
||||
description=m.NetworkAdapter.speed.comment)
|
||||
|
@ -120,18 +148,20 @@ class NetworkMixin:
|
|||
|
||||
|
||||
class Monitor(DisplayMixin, Device):
|
||||
pass
|
||||
__doc__ = m.Monitor.__doc__
|
||||
|
||||
|
||||
class ComputerMonitor(Monitor):
|
||||
pass
|
||||
__doc__ = m.ComputerMonitor.__doc__
|
||||
|
||||
|
||||
class TelevisionSet(Monitor):
|
||||
pass
|
||||
__doc__ = m.TelevisionSet.__doc__
|
||||
|
||||
|
||||
class Mobile(Device):
|
||||
__doc__ = m.Mobile.__doc__
|
||||
|
||||
imei = Integer(description=m.Mobile.imei.comment)
|
||||
meid = Str(description=m.Mobile.meid.comment)
|
||||
|
||||
|
@ -145,31 +175,38 @@ class Mobile(Device):
|
|||
def convert_check_meid(self, data: dict):
|
||||
if data.get('meid', None):
|
||||
data['meid'] = meid.compact(data['meid'])
|
||||
return data
|
||||
|
||||
|
||||
class Smartphone(Mobile):
|
||||
pass
|
||||
__doc__ = m.Smartphone.__doc__
|
||||
|
||||
|
||||
class Tablet(Mobile):
|
||||
pass
|
||||
__doc__ = m.Tablet.__doc__
|
||||
|
||||
|
||||
class Cellphone(Mobile):
|
||||
pass
|
||||
__doc__ = m.Cellphone.__doc__
|
||||
|
||||
|
||||
class Component(Device):
|
||||
__doc__ = m.Component.__doc__
|
||||
|
||||
parent = NestedOn(Device, dump_only=True)
|
||||
|
||||
|
||||
class GraphicCard(Component):
|
||||
__doc__ = m.GraphicCard.__doc__
|
||||
|
||||
memory = Integer(validate=Range(0, 10000),
|
||||
unit=UnitCodes.mbyte,
|
||||
description=m.GraphicCard.memory.comment)
|
||||
|
||||
|
||||
class DataStorage(Component):
|
||||
__doc__ = m.DataStorage.__doc__
|
||||
|
||||
size = Integer(validate=Range(0, 10 ** 8),
|
||||
unit=UnitCodes.mbyte,
|
||||
description=m.DataStorage.size.comment)
|
||||
|
@ -178,128 +215,147 @@ class DataStorage(Component):
|
|||
|
||||
|
||||
class HardDrive(DataStorage):
|
||||
pass
|
||||
__doc__ = m.HardDrive.__doc__
|
||||
|
||||
|
||||
class SolidStateDrive(DataStorage):
|
||||
pass
|
||||
__doc__ = m.SolidStateDrive.__doc__
|
||||
|
||||
|
||||
class Motherboard(Component):
|
||||
__doc__ = m.Motherboard.__doc__
|
||||
|
||||
slots = Integer(validate=Range(0, 20),
|
||||
description=m.Motherboard.slots.comment)
|
||||
usb = Integer(validate=Range(0, 20))
|
||||
firewire = Integer(validate=Range(0, 20))
|
||||
serial = Integer(validate=Range(0, 20))
|
||||
pcmcia = Integer(validate=Range(0, 20))
|
||||
usb = Integer(validate=Range(0, 20), description=m.Motherboard.usb.comment)
|
||||
firewire = Integer(validate=Range(0, 20), description=m.Motherboard.firewire.comment)
|
||||
serial = Integer(validate=Range(0, 20), description=m.Motherboard.serial.comment)
|
||||
pcmcia = Integer(validate=Range(0, 20), description=m.Motherboard.pcmcia.comment)
|
||||
|
||||
|
||||
class NetworkAdapter(NetworkMixin, Component):
|
||||
pass
|
||||
__doc__ = m.NetworkAdapter.__doc__
|
||||
|
||||
|
||||
class Processor(Component):
|
||||
speed = Float(validate=Range(min=0.1, max=15), unit=UnitCodes.ghz)
|
||||
cores = Integer(validate=Range(min=1, max=10))
|
||||
threads = Integer(validate=Range(min=1, max=20))
|
||||
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}))
|
||||
__doc__ = m.Processor.__doc__
|
||||
|
||||
speed = Float(validate=Range(min=0.1, max=15),
|
||||
unit=UnitCodes.ghz,
|
||||
description=m.Processor.speed.comment)
|
||||
cores = Integer(validate=Range(min=1, max=10), description=m.Processor.cores.comment)
|
||||
threads = Integer(validate=Range(min=1, max=20), description=m.Processor.threads.comment)
|
||||
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}),
|
||||
description=m.Processor.address.comment)
|
||||
|
||||
|
||||
class RamModule(Component):
|
||||
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
|
||||
__doc__ = m.RamModule.__doc__
|
||||
|
||||
size = Integer(validate=Range(min=128, max=17000),
|
||||
unit=UnitCodes.mbyte,
|
||||
description=m.RamModule.size.comment)
|
||||
speed = Integer(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
|
||||
interface = EnumField(enums.RamInterface)
|
||||
format = EnumField(enums.RamFormat)
|
||||
|
||||
|
||||
class SoundCard(Component):
|
||||
pass
|
||||
__doc__ = m.SoundCard.__doc__
|
||||
|
||||
|
||||
class Display(DisplayMixin, Component):
|
||||
pass
|
||||
__doc__ = m.Display.__doc__
|
||||
|
||||
|
||||
class Manufacturer(Schema):
|
||||
__doc__ = m.Manufacturer.__doc__
|
||||
|
||||
name = String(dump_only=True)
|
||||
url = URL(dump_only=True)
|
||||
logo = URL(dump_only=True)
|
||||
|
||||
|
||||
class ComputerAccessory(Device):
|
||||
pass
|
||||
__doc__ = m.ComputerAccessory.__doc__
|
||||
|
||||
|
||||
class Mouse(ComputerAccessory):
|
||||
pass
|
||||
__doc__ = m.Mouse.__doc__
|
||||
|
||||
|
||||
class MemoryCardReader(ComputerAccessory):
|
||||
pass
|
||||
__doc__ = m.MemoryCardReader.__doc__
|
||||
|
||||
|
||||
class SAI(ComputerAccessory):
|
||||
pass
|
||||
__doc__ = m.SAI.__doc__
|
||||
|
||||
|
||||
class Keyboard(ComputerAccessory):
|
||||
__doc__ = m.Keyboard.__doc__
|
||||
|
||||
layout = EnumField(Layouts)
|
||||
|
||||
|
||||
class Networking(NetworkMixin, Device):
|
||||
pass
|
||||
__doc__ = m.Networking.__doc__
|
||||
|
||||
|
||||
class Router(Networking):
|
||||
pass
|
||||
__doc__ = m.Router.__doc__
|
||||
|
||||
|
||||
class Switch(Networking):
|
||||
pass
|
||||
__doc__ = m.Switch.__doc__
|
||||
|
||||
|
||||
class Hub(Networking):
|
||||
pass
|
||||
__doc__ = m.Hub.__doc__
|
||||
|
||||
|
||||
class WirelessAccessPoint(Networking):
|
||||
pass
|
||||
__doc__ = m.WirelessAccessPoint.__doc__
|
||||
|
||||
|
||||
class Printer(Device):
|
||||
wireless = Boolean(required=True, missing=False)
|
||||
scanning = Boolean(required=True, missing=False)
|
||||
technology = EnumField(enums.PrinterTechnology, required=True)
|
||||
monochrome = Boolean(required=True, missing=True)
|
||||
__doc__ = m.Printer.__doc__
|
||||
|
||||
wireless = Boolean(required=True, missing=False, description=m.Printer.wireless.comment)
|
||||
scanning = Boolean(required=True, missing=False, description=m.Printer.scanning.comment)
|
||||
technology = EnumField(enums.PrinterTechnology,
|
||||
required=True,
|
||||
description=m.Printer.technology.comment)
|
||||
monochrome = Boolean(required=True, missing=True, description=m.Printer.monochrome.comment)
|
||||
|
||||
|
||||
class LabelPrinter(Printer):
|
||||
pass
|
||||
__doc__ = m.LabelPrinter.__doc__
|
||||
|
||||
|
||||
class Sound(Device):
|
||||
pass
|
||||
__doc__ = m.Sound.__doc__
|
||||
|
||||
|
||||
class Microphone(Sound):
|
||||
pass
|
||||
__doc__ = m.Microphone.__doc__
|
||||
|
||||
|
||||
class Video(Device):
|
||||
pass
|
||||
__doc__ = m.Video.__doc__
|
||||
|
||||
|
||||
class VideoScaler(Video):
|
||||
pass
|
||||
__doc__ = m.VideoScaler.__doc__
|
||||
|
||||
|
||||
class Videoconference(Video):
|
||||
pass
|
||||
__doc__ = m.Videoconference.__doc__
|
||||
|
||||
|
||||
class Cooking(Device):
|
||||
pass
|
||||
__doc__ = m.Cooking.__doc__
|
||||
|
||||
|
||||
class Mixer(Cooking):
|
||||
pass
|
||||
__doc__ = m.Mixer.__doc__
|
||||
|
|
|
@ -24,18 +24,20 @@ class DeviceSearch(db.Model):
|
|||
primary_key=True)
|
||||
device = db.relationship(Device, primaryjoin=Device.id == device_id)
|
||||
|
||||
properties = db.Column(TSVECTOR,
|
||||
nullable=False,
|
||||
index=db.Index('properties gist',
|
||||
postgresql_using='gist',
|
||||
postgresql_concurrently=True))
|
||||
tags = db.Column(TSVECTOR, index=db.Index('tags gist',
|
||||
postgresql_using='gist',
|
||||
postgresql_concurrently=True))
|
||||
properties = db.Column(TSVECTOR, nullable=False)
|
||||
tags = db.Column(TSVECTOR)
|
||||
|
||||
__table_args__ = {
|
||||
'prefixes': ['UNLOGGED'] # Only for temporal tables, can cause table to empty on turn on
|
||||
}
|
||||
__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'),
|
||||
{
|
||||
'prefixes': ['UNLOGGED']
|
||||
# Only for temporal tables, can cause table to empty on turn on
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def update_modified_devices(cls, session: db.Session):
|
||||
|
|
|
@ -16,6 +16,19 @@ class State(Enum):
|
|||
|
||||
|
||||
class Trading(State):
|
||||
"""
|
||||
Trading states.
|
||||
|
||||
:cvar Reserved: The device has been reserved.
|
||||
:cvar Cancelled: The device has been cancelled.
|
||||
:cvar Sold: The device has been sold.
|
||||
:cvar Donated: The device is donated.
|
||||
:cvar Renting: The device is in renting
|
||||
:cvar ToBeDisposed: The device is disposed.
|
||||
This is the end of life of a device.
|
||||
:cvar ProductDisposed: The device has been removed
|
||||
from the facility. It does not mean end-of-life.
|
||||
"""
|
||||
Reserved = e.Reserve
|
||||
Cancelled = e.CancelTrade
|
||||
Sold = e.Sell
|
||||
|
@ -27,6 +40,16 @@ class Trading(State):
|
|||
|
||||
|
||||
class Physical(State):
|
||||
"""
|
||||
Physical states.
|
||||
|
||||
:cvar ToBeRepaired: The device has been selected for reparation.
|
||||
:cvar Repaired: The device has been repaired.
|
||||
:cvar Preparing: The device is going to be or being prepared.
|
||||
:cvar Prepared: The device has been prepared.
|
||||
:cvar ReadyToBeUsed: The device is in working conditions.
|
||||
:cvar InUse: The device is being reported to be in active use.
|
||||
"""
|
||||
ToBeRepaired = e.ToRepair
|
||||
Repaired = e.Repair
|
||||
Preparing = e.ToPrepare
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import difflib
|
||||
from contextlib import suppress
|
||||
from itertools import groupby
|
||||
from typing import Iterable, Set
|
||||
|
||||
import yaml
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.util import OrderedSet
|
||||
|
@ -103,6 +105,8 @@ class Sync:
|
|||
try:
|
||||
if component.hid:
|
||||
db_component = Device.query.filter_by(hid=component.hid).one()
|
||||
assert isinstance(db_component, Device), \
|
||||
'{} must be a component'.format(db_component)
|
||||
else:
|
||||
# Is there a component similar to ours?
|
||||
db_component = component.similar_one(parent, blacklist)
|
||||
|
@ -166,11 +170,16 @@ class Sync:
|
|||
sample_tag = next(iter(linked_tags))
|
||||
for tag in linked_tags:
|
||||
if tag.device_id != sample_tag.device_id:
|
||||
raise MismatchBetweenTags(tag, sample_tag) # Linked to different devices
|
||||
raise MismatchBetweenTags(tag, sample_tag) # Tags linked to different devices
|
||||
if db_device: # Device from hid
|
||||
if sample_tag.device_id != db_device.id: # Device from hid != device from tags
|
||||
raise MismatchBetweenTagsAndHid(db_device.id, db_device.hid)
|
||||
else: # There was no device from hid
|
||||
if sample_tag.device.physical_properties != device.physical_properties:
|
||||
# Incoming physical props of device != props from tag's device
|
||||
# which means that the devices are not the same
|
||||
raise MismatchBetweenProperties(sample_tag.device.physical_properties,
|
||||
device.physical_properties)
|
||||
db_device = sample_tag.device
|
||||
if db_device: # Device from hid or tags
|
||||
self.merge(device, db_device)
|
||||
|
@ -254,3 +263,12 @@ class MismatchBetweenTagsAndHid(ValidationError):
|
|||
message = 'Tags are linked to device {} but hid refers to device {}.'.format(device_id,
|
||||
hid)
|
||||
super().__init__(message, field_names)
|
||||
|
||||
|
||||
class MismatchBetweenProperties(ValidationError):
|
||||
def __init__(self, props1, props2, field_names={'device'}):
|
||||
message = 'The device from the tag and the passed-in differ the following way:'
|
||||
message += '\n'.join(
|
||||
difflib.ndiff(yaml.dump(props1).splitlines(), yaml.dump(props2).splitlines())
|
||||
)
|
||||
super().__init__(message, field_names)
|
||||
|
|
|
@ -2,220 +2,222 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootswatch/3.3.7/flatly/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-+ENW/yibaokMnme+vBLnHMphUYxHs34h9lpdbSLuAwGkOKFRl4C34WkjazBtb7eT"
|
||||
crossorigin="anonymous">
|
||||
<title>Devicehub | {{ device.__format__('t') }}</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootswatch/3.3.7/flatly/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-+ENW/yibaokMnme+vBLnHMphUYxHs34h9lpdbSLuAwGkOKFRl4C34WkjazBtb7eT"
|
||||
crossorigin="anonymous">
|
||||
<title>Devicehub | {{ device.__format__('t') }}</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-default" style="background-color: gainsboro; margin: 0 !important">
|
||||
<div class="container-fluid">
|
||||
<a href="https://www.ereuse.org/" target="_blank">
|
||||
<img alt="Brand"
|
||||
class="center-block"
|
||||
style="height: 4em; padding-bottom: 0.1em"
|
||||
src="{{ url_for('Device.static', filename='ereuse-logo.svg') }}">
|
||||
</a>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<a href="https://www.ereuse.org/" target="_blank">
|
||||
<img alt="Brand"
|
||||
class="center-block"
|
||||
style="height: 4em; padding-bottom: 0.1em"
|
||||
src="{{ url_for('Device.static', filename='ereuse-logo.svg') }}">
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="jumbotron">
|
||||
<img class="center-block"
|
||||
style="height: 13em; padding-bottom: 0.1em"
|
||||
src="{{ url_for('Device.static', filename='magrama.svg') }}">
|
||||
<img class="center-block"
|
||||
style="height: 13em; padding-bottom: 0.1em"
|
||||
src="{{ url_for('Device.static', filename='magrama.svg') }}">
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1>{{ device.__format__('t') }}<br>
|
||||
<small>{{ device.__format__('s') }}</small>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="page-header">
|
||||
<h1>{{ device.__format__('t') }}<br>
|
||||
<small>{{ device.__format__('s') }}</small>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<h2 class='text-center'>
|
||||
This is your {{ device.t }}.
|
||||
</h2>
|
||||
<h2 class='text-center'>
|
||||
This is your {{ device.t }}.
|
||||
</h2>
|
||||
|
||||
<p class="text-center">
|
||||
{% if device.trading %}
|
||||
{{ device.trading }}
|
||||
{% endif %}
|
||||
{% if device.trading and device.physical %}
|
||||
and
|
||||
{% endif %}
|
||||
{% if device.physical %}
|
||||
{{ device.physical }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="row">
|
||||
<article class="col-md-6">
|
||||
<h3>You can verify the originality of your device.</h3>
|
||||
<p>
|
||||
If your device comes with the following tag
|
||||
<img class="img-responsive center-block" style="width: 12em;"
|
||||
src="{{ url_for('Device.static', filename='photochromic-alone.svg') }}">
|
||||
it means it has been refurbished by an eReuse.org
|
||||
certified organization.
|
||||
</p>
|
||||
<p>
|
||||
The tag is special –illuminate it with the torch of
|
||||
your phone for 6 seconds and it will react like in
|
||||
the following image:
|
||||
<img class="img-responsive center-block" style="width: 30em;"
|
||||
src="{{ url_for('Device.static', filename='photochromic-tag-web.svg') }}">
|
||||
This is proof that this device is genuine.
|
||||
</p>
|
||||
</article>
|
||||
<article class="col-md-6">
|
||||
<h3>These are the specifications</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Range</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if device.processor_model %}
|
||||
<tr>
|
||||
<td>
|
||||
CPU – {{ device.processor_model }}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.rate %}
|
||||
{{ device.rate.processor_range }}
|
||||
({{ device.rate.processor }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.ram_size %}
|
||||
<tr>
|
||||
<td>
|
||||
RAM – {{ device.ram_size // 1000 }} GB
|
||||
{{ macros.component_type(device.components, 'RamModule') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.rate %}
|
||||
{{ device.rate.ram_range }}
|
||||
({{ device.rate.ram }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.data_storage_size %}
|
||||
<tr>
|
||||
<td>
|
||||
Data Storage – {{ device.data_storage_size // 1000 }} GB
|
||||
{{ macros.component_type(device.components, 'SolidStateDrive') }}
|
||||
{{ macros.component_type(device.components, 'HardDrive') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.rate %}
|
||||
{{ device.rate.data_storage_range }}
|
||||
({{ device.rate.data_storage }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.graphic_card_model %}
|
||||
<tr>
|
||||
<td>
|
||||
Graphics – {{ device.graphic_card_model }}
|
||||
{{ macros.component_type(device.components, 'GraphicCard') }}
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.network_speeds %}
|
||||
<tr>
|
||||
<td>
|
||||
Network –
|
||||
{% if device.network_speeds[0] %}
|
||||
Ethernet
|
||||
{% if device.network_speeds[0] != None %}
|
||||
max. {{ device.network_speeds[0] }} Mbps
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if device.network_speeds[0] and device.network_speeds[1] %}
|
||||
+
|
||||
{% endif %}
|
||||
{% if device.network_speeds[1] %}
|
||||
WiFi
|
||||
{% if device.network_speeds[1] != None %}
|
||||
max. {{ device.network_speeds[1] }} Mbps
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ macros.component_type(device.components, 'NetworkAdapter') }}
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.rate %}
|
||||
<tr class="active">
|
||||
<td class="text-right">
|
||||
Total rate
|
||||
</td>
|
||||
<td>
|
||||
{{ device.rate.rating_range }}
|
||||
({{ device.rate.rating }})
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.rate and device.rate.price %}
|
||||
<tr class="active">
|
||||
<td class="text-right">
|
||||
Algorithm price
|
||||
</td>
|
||||
<td>
|
||||
{{ device.rate.price }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.price %}
|
||||
<tr class="active">
|
||||
<td class="text-right">
|
||||
Actual price
|
||||
</td>
|
||||
<td>
|
||||
{{ device.price }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-center">
|
||||
{% if device.trading %}
|
||||
{{ device.trading }}
|
||||
{% endif %}
|
||||
{% if device.trading and device.physical %}
|
||||
and
|
||||
{% endif %}
|
||||
{% if device.physical %}
|
||||
{{ device.physical }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="row">
|
||||
<article class="col-md-6">
|
||||
<h3>You can verify the originality of your device.</h3>
|
||||
<p>
|
||||
If your device comes with the following tag
|
||||
<img class="img-responsive center-block" style="width: 12em;"
|
||||
src="{{ url_for('Device.static', filename='photochromic-alone.svg') }}">
|
||||
it means it has been refurbished by an eReuse.org
|
||||
certified organization.
|
||||
</p>
|
||||
<p>
|
||||
The tag is special –illuminate it with the torch of
|
||||
your phone for 6 seconds and it will react like in
|
||||
the following image:
|
||||
<img class="img-responsive center-block" style="width: 30em;"
|
||||
src="{{ url_for('Device.static', filename='photochromic-tag-web.svg') }}">
|
||||
This is proof that this device is genuine.
|
||||
</p>
|
||||
</article>
|
||||
<article class="col-md-6">
|
||||
<h3>These are the specifications</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Range</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if device.processor_model %}
|
||||
<tr>
|
||||
<td>
|
||||
CPU – {{ device.processor_model }}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.rate %}
|
||||
{{ device.rate.processor_range }}
|
||||
({{ device.rate.processor }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.ram_size %}
|
||||
<tr>
|
||||
<td>
|
||||
RAM – {{ device.ram_size // 1000 }} GB
|
||||
{{ macros.component_type(device.components, 'RamModule') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.rate %}
|
||||
{{ device.rate.ram_range }}
|
||||
({{ device.rate.ram }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.data_storage_size %}
|
||||
<tr>
|
||||
<td>
|
||||
Data Storage – {{ device.data_storage_size // 1000 }} GB
|
||||
{{ macros.component_type(device.components, 'SolidStateDrive') }}
|
||||
{{ macros.component_type(device.components, 'HardDrive') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.rate %}
|
||||
{{ device.rate.data_storage_range }}
|
||||
({{ device.rate.data_storage }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.graphic_card_model %}
|
||||
<tr>
|
||||
<td>
|
||||
Graphics – {{ device.graphic_card_model }}
|
||||
{{ macros.component_type(device.components, 'GraphicCard') }}
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.network_speeds %}
|
||||
<tr>
|
||||
<td>
|
||||
Network –
|
||||
{% if device.network_speeds[0] %}
|
||||
Ethernet
|
||||
{% if device.network_speeds[0] != None %}
|
||||
max. {{ device.network_speeds[0] }} Mbps
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if device.network_speeds[0] and device.network_speeds[1] %}
|
||||
+
|
||||
{% endif %}
|
||||
{% if device.network_speeds[1] %}
|
||||
WiFi
|
||||
{% if device.network_speeds[1] != None %}
|
||||
max. {{ device.network_speeds[1] }} Mbps
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ macros.component_type(device.components, 'NetworkAdapter') }}
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.rate %}
|
||||
<tr class="active">
|
||||
<td class="text-right">
|
||||
Total rate
|
||||
</td>
|
||||
<td>
|
||||
{{ device.rate.rating_range }}
|
||||
({{ device.rate.rating }})
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.rate and device.rate.price %}
|
||||
<tr class="active">
|
||||
<td class="text-right">
|
||||
Algorithm price
|
||||
</td>
|
||||
<td>
|
||||
{{ device.rate.price }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.price %}
|
||||
<tr class="active">
|
||||
<td class="text-right">
|
||||
Actual price
|
||||
</td>
|
||||
<td>
|
||||
{{ device.price }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h3>This is the traceability log of your device</h3>
|
||||
<div class="text-right">
|
||||
<small>Latest one.</small>
|
||||
</div>
|
||||
<ol>
|
||||
{% for event in device.events|reverse %}
|
||||
<li>
|
||||
<strong>
|
||||
{{ event.type }}
|
||||
</strong>
|
||||
—
|
||||
{{ event }}
|
||||
<br>
|
||||
<div class="text-muted">
|
||||
<small>
|
||||
{{ event._date_str }}
|
||||
</small>
|
||||
</div>
|
||||
<h3>This is the traceability log of your device</h3>
|
||||
<div class="text-right">
|
||||
<small>Latest one.</small>
|
||||
</div>
|
||||
<ol>
|
||||
{% for event in device.events|reverse %}
|
||||
<li>
|
||||
<strong>
|
||||
{{ event.type }}
|
||||
</strong>
|
||||
—
|
||||
{{ event }}
|
||||
<br>
|
||||
<div class="text-muted">
|
||||
<small>
|
||||
{{ event._date_str }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
<div class="text-right">
|
||||
<small>Oldest one.</small>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% if event.certificate %}
|
||||
<a href="{{ event.certificate.to_text() }}">See the certificate</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
<div class="text-right">
|
||||
<small>Oldest one.</small>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -15,13 +15,13 @@ from teal.resource import View
|
|||
|
||||
from ereuse_devicehub import auth
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.query import SearchQueryParser
|
||||
from ereuse_devicehub.query import SearchQueryParser, things_response
|
||||
from ereuse_devicehub.resources import search
|
||||
from ereuse_devicehub.resources.device.models import Component, Computer, Device, Manufacturer, \
|
||||
Display, Processor, GraphicCard, Motherboard, NetworkAdapter, DataStorage, RamModule, \
|
||||
SoundCard
|
||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||
from ereuse_devicehub.resources.event.models import Rate
|
||||
from ereuse_devicehub.resources.event import models as events
|
||||
from ereuse_devicehub.resources.lot.models import LotDeviceDescendants
|
||||
from ereuse_devicehub.resources.tag.model import Tag
|
||||
|
||||
|
@ -37,9 +37,9 @@ class OfType(f.Str):
|
|||
|
||||
|
||||
class RateQ(query.Query):
|
||||
rating = query.Between(Rate.rating, f.Float())
|
||||
appearance = query.Between(Rate.appearance, f.Float())
|
||||
functionality = query.Between(Rate.functionality, f.Float())
|
||||
rating = query.Between(events.Rate.rating, f.Float())
|
||||
appearance = query.Between(events.Rate.appearance, f.Float())
|
||||
functionality = query.Between(events.Rate.functionality, f.Float())
|
||||
|
||||
|
||||
class TagQ(query.Query):
|
||||
|
@ -52,11 +52,15 @@ class LotQ(query.Query):
|
|||
|
||||
|
||||
class Filters(query.Query):
|
||||
id = query.Or(query.Equal(Device.id, fields.Integer()))
|
||||
type = query.Or(OfType(Device.type))
|
||||
model = query.ILike(Device.model)
|
||||
manufacturer = query.ILike(Device.manufacturer)
|
||||
serialNumber = query.ILike(Device.serial_number)
|
||||
rating = query.Join(Device.id == Rate.device_id, RateQ)
|
||||
# todo test query for rating (and possibly other filters)
|
||||
rating = query.Join((Device.id == events.EventWithOneDevice.device_id)
|
||||
& (events.EventWithOneDevice.id == events.Rate.id),
|
||||
RateQ)
|
||||
tag = query.Join(Device.id == Tag.device_id, TagQ)
|
||||
# todo This part of the query is really slow
|
||||
# And forces usage of distinct, as it returns many rows
|
||||
|
@ -67,6 +71,7 @@ class Filters(query.Query):
|
|||
class Sorting(query.Sort):
|
||||
id = query.SortField(Device.id)
|
||||
created = query.SortField(Device.created)
|
||||
updated = query.SortField(Device.updated)
|
||||
|
||||
|
||||
class DeviceView(View):
|
||||
|
@ -75,7 +80,7 @@ class DeviceView(View):
|
|||
class FindArgs(marshmallow.Schema):
|
||||
search = f.Str()
|
||||
filter = f.Nested(Filters, missing=[])
|
||||
sort = f.Nested(Sorting, missing=[])
|
||||
sort = f.Nested(Sorting, missing=[Device.id.asc()])
|
||||
page = f.Integer(validate=v.Range(min=1), missing=1)
|
||||
|
||||
def get(self, id):
|
||||
|
@ -86,21 +91,13 @@ class DeviceView(View):
|
|||
parameters:
|
||||
- name: id
|
||||
type: integer
|
||||
in: path
|
||||
in: path}
|
||||
description: The identifier of the device.
|
||||
responses:
|
||||
200:
|
||||
description: The device or devices.
|
||||
"""
|
||||
# Majority of code is from teal
|
||||
if id:
|
||||
response = self.one(id)
|
||||
else:
|
||||
args = self.QUERY_PARSER.parse(self.find_args,
|
||||
request,
|
||||
locations=('querystring',))
|
||||
response = self.find(args)
|
||||
return response
|
||||
return super().get(id)
|
||||
|
||||
def one(self, id: int):
|
||||
"""Gets one device."""
|
||||
|
@ -119,10 +116,20 @@ class DeviceView(View):
|
|||
return self.schema.jsonify(device)
|
||||
|
||||
@auth.Auth.requires_auth
|
||||
@cache(datetime.timedelta(minutes=1))
|
||||
def find(self, args: dict):
|
||||
"""Gets many devices."""
|
||||
search_p = args.get('search', None)
|
||||
# Compute query
|
||||
query = self.query(args)
|
||||
devices = query.paginate(page=args['page'], per_page=30) # type: Pagination
|
||||
return things_response(
|
||||
self.schema.dump(devices.items, many=True, nested=1),
|
||||
devices.page, devices.per_page, devices.total, devices.prev_num, devices.next_num
|
||||
)
|
||||
|
||||
def query(self, args):
|
||||
query = Device.query.distinct() # todo we should not force to do this if the query is ok
|
||||
search_p = args.get('search', None)
|
||||
if search_p:
|
||||
properties = DeviceSearch.properties
|
||||
tags = DeviceSearch.tags
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
import enum
|
||||
import uuid
|
||||
from typing import Callable, Iterable, Tuple
|
||||
|
||||
import boltons
|
||||
import flask
|
||||
import flask_weasyprint
|
||||
import teal.marshmallow
|
||||
from boltons import urlutils
|
||||
from teal.resource import Resource
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.device import models as devs
|
||||
from ereuse_devicehub.resources.device.views import DeviceView
|
||||
from ereuse_devicehub.resources.event import models as evs
|
||||
|
||||
|
||||
class Format(enum.Enum):
|
||||
HTML = 'HTML'
|
||||
PDF = 'PDF'
|
||||
|
||||
|
||||
class DocumentView(DeviceView):
|
||||
class FindArgs(DeviceView.FindArgs):
|
||||
format = teal.marshmallow.EnumField(Format, missing=None)
|
||||
|
||||
def get(self, id):
|
||||
"""Get a collection of resources or a specific one.
|
||||
---
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: The identifier of the resource.
|
||||
type: string
|
||||
required: false
|
||||
responses:
|
||||
200:
|
||||
description: Return the collection or the specific one.
|
||||
"""
|
||||
args = self.QUERY_PARSER.parse(self.find_args,
|
||||
flask.request,
|
||||
locations=('querystring',))
|
||||
if id:
|
||||
# todo we assume we can pass both device id and event id
|
||||
# for certificates... how is it going to end up being?
|
||||
try:
|
||||
id = uuid.UUID(id)
|
||||
except ValueError:
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
raise teal.marshmallow.ValidationError('Document must be an ID or UUID.')
|
||||
else:
|
||||
query = devs.Device.query.filter_by(id=id)
|
||||
else:
|
||||
query = evs.Event.query.filter_by(id=id)
|
||||
else:
|
||||
flask.current_app.auth.requires_auth(lambda: None)() # todo not nice
|
||||
query = self.query(args)
|
||||
|
||||
type = urlutils.URL(flask.request.url).path_parts[-2]
|
||||
if type == 'erasures':
|
||||
template = self.erasure(query)
|
||||
if args.get('format') == Format.PDF:
|
||||
res = flask_weasyprint.render_pdf(
|
||||
flask_weasyprint.HTML(string=template), download_filename='{}.pdf'.format(type)
|
||||
)
|
||||
else:
|
||||
res = flask.make_response(template)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def erasure(query: db.Query):
|
||||
def erasures():
|
||||
for model in query:
|
||||
if isinstance(model, devs.Computer):
|
||||
for erasure in model.privacy:
|
||||
yield erasure
|
||||
elif isinstance(model, devs.DataStorage):
|
||||
erasure = model.privacy
|
||||
if erasure:
|
||||
yield erasure
|
||||
else:
|
||||
assert isinstance(model, evs.EraseBasic)
|
||||
yield model
|
||||
|
||||
url_pdf = boltons.urlutils.URL(flask.request.url)
|
||||
url_pdf.query_params['format'] = 'PDF'
|
||||
url_web = boltons.urlutils.URL(flask.request.url)
|
||||
url_web.query_params['format'] = 'HTML'
|
||||
params = {
|
||||
'title': 'Erasure Certificate',
|
||||
'erasures': tuple(erasures()),
|
||||
'url_pdf': url_pdf.to_text(),
|
||||
'url_web': url_web.to_text()
|
||||
}
|
||||
return flask.render_template('documents/erasure.html', **params)
|
||||
|
||||
|
||||
class DocumentDef(Resource):
|
||||
__type__ = 'Document'
|
||||
SCHEMA = None
|
||||
VIEW = None # We do not want to create default / documents endpoint
|
||||
AUTH = False
|
||||
|
||||
def __init__(self, app,
|
||||
import_name=__name__,
|
||||
static_folder='static',
|
||||
static_url_path=None,
|
||||
template_folder='templates',
|
||||
url_prefix=None,
|
||||
subdomain=None,
|
||||
url_defaults=None,
|
||||
root_path=None,
|
||||
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
|
||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||
d = {'id': None}
|
||||
get = {'GET'}
|
||||
|
||||
view = DocumentView.as_view('main', definition=self, auth=app.auth)
|
||||
if self.AUTH:
|
||||
view = app.auth.requires_auth(view)
|
||||
self.add_url_rule('/erasures/', defaults=d, view_func=view, methods=get)
|
||||
self.add_url_rule('/erasures/<{}:{}>'.format(self.ID_CONVERTER.value, self.ID_NAME),
|
||||
view_func=view, methods=get)
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
Devicehub uses Weasyprint to generate the PDF.
|
||||
|
||||
This print.css provides helpful markup to generate the PDF (pages, margins, etc).
|
||||
|
||||
The most important things to remember are:
|
||||
- DOM elements with a class `page-break` create a new page.
|
||||
- DOM elements with a class `no-page-break` do not break between pages.
|
||||
- Pages are in A4 by default an 12px.
|
||||
*/
|
||||
body {
|
||||
background-color: transparent !important;
|
||||
font-size: 12px !important
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
@bottom-right {
|
||||
font-family: "Source Sans Pro", Calibri, Candra, Sans serif;
|
||||
margin-right: 3em;
|
||||
content: counter(page) " / " counter(pages) !important
|
||||
}
|
||||
}
|
||||
|
||||
/* Sections produce a new page*/
|
||||
.page-break:not(section:first-of-type) {
|
||||
page-break-before: always
|
||||
}
|
||||
|
||||
/* Do not break divs with not-break between pages*/
|
||||
.no-page-break {
|
||||
page-break-inside: avoid
|
||||
}
|
||||
|
||||
.print-only, .print-only * {
|
||||
display: none
|
||||
}
|
||||
|
||||
/* Do not print divs with no-print in them */
|
||||
@media print {
|
||||
.no-print, .no-print * {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.print-only, .print-only * {
|
||||
display: initial;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
{% extends "documents/layout.html" %}
|
||||
{% block body %}
|
||||
<div>
|
||||
<h2>Resumé</h2>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>S/N</th>
|
||||
<th>Tags</th>
|
||||
<th>S/N Data Storage</th>
|
||||
<th>Type of erasure</th>
|
||||
<th>Result</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for erasure in erasures %}
|
||||
<tr>
|
||||
{% if erasure.parent.serial_number %}
|
||||
<td>
|
||||
{{ erasure.parent.serial_number.upper() }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{{ erasure.parent.tags.__format__('') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ erasure.device.serial_number.upper() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ erasure.type }}
|
||||
</td>
|
||||
<td>
|
||||
{{ erasure.severity }}
|
||||
</td>
|
||||
<td>
|
||||
{{ erasure.date_str }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="page-break row">
|
||||
<h2>Details</h2>
|
||||
{% for erasure in erasures %}
|
||||
<div class="col-md-6 no-page-break">
|
||||
<h4>{{ erasure.device.__format__('t') }}</h4>
|
||||
<dl>
|
||||
<dt>Data storage:</dt>
|
||||
<dd>{{ erasure.device.__format__('ts') }}</dd>
|
||||
<dt>Computer:</dt>
|
||||
<dd>{{ erasure.parent.__format__('ts') }}</dd>
|
||||
<dt>Tags:</dt>
|
||||
<dd>{{ erasure.parent.tags }}</dd>
|
||||
<dt>Erasure:</dt>
|
||||
<dd>{{ erasure.__format__('ts') }}</dd>
|
||||
{% if erasure.steps %}
|
||||
<dt>Erasure steps:</dt>
|
||||
<dd>
|
||||
<ol>
|
||||
{% for step in erasure.steps %}
|
||||
<li>{{ step.__format__('') }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="no-page-break">
|
||||
<h2>Glossary</h2>
|
||||
<dl>
|
||||
<dt>Erase Basic</dt>
|
||||
<dd>
|
||||
A software-based fast non-100%-secured way of erasing data storage,
|
||||
using <a href="https://en.wikipedia.org/wiki/Shred_(Unix)">shred</a>.
|
||||
</dd>
|
||||
<dt>Erase Sectors</dt>
|
||||
<dd>
|
||||
A secured-way of erasing data storages, checking sector-by-sector
|
||||
the erasure, using <a href="https://en.wikipedia.org/wiki/Badblocks">badblocks</a>.
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="no-print">
|
||||
<a href="{{ url_pdf }}">Click here to download the PDF.</a>
|
||||
</div>
|
||||
<div class="print-only">
|
||||
<a href="{{ url_web }}">Verify on-line the integrity of this document</a>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,26 @@
|
|||
{% import 'devices/macros.html' as macros %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootswatch/3.3.7/flatly/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-+ENW/yibaokMnme+vBLnHMphUYxHs34h9lpdbSLuAwGkOKFRl4C34WkjazBtb7eT"
|
||||
crossorigin="anonymous">
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ url_for('Document.static', filename='print.css') }}">
|
||||
<title>Devicehub | {{ title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<header class="page-header">
|
||||
<h1> {{ title }}</h1>
|
||||
</header>
|
||||
</div>
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +1,7 @@
|
|||
from contextlib import suppress
|
||||
from distutils.version import StrictVersion
|
||||
from enum import Enum, IntEnum, unique
|
||||
from typing import Union
|
||||
from typing import Set, Union
|
||||
|
||||
import inflection
|
||||
|
||||
|
@ -43,7 +44,13 @@ class RatingRange(IntEnum):
|
|||
"""
|
||||
The human translation to score range.
|
||||
|
||||
You can compare them: ScoreRange.VERY_LOW < ScoreRange.LOW
|
||||
You can compare them: ScoreRange.VERY_LOW < ScoreRange.LOW.
|
||||
There are four levels:
|
||||
|
||||
1. Very low.
|
||||
2. Low.
|
||||
3. Medium.
|
||||
4. High.
|
||||
"""
|
||||
VERY_LOW = 2
|
||||
LOW = 3
|
||||
|
@ -271,17 +278,15 @@ class PrinterTechnology(Enum):
|
|||
|
||||
class Severity(IntEnum):
|
||||
"""A flag evaluating the event execution. Ex. failed events
|
||||
have the value `Severity.Error`.
|
||||
have the value `Severity.Error`. Devicehub uses 4 severity levels:
|
||||
|
||||
Devicehub uses 4 severity levels:
|
||||
|
||||
- Info: default neutral severity. The event succeeded.
|
||||
- Notice: The event succeeded but it is raising awareness.
|
||||
* Info: default neutral severity. The event succeeded.
|
||||
* Notice: The event succeeded but it is raising awareness.
|
||||
Notices are not usually that important but something
|
||||
(good or bad) worth checking.
|
||||
- Warning: The event succeeded but there is something important
|
||||
* Warning: The event succeeded but there is something important
|
||||
to check negatively affecting the event.
|
||||
- Error: the event failed.
|
||||
* Error: the event failed.
|
||||
|
||||
Devicehub specially raises user awareness when an event
|
||||
has a Severity of ``Warning`` or greater.
|
||||
|
@ -302,3 +307,59 @@ class Severity(IntEnum):
|
|||
else:
|
||||
m = '❌'
|
||||
return m
|
||||
|
||||
def __format__(self, format_spec):
|
||||
return str(self)
|
||||
|
||||
|
||||
class PhysicalErasureMethod(Enum):
|
||||
"""Methods of physically erasing the data-storage, usually
|
||||
destroying the whole component.
|
||||
|
||||
Certified data-storage destruction mean, as of `UNE-EN 15713
|
||||
<https://www.une.org/encuentra-tu-norma/busca-tu-norma/norma?c=N0044792>`_,
|
||||
reducing the material to a size making it undecipherable, illegible,
|
||||
and non able to be re-built.
|
||||
"""
|
||||
|
||||
Shred = 'Reduction of the data-storage to the required certified ' \
|
||||
'standard sizes.'
|
||||
Disintegration = 'Reduction of the data-storage to smaller sizes ' \
|
||||
'than the certified standard ones.'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ErasureStandards(Enum):
|
||||
"""Software erasure standards."""
|
||||
|
||||
HMG_IS5 = 'British HMG Infosec Standard 5 (HMG IS5)'
|
||||
"""`British HMG Infosec Standard 5 (HMG IS5)
|
||||
<https://en.wikipedia.org/wiki/Infosec_Standard_5>`_.
|
||||
|
||||
In order to follow this standard, an erasure must have the
|
||||
following steps:
|
||||
|
||||
1. A first step writing zeroes to the data-storage units.
|
||||
2. A second step erasing with random data, verifying the erasure
|
||||
success in each hard-drive sector.
|
||||
|
||||
And be an :class:`ereuse_devicehub.resources.event.models.EraseSectors`.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def from_data_storage(cls, erasure) -> Set['ErasureStandards']:
|
||||
"""Returns a set of erasure standards."""
|
||||
from ereuse_devicehub.resources.event import models as events
|
||||
standards = set()
|
||||
if isinstance(erasure, events.EraseSectors):
|
||||
with suppress(ValueError):
|
||||
first_step, *other_steps = erasure.steps
|
||||
if isinstance(first_step, events.StepZero) \
|
||||
and all(isinstance(step, events.StepRandom) for step in other_steps):
|
||||
standards.add(cls.HMG_IS5)
|
||||
return standards
|
||||
|
|
|
@ -4,7 +4,7 @@ from teal.resource import Converters, Resource
|
|||
|
||||
from ereuse_devicehub.resources.device.sync import Sync
|
||||
from ereuse_devicehub.resources.event import schemas
|
||||
from ereuse_devicehub.resources.event.views import EventView, SnapshotView
|
||||
from ereuse_devicehub.resources.event.views import EventView
|
||||
|
||||
|
||||
class EventDef(Resource):
|
||||
|
@ -34,6 +34,11 @@ class EraseSectorsDef(EraseBasicDef):
|
|||
SCHEMA = schemas.EraseSectors
|
||||
|
||||
|
||||
class ErasePhysicalDef(EraseBasicDef):
|
||||
VIEW = None
|
||||
SCHEMA = schemas.ErasePhysical
|
||||
|
||||
|
||||
class StepDef(Resource):
|
||||
VIEW = None
|
||||
SCHEMA = schemas.Step
|
||||
|
@ -85,13 +90,14 @@ class InstallDef(EventDef):
|
|||
|
||||
|
||||
class SnapshotDef(EventDef):
|
||||
VIEW = SnapshotView
|
||||
VIEW = None
|
||||
SCHEMA = schemas.Snapshot
|
||||
|
||||
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
|
||||
static_url_path=None,
|
||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
|
||||
url_prefix = '/{}'.format(EventDef.resource)
|
||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||
self.sync = Sync()
|
||||
|
|
|
@ -2,7 +2,7 @@ from collections import Iterable
|
|||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_UP
|
||||
from distutils.version import StrictVersion
|
||||
from typing import Set, Union
|
||||
from typing import Optional, Set, Union
|
||||
from uuid import uuid4
|
||||
|
||||
import inflection
|
||||
|
@ -10,7 +10,7 @@ import teal.db
|
|||
from boltons import urlutils
|
||||
from citext import CIText
|
||||
from flask import current_app as app, g
|
||||
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \
|
||||
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, Enum as DBEnum, \
|
||||
Float, ForeignKey, Integer, Interval, JSON, Numeric, SmallInteger, Unicode, event, orm
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
@ -28,9 +28,10 @@ from ereuse_devicehub.db import db
|
|||
from ereuse_devicehub.resources.agent.models import Agent
|
||||
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
|
||||
Device, Laptop, Server
|
||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||
PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \
|
||||
Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, ErasureStandards, \
|
||||
FunctionalityRange, PhysicalErasureMethod, PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, \
|
||||
RatingRange, RatingSoftware, ReceiverRole, Severity, SnapshotExpectedEvents, SnapshotSoftware, \
|
||||
TestDataStorageLength
|
||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
|
||||
|
@ -43,8 +44,12 @@ class JoinedTableMixin:
|
|||
|
||||
|
||||
class Event(Thing):
|
||||
"""Event performed on a device.
|
||||
|
||||
This class extends `Schema's Action <https://schema.org/Action>`_.
|
||||
"""
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||
type = Column(Unicode, nullable=False, index=True)
|
||||
type = Column(Unicode, nullable=False)
|
||||
name = Column(CIText(), default='', nullable=False)
|
||||
name.comment = """
|
||||
A name or title for the event. Used when searching for events.
|
||||
|
@ -141,7 +146,7 @@ class Event(Thing):
|
|||
For Add and Remove though, this has another meaning: the components
|
||||
that are added or removed.
|
||||
"""
|
||||
parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True)
|
||||
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
||||
parent = relationship(Computer,
|
||||
backref=backref('events_parent',
|
||||
lazy=True,
|
||||
|
@ -156,11 +161,27 @@ class Event(Thing):
|
|||
would point to the computer that contained this data storage, if any.
|
||||
"""
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('ix_id', id, postgresql_using='hash'),
|
||||
db.Index('ix_type', type, postgresql_using='hash'),
|
||||
db.Index('ix_parent_id', parent_id, postgresql_using='hash')
|
||||
)
|
||||
|
||||
@property
|
||||
def elapsed(self):
|
||||
"""Returns the elapsed time with seconds precision."""
|
||||
t = self.end_time - self.start_time
|
||||
return timedelta(seconds=t.seconds)
|
||||
|
||||
@property
|
||||
def url(self) -> urlutils.URL:
|
||||
"""The URL where to GET this event."""
|
||||
return urlutils.URL(url_for_resource(Event, item_id=self.id))
|
||||
|
||||
@property
|
||||
def certificate(self) -> Optional[urlutils.URL]:
|
||||
return None
|
||||
|
||||
# noinspection PyMethodParameters
|
||||
@declared_attr
|
||||
def __mapper_args__(cls):
|
||||
|
@ -192,7 +213,7 @@ class Event(Thing):
|
|||
return start_time
|
||||
|
||||
@property
|
||||
def _date_str(self):
|
||||
def date_str(self):
|
||||
return '{:%c}'.format(self.end_time or self.created)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
@ -215,7 +236,7 @@ class JoinedWithOneDeviceMixin:
|
|||
|
||||
|
||||
class EventWithOneDevice(JoinedTableMixin, Event):
|
||||
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False, index=True)
|
||||
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
||||
device = relationship(Device,
|
||||
backref=backref('events_one',
|
||||
lazy=True,
|
||||
|
@ -224,6 +245,10 @@ class EventWithOneDevice(JoinedTableMixin, Event):
|
|||
collection_class=OrderedSet),
|
||||
primaryjoin=Device.id == device_id)
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('event_one_device_id_index', device_id, postgresql_using='hash'),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<{0.t} {0.id} {0.severity} device={0.device!r}>'.format(self)
|
||||
|
||||
|
@ -306,38 +331,56 @@ class EraseBasic(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
|||
that has overwritten data with random bits, and ``StepZero``,
|
||||
for an erasure step that has overwritten data with zeros.
|
||||
|
||||
For example, if steps are set in the following order and the user
|
||||
used `EraseSectors`, the event represents a
|
||||
`British HMG Infosec Standard 5 (HMG IS5) <https://en.wikipedia.org/
|
||||
wiki/Infosec_Standard_5>`_:
|
||||
|
||||
1. A first step writing zeroes to the hard-drives.
|
||||
2. A second step erasing with random data, verifying the erasure
|
||||
success in each hard-drive sector.
|
||||
"""
|
||||
zeros = Column(Boolean, nullable=False)
|
||||
zeros.comment = """
|
||||
Whether this erasure had a first erasure step consisting of
|
||||
only writing zeros.
|
||||
Erasure standards define steps and methodologies to use.
|
||||
Devicehub automatically shows the standards that each erasure
|
||||
follows.
|
||||
"""
|
||||
method = 'Shred'
|
||||
"""The method or software used to destroy the data."""
|
||||
|
||||
# todo return erasure properties like num steps, if it is british...
|
||||
@property
|
||||
def standards(self):
|
||||
"""A set of standards that this erasure follows."""
|
||||
return ErasureStandards.from_data_storage(self)
|
||||
|
||||
@property
|
||||
def certificate(self):
|
||||
"""The URL of this erasure certificate."""
|
||||
# todo will this url_for_resoure work for other resources?
|
||||
return urlutils.URL(url_for_resource('Document', item_id=self.id))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '{} on {}.'.format(self.severity, self.end_time)
|
||||
return '{} on {}.'.format(self.severity, self.date_str)
|
||||
|
||||
def __format__(self, format_spec: str) -> str:
|
||||
v = ''
|
||||
if 't' in format_spec:
|
||||
v += '{} {}'.format(self.type, self.severity)
|
||||
if 't' in format_spec and 's' in format_spec:
|
||||
v += '. '
|
||||
if 's' in format_spec:
|
||||
if self.standards:
|
||||
std = 'with standards {}'.format(self.standards)
|
||||
else:
|
||||
std = 'no standard'
|
||||
v += 'Method used: {}, {}. '.format(self.method, std)
|
||||
if self.end_time and self.start_time:
|
||||
v += '{} elapsed. '.format(self.elapsed)
|
||||
|
||||
v += 'On {}'.format(self.date_str)
|
||||
return v
|
||||
|
||||
|
||||
class EraseSectors(EraseBasic):
|
||||
"""A secured-way of erasing data storages, checking sector-by-sector
|
||||
the erasure, using `badblocks <https://en.wikipedia.org/wiki/Badblocks>`_.
|
||||
"""
|
||||
# todo make a property that says if the data wiping process is british...
|
||||
method = 'Badblocks'
|
||||
|
||||
|
||||
class ErasePhysical(EraseBasic):
|
||||
"""The act of physically destroying a data storage unit."""
|
||||
# todo add attributes
|
||||
pass
|
||||
method = Column(DBEnum(PhysicalErasureMethod))
|
||||
|
||||
|
||||
class Step(db.Model):
|
||||
|
@ -345,9 +388,10 @@ class Step(db.Model):
|
|||
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
||||
num = Column(SmallInteger, primary_key=True)
|
||||
severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False)
|
||||
start_time = Column(DateTime, nullable=False)
|
||||
start_time = Column(db.TIMESTAMP(timezone=True), nullable=False)
|
||||
start_time.comment = Event.start_time.comment
|
||||
end_time = Column(DateTime, CheckConstraint('end_time > start_time'), nullable=False)
|
||||
end_time = Column(db.TIMESTAMP(timezone=True), CheckConstraint('end_time > start_time'),
|
||||
nullable=False)
|
||||
end_time.comment = Event.end_time.comment
|
||||
|
||||
erasure = relationship(EraseBasic,
|
||||
|
@ -356,6 +400,12 @@ class Step(db.Model):
|
|||
order_by=num,
|
||||
collection_class=ordering_list('num')))
|
||||
|
||||
@property
|
||||
def elapsed(self):
|
||||
"""Returns the elapsed time with seconds precision."""
|
||||
t = self.end_time - self.start_time
|
||||
return timedelta(seconds=t.seconds)
|
||||
|
||||
# noinspection PyMethodParameters
|
||||
@declared_attr
|
||||
def __mapper_args__(cls):
|
||||
|
@ -371,6 +421,9 @@ class Step(db.Model):
|
|||
args[POLYMORPHIC_ON] = cls.type
|
||||
return args
|
||||
|
||||
def __format__(self, format_spec: str) -> str:
|
||||
return '{} – {} {}'.format(self.severity, self.type, self.elapsed)
|
||||
|
||||
|
||||
class StepZero(Step):
|
||||
pass
|
||||
|
@ -486,6 +539,7 @@ class Install(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
|||
storage unit.
|
||||
"""
|
||||
elapsed = Column(Interval, nullable=False)
|
||||
address = Column(SmallInteger, check_range('address', 8, 256))
|
||||
|
||||
|
||||
class SnapshotRequest(db.Model):
|
||||
|
@ -499,19 +553,8 @@ class SnapshotRequest(db.Model):
|
|||
|
||||
|
||||
class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||
"""Devicehub generates an rating for a device taking into consideration the
|
||||
visual, functional, and performance.
|
||||
|
||||
A Workflow is as follows:
|
||||
|
||||
1. An agent generates feedback from the device in the form of benchmark,
|
||||
visual, and functional information; which is filled in a ``Rate``
|
||||
event. This is done through a **software**, defining the type
|
||||
of ``Rate`` event. At the moment we have ``WorkbenchRate``.
|
||||
2. Devicehub gathers this information and computes a score that updates
|
||||
the ``Rate`` event.
|
||||
3. Devicehub aggregates different rates and computes a final score for
|
||||
the device by performing a new ``AggregateRating`` event.
|
||||
"""The act of grading the appearance, performance, and functionality
|
||||
of a device.
|
||||
|
||||
There are two base **types** of ``Rate``: ``WorkbenchRate``,
|
||||
``ManualRate``. ``WorkbenchRate`` can have different
|
||||
|
@ -523,16 +566,24 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
|||
if an agent fulfills a ``WorkbenchRate`` and there are 2 software
|
||||
algorithms and each has two versions, Devicehub will generate 4 rates.
|
||||
Devicehub understands that only one software and version are the
|
||||
**oficial** (set in the settings of each inventory),
|
||||
**official** (set in the settings of each inventory),
|
||||
and it will generate an ``AggregateRating`` for only the official
|
||||
versions. At the same time, ``Price`` only computes the price of
|
||||
the **oficial** version.
|
||||
the **official** version.
|
||||
|
||||
There are two ways of rating a device:
|
||||
|
||||
1. When processing the device with Workbench and the Android App.
|
||||
2. Anytime after with the Android App or website.
|
||||
|
||||
Refer to *processes* in the documentation to get more info with
|
||||
the process.
|
||||
|
||||
The technical Workflow in Devicehub is as follows:
|
||||
|
||||
1. In **T1**, the user performs a ``Snapshot`` by processing the device
|
||||
1. In **T1**, the agent performs a ``Snapshot`` by processing the device
|
||||
through the Workbench. From the benchmarks and the visual and
|
||||
functional ratings the user does in the device, the system generates
|
||||
functional ratings the agent does in the device, the system generates
|
||||
many ``WorkbenchRate`` (as many as software and versions defined).
|
||||
With only this information, the system generates an ``AggregateRating``,
|
||||
which is the event that the user will see in the web.
|
||||
|
@ -542,7 +593,12 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
|||
plus the ``WorkbenchRate`` from 1.
|
||||
"""
|
||||
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
|
||||
rating.comment = """The rating for the content."""
|
||||
rating.comment = """The rating for the content.
|
||||
|
||||
This value is automatically set by rating algorithms. In case that
|
||||
no algorithm is defined per the device and type of rate, this
|
||||
value is None.
|
||||
"""
|
||||
software = Column(DBEnum(RatingSoftware))
|
||||
software.comment = """The algorithm used to produce this rating."""
|
||||
version = Column(StrictVersionType)
|
||||
|
@ -553,8 +609,7 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
|||
|
||||
@property
|
||||
def rating_range(self) -> RatingRange:
|
||||
if self.rating:
|
||||
return RatingRange.from_score(self.rating)
|
||||
return RatingRange.from_score(self.rating) if self.rating else None
|
||||
|
||||
@declared_attr
|
||||
def __mapper_args__(cls):
|
||||
|
@ -595,6 +650,9 @@ class ManualRate(IndividualRate):
|
|||
self.functionality_range
|
||||
)
|
||||
|
||||
def ratings(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class WorkbenchRate(ManualRate):
|
||||
id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id), primary_key=True)
|
||||
|
@ -615,7 +673,8 @@ class WorkbenchRate(ManualRate):
|
|||
"""
|
||||
Computes all the possible rates taking this rating as a model.
|
||||
|
||||
Returns a set of ratings, including this one, which is mutated.
|
||||
Returns a set of ratings, including this one, which is mutated,
|
||||
and the final :class:`.AggregateRate`.
|
||||
"""
|
||||
from ereuse_devicehub.resources.event.rate.main import main
|
||||
return main(self, **app.config.get_namespace('WORKBENCH_RATE_'))
|
||||
|
@ -727,20 +786,20 @@ class AggregateRate(Rate):
|
|||
|
||||
@classmethod
|
||||
def from_workbench_rate(cls, rate: WorkbenchRate):
|
||||
aggregate = cls()
|
||||
aggregate.rating = rate.rating
|
||||
aggregate.software = rate.software
|
||||
aggregate.appearance = rate.appearance
|
||||
aggregate.functionality = rate.functionality
|
||||
aggregate.device = rate.device
|
||||
aggregate.workbench = rate
|
||||
aggregate = cls(rating=rate.rating,
|
||||
software=rate.software,
|
||||
appearance=rate.appearance,
|
||||
functionality=rate.functionality,
|
||||
device=rate.device,
|
||||
workbench=rate)
|
||||
return aggregate
|
||||
|
||||
|
||||
class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||
"""Price states a selling price for the device, but not
|
||||
necessarily the final price this is sold (which is set in the Sell
|
||||
event).
|
||||
"""The act of setting a trading price for the device.
|
||||
|
||||
This does not imply that the device is ultimately traded for that
|
||||
price. Use the :class:`.Sell` for that.
|
||||
|
||||
Devicehub automatically computes a price from ``AggregateRating``
|
||||
events. As in a **Rate**, price can have **software** and **version**,
|
||||
|
@ -780,7 +839,7 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
|||
@classmethod
|
||||
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
|
||||
"""Returns a Decimal value with the correct scale for Price.price."""
|
||||
if isinstance(value, float):
|
||||
if isinstance(value, (float, int)):
|
||||
value = Decimal(value)
|
||||
# equation from marshmallow.fields.Decimal
|
||||
return value.quantize(Decimal((0, (1,), -cls.SCALE)), rounding=rounding)
|
||||
|
@ -804,7 +863,13 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
|||
|
||||
|
||||
class EreusePrice(Price):
|
||||
"""A Price class that auto-computes its amount by"""
|
||||
"""The act of setting a price by guessing it using the eReuse.org
|
||||
algorithm.
|
||||
|
||||
This algorithm states that the price is the use value of the device
|
||||
(represented by its last :class:`.Rate`) multiplied by a constants
|
||||
value agreed by a circuit or platform.
|
||||
"""
|
||||
MULTIPLIER = {
|
||||
Desktop: 20,
|
||||
Laptop: 30
|
||||
|
@ -858,8 +923,8 @@ class EreusePrice(Price):
|
|||
self.warranty2 = EreusePrice.Type(rate[self.WARRANTY2][role], price)
|
||||
|
||||
def __init__(self, rating: AggregateRate, **kwargs) -> None:
|
||||
if rating.rating_range == RatingRange.VERY_LOW:
|
||||
raise ValueError('Cannot compute price for Range.VERY_LOW')
|
||||
if not rating.rating_range or rating.rating_range == RatingRange.VERY_LOW:
|
||||
raise InvalidRangeForPrice()
|
||||
# We pass ROUND_UP strategy so price is always greater than what refurbisher... amounts
|
||||
price = self.to_price(rating.rating * self.MULTIPLIER[rating.device.__class__], ROUND_UP)
|
||||
super().__init__(rating=rating,
|
||||
|
@ -931,7 +996,7 @@ class TestDataStorage(Test):
|
|||
assessment = Column(Boolean)
|
||||
reallocated_sector_count = Column(SmallInteger)
|
||||
power_cycle_count = Column(SmallInteger)
|
||||
reported_uncorrectable_errors = Column(SmallInteger)
|
||||
_reported_uncorrectable_errors = Column('reported_uncorrectable_errors', Integer)
|
||||
command_timeout = Column(Integer)
|
||||
current_pending_sector_count = Column(SmallInteger)
|
||||
offline_uncorrectable = Column(SmallInteger)
|
||||
|
@ -959,6 +1024,16 @@ class TestDataStorage(Test):
|
|||
t += self.description
|
||||
return t
|
||||
|
||||
@property
|
||||
def reported_uncorrectable_errors(self):
|
||||
return self._reported_uncorrectable_errors
|
||||
|
||||
@reported_uncorrectable_errors.setter
|
||||
def reported_uncorrectable_errors(self, value):
|
||||
# There is no value for a stratospherically big number
|
||||
self._reported_uncorrectable_errors = min(value, db.PSQL_INT_MAX)
|
||||
|
||||
|
||||
|
||||
class StressTest(Test):
|
||||
"""The act of stressing (putting to the maximum capacity)
|
||||
|
@ -1111,7 +1186,7 @@ class Organize(JoinedTableMixin, EventWithMultipleDevices):
|
|||
|
||||
|
||||
class Reserve(Organize):
|
||||
"""The act of reserving devices and cancelling them.
|
||||
"""The act of reserving devices.
|
||||
|
||||
After this event is performed, the user is the **reservee** of the
|
||||
devices. There can only be one non-cancelled reservation for
|
||||
|
@ -1132,8 +1207,11 @@ class Trade(JoinedTableMixin, EventWithMultipleDevices):
|
|||
|
||||
Performing trade events changes the *Trading* state of the
|
||||
device —:class:`ereuse_devicehub.resources.device.states.Trading`.
|
||||
|
||||
This class and its inheritors
|
||||
extend `Schema's Trade <http://schema.org/TradeAction>`_.
|
||||
"""
|
||||
shipping_date = Column(DateTime)
|
||||
shipping_date = Column(db.TIMESTAMP(timezone=True))
|
||||
shipping_date.comment = """
|
||||
When are the devices going to be ready for shipping?
|
||||
"""
|
||||
|
@ -1224,6 +1302,11 @@ class Receive(JoinedTableMixin, EventWithMultipleDevices):
|
|||
they are the
|
||||
:attr:`ereuse_devicehub.resources.device.models.Device.physical_possessor`.
|
||||
|
||||
This differs from :class:`.Trade` in that trading changes the
|
||||
political possession. As an example, a transporter can *receive*
|
||||
a device but it is not it's owner. After the delivery, the
|
||||
transporter performs another *receive* to the final owner.
|
||||
|
||||
The receiver can optionally take a
|
||||
:class:`ereuse_devicehub.resources.enums.ReceiverRole`.
|
||||
"""
|
||||
|
@ -1331,3 +1414,7 @@ def update_parent(target: Union[EraseBasic, Test, Install], device: Device, _, _
|
|||
target.parent = None
|
||||
if isinstance(device, Component):
|
||||
target.parent = device.parent
|
||||
|
||||
|
||||
class InvalidRangeForPrice(ValueError):
|
||||
pass
|
||||
|
|
|
@ -2,7 +2,7 @@ import ipaddress
|
|||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from distutils.version import StrictVersion
|
||||
from typing import Dict, List, Set, Union
|
||||
from typing import Dict, List, Optional, Set, Union
|
||||
from uuid import UUID
|
||||
|
||||
from boltons import urlutils
|
||||
|
@ -16,9 +16,9 @@ from teal.enums import Country
|
|||
|
||||
from ereuse_devicehub.resources.agent.models import Agent
|
||||
from ereuse_devicehub.resources.device.models import Component, Computer, Device
|
||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||
PriceSoftware, RatingSoftware, ReceiverRole, Severity, SnapshotExpectedEvents, \
|
||||
SnapshotSoftware, TestDataStorageLength
|
||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, ErasureStandards, \
|
||||
FunctionalityRange, PhysicalErasureMethod, PriceSoftware, RatingSoftware, ReceiverRole, \
|
||||
Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
|
||||
|
@ -61,6 +61,18 @@ class Event(Thing):
|
|||
def url(self) -> urlutils.URL:
|
||||
pass
|
||||
|
||||
@property
|
||||
def elapsed(self) -> timedelta:
|
||||
pass
|
||||
|
||||
@property
|
||||
def certificate(self) -> Optional[urlutils.URL]:
|
||||
return None
|
||||
|
||||
@property
|
||||
def date_str(self):
|
||||
return '{:%c}'.format(self.end_time or self.created)
|
||||
|
||||
|
||||
class EventWithOneDevice(Event):
|
||||
|
||||
|
@ -124,11 +136,15 @@ class Snapshot(EventWithOneDevice):
|
|||
|
||||
|
||||
class Install(EventWithOneDevice):
|
||||
name = ... # type: Column
|
||||
elapsed = ... # type: Column
|
||||
address = ... # type: Column
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.name = ... # type: str
|
||||
self.elapsed = ... # type: timedelta
|
||||
self.success = ... # type: bool
|
||||
self.address = ... # type: Optional[int]
|
||||
|
||||
|
||||
class SnapshotRequest(Model):
|
||||
|
@ -229,6 +245,9 @@ class ManualRate(IndividualRate):
|
|||
self.functionality_range = ... # type: FunctionalityRange
|
||||
self.aggregate_rate_manual = ... #type: AggregateRate
|
||||
|
||||
def ratings(self) -> Set[Rate]:
|
||||
pass
|
||||
|
||||
|
||||
class WorkbenchRate(ManualRate):
|
||||
processor = ... # type: Column
|
||||
|
@ -249,9 +268,6 @@ class WorkbenchRate(ManualRate):
|
|||
self.bios = ... # type: float
|
||||
self.aggregate_rate_workbench = ... #type: AggregateRate
|
||||
|
||||
def ratings(self) -> Set[Rate]:
|
||||
pass
|
||||
|
||||
@property
|
||||
def data_storage_range(self):
|
||||
pass
|
||||
|
@ -354,12 +370,28 @@ class EraseBasic(EventWithOneDevice):
|
|||
self.zeros = ... # type: bool
|
||||
self.success = ... # type: bool
|
||||
|
||||
@property
|
||||
def standards(self) -> Set[ErasureStandards]:
|
||||
pass
|
||||
|
||||
@property
|
||||
def certificate(self) -> urlutils.URL:
|
||||
pass
|
||||
|
||||
|
||||
class EraseSectors(EraseBasic):
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class ErasePhysical(EraseBasic):
|
||||
method = ... # type: Column
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.method = ... # type: PhysicalErasureMethod
|
||||
|
||||
|
||||
class Benchmark(EventWithOneDevice):
|
||||
pass
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ from typing import Set, Union
|
|||
|
||||
from ereuse_devicehub.resources.device.models import Device
|
||||
from ereuse_devicehub.resources.enums import RatingSoftware
|
||||
from ereuse_devicehub.resources.event.models import AggregateRate, EreusePrice, Rate, \
|
||||
WorkbenchRate
|
||||
from ereuse_devicehub.resources.event.models import AggregateRate, EreusePrice, \
|
||||
InvalidRangeForPrice, Rate, WorkbenchRate
|
||||
from ereuse_devicehub.resources.event.rate.workbench import v1_0
|
||||
|
||||
RATE_TYPES = {
|
||||
|
@ -72,7 +72,6 @@ def main(rating_model: WorkbenchRate,
|
|||
if soft == software and vers == version:
|
||||
aggregation = AggregateRate.from_workbench_rate(rating)
|
||||
events.add(aggregation)
|
||||
with suppress(ValueError):
|
||||
# We will have exception if range == VERY_LOW
|
||||
with suppress(InvalidRangeForPrice): # We will have exception if range == VERY_LOW
|
||||
events.add(EreusePrice(aggregation))
|
||||
return events
|
||||
|
|
|
@ -105,6 +105,7 @@ class ProcessorRate(BaseRate):
|
|||
speed = processor.speed or self.DEFAULT_SPEED
|
||||
# todo fix StopIteration if don't exists BenchmarkProcessor
|
||||
benchmark_cpu = next(e for e in processor.events if isinstance(e, BenchmarkProcessor))
|
||||
# todo fix if benchmark_cpu.rate == 0
|
||||
benchmark_cpu = benchmark_cpu.rate or self.DEFAULT_SCORE
|
||||
|
||||
# STEP: Fusion components
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
from flask import current_app as app
|
||||
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
|
||||
from marshmallow import Schema as MarshmallowSchema, ValidationError, fields as f, validates_schema
|
||||
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \
|
||||
TimeDelta, UUID
|
||||
from marshmallow.validate import Length, Range
|
||||
from marshmallow.validate import Length, OneOf, Range
|
||||
from sqlalchemy.util import OrderedSet
|
||||
from teal.enums import Country, Currency, Subdivision
|
||||
from teal.marshmallow import EnumField, IP, SanitizedStr, URL, Version
|
||||
from teal.resource import Schema
|
||||
|
||||
from ereuse_devicehub.marshmallow import NestedOn
|
||||
from ereuse_devicehub.resources.agent.schemas import Agent
|
||||
from ereuse_devicehub.resources.device.schemas import Component, Computer, Device
|
||||
from ereuse_devicehub.resources import enums
|
||||
from ereuse_devicehub.resources.agent import schemas as s_agent
|
||||
from ereuse_devicehub.resources.device import schemas as s_device
|
||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||
PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, Severity, \
|
||||
SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
||||
PhysicalErasureMethod, PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \
|
||||
Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
||||
from ereuse_devicehub.resources.event import models as m
|
||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
from ereuse_devicehub.resources.user.schemas import User
|
||||
from ereuse_devicehub.resources.user import schemas as s_user
|
||||
|
||||
|
||||
class Event(Thing):
|
||||
__doc__ = m.Event.__doc__
|
||||
id = UUID(dump_only=True)
|
||||
name = SanitizedStr(default='',
|
||||
validate=Length(max=STR_BIG_SIZE),
|
||||
|
@ -31,31 +33,34 @@ class Event(Thing):
|
|||
start_time = DateTime(data_key='startTime', description=m.Event.start_time.comment)
|
||||
end_time = DateTime(data_key='endTime', description=m.Event.end_time.comment)
|
||||
snapshot = NestedOn('Snapshot', dump_only=True)
|
||||
agent = NestedOn(Agent, description=m.Event.agent_id.comment)
|
||||
author = NestedOn(User, dump_only=True, exclude=('token',))
|
||||
components = NestedOn(Component, dump_only=True, many=True)
|
||||
parent = NestedOn(Computer, dump_only=True, description=m.Event.parent_id.comment)
|
||||
agent = NestedOn(s_agent.Agent, description=m.Event.agent_id.comment)
|
||||
author = NestedOn(s_user.User, dump_only=True, exclude=('token',))
|
||||
components = NestedOn(s_device.Component, dump_only=True, many=True)
|
||||
parent = NestedOn(s_device.Computer, dump_only=True, description=m.Event.parent_id.comment)
|
||||
url = URL(dump_only=True, description=m.Event.url.__doc__)
|
||||
|
||||
|
||||
class EventWithOneDevice(Event):
|
||||
device = NestedOn(Device, only_query='id')
|
||||
__doc__ = m.EventWithOneDevice.__doc__
|
||||
device = NestedOn(s_device.Device, only_query='id')
|
||||
|
||||
|
||||
class EventWithMultipleDevices(Event):
|
||||
devices = NestedOn(Device, many=True, only_query='id', collection_class=OrderedSet)
|
||||
__doc__ = m.EventWithMultipleDevices.__doc__
|
||||
devices = NestedOn(s_device.Device, many=True, only_query='id', collection_class=OrderedSet)
|
||||
|
||||
|
||||
class Add(EventWithOneDevice):
|
||||
pass
|
||||
__doc__ = m.Add.__doc__
|
||||
|
||||
|
||||
class Remove(EventWithOneDevice):
|
||||
pass
|
||||
__doc__ = m.Remove.__doc__
|
||||
|
||||
|
||||
class Allocate(EventWithMultipleDevices):
|
||||
to = NestedOn(User,
|
||||
__doc__ = m.Allocate.__doc__
|
||||
to = NestedOn(s_user.User,
|
||||
description='The user the devices are allocated to.')
|
||||
organization = SanitizedStr(validate=Length(max=STR_SIZE),
|
||||
description='The organization where the '
|
||||
|
@ -63,7 +68,8 @@ class Allocate(EventWithMultipleDevices):
|
|||
|
||||
|
||||
class Deallocate(EventWithMultipleDevices):
|
||||
from_rel = Nested(User,
|
||||
__doc__ = m.Deallocate.__doc__
|
||||
from_rel = Nested(s_user.User,
|
||||
data_key='from',
|
||||
description='The user where the devices are not allocated to anymore.')
|
||||
organization = SanitizedStr(validate=Length(max=STR_SIZE),
|
||||
|
@ -72,15 +78,23 @@ class Deallocate(EventWithMultipleDevices):
|
|||
|
||||
|
||||
class EraseBasic(EventWithOneDevice):
|
||||
zeros = Boolean(required=True, description=m.EraseBasic.zeros.comment)
|
||||
steps = NestedOn('Step', many=True, required=True)
|
||||
__doc__ = m.EraseBasic.__doc__
|
||||
steps = NestedOn('Step', many=True)
|
||||
standards = f.List(EnumField(enums.ErasureStandards), dump_only=True)
|
||||
certificate = URL(dump_only=True)
|
||||
|
||||
|
||||
class EraseSectors(EraseBasic):
|
||||
pass
|
||||
__doc__ = m.EraseSectors.__doc__
|
||||
|
||||
|
||||
class ErasePhysical(EraseBasic):
|
||||
__doc__ = m.ErasePhysical.__doc__
|
||||
method = EnumField(PhysicalErasureMethod, description=PhysicalErasureMethod.__doc__)
|
||||
|
||||
|
||||
class Step(Schema):
|
||||
__doc__ = m.Step.__doc__
|
||||
type = String(description='Only required when it is nested.')
|
||||
start_time = DateTime(required=True, data_key='startTime')
|
||||
end_time = DateTime(required=True, data_key='endTime')
|
||||
|
@ -88,14 +102,15 @@ class Step(Schema):
|
|||
|
||||
|
||||
class StepZero(Step):
|
||||
pass
|
||||
__doc__ = m.StepZero.__doc__
|
||||
|
||||
|
||||
class StepRandom(Step):
|
||||
pass
|
||||
__doc__ = m.StepRandom.__doc__
|
||||
|
||||
|
||||
class Rate(EventWithOneDevice):
|
||||
__doc__ = m.Rate.__doc__
|
||||
rating = Integer(validate=Range(*RATE_POSITIVE),
|
||||
dump_only=True,
|
||||
description=m.Rate.rating.comment)
|
||||
|
@ -110,10 +125,11 @@ class Rate(EventWithOneDevice):
|
|||
|
||||
|
||||
class IndividualRate(Rate):
|
||||
pass
|
||||
__doc__ = m.IndividualRate.__doc__
|
||||
|
||||
|
||||
class ManualRate(IndividualRate):
|
||||
__doc__ = m.ManualRate.__doc__
|
||||
appearance_range = EnumField(AppearanceRange,
|
||||
required=True,
|
||||
data_key='appearanceRange',
|
||||
|
@ -126,6 +142,7 @@ class ManualRate(IndividualRate):
|
|||
|
||||
|
||||
class WorkbenchRate(ManualRate):
|
||||
__doc__ = m.WorkbenchRate.__doc__
|
||||
processor = Float()
|
||||
ram = Float()
|
||||
data_storage = Float()
|
||||
|
@ -141,6 +158,7 @@ class WorkbenchRate(ManualRate):
|
|||
|
||||
|
||||
class AggregateRate(Rate):
|
||||
__doc__ = m.AggregateRate.__doc__
|
||||
workbench = NestedOn(WorkbenchRate, dump_only=True,
|
||||
description=m.AggregateRate.workbench_id.comment)
|
||||
manual = NestedOn(ManualRate,
|
||||
|
@ -170,6 +188,7 @@ class AggregateRate(Rate):
|
|||
|
||||
|
||||
class Price(EventWithOneDevice):
|
||||
__doc__ = m.Price.__doc__
|
||||
currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
|
||||
price = Decimal(places=m.Price.SCALE,
|
||||
rounding=m.Price.ROUND,
|
||||
|
@ -181,6 +200,8 @@ class Price(EventWithOneDevice):
|
|||
|
||||
|
||||
class EreusePrice(Price):
|
||||
__doc__ = m.EreusePrice.__doc__
|
||||
|
||||
class Service(MarshmallowSchema):
|
||||
class Type(MarshmallowSchema):
|
||||
amount = Float()
|
||||
|
@ -196,13 +217,16 @@ class EreusePrice(Price):
|
|||
|
||||
|
||||
class Install(EventWithOneDevice):
|
||||
__doc__ = m.Install.__doc__
|
||||
name = SanitizedStr(validate=Length(min=4, max=STR_BIG_SIZE),
|
||||
required=True,
|
||||
description='The name of the OS installed.')
|
||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
|
||||
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}))
|
||||
|
||||
|
||||
class Snapshot(EventWithOneDevice):
|
||||
__doc__ = m.Snapshot.__doc__
|
||||
"""
|
||||
The Snapshot updates the state of the device with information about
|
||||
its components and events performed at them.
|
||||
|
@ -222,7 +246,7 @@ class Snapshot(EventWithOneDevice):
|
|||
'the async Snapshot.')
|
||||
|
||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
|
||||
components = NestedOn(Component,
|
||||
components = NestedOn(s_device.Component,
|
||||
many=True,
|
||||
description='A list of components that are inside of the device'
|
||||
'at the moment of this Snapshot.'
|
||||
|
@ -267,10 +291,12 @@ class Snapshot(EventWithOneDevice):
|
|||
|
||||
|
||||
class Test(EventWithOneDevice):
|
||||
__doc__ = m.Test.__doc__
|
||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
|
||||
|
||||
|
||||
class TestDataStorage(Test):
|
||||
__doc__ = m.TestDataStorage.__doc__
|
||||
length = EnumField(TestDataStorageLength, required=True)
|
||||
status = SanitizedStr(lower=True, validate=Length(max=STR_SIZE), required=True)
|
||||
lifetime = TimeDelta(precision=TimeDelta.HOURS)
|
||||
|
@ -285,55 +311,59 @@ class TestDataStorage(Test):
|
|||
|
||||
|
||||
class StressTest(Test):
|
||||
pass
|
||||
__doc__ = m.StressTest.__doc__
|
||||
|
||||
|
||||
class Benchmark(EventWithOneDevice):
|
||||
__doc__ = m.Benchmark.__doc__
|
||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
|
||||
|
||||
|
||||
class BenchmarkDataStorage(Benchmark):
|
||||
__doc__ = m.BenchmarkDataStorage.__doc__
|
||||
read_speed = Float(required=True, data_key='readSpeed')
|
||||
write_speed = Float(required=True, data_key='writeSpeed')
|
||||
|
||||
|
||||
class BenchmarkWithRate(Benchmark):
|
||||
__doc__ = m.BenchmarkWithRate.__doc__
|
||||
rate = Float(required=True)
|
||||
|
||||
|
||||
class BenchmarkProcessor(BenchmarkWithRate):
|
||||
pass
|
||||
__doc__ = m.BenchmarkProcessor.__doc__
|
||||
|
||||
|
||||
class BenchmarkProcessorSysbench(BenchmarkProcessor):
|
||||
pass
|
||||
__doc__ = m.BenchmarkProcessorSysbench.__doc__
|
||||
|
||||
|
||||
class BenchmarkRamSysbench(BenchmarkWithRate):
|
||||
pass
|
||||
__doc__ = m.BenchmarkRamSysbench.__doc__
|
||||
|
||||
|
||||
class ToRepair(EventWithMultipleDevices):
|
||||
pass
|
||||
__doc__ = m.ToRepair.__doc__
|
||||
|
||||
|
||||
class Repair(EventWithMultipleDevices):
|
||||
pass
|
||||
__doc__ = m.Repair.__doc__
|
||||
|
||||
|
||||
class ReadyToUse(EventWithMultipleDevices):
|
||||
pass
|
||||
__doc__ = m.ReadyToUse.__doc__
|
||||
|
||||
|
||||
class ToPrepare(EventWithMultipleDevices):
|
||||
pass
|
||||
__doc__ = m.ToPrepare.__doc__
|
||||
|
||||
|
||||
class Prepare(EventWithMultipleDevices):
|
||||
pass
|
||||
__doc__ = m.Prepare.__doc__
|
||||
|
||||
|
||||
class Live(EventWithOneDevice):
|
||||
__doc__ = m.Live.__doc__
|
||||
ip = IP(dump_only=True)
|
||||
subdivision_confidence = Integer(dump_only=True, data_key='subdivisionConfidence')
|
||||
subdivision = EnumField(Subdivision, dump_only=True)
|
||||
|
@ -346,60 +376,63 @@ class Live(EventWithOneDevice):
|
|||
|
||||
|
||||
class Organize(EventWithMultipleDevices):
|
||||
pass
|
||||
__doc__ = m.Organize.__doc__
|
||||
|
||||
|
||||
class Reserve(Organize):
|
||||
pass
|
||||
__doc__ = m.Reserve.__doc__
|
||||
|
||||
|
||||
class CancelReservation(Organize):
|
||||
pass
|
||||
__doc__ = m.CancelReservation.__doc__
|
||||
|
||||
|
||||
class Trade(EventWithMultipleDevices):
|
||||
__doc__ = m.Trade.__doc__
|
||||
shipping_date = DateTime(data_key='shippingDate')
|
||||
invoice_number = SanitizedStr(validate=Length(max=STR_SIZE), data_key='invoiceNumber')
|
||||
price = NestedOn(Price)
|
||||
to = NestedOn(Agent, only_query='id', required=True, comment=m.Trade.to_comment)
|
||||
to = NestedOn(s_agent.Agent, only_query='id', required=True, comment=m.Trade.to_comment)
|
||||
confirms = NestedOn(Organize)
|
||||
|
||||
|
||||
class Sell(Trade):
|
||||
pass
|
||||
__doc__ = m.Sell.__doc__
|
||||
|
||||
|
||||
class Donate(Trade):
|
||||
pass
|
||||
__doc__ = m.Donate.__doc__
|
||||
|
||||
|
||||
class Rent(Trade):
|
||||
pass
|
||||
__doc__ = m.Rent.__doc__
|
||||
|
||||
|
||||
class CancelTrade(Trade):
|
||||
pass
|
||||
__doc__ = m.CancelTrade.__doc__
|
||||
|
||||
|
||||
class ToDisposeProduct(Trade):
|
||||
pass
|
||||
__doc__ = m.ToDisposeProduct.__doc__
|
||||
|
||||
|
||||
class DisposeProduct(Trade):
|
||||
pass
|
||||
__doc__ = m.DisposeProduct.__doc__
|
||||
|
||||
|
||||
class Receive(EventWithMultipleDevices):
|
||||
__doc__ = m.Receive.__doc__
|
||||
role = EnumField(ReceiverRole)
|
||||
|
||||
|
||||
class Migrate(EventWithMultipleDevices):
|
||||
__doc__ = m.Migrate.__doc__
|
||||
other = URL()
|
||||
|
||||
|
||||
class MigrateTo(Migrate):
|
||||
pass
|
||||
__doc__ = m.MigrateTo.__doc__
|
||||
|
||||
|
||||
class MigrateFrom(Migrate):
|
||||
pass
|
||||
__doc__ = m.MigrateFrom.__doc__
|
||||
|
|
|
@ -4,6 +4,7 @@ from uuid import UUID
|
|||
|
||||
from flask import current_app as app, request
|
||||
from sqlalchemy.util import OrderedSet
|
||||
from teal.marshmallow import ValidationError
|
||||
from teal.resource import View
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
|
@ -11,18 +12,28 @@ from ereuse_devicehub.resources.device.models import Component, Computer
|
|||
from ereuse_devicehub.resources.enums import SnapshotSoftware
|
||||
from ereuse_devicehub.resources.event.models import Event, Snapshot, WorkbenchRate
|
||||
|
||||
SUPPORTED_WORKBENCH = StrictVersion('11.0')
|
||||
|
||||
|
||||
class EventView(View):
|
||||
def post(self):
|
||||
"""Posts an event."""
|
||||
json = request.get_json(validate=False)
|
||||
e = app.resources[json['type']].schema.load(json)
|
||||
if not json or 'type' not in json:
|
||||
raise ValidationError('Resource needs a type.')
|
||||
# todo there should be a way to better get subclassess resource
|
||||
# defs
|
||||
resource_def = app.resources[json['type']]
|
||||
e = resource_def.schema.load(json)
|
||||
if json['type'] == Snapshot.t:
|
||||
return self.snapshot(e, resource_def)
|
||||
Model = db.Model._decl_class_registry.data[json['type']]()
|
||||
event = Model(**e)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
db.session().final_flush()
|
||||
ret = self.schema.jsonify(event)
|
||||
ret.status_code = 201
|
||||
db.session.commit()
|
||||
return ret
|
||||
|
||||
def one(self, id: UUID):
|
||||
|
@ -30,25 +41,20 @@ class EventView(View):
|
|||
event = Event.query.filter_by(id=id).one()
|
||||
return self.schema.jsonify(event)
|
||||
|
||||
|
||||
SUPPORTED_WORKBENCH = StrictVersion('11.0')
|
||||
|
||||
|
||||
class SnapshotView(View):
|
||||
def post(self):
|
||||
def snapshot(self, snapshot_json: dict, resource_def):
|
||||
"""
|
||||
Performs a Snapshot.
|
||||
|
||||
See `Snapshot` section in docs for more info.
|
||||
"""
|
||||
s = request.get_json()
|
||||
# Note that if we set the device / components into the snapshot
|
||||
# model object, when we flush them to the db we will flush
|
||||
# snapshot, and we want to wait to flush snapshot at the end
|
||||
device = s.pop('device') # type: Computer
|
||||
components = s.pop('components') \
|
||||
if s['software'] == SnapshotSoftware.Workbench else None # type: List[Component]
|
||||
snapshot = Snapshot(**s)
|
||||
device = snapshot_json.pop('device') # type: Computer
|
||||
components = None
|
||||
if snapshot_json['software'] == SnapshotSoftware.Workbench:
|
||||
components = snapshot_json.pop('components') # type: List[Component]
|
||||
snapshot = Snapshot(**snapshot_json)
|
||||
|
||||
# Remove new events from devices so they don't interfere with sync
|
||||
events_device = set(e for e in device.events_one)
|
||||
|
@ -58,10 +64,9 @@ class SnapshotView(View):
|
|||
for component in components:
|
||||
component.events_one.clear()
|
||||
|
||||
# noinspection PyArgumentList
|
||||
assert not device.events_one
|
||||
assert all(not c.events_one for c in components) if components else True
|
||||
db_device, remove_events = self.resource_def.sync.run(device, components)
|
||||
db_device, remove_events = resource_def.sync.run(device, components)
|
||||
snapshot.device = db_device
|
||||
snapshot.events |= remove_events | events_device # Set events to snapshot
|
||||
# commit will change the order of the components by what
|
||||
|
@ -81,13 +86,8 @@ class SnapshotView(View):
|
|||
snapshot.events |= rates
|
||||
|
||||
db.session.add(snapshot)
|
||||
db.session.commit()
|
||||
# todo we are setting snapshot dirty again with this components but
|
||||
# we do not want to update it.
|
||||
# The real solution is https://stackoverflow.com/questions/
|
||||
# 24480581/set-the-insert-order-of-a-many-to-many-sqlalchemy-
|
||||
# flask-app-sqlite-db?noredirect=1&lq=1
|
||||
snapshot.components = ordered_components
|
||||
db.session().final_flush()
|
||||
ret = self.schema.jsonify(snapshot) # transform it back
|
||||
ret.status_code = 201
|
||||
db.session.commit()
|
||||
return ret
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import uuid
|
||||
|
||||
import boltons.urlutils
|
||||
from flask import current_app
|
||||
from teal.db import ResourceNotFound
|
||||
from teal.resource import Resource
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.inventory import schema
|
||||
from ereuse_devicehub.resources.inventory.model import Inventory
|
||||
|
||||
|
||||
class InventoryDef(Resource):
|
||||
SCHEMA = schema.Inventory
|
||||
VIEW = None
|
||||
|
||||
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
|
||||
static_url_path=None,
|
||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||
root_path=None):
|
||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||
url_prefix, subdomain, url_defaults, root_path)
|
||||
|
||||
@classmethod
|
||||
def set_inventory_config(cls,
|
||||
name: str = None,
|
||||
org_name: str = None,
|
||||
org_id: str = None,
|
||||
tag_url: boltons.urlutils.URL = None,
|
||||
tag_token: uuid.UUID = None):
|
||||
try:
|
||||
inventory = Inventory.current
|
||||
except ResourceNotFound: # No inventory defined in db yet
|
||||
inventory = Inventory(id=current_app.id,
|
||||
name=name,
|
||||
tag_provider=tag_url,
|
||||
tag_token=tag_token)
|
||||
db.session.add(inventory)
|
||||
if org_name or org_id:
|
||||
from ereuse_devicehub.resources.agent.models import Organization
|
||||
try:
|
||||
org = Organization.query.filter_by(tax_id=org_id, name=org_name).one()
|
||||
except ResourceNotFound:
|
||||
org = Organization(tax_id=org_id, name=org_name)
|
||||
org.default_of = inventory
|
||||
if tag_url:
|
||||
inventory.tag_provider = tag_url
|
||||
if tag_token:
|
||||
inventory.tag_token = tag_token
|
||||
|
||||
@classmethod
|
||||
def delete_inventory(cls):
|
||||
"""Removes an inventory alongside with the users that have
|
||||
only access to this inventory.
|
||||
"""
|
||||
from ereuse_devicehub.resources.user.models import User, UserInventory
|
||||
inv = Inventory.query.filter_by(id=current_app.id).one()
|
||||
db.session.delete(inv)
|
||||
db.session.flush()
|
||||
# Remove users that end-up without any inventory
|
||||
# todo this should be done in a trigger / event
|
||||
users = User.query \
|
||||
.filter(User.id.notin_(db.session.query(UserInventory.user_id).distinct()))
|
||||
for user in users:
|
||||
db.session.delete(user)
|
|
@ -0,0 +1,27 @@
|
|||
from boltons.typeutils import classproperty
|
||||
from flask import current_app
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
|
||||
|
||||
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()
|
|
@ -0,0 +1,10 @@
|
|||
import teal.marshmallow
|
||||
from marshmallow import fields as mf
|
||||
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
|
||||
|
||||
class Inventory(Thing):
|
||||
id = mf.String(dump_only=True)
|
||||
name = mf.String(dump_only=True)
|
||||
tag_provider = teal.marshmallow.URL(dump_only=True, data_key='tagProvider')
|
|
@ -34,7 +34,7 @@ class LotDef(Resource):
|
|||
view_func=lot_device,
|
||||
methods={'POST', 'DELETE'})
|
||||
|
||||
def init_db(self, db: 'db.SQLAlchemy'):
|
||||
def init_db(self, db: 'db.SQLAlchemy', exclude_schema=None):
|
||||
# Create functions
|
||||
with pathlib.Path(__file__).parent.joinpath('dag.sql').open() as f:
|
||||
sql = f.read()
|
||||
|
|
|
@ -75,6 +75,10 @@ class Lot(Thing):
|
|||
super().__init__(id=uuid.uuid4(), name=name, closed=closed, description=description)
|
||||
Path(self) # Lots have always one edge per default.
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def url(self) -> urlutils.URL:
|
||||
"""The URL where to GET this event."""
|
||||
|
@ -178,7 +182,7 @@ 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, index=True)
|
||||
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,
|
||||
|
@ -195,7 +199,8 @@ class Path(db.Model):
|
|||
# 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('path_btree', path, postgresql_using='btree'),
|
||||
db.Index('lot_id_index', lot_id, postgresql_using='hash')
|
||||
)
|
||||
|
||||
def __init__(self, lot: Lot) -> None:
|
||||
|
@ -251,7 +256,7 @@ class LotDeviceDescendants(db.Model):
|
|||
_desc.c.id.label('parent_lot_id'),
|
||||
_ancestor.c.id.label('ancestor_lot_id'),
|
||||
None
|
||||
]).select_from(_ancestor).select_from(lot_device).where(descendants)
|
||||
]).select_from(_ancestor).select_from(lot_device).where(db.text(descendants))
|
||||
|
||||
# Components
|
||||
_parent_device = Device.__table__.alias(name='parent_device')
|
||||
|
@ -266,7 +271,7 @@ class LotDeviceDescendants(db.Model):
|
|||
_desc.c.id.label('parent_lot_id'),
|
||||
_ancestor.c.id.label('ancestor_lot_id'),
|
||||
LotDevice.device_id.label('device_parent_id'),
|
||||
]).select_from(_ancestor).select_from(lot_device_component).where(descendants)
|
||||
]).select_from(_ancestor).select_from(lot_device_component).where(db.text(descendants))
|
||||
|
||||
__table__ = create_view('lot_device_descendants', devices.union(components))
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from marshmallow import fields as f
|
|||
from teal.marshmallow import SanitizedStr, URL
|
||||
|
||||
from ereuse_devicehub.marshmallow import NestedOn
|
||||
from ereuse_devicehub.resources.device.schemas import Device
|
||||
from ereuse_devicehub.resources.device import schemas as s_device
|
||||
from ereuse_devicehub.resources.lot import models as m
|
||||
from ereuse_devicehub.resources.models import STR_SIZE
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
|
@ -13,7 +13,7 @@ class Lot(Thing):
|
|||
name = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), required=True)
|
||||
description = SanitizedStr(description=m.Lot.description.comment)
|
||||
closed = f.Boolean(missing=False, description=m.Lot.closed.comment)
|
||||
devices = NestedOn(Device, many=True, dump_only=True)
|
||||
devices = NestedOn(s_device.Device, many=True, dump_only=True)
|
||||
children = NestedOn('Lot', many=True, dump_only=True)
|
||||
parents = NestedOn('Lot', many=True, dump_only=True)
|
||||
url = URL(dump_only=True, description=m.Lot.url.__doc__)
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import datetime
|
||||
import uuid
|
||||
from collections import deque
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Set, Union
|
||||
|
||||
import marshmallow as ma
|
||||
import teal.cache
|
||||
from flask import Response, jsonify, request
|
||||
from marshmallow import Schema as MarshmallowSchema, fields as f
|
||||
from teal.marshmallow import EnumField
|
||||
from teal.resource import View
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.query import things_response
|
||||
from ereuse_devicehub.resources.device.models import Device
|
||||
from ereuse_devicehub.resources.lot.models import Lot, Path
|
||||
|
||||
|
@ -31,13 +34,15 @@ class LotView(View):
|
|||
l = request.get_json()
|
||||
lot = Lot(**l)
|
||||
db.session.add(lot)
|
||||
db.session.commit()
|
||||
db.session().final_flush()
|
||||
ret = self.schema.jsonify(lot)
|
||||
ret.status_code = 201
|
||||
db.session.commit()
|
||||
return ret
|
||||
|
||||
def patch(self, id):
|
||||
l = request.get_json()
|
||||
patch_schema = self.resource_def.SCHEMA(only=('name', 'description'), partial=True)
|
||||
l = request.get_json(schema=patch_schema)
|
||||
lot = Lot.query.filter_by(id=id).one()
|
||||
for key, value in l.items():
|
||||
setattr(lot, key, value)
|
||||
|
@ -49,6 +54,7 @@ class LotView(View):
|
|||
lot = Lot.query.filter_by(id=id).one() # type: Lot
|
||||
return self.schema.jsonify(lot)
|
||||
|
||||
@teal.cache.cache(datetime.timedelta(minutes=5))
|
||||
def find(self, args: dict):
|
||||
"""
|
||||
Gets lots.
|
||||
|
@ -78,17 +84,10 @@ class LotView(View):
|
|||
if args['search']:
|
||||
query = query.filter(Lot.name.ilike(args['search'] + '%'))
|
||||
lots = query.paginate(per_page=6 if args['search'] else 30)
|
||||
ret = {
|
||||
'items': self.schema.dump(lots.items, many=True, nested=0),
|
||||
'pagination': {
|
||||
'page': lots.page,
|
||||
'perPage': lots.per_page,
|
||||
'total': lots.total,
|
||||
'previous': lots.prev_num,
|
||||
'next': lots.next_num
|
||||
},
|
||||
'url': request.path
|
||||
}
|
||||
return things_response(
|
||||
self.schema.dump(lots.items, many=True, nested=0),
|
||||
lots.page, lots.per_page, lots.total, lots.prev_num, lots.next_num
|
||||
)
|
||||
return jsonify(ret)
|
||||
|
||||
def delete(self, id):
|
||||
|
@ -147,17 +146,21 @@ class LotBaseChildrenView(View):
|
|||
def post(self, id: uuid.UUID):
|
||||
lot = self.get_lot(id)
|
||||
self._post(lot, self.get_ids())
|
||||
db.session.commit()
|
||||
|
||||
db.session().final_flush()
|
||||
ret = self.schema.jsonify(lot)
|
||||
ret.status_code = 201
|
||||
|
||||
db.session.commit()
|
||||
return ret
|
||||
|
||||
def delete(self, id: uuid.UUID):
|
||||
lot = self.get_lot(id)
|
||||
self._delete(lot, self.get_ids())
|
||||
db.session().final_flush()
|
||||
response = self.schema.jsonify(lot)
|
||||
db.session.commit()
|
||||
return self.schema.jsonify(lot)
|
||||
return response
|
||||
|
||||
def _post(self, lot: Lot, ids: Set[uuid.UUID]):
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -9,21 +9,26 @@ STR_XSM_SIZE = 16
|
|||
|
||||
|
||||
class Thing(db.Model):
|
||||
"""The base class of all Devicehub resources.
|
||||
|
||||
This is a loose copy of
|
||||
`schema.org's Thing class <https://schema.org/Thing>`_
|
||||
using only needed fields.
|
||||
"""
|
||||
__abstract__ = True
|
||||
# todo make updated to auto-update
|
||||
updated = db.Column(db.TIMESTAMP(timezone=True),
|
||||
nullable=False,
|
||||
index=True,
|
||||
server_default=db.text('CURRENT_TIMESTAMP'))
|
||||
updated.comment = """
|
||||
When this was last changed.
|
||||
The last time Devicehub recorded a change for this thing.
|
||||
"""
|
||||
created = db.Column(db.TIMESTAMP(timezone=True),
|
||||
nullable=False,
|
||||
index=True,
|
||||
server_default=db.text('CURRENT_TIMESTAMP'))
|
||||
created.comment = """
|
||||
When Devicehub created this.
|
||||
When Devicehub created this.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from marshmallow import post_load
|
||||
from marshmallow.fields import DateTime, List, String
|
||||
from marshmallow.schema import SchemaMeta
|
||||
from teal.marshmallow import URL
|
||||
from teal.resource import Schema
|
||||
|
||||
|
@ -18,10 +20,59 @@ class UnitCodes(Enum):
|
|||
kgm = 'KGM'
|
||||
m = 'MTR'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
# The following SchemaMeta modifications allow us to generate
|
||||
# documentation using our directive. This is their only purpose.
|
||||
# Marshmallow's meta class removes variables from our defined
|
||||
# classes, so we put some home made proxies in order to intercept
|
||||
# those values and safe them in our classes.
|
||||
# What we do is:
|
||||
# 1. Make our ``Meta`` class be the superclass of Marshmallow's
|
||||
# SchemaMeta and provide a new that stores in class, so we
|
||||
# can save some vars.
|
||||
# 2. Substitute SchemaMeta.get_declared_fields with our own method
|
||||
# that saves more variables.
|
||||
# Then the directive in our docs/config.py file reads these variables
|
||||
# generating the documentation.
|
||||
|
||||
class Meta(type):
|
||||
|
||||
def __new__(cls, *args, **kw) -> Any:
|
||||
base_name = args[1][0].__name__
|
||||
y = super().__new__(cls, *args, **kw)
|
||||
y._base_class = base_name
|
||||
return y
|
||||
|
||||
|
||||
SchemaMeta.__bases__ = Meta,
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_declared_fields(mcs, klass, cls_fields, inherited_fields, dict_cls):
|
||||
klass._own = cls_fields
|
||||
klass._inherited = inherited_fields
|
||||
return dict_cls(inherited_fields + cls_fields)
|
||||
|
||||
|
||||
SchemaMeta.get_declared_fields = get_declared_fields
|
||||
|
||||
_type_description = """The name of the type of Thing,
|
||||
like "Device" or "Receive". This is the same as JSON-LD ``@type``.
|
||||
|
||||
This field is required when submitting values
|
||||
so Devicehub knows the type of object. Devicehub always returns this
|
||||
value.
|
||||
"""
|
||||
|
||||
|
||||
class Thing(Schema):
|
||||
type = String(description='Only required when it is nested.')
|
||||
same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs')
|
||||
type = String(description=_type_description)
|
||||
same_as = List(URL(dump_only=True),
|
||||
dump_only=True,
|
||||
data_key='sameAs')
|
||||
updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment)
|
||||
created = DateTime('iso', dump_only=True, description=m.Thing.created.comment)
|
||||
|
||||
|
|
|
@ -30,12 +30,12 @@ class Search:
|
|||
@staticmethod
|
||||
def match(column: db.Column, search: str, lang=LANG):
|
||||
"""Query that matches a TSVECTOR column with search words."""
|
||||
return column.op('@@')(db.func.plainto_tsquery(lang, search))
|
||||
return column.op('@@')(db.func.websearch_to_tsquery(lang, search))
|
||||
|
||||
@staticmethod
|
||||
def rank(column: db.Column, search: str, lang=LANG):
|
||||
"""Query that ranks a TSVECTOR column with search words."""
|
||||
return db.func.ts_rank(column, db.func.plainto_tsquery(lang, search))
|
||||
return db.func.ts_rank(column, db.func.websearch_to_tsquery(lang, search))
|
||||
|
||||
@staticmethod
|
||||
def _vectorize(col: db.Column, weight: Weight = Weight.D, lang=LANG):
|
||||
|
|
|
@ -29,8 +29,8 @@ class TagDef(Resource):
|
|||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||
root_path=None):
|
||||
cli_commands = (
|
||||
(self.create_tag, 'create-tag'),
|
||||
(self.create_tags_csv, 'create-tags-csv')
|
||||
(self.create_tag, 'add'),
|
||||
(self.create_tags_csv, 'add-csv')
|
||||
)
|
||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
from contextlib import suppress
|
||||
from typing import Set
|
||||
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey, Unicode, UniqueConstraint
|
||||
from boltons import urlutils
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import backref, relationship, validates
|
||||
from teal.db import DB_CASCADE_SET_NULL, Query, URL, check_lower
|
||||
from teal.db import DB_CASCADE_SET_NULL, Query, URL
|
||||
from teal.marshmallow import ValidationError
|
||||
from teal.resource import url_for_resource
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.agent.models import Organization
|
||||
from ereuse_devicehub.resources.device.models import Device
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
|
||||
|
||||
class Tags(Set['Tag']):
|
||||
def __str__(self) -> str:
|
||||
return ', '.join(str(tag) for tag in self).strip()
|
||||
|
||||
def __format__(self, format_spec):
|
||||
return ', '.join(format(tag, format_spec) for tag in self).strip()
|
||||
|
||||
|
||||
class Tag(Thing):
|
||||
id = Column(Unicode(), check_lower('id'), primary_key=True)
|
||||
id = Column(db.CIText(), primary_key=True)
|
||||
id.comment = """The ID of the tag."""
|
||||
org_id = Column(UUID(as_uuid=True),
|
||||
ForeignKey(Organization.id),
|
||||
|
@ -32,18 +44,21 @@ class Tag(Thing):
|
|||
"""
|
||||
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),
|
||||
index=True)
|
||||
ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL))
|
||||
device = relationship(Device,
|
||||
backref=backref('tags', lazy=True, collection_class=set),
|
||||
backref=backref('tags', lazy=True, collection_class=Tags),
|
||||
primaryjoin=Device.id == device_id)
|
||||
"""The device linked to this tag."""
|
||||
secondary = Column(Unicode(), check_lower('secondary'), index=True)
|
||||
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)
|
||||
|
||||
|
@ -80,5 +95,35 @@ class Tag(Thing):
|
|||
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)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from uuid import UUID
|
||||
|
||||
from boltons import urlutils
|
||||
from boltons.urlutils import URL
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.orm import relationship
|
||||
|
@ -39,3 +40,15 @@ class Tag(Thing):
|
|||
|
||||
def like_etag(self) -> bool:
|
||||
pass
|
||||
|
||||
@property
|
||||
def printable(self) -> bool:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def is_printable_q(cls):
|
||||
pass
|
||||
|
||||
@property
|
||||
def url(self) -> urlutils.URL:
|
||||
pass
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from sqlalchemy.util import OrderedSet
|
||||
from marshmallow.fields import Boolean
|
||||
from teal.marshmallow import SanitizedStr, URL
|
||||
|
||||
from ereuse_devicehub.marshmallow import NestedOn
|
||||
|
@ -23,3 +24,5 @@ class Tag(Thing):
|
|||
device = NestedOn(Device, dump_only=True)
|
||||
org = NestedOn(Organization, collection_class=OrderedSet, only_query='id')
|
||||
secondary = SanitizedStr(lower=True, description=m.Tag.secondary.comment)
|
||||
printable = Boolean(dump_only=True, decsription=m.Tag.printable.__doc__)
|
||||
url = URL(dump_only=True, description=m.Tag.url.__doc__)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from flask import Response, current_app as app, redirect, request
|
||||
from flask import Response, current_app as app, g, redirect, request
|
||||
from flask_sqlalchemy import Pagination
|
||||
from teal.marshmallow import ValidationError
|
||||
from teal.resource import View, url_for_resource
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.query import things_response
|
||||
from ereuse_devicehub.resources.device.models import Device
|
||||
from ereuse_devicehub.resources.tag import Tag
|
||||
|
||||
|
@ -10,11 +12,39 @@ from ereuse_devicehub.resources.tag import Tag
|
|||
class TagView(View):
|
||||
def post(self):
|
||||
"""Creates a tag."""
|
||||
num = request.args.get('num', type=int)
|
||||
if num:
|
||||
res = self._create_many_regular_tags(num)
|
||||
else:
|
||||
res = self._post_one()
|
||||
return res
|
||||
|
||||
def find(self, args: dict):
|
||||
tags = Tag.query.filter(Tag.is_printable_q()) \
|
||||
.order_by(Tag.created.desc()) \
|
||||
.paginate(per_page=200) # type: Pagination
|
||||
return things_response(
|
||||
self.schema.dump(tags.items, many=True, nested=0),
|
||||
tags.page, tags.per_page, tags.total, tags.prev_num, tags.next_num
|
||||
)
|
||||
|
||||
def _create_many_regular_tags(self, num: int):
|
||||
tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)])
|
||||
tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id]
|
||||
db.session.add_all(tags)
|
||||
db.session().final_flush()
|
||||
response = things_response(self.schema.dump(tags, many=True, nested=1), code=201)
|
||||
db.session.commit()
|
||||
return response
|
||||
|
||||
def _post_one(self):
|
||||
# todo do we use this?
|
||||
t = request.get_json()
|
||||
tag = Tag(**t)
|
||||
if tag.like_etag():
|
||||
raise CannotCreateETag(tag.id)
|
||||
db.session.add(tag)
|
||||
db.session().final_flush()
|
||||
db.session.commit()
|
||||
return Response(status=201)
|
||||
|
||||
|
@ -42,6 +72,7 @@ class TagDeviceView(View):
|
|||
raise LinkedToAnotherDevice(tag.device_id)
|
||||
else:
|
||||
tag.device_id = device_id
|
||||
db.session().final_flush()
|
||||
db.session.commit()
|
||||
return Response(status=204)
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Iterable
|
||||
|
||||
from click import argument, option
|
||||
from flask import current_app
|
||||
from teal.resource import Converters, Resource
|
||||
|
@ -17,28 +19,41 @@ class UserDef(Resource):
|
|||
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
|
||||
static_url_path=None, template_folder=None, url_prefix=None, subdomain=None,
|
||||
url_defaults=None, root_path=None):
|
||||
cli_commands = ((self.create_user, 'create-user'),)
|
||||
cli_commands = ((self.create_user, 'add'),)
|
||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||
self.add_url_rule('/login', view_func=login, methods={'POST'})
|
||||
self.add_url_rule('/login/', view_func=login, methods={'POST'})
|
||||
|
||||
@argument('email')
|
||||
@option('-a', '--agent', help='The name of an agent to create with the user.')
|
||||
@option('-i', '--inventory',
|
||||
multiple=True,
|
||||
help='Inventories user has access to. By default this one.')
|
||||
@option('-a', '--agent',
|
||||
help='Create too an Individual agent representing this user, '
|
||||
'and give a name to this individual.')
|
||||
@option('-c', '--country', help='The country of the agent (if --agent is set).')
|
||||
@option('-t', '--telephone', help='The telephone of the agent (if --agent is set).')
|
||||
@option('-t', '--tax-id', help='The tax id of the agent (if --agent is set).')
|
||||
@option('-p', '--password', prompt=True, hide_input=True, confirmation_prompt=True)
|
||||
def create_user(self, email: str, password: str, agent: str = None, country: str = None,
|
||||
telephone: str = None, tax_id: str = None) -> dict:
|
||||
"""Creates an user.
|
||||
def create_user(self, email: str,
|
||||
password: str,
|
||||
inventory: Iterable[str] = tuple(),
|
||||
agent: str = None,
|
||||
country: str = None,
|
||||
telephone: str = None,
|
||||
tax_id: str = None) -> dict:
|
||||
"""Create an user.
|
||||
|
||||
If ``--agent`` is passed, it creates an ``Individual`` agent
|
||||
that represents the user.
|
||||
If ``--agent`` is passed, it creates too an ``Individual``
|
||||
agent that represents the user.
|
||||
"""
|
||||
from ereuse_devicehub.resources.agent.models import Individual
|
||||
u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
|
||||
.load({'email': email, 'password': password})
|
||||
user = User(**u)
|
||||
if inventory:
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
inventory = Inventory.query.filter(Inventory.id.in_(inventory))
|
||||
user = User(**u, inventories=inventory)
|
||||
agent = Individual(**current_app.resources[Individual.t].schema.load(
|
||||
dict(name=agent, email=email, country=country, telephone=telephone, taxId=tax_id)
|
||||
))
|
||||
|
|
|
@ -5,6 +5,8 @@ from sqlalchemy import Column
|
|||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy_utils import EmailType, PasswordType
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.inventory.model import Inventory
|
||||
from ereuse_devicehub.resources.models import STR_SIZE, Thing
|
||||
|
||||
|
||||
|
@ -17,17 +19,41 @@ class User(Thing):
|
|||
schemes=app.config['PASSWORD_SCHEMES'],
|
||||
**kwargs
|
||||
)))
|
||||
"""
|
||||
Password field.
|
||||
From `here <https://sqlalchemy-utils.readthedocs.io/en/latest/
|
||||
data_types.html#module-sqlalchemy_utils.types.password>`_
|
||||
"""
|
||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True)
|
||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
||||
inventories = db.relationship(Inventory,
|
||||
backref=db.backref('users', lazy=True, collection_class=set),
|
||||
secondary=lambda: UserInventory.__table__,
|
||||
collection_class=set)
|
||||
|
||||
# todo set restriction that user has, at least, one active db
|
||||
|
||||
def __init__(self, email, password=None, inventories=None) -> None:
|
||||
"""
|
||||
Creates an user.
|
||||
:param email:
|
||||
:param password:
|
||||
:param inventories: A set of Inventory where the user has
|
||||
access to. If none, the user is granted access to the current
|
||||
inventory.
|
||||
"""
|
||||
inventories = inventories or {Inventory.current}
|
||||
super().__init__(email=email, password=password, inventories=inventories)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<User {0.email}>'.format(self)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def individual(self):
|
||||
"""The individual associated for this database, or None."""
|
||||
return next(iter(self.individuals), None)
|
||||
|
||||
|
||||
class UserInventory(db.Model):
|
||||
"""Relationship between users and their inventories."""
|
||||
__table_args__ = {'schema': 'common'}
|
||||
user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id), primary_key=True)
|
||||
inventory_id = db.Column(db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True)
|
||||
|
|
|
@ -2,9 +2,12 @@ from typing import Set, Union
|
|||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy_utils import Password
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.agent.models import Individual
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
|
||||
|
||||
|
@ -13,15 +16,22 @@ class User(Thing):
|
|||
email = ... # type: Column
|
||||
password = ... # type: Column
|
||||
token = ... # type: Column
|
||||
inventories = ... # type: relationship
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
def __init__(self, email: str, password: str = None,
|
||||
inventories: Set[Inventory] = None) -> None:
|
||||
super().__init__()
|
||||
self.id = ... # type: UUID
|
||||
self.email = ... # type: str
|
||||
self.password = ... # type: Password
|
||||
self.individuals = ... # type: Set[Individual]
|
||||
self.token = ... # type: UUID
|
||||
self.inventories = ... # type: Set[Inventory]
|
||||
|
||||
@property
|
||||
def individual(self) -> Union[Individual, None]:
|
||||
pass
|
||||
|
||||
|
||||
class UserInventory(db.Model):
|
||||
pass
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
from base64 import b64encode
|
||||
|
||||
from marshmallow import post_dump
|
||||
from marshmallow.fields import Email, String, UUID
|
||||
from teal.marshmallow import SanitizedStr
|
||||
|
||||
from ereuse_devicehub import auth
|
||||
from ereuse_devicehub.marshmallow import NestedOn
|
||||
from ereuse_devicehub.resources.agent.schemas import Individual
|
||||
from ereuse_devicehub.resources.inventory.schema import Inventory
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
|
||||
|
||||
|
@ -18,6 +18,7 @@ class User(Thing):
|
|||
token = String(dump_only=True,
|
||||
description='Use this token in an Authorization header to access the app.'
|
||||
'The token can change overtime.')
|
||||
inventories = NestedOn(Inventory, many=True, dump_only=True)
|
||||
|
||||
def __init__(self,
|
||||
only=None,
|
||||
|
@ -42,5 +43,5 @@ class User(Thing):
|
|||
if 'token' in data:
|
||||
# In many cases we don't dump the token (ex. relationships)
|
||||
# Framework needs ':' at the end
|
||||
data['token'] = b64encode(str.encode(str(data['token']) + ':')).decode()
|
||||
data['token'] = auth.Auth.encode(data['token'])
|
||||
return data
|
||||
|
|
|
@ -6,9 +6,9 @@ Define servername api.devicetag.io
|
|||
# The domain used to access the server
|
||||
Define appdir /home/devicetag/sites/${servername}/source/
|
||||
# The path where the app directory is. Apache must have access to this folder.
|
||||
Define wsgipath ${appdir}/wsgi.wsgi
|
||||
Define wsgipath ${appdir}/wsgi.py
|
||||
# The location of the .wsgi file
|
||||
Define pyvenv ${appdir}/venv/
|
||||
Define pyvenv ${appdir}../venv/
|
||||
# The path where the virtual environment is (the folder containing bin/activate)
|
||||
|
||||
<VirtualHost *:80>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
|
||||
"""
|
||||
|
@ -7,10 +6,4 @@ Example app with minimal configuration.
|
|||
Use this as a starting point.
|
||||
"""
|
||||
|
||||
|
||||
class MyConfig(DevicehubConfig):
|
||||
ORGANIZATION_NAME = 'My org'
|
||||
ORGANIZATION_TAX_ID = 'foo-bar'
|
||||
|
||||
|
||||
app = Devicehub(MyConfig())
|
||||
app = Devicehub(inventory='db1')
|
||||
|
|
|
@ -11,3 +11,4 @@ psql -d $1 -c "GRANT ALL PRIVILEGES ON DATABASE $1 TO $2;" # Give access to the
|
|||
psql -d $1 -c "CREATE EXTENSION pgcrypto SCHEMA public;" # Enable pgcrypto
|
||||
psql -d $1 -c "CREATE EXTENSION ltree SCHEMA public;" # Enable ltree
|
||||
psql -d $1 -c "CREATE EXTENSION citext SCHEMA public;" # Enable citext
|
||||
psql -d $1 -c "CREATE EXTENSION pg_trgm SCHEMA public;" # Enable pg_trgm
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
An exemplifying Apache python WSGI to a Devicehub app with a dispatcher.
|
||||
"""
|
||||
from ereuse_devicehub.dispatchers import PathDispatcher
|
||||
|
||||
application = PathDispatcher()
|
|
@ -1,11 +1,11 @@
|
|||
anytree==2.4.3
|
||||
apispec==0.39.0
|
||||
boltons==18.0.0
|
||||
boltons==18.0.1
|
||||
click==6.7
|
||||
click-spinner==0.1.8
|
||||
colorama==0.3.9
|
||||
colour==0.1.5
|
||||
ereuse-utils==0.4.0b10
|
||||
ereuse-utils[naming, test, session, cli]==0.4.0b21
|
||||
Flask==1.0.2
|
||||
Flask-Cors==3.0.6
|
||||
Flask-SQLAlchemy==2.3.2
|
||||
|
@ -15,17 +15,19 @@ marshmallow==3.0.0b11
|
|||
marshmallow-enum==1.4.1
|
||||
passlib==1.7.1
|
||||
phonenumbers==8.9.11
|
||||
pySMART.smartx==0.3.9
|
||||
pytest==3.7.2
|
||||
pytest-runner==4.2
|
||||
python-dateutil==2.7.3
|
||||
python-stdnum==1.9
|
||||
PyYAML==3.13
|
||||
requests==2.19.1
|
||||
requests[security]==2.19.1
|
||||
requests-mock==1.5.2
|
||||
SQLAlchemy==1.2.14
|
||||
SQLAlchemy-Utils==0.33.6
|
||||
teal==0.2.0a30
|
||||
SQLAlchemy==1.2.17
|
||||
SQLAlchemy-Utils==0.33.11
|
||||
teal==0.2.0a38
|
||||
webargs==4.0.0
|
||||
Werkzeug==0.14.1
|
||||
sqlalchemy-citext==1.3.post0
|
||||
flask-weasyprint==0.5
|
||||
weasyprint==44
|
||||
psycopg2-binary==2.7.5
|
||||
|
|
14
setup.py
14
setup.py
|
@ -12,7 +12,7 @@ test_requires = [
|
|||
|
||||
setup(
|
||||
name='ereuse-devicehub',
|
||||
version='0.2.0b1',
|
||||
version='0.2.0b3',
|
||||
url='https://github.com/ereuse/devicehub-teal',
|
||||
project_urls=OrderedDict((
|
||||
('Documentation', 'http://devicheub.ereuse.org'),
|
||||
|
@ -29,19 +29,20 @@ setup(
|
|||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
install_requires=[
|
||||
'teal>=0.2.0a30', # teal always first
|
||||
'teal>=0.2.0a38', # teal always first
|
||||
'click',
|
||||
'click-spinner',
|
||||
'ereuse-utils[Naming]>=0.4b10',
|
||||
'ereuse-utils[naming, test, session, cli]>=0.4b21',
|
||||
'hashids',
|
||||
'marshmallow_enum',
|
||||
'psycopg2-binary',
|
||||
'python-stdnum',
|
||||
'PyYAML',
|
||||
'requests',
|
||||
'requests[security]',
|
||||
'requests-toolbelt',
|
||||
'sqlalchemy-citext',
|
||||
'sqlalchemy-utils[password, color, phone]',
|
||||
'Flask-WeasyPrint'
|
||||
],
|
||||
extras_require={
|
||||
'docs': [
|
||||
|
@ -56,6 +57,11 @@ setup(
|
|||
'test': test_requires
|
||||
},
|
||||
tests_require=test_requires,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'dh = ereuse_devicehub.cli:cli'
|
||||
]
|
||||
},
|
||||
setup_requires=[
|
||||
'pytest-runner'
|
||||
],
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import io
|
||||
import uuid
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import boltons.urlutils
|
||||
import pytest
|
||||
import yaml
|
||||
from psycopg2 import IntegrityError
|
||||
|
@ -26,10 +28,7 @@ T = {'start_time': STARTT, 'end_time': ENDT}
|
|||
|
||||
class TestConfig(DevicehubConfig):
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/dh_test'
|
||||
SCHEMA = 'test'
|
||||
TESTING = True
|
||||
ORGANIZATION_NAME = 'FooOrg'
|
||||
ORGANIZATION_TAX_ID = 'foo-org-id'
|
||||
SERVER_NAME = 'localhost'
|
||||
|
||||
|
||||
|
@ -40,7 +39,7 @@ def config():
|
|||
|
||||
@pytest.fixture(scope='session')
|
||||
def _app(config: TestConfig) -> Devicehub:
|
||||
return Devicehub(config=config, db=db)
|
||||
return Devicehub(inventory='test', config=config, db=db)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -50,14 +49,23 @@ def app(request, _app: Devicehub) -> Devicehub:
|
|||
with _app.app_context():
|
||||
db.drop_all()
|
||||
|
||||
def _init():
|
||||
_app.init_db(name='Test Inventory',
|
||||
org_name='FooOrg',
|
||||
org_id='foo-org-id',
|
||||
tag_url=boltons.urlutils.URL('https://example.com'),
|
||||
tag_token=uuid.UUID('52dacef0-6bcb-4919-bfed-f10d2c96ecee'),
|
||||
erase=False,
|
||||
common=True)
|
||||
|
||||
with _app.app_context():
|
||||
try:
|
||||
with redirect_stdout(io.StringIO()):
|
||||
_app.init_db()
|
||||
except (ProgrammingError, IntegrityError):
|
||||
_init()
|
||||
except (ProgrammingError, IntegrityError, AssertionError):
|
||||
print('Database was not correctly emptied. Re-empty and re-installing...')
|
||||
_drop()
|
||||
_app.init_db()
|
||||
_init()
|
||||
|
||||
request.addfinalizer(_drop)
|
||||
return _app
|
||||
|
|
|
@ -5,20 +5,21 @@ device:
|
|||
type: Desktop
|
||||
chassis: Tower
|
||||
components:
|
||||
- manufacturer: p1c1m
|
||||
serialNumber: p1c1s
|
||||
type: Motherboard
|
||||
- manufacturer: p1c2m
|
||||
serialNumber: p1c2s
|
||||
model: p1c2
|
||||
speed: 1.23
|
||||
cores: 2
|
||||
type: Processor
|
||||
- manufacturer: p1c3m
|
||||
serialNumber: p1c3s
|
||||
type: GraphicCard
|
||||
memory: 1.5
|
||||
- manufacturer: p1c1m
|
||||
serialNumber: p1c1s
|
||||
type: Motherboard
|
||||
- manufacturer: p1c2m
|
||||
serialNumber: p1c2s
|
||||
model: p1c2
|
||||
speed: 1.23
|
||||
cores: 2
|
||||
type: Processor
|
||||
- manufacturer: p1c3m
|
||||
serialNumber: p1c3s
|
||||
type: GraphicCard
|
||||
memory: 1.5
|
||||
elapsed: 25
|
||||
software: Workbench
|
||||
uuid: 76860eca-c3fd-41f6-a801-6af7bd8cf832
|
||||
version: '11.0'
|
||||
type: Snapshot
|
||||
|
|
|
@ -5,16 +5,17 @@ device:
|
|||
type: Desktop
|
||||
chassis: Microtower
|
||||
components:
|
||||
- manufacturer: p2c1m
|
||||
serialNumber: p2c1s
|
||||
type: Motherboard
|
||||
- manufacturer: p1c2m
|
||||
serialNumber: p1c2s
|
||||
model: p1c2
|
||||
speed: 1.23
|
||||
cores: 2
|
||||
type: Processor
|
||||
- manufacturer: p2c1m
|
||||
serialNumber: p2c1s
|
||||
type: Motherboard
|
||||
- manufacturer: p1c2m
|
||||
serialNumber: p1c2s
|
||||
model: p1c2
|
||||
speed: 1.23
|
||||
cores: 2
|
||||
type: Processor
|
||||
elapsed: 25
|
||||
software: Workbench
|
||||
uuid: f2e02261-87a1-4a50-b9b7-92c0e476e5f2
|
||||
version: '11.0'
|
||||
type: Snapshot
|
||||
|
|
|
@ -5,17 +5,18 @@ device:
|
|||
type: Desktop
|
||||
chassis: Microtower
|
||||
components:
|
||||
- manufacturer: p1c2m
|
||||
serialNumber: p1c2s
|
||||
model: p1c2
|
||||
type: Processor
|
||||
cores: 2
|
||||
speed: 1.23
|
||||
- manufacturer: p1c3m
|
||||
serialNumber: p1c3s
|
||||
type: GraphicCard
|
||||
memory: 1.5
|
||||
- manufacturer: p1c2m
|
||||
serialNumber: p1c2s
|
||||
model: p1c2
|
||||
type: Processor
|
||||
cores: 2
|
||||
speed: 1.23
|
||||
- manufacturer: p1c3m
|
||||
serialNumber: p1c3s
|
||||
type: GraphicCard
|
||||
memory: 1.5
|
||||
elapsed: 30
|
||||
software: Workbench
|
||||
uuid: 3be271b6-5ef4-47d8-8237-5e1133eebfc6
|
||||
version: '11.0'
|
||||
type: Snapshot
|
||||
|
|
|
@ -5,16 +5,17 @@ device:
|
|||
type: Desktop
|
||||
chassis: Tower
|
||||
components:
|
||||
- manufacturer: p1c4m
|
||||
serialNumber: p1c4s
|
||||
type: NetworkAdapter
|
||||
speed: 1000
|
||||
wireless: False
|
||||
- manufacturer: p1c3m
|
||||
serialNumber: p1c3s
|
||||
type: GraphicCard
|
||||
memory: 1.5
|
||||
- manufacturer: p1c4m
|
||||
serialNumber: p1c4s
|
||||
type: NetworkAdapter
|
||||
speed: 1000
|
||||
wireless: False
|
||||
- manufacturer: p1c3m
|
||||
serialNumber: p1c3s
|
||||
type: GraphicCard
|
||||
memory: 1.5
|
||||
elapsed: 25
|
||||
software: Workbench
|
||||
uuid: fd007eb4-48e3-454a-8763-169491904c6e
|
||||
version: '11.0'
|
||||
type: Snapshot
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
{
|
||||
"closed": true,
|
||||
"components": [
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Intel Corporation",
|
||||
"model": "NM10/ICH7 Family High Definition Audio Controller",
|
||||
"serialNumber": null,
|
||||
"type": "SoundCard"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Broadcom Inc. and subsidiaries",
|
||||
"model": "NetLink BCM5786 Gigabit Ethernet PCI Express",
|
||||
"serialNumber": "00:1a:6b:5e:7f:10",
|
||||
"speed": 1000,
|
||||
"type": "NetworkAdapter",
|
||||
"wireless": false
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"format": "DIMM",
|
||||
"interface": "DDR",
|
||||
"manufacturer": null,
|
||||
"model": null,
|
||||
"serialNumber": null,
|
||||
"size": 1024,
|
||||
"speed": 133.0,
|
||||
"type": "RamModule"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"format": "DIMM",
|
||||
"interface": "DDR",
|
||||
"manufacturer": null,
|
||||
"model": null,
|
||||
"serialNumber": null,
|
||||
"size": 1024,
|
||||
"speed": 133.0,
|
||||
"type": "RamModule"
|
||||
},
|
||||
{
|
||||
"address": 64,
|
||||
"events": [
|
||||
{
|
||||
"elapsed": 33,
|
||||
"rate": 32.9274,
|
||||
"type": "BenchmarkProcessorSysbench"
|
||||
},
|
||||
{
|
||||
"elapsed": 0,
|
||||
"rate": 8771.5,
|
||||
"type": "BenchmarkProcessor"
|
||||
}
|
||||
],
|
||||
"manufacturer": "Intel Corp.",
|
||||
"model": "Intel Core2 Duo CPU E4500 @ 2.20GHz",
|
||||
"serialNumber": null,
|
||||
"speed": 1.1,
|
||||
"threads": 2,
|
||||
"type": "Processor"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Intel Corporation",
|
||||
"memory": 256.0,
|
||||
"model": "82946GZ/GL Integrated Graphics Controller",
|
||||
"serialNumber": null,
|
||||
"type": "GraphicCard"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"firewire": 0,
|
||||
"manufacturer": "LENOVO",
|
||||
"model": "LENOVO",
|
||||
"pcmcia": 0,
|
||||
"serial": 1,
|
||||
"serialNumber": null,
|
||||
"slots": 0,
|
||||
"type": "Motherboard",
|
||||
"usb": 5
|
||||
}
|
||||
],
|
||||
"device": {
|
||||
"chassis": "Microtower",
|
||||
"events": [
|
||||
{
|
||||
"appearanceRange": "D",
|
||||
"biosRange": "E",
|
||||
"functionalityRange": "D",
|
||||
"type": "WorkbenchRate"
|
||||
},
|
||||
{
|
||||
"elapsed": 300,
|
||||
"severity": "Info",
|
||||
"type": "StressTest"
|
||||
},
|
||||
{
|
||||
"elapsed": 2,
|
||||
"rate": 1.4968,
|
||||
"type": "BenchmarkRamSysbench"
|
||||
}
|
||||
],
|
||||
"manufacturer": "LENOVO",
|
||||
"model": "9644W8N",
|
||||
"serialNumber": "0169622",
|
||||
"type": "Desktop"
|
||||
},
|
||||
"elapsed": 338,
|
||||
"endTime": "2019-02-13T11:57:31.378330+00:00",
|
||||
"expectedEvents": [
|
||||
"Benchmark",
|
||||
"TestDataStorage",
|
||||
"StressTest",
|
||||
"Install"
|
||||
],
|
||||
"software": "Workbench",
|
||||
"type": "Snapshot",
|
||||
"uuid": "d7904bd3-7d0f-4918-86b1-e21bfab738f9",
|
||||
"version": "11.0b5"
|
||||
}
|
|
@ -75,7 +75,7 @@
|
|||
],
|
||||
"type": "EraseBasic",
|
||||
"severity": "Info",
|
||||
"zeros": false,
|
||||
|
||||
"startTime": "2018-07-13T10:52:45.092612"
|
||||
},
|
||||
{
|
||||
|
@ -113,7 +113,6 @@
|
|||
],
|
||||
"type": "EraseBasic",
|
||||
"severity": "Info",
|
||||
"zeros": false,
|
||||
"startTime": "2018-07-13T11:54:55.100667"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -10,29 +10,28 @@ device:
|
|||
model: pc1ml
|
||||
manufacturer: pc1mr
|
||||
components:
|
||||
- type: SolidStateDrive
|
||||
serialNumber: c1s
|
||||
model: c1ml
|
||||
manufacturer: c1mr
|
||||
events:
|
||||
- type: EraseSectors
|
||||
zeros: True
|
||||
startTime: 2018-06-01T08:12:06
|
||||
endTime: 2018-06-01T09:12:06
|
||||
steps:
|
||||
- type: StepZero
|
||||
severity: Info
|
||||
startTime: 2018-06-01T08:15:00
|
||||
endTime: 2018-06-01T09:16:00
|
||||
- type: StepZero
|
||||
severity: Info
|
||||
startTime: 2018-06-01T08:16:00
|
||||
endTime: 2018-06-01T09:17:00
|
||||
- type: Processor
|
||||
serialNumber: p1s
|
||||
model: p1ml
|
||||
manufacturer: p1mr
|
||||
- type: RamModule
|
||||
serialNumber: rm1s
|
||||
model: rm1ml
|
||||
manufacturer: rm1mr
|
||||
- type: SolidStateDrive
|
||||
serialNumber: c1s
|
||||
model: c1ml
|
||||
manufacturer: c1mr
|
||||
events:
|
||||
- type: EraseSectors
|
||||
startTime: '2018-06-01T08:12:06+02:00'
|
||||
endTime: '2018-06-01T09:12:06+02:00'
|
||||
steps:
|
||||
- type: StepZero
|
||||
severity: Info
|
||||
startTime: '2018-06-01T08:15:00+02:00'
|
||||
endTime: '2018-06-01T09:16:00+02:00'
|
||||
- type: StepRandom
|
||||
severity: Info
|
||||
startTime: '2018-06-01T08:16:00+02:00'
|
||||
endTime: '2018-06-01T09:17:00+02:00'
|
||||
- type: Processor
|
||||
serialNumber: p1s
|
||||
model: p1ml
|
||||
manufacturer: p1mr
|
||||
- type: RamModule
|
||||
serialNumber: rm1s
|
||||
model: rm1ml
|
||||
manufacturer: rm1mr
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
{
|
||||
"closed": true,
|
||||
"components": [
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Qualcomm Atheros",
|
||||
"model": "QCA9565 / AR9565 Wireless Network Adapter",
|
||||
"serialNumber": "ac:e0:10:c2:e3:ac",
|
||||
"type": "NetworkAdapter",
|
||||
"wireless": true
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Realtek Semiconductor Co., Ltd.",
|
||||
"model": "RTL810xE PCI Express Fast Ethernet controller",
|
||||
"serialNumber": "30:8d:99:25:6c:d9",
|
||||
"speed": 100,
|
||||
"type": "NetworkAdapter",
|
||||
"wireless": false
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Advanced Micro Devices, Inc. AMD/ATI",
|
||||
"model": "Kabini HDMI/DP Audio",
|
||||
"serialNumber": null,
|
||||
"type": "SoundCard"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Chicony Electronics Co.,Ltd.",
|
||||
"model": "HP Webcam",
|
||||
"serialNumber": "0x0001",
|
||||
"type": "SoundCard"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Advanced Micro Devices, Inc. AMD",
|
||||
"model": "FCH Azalia Controller",
|
||||
"serialNumber": null,
|
||||
"type": "SoundCard"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"format": "SODIMM",
|
||||
"interface": "DDR3",
|
||||
"manufacturer": "Hynix",
|
||||
"model": "HMT451S6AFR8A-PB",
|
||||
"serialNumber": "11743764",
|
||||
"size": 4096,
|
||||
"speed": 667.0,
|
||||
"type": "RamModule"
|
||||
},
|
||||
{
|
||||
"address": 64,
|
||||
"cores": 2,
|
||||
"events": [
|
||||
{
|
||||
"elapsed": 0,
|
||||
"rate": 3992.32,
|
||||
"type": "BenchmarkProcessor"
|
||||
},
|
||||
{
|
||||
"elapsed": 65,
|
||||
"rate": 65.3007,
|
||||
"type": "BenchmarkProcessorSysbench"
|
||||
}
|
||||
],
|
||||
"manufacturer": "Advanced Micro Devices AMD",
|
||||
"model": "AMD E1-2100 APU with Radeon HD Graphics",
|
||||
"serialNumber": null,
|
||||
"speed": 0.9,
|
||||
"threads": 2,
|
||||
"type": "Processor"
|
||||
},
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"elapsed": 12,
|
||||
"readSpeed": 90.0,
|
||||
"type": "BenchmarkDataStorage",
|
||||
"writeSpeed": 30.7
|
||||
},
|
||||
{
|
||||
"assessment": true,
|
||||
"commandTimeout": 1341,
|
||||
"currentPendingSectorCount": 0,
|
||||
"elapsed": 113,
|
||||
"length": "Short",
|
||||
"lifetime": 1782,
|
||||
"offlineUncorrectable": 0,
|
||||
"powerCycleCount": 806,
|
||||
"reallocatedSectorCount": 224,
|
||||
"reportedUncorrectableErrors": 9961472,
|
||||
"severity": "Info",
|
||||
"status": "Completed without error",
|
||||
"type": "TestDataStorage"
|
||||
},
|
||||
{
|
||||
"address": 32,
|
||||
"elapsed": 690,
|
||||
"name": "LinuxMint-19-x86-es-2018-12.fsa",
|
||||
"severity": "Info",
|
||||
"type": "Install"
|
||||
}
|
||||
],
|
||||
"interface": "ATA",
|
||||
"manufacturer": null,
|
||||
"model": "HGST HTS545050A7",
|
||||
"serialNumber": "TE85134N34LNSN",
|
||||
"size": 476940,
|
||||
"type": "HardDrive"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"manufacturer": "Advanced Micro Devices, Inc. AMD/ATI",
|
||||
"memory": 256.0,
|
||||
"model": "Kabini Radeon HD 8210",
|
||||
"serialNumber": null,
|
||||
"type": "GraphicCard"
|
||||
},
|
||||
{
|
||||
"events": [],
|
||||
"firewire": 0,
|
||||
"manufacturer": "Hewlett-Packard",
|
||||
"model": "21F7",
|
||||
"pcmcia": 0,
|
||||
"serial": 1,
|
||||
"serialNumber": "PEHERF41U8P9TV",
|
||||
"slots": 0,
|
||||
"type": "Motherboard",
|
||||
"usb": 5
|
||||
}
|
||||
],
|
||||
"device": {
|
||||
"chassis": "Netbook",
|
||||
"events": [
|
||||
{
|
||||
"appearanceRange": "A",
|
||||
"functionalityRange": "A",
|
||||
"type": "WorkbenchRate"
|
||||
},
|
||||
{
|
||||
"elapsed": 300,
|
||||
"severity": "Info",
|
||||
"type": "StressTest"
|
||||
},
|
||||
{
|
||||
"elapsed": 6,
|
||||
"rate": 5.8783,
|
||||
"type": "BenchmarkRamSysbench"
|
||||
}
|
||||
],
|
||||
"manufacturer": "Hewlett-Packard",
|
||||
"model": "HP 255 G3 Notebook",
|
||||
"serialNumber": "CND52270FW",
|
||||
"type": "Laptop"
|
||||
},
|
||||
"elapsed": 1194,
|
||||
"endTime": "2019-02-13T10:13:50.535387+00:00",
|
||||
"expectedEvents": [
|
||||
"Benchmark",
|
||||
"TestDataStorage",
|
||||
"StressTest",
|
||||
"Install"
|
||||
],
|
||||
"software": "Workbench",
|
||||
"type": "Snapshot",
|
||||
"uuid": "ca564895-567e-4ac2-9a0d-2d1402528687",
|
||||
"version": "11.0b5"
|
||||
}
|
|
@ -10,7 +10,6 @@ type: 'EraseSectors'
|
|||
severity: Info
|
||||
# snapshot: None fulfill!
|
||||
# device: None fulfill!
|
||||
zeros: False
|
||||
startTime: 2018-01-01T10:10:10
|
||||
endTime: 2018-01-01T12:10:10
|
||||
steps:
|
||||
|
|
|
@ -107,8 +107,7 @@ def test_default_org_exists(config: DevicehubConfig):
|
|||
initialization and that is accessible for the method
|
||||
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
|
||||
"""
|
||||
assert models.Organization.query.filter_by(name=config.ORGANIZATION_NAME,
|
||||
tax_id=config.ORGANIZATION_TAX_ID).one()
|
||||
assert models.Organization.query.filter_by(name='FooOrg', tax_id='foo-org-id').one()
|
||||
assert isinstance(models.Organization.get_default_org_id(), UUID)
|
||||
|
||||
|
||||
|
|
|
@ -21,13 +21,14 @@ def test_api_docs(client: Client):
|
|||
'/users/',
|
||||
'/devices/',
|
||||
'/tags/',
|
||||
'/snapshots/',
|
||||
'/users/login',
|
||||
'/users/login/',
|
||||
'/events/',
|
||||
'/lots/',
|
||||
'/manufacturers/',
|
||||
'/lots/{id}/children',
|
||||
'/lots/{id}/devices',
|
||||
'/documents/erasures/',
|
||||
'/documents/static/{filename}',
|
||||
'/tags/{tag_id}/device/{device_id}',
|
||||
'/devices/static/{filename}'
|
||||
}
|
||||
|
@ -40,4 +41,4 @@ def test_api_docs(client: Client):
|
|||
'scheme': 'basic',
|
||||
'name': 'Authorization'
|
||||
}
|
||||
assert 94 == len(docs['definitions'])
|
||||
assert len(docs['definitions']) == 96
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from teal.db import UniqueViolation
|
||||
|
||||
|
||||
def test_unique_violation():
|
||||
class IntegrityErrorMock:
|
||||
def __init__(self) -> None:
|
||||
self.params = {
|
||||
'uuid': UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9'),
|
||||
'version': '11.0',
|
||||
'software': 'Workbench', 'elapsed': datetime.timedelta(0, 4),
|
||||
'expected_events': None,
|
||||
'id': UUID('dbdef3d8-2cac-48cb-adb8-419bc3e59687')
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return """(psycopg2.IntegrityError) duplicate key value violates unique constraint "snapshot_uuid_key"
|
||||
DETAIL: Key (uuid)=(f5efd26e-8754-46bc-87bf-fbccc39d60d9) already exists.
|
||||
[SQL: 'INSERT INTO snapshot (uuid, version, software, elapsed, expected_events, id)
|
||||
VALUES (%(uuid)s, %(version)s, %(software)s, %(elapsed)s, CAST(%(expected_events)s
|
||||
AS snapshotexpectedevents[]), %(id)s)'] [parameters: {'uuid': UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9'),
|
||||
'version': '11.0', 'software': 'Workbench', 'elapsed': datetime.timedelta(0, 4), 'expected_events': None,
|
||||
'id': UUID('dbdef3d8-2cac-48cb-adb8-419bc3e59687')}] (Background on this error at: http://sqlalche.me/e/gkpj)"""
|
||||
|
||||
u = UniqueViolation(IntegrityErrorMock())
|
||||
assert u.constraint == 'snapshot_uuid_key'
|
||||
assert u.field_name == 'uuid'
|
||||
assert u.field_value == UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9')
|
|
@ -116,19 +116,11 @@ def test_physical_properties():
|
|||
'serial': None,
|
||||
'firewire': None,
|
||||
'manufacturer': 'mr',
|
||||
'weight': None,
|
||||
'height': None,
|
||||
'width': 2.0,
|
||||
'depth': None
|
||||
}
|
||||
assert pc.physical_properties == {
|
||||
'model': 'foo',
|
||||
'manufacturer': 'bar',
|
||||
'serial_number': 'foo-bar',
|
||||
'weight': 2.8,
|
||||
'width': 1.4,
|
||||
'height': 2.1,
|
||||
'depth': None,
|
||||
'chassis': ComputerChassis.Tower
|
||||
}
|
||||
|
||||
|
@ -350,7 +342,7 @@ def test_sync_execute_register_tag_linked_other_device_mismatch_between_tags():
|
|||
db.session.add(Tag(id='foo-1', device=pc1))
|
||||
pc2 = d.Desktop(**conftest.file('pc-components.db')['device'])
|
||||
pc2.serial_number = 'pc2-serial'
|
||||
pc2.hid = Naming.hid(pc2.manufacturer, pc2.serial_number, pc2.model)
|
||||
pc2.hid = Naming.hid(pc2.type, pc2.manufacturer, pc2.model, pc2.serial_number)
|
||||
db.session.add(Tag(id='foo-2', device=pc2))
|
||||
db.session.commit()
|
||||
|
||||
|
@ -374,7 +366,7 @@ def test_sync_execute_register_mismatch_between_tags_and_hid():
|
|||
db.session.add(Tag(id='foo-1', device=pc1))
|
||||
pc2 = d.Desktop(**conftest.file('pc-components.db')['device'])
|
||||
pc2.serial_number = 'pc2-serial'
|
||||
pc2.hid = Naming.hid(pc2.manufacturer, pc2.serial_number, pc2.model)
|
||||
pc2.hid = Naming.hid(pc2.type, pc2.manufacturer, pc2.model, pc2.serial_number)
|
||||
db.session.add(Tag(id='foo-2', device=pc2))
|
||||
db.session.commit()
|
||||
|
||||
|
@ -414,7 +406,7 @@ def test_get_device(app: Devicehub, user: UserClient):
|
|||
assert 'events_one' not in pc, 'they are internal use only'
|
||||
assert 'author' not in pc
|
||||
assert tuple(c['id'] for c in pc['components']) == (2, 3)
|
||||
assert pc['hid'] == 'p1ma-p1s-p1mo'
|
||||
assert pc['hid'] == 'desktop-p1ma-p1mo-p1s'
|
||||
assert pc['model'] == 'p1mo'
|
||||
assert pc['manufacturer'] == 'p1ma'
|
||||
assert pc['serialNumber'] == 'p1s'
|
||||
|
@ -462,16 +454,6 @@ def test_computer_monitor():
|
|||
db.session.commit()
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Make test')
|
||||
def test_mobile_meid():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Make test')
|
||||
def test_mobile_imei():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Make test')
|
||||
def test_computer_with_display():
|
||||
pass
|
||||
|
|
|
@ -184,11 +184,6 @@ def test_device_query(user: UserClient):
|
|||
assert not pc['tags']
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Functionality not yet developed.')
|
||||
def test_device_lots_query(user: UserClient):
|
||||
pass
|
||||
|
||||
|
||||
def test_device_search_all_devices_token_if_empty(app: Devicehub, user: UserClient):
|
||||
"""Ensures DeviceSearch can regenerate itself when the table is empty."""
|
||||
user.post(file('basic.snapshot'), res=Snapshot)
|
||||
|
@ -214,7 +209,7 @@ def test_device_search_regenerate_table(app: DeviceSearch, user: UserClient):
|
|||
i, _ = user.get(res=Device, query=[('search', 'Desktop')])
|
||||
assert not i['items'], 'Truncate deleted all items'
|
||||
runner = app.test_cli_runner()
|
||||
runner.invoke(args=['regenerate-search'], catch_exceptions=False)
|
||||
runner.invoke('inv', 'search')
|
||||
i, _ = user.get(res=Device, query=[('search', 'Desktop')])
|
||||
assert i['items'], 'Regenerated re-made the table'
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.dispatchers import PathDispatcher
|
||||
from tests.conftest import TestConfig
|
||||
|
||||
|
||||
def noop():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dispatcher(app: Devicehub, config: TestConfig) -> PathDispatcher:
|
||||
PathDispatcher.call = Mock(side_effect=lambda *args: args[0])
|
||||
return PathDispatcher(config_cls=config)
|
||||
|
||||
|
||||
def test_dispatcher_default(dispatcher: PathDispatcher):
|
||||
"""The dispatcher returns not found for an URL that does not
|
||||
route to an app.
|
||||
"""
|
||||
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/'}, noop)
|
||||
assert app == PathDispatcher.NOT_FOUND
|
||||
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/foo/foo'}, noop)
|
||||
assert app == PathDispatcher.NOT_FOUND
|
||||
|
||||
|
||||
def test_dispatcher_return_app(dispatcher: PathDispatcher):
|
||||
"""The dispatcher returns the correct app for the URL"""
|
||||
# Note that the dispatcher does not check if the URL points
|
||||
# to a well-known endpoint for the app.
|
||||
# Only if can route it to an app. And then the app checks
|
||||
# if the path exists
|
||||
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/test/foo/'}, noop)
|
||||
assert isinstance(app, Devicehub)
|
||||
assert app.id == 'test'
|
||||
|
||||
|
||||
def test_dispatcher_users(dispatcher: PathDispatcher):
|
||||
"""Users special endpoint returns an app"""
|
||||
# For now returns the first app, as all apps
|
||||
# can answer {}/users/login
|
||||
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/users/'}, noop)
|
||||
assert isinstance(app, Devicehub)
|
||||
assert app.id == 'test'
|
|
@ -0,0 +1,65 @@
|
|||
import teal.marshmallow
|
||||
from ereuse_utils.test import ANY
|
||||
|
||||
from ereuse_devicehub.client import Client, UserClient
|
||||
from ereuse_devicehub.resources.documents import documents as docs
|
||||
from ereuse_devicehub.resources.event import models as e
|
||||
from tests.conftest import file
|
||||
|
||||
|
||||
def test_erasure_certificate_public_one(user: UserClient, client: Client):
|
||||
"""Public user can get certificate from one device as HTML or PDF."""
|
||||
s = file('erase-sectors.snapshot')
|
||||
snapshot, _ = user.post(s, res=e.Snapshot)
|
||||
|
||||
doc, response = client.get(res=docs.DocumentDef.t,
|
||||
item='erasures/{}'.format(snapshot['device']['id']),
|
||||
accept=ANY)
|
||||
assert 'html' in response.content_type
|
||||
assert '<html' in doc
|
||||
assert '2018' in doc
|
||||
|
||||
doc, response = client.get(res=docs.DocumentDef.t,
|
||||
item='erasures/{}'.format(snapshot['device']['id']),
|
||||
query=[('format', 'PDF')],
|
||||
accept='application/pdf')
|
||||
assert 'application/pdf' == response.content_type
|
||||
|
||||
erasure = next(e for e in snapshot['events'] if e['type'] == 'EraseSectors')
|
||||
|
||||
doc, response = client.get(res=docs.DocumentDef.t,
|
||||
item='erasures/{}'.format(erasure['id']),
|
||||
accept=ANY)
|
||||
assert 'html' in response.content_type
|
||||
assert '<html' in doc
|
||||
assert '2018' in doc
|
||||
|
||||
|
||||
def test_erasure_certificate_private_query(user: UserClient):
|
||||
"""Logged-in user can get certificates using queries as HTML and
|
||||
PDF.
|
||||
"""
|
||||
s = file('erase-sectors.snapshot')
|
||||
snapshot, response = user.post(s, res=e.Snapshot)
|
||||
|
||||
doc, response = user.get(res=docs.DocumentDef.t,
|
||||
item='erasures/',
|
||||
query=[('filter', {'id': [snapshot['device']['id']]})],
|
||||
accept=ANY)
|
||||
assert 'html' in response.content_type
|
||||
assert '<html' in doc
|
||||
assert '2018' in doc
|
||||
|
||||
doc, response = user.get(res=docs.DocumentDef.t,
|
||||
item='erasures/',
|
||||
query=[
|
||||
('filter', {'id': [snapshot['device']['id']]}),
|
||||
('format', 'PDF')
|
||||
],
|
||||
accept='application/pdf')
|
||||
assert 'application/pdf' == response.content_type
|
||||
|
||||
|
||||
def test_erasure_certificate_wrong_id(client: Client):
|
||||
client.get(res=docs.DocumentDef.t, item='erasures/this-is-not-an-id',
|
||||
status=teal.marshmallow.ValidationError)
|
|
@ -4,6 +4,6 @@ from ereuse_devicehub.devicehub import Devicehub
|
|||
def test_dummy(_app: Devicehub):
|
||||
"""Tests the dummy cli command."""
|
||||
runner = _app.test_cli_runner()
|
||||
runner.invoke(args=['dummy', '--yes'], catch_exceptions=False)
|
||||
runner.invoke('dummy', '--yes')
|
||||
with _app.app_context():
|
||||
_app.db.drop_all()
|
||||
|
|
|
@ -10,6 +10,7 @@ from teal.enums import Currency, Subdivision
|
|||
|
||||
from ereuse_devicehub.client import UserClient
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources import enums
|
||||
from ereuse_devicehub.resources.device import states
|
||||
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \
|
||||
RamModule, SolidStateDrive
|
||||
|
@ -40,7 +41,10 @@ def test_author():
|
|||
def test_erase_basic():
|
||||
erasure = models.EraseBasic(
|
||||
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
||||
zeros=True,
|
||||
steps=[
|
||||
models.StepZero(**conftest.T),
|
||||
models.StepRandom(**conftest.T)
|
||||
],
|
||||
**conftest.T
|
||||
)
|
||||
db.session.add(erasure)
|
||||
|
@ -48,6 +52,7 @@ def test_erase_basic():
|
|||
db_erasure = models.EraseBasic.query.one()
|
||||
assert erasure == db_erasure
|
||||
assert next(iter(db_erasure.device.events)) == erasure
|
||||
assert not erasure.standards, 'EraseBasic themselves do not have standards'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||
|
@ -65,14 +70,13 @@ def test_validate_device_data_storage():
|
|||
|
||||
|
||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||
def test_erase_sectors_steps():
|
||||
def test_erase_sectors_steps_erasure_standards_hmg_is5():
|
||||
erasure = models.EraseSectors(
|
||||
device=SolidStateDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
||||
zeros=True,
|
||||
steps=[
|
||||
models.StepZero(**conftest.T),
|
||||
models.StepRandom(**conftest.T),
|
||||
models.StepZero(**conftest.T)
|
||||
models.StepRandom(**conftest.T)
|
||||
],
|
||||
**conftest.T
|
||||
)
|
||||
|
@ -83,6 +87,7 @@ def test_erase_sectors_steps():
|
|||
assert db_erasure.steps[0].num == 0
|
||||
assert db_erasure.steps[1].num == 1
|
||||
assert db_erasure.steps[2].num == 2
|
||||
assert {enums.ErasureStandards.HMG_IS5} == erasure.standards
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||
|
@ -254,8 +259,10 @@ def test_live_geoip():
|
|||
|
||||
|
||||
@pytest.mark.xfail(reson='Develop reserve')
|
||||
def test_reserve(user: UserClient):
|
||||
"""Performs a reservation and then cancels it."""
|
||||
def test_reserve_and_cancel(user: UserClient):
|
||||
"""Performs a reservation and then cancels it,
|
||||
checking the attribute `reservees`.
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.parametrize('event_model_state', [
|
||||
|
@ -308,9 +315,21 @@ def test_price_custom():
|
|||
assert c['price']['id'] == p['id']
|
||||
|
||||
|
||||
@pytest.mark.xfail(reson='Develop test')
|
||||
def test_price_custom_client():
|
||||
def test_price_custom_client(user: UserClient):
|
||||
"""As test_price_custom but creating the price through the API."""
|
||||
s = file('basic.snapshot')
|
||||
snapshot, _ = user.post(s, res=models.Snapshot)
|
||||
price, _ = user.post({
|
||||
'type': 'Price',
|
||||
'price': 25,
|
||||
'currency': Currency.EUR.name,
|
||||
'device': snapshot['device']['id']
|
||||
}, res=models.Event)
|
||||
assert 25 == price['price']
|
||||
assert Currency.EUR.name == price['currency']
|
||||
|
||||
device, _ = user.get(res=Device, item=price['device']['id'])
|
||||
assert 25 == device['price']['price']
|
||||
|
||||
|
||||
@pytest.mark.xfail(reson='Develop test')
|
||||
|
@ -320,3 +339,41 @@ def test_ereuse_price():
|
|||
return correct results."""
|
||||
# important to check Range.low no returning warranty2
|
||||
# Range.verylow not returning nothing
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||
def test_erase_physical():
|
||||
erasure = models.ErasePhysical(
|
||||
device=HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar'),
|
||||
method=enums.PhysicalErasureMethod.Disintegration
|
||||
)
|
||||
db.session.add(erasure)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@pytest.mark.xfail(reson='Adapt rate algorithm to re-compute by passing a manual rate.')
|
||||
def test_manual_rate_after_workbench_rate(user: UserClient):
|
||||
"""Perform a WorkbenchRate and then update the device with a ManualRate.
|
||||
|
||||
Devicehub must make the final rate with the first workbench rate
|
||||
plus the new manual rate, without considering the appearance /
|
||||
functionality values of the workbench rate.
|
||||
"""
|
||||
s = file('real-hp.snapshot.11')
|
||||
snapshot, _ = user.post(s, res=models.Snapshot)
|
||||
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
||||
assert 'B' == device['rate']['appearanceRange']
|
||||
assert device['rate'] == 1
|
||||
user.post({
|
||||
'type': 'ManualRate',
|
||||
'device': device['id'],
|
||||
'appearanceRange': 'A',
|
||||
'functionalityRange': 'A'
|
||||
}, res=models.Event)
|
||||
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
||||
assert 'A' == device['rate']['appearanceRange']
|
||||
|
||||
|
||||
@pytest.mark.xfail(reson='Develop an algorithm that can make rates only from manual rates')
|
||||
def test_manual_rate_without_workbench_rate(user: UserClient):
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
import click.testing
|
||||
import pytest
|
||||
from boltons.urlutils import URL
|
||||
|
||||
import ereuse_devicehub.cli
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.resources.agent.models import Organization
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
from ereuse_devicehub.resources.user import User
|
||||
from tests.conftest import TestConfig
|
||||
|
||||
"""
|
||||
Tests the management of inventories in a multi-inventory environment
|
||||
(several Devicehub instances that point at different schemas).
|
||||
"""
|
||||
|
||||
|
||||
class NoExcCliRunner(click.testing.CliRunner):
|
||||
"""Runner that interfaces with the Devicehub CLI."""
|
||||
|
||||
def invoke(self, *args, input=None, env=None, catch_exceptions=False, color=False,
|
||||
**extra):
|
||||
r = super().invoke(ereuse_devicehub.cli.cli,
|
||||
args, input, env, catch_exceptions, color, **extra)
|
||||
assert r.exit_code == 0, 'CLI code {}: {}'.format(r.exit_code, r.output)
|
||||
return r
|
||||
|
||||
def inv(self, name: str):
|
||||
"""Set an inventory as an environment variable."""
|
||||
self.env = {'dhi': name}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def cli(config, _app):
|
||||
"""Returns an interface for the dh CLI client,
|
||||
cleaning the database afterwards.
|
||||
"""
|
||||
|
||||
def drop_schemas():
|
||||
with _app.app_context():
|
||||
_app.db.drop_schema(schema='tdb1')
|
||||
_app.db.drop_schema(schema='tdb2')
|
||||
_app.db.drop_schema(schema='common')
|
||||
|
||||
drop_schemas()
|
||||
ereuse_devicehub.cli.DevicehubGroup.CONFIG = TestConfig
|
||||
yield NoExcCliRunner()
|
||||
drop_schemas()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tdb1(config):
|
||||
return Devicehub(inventory='tdb1', config=config, db=db)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tdb2(config):
|
||||
return Devicehub(inventory='tdb2', config=config, db=db)
|
||||
|
||||
|
||||
def test_inventory_create_delete_user(cli, tdb1, tdb2):
|
||||
"""Tests creating two inventories with users, one user has
|
||||
access to the first inventory and the other to both. Finally, deletes
|
||||
the first inventory, deleting only the first user too.
|
||||
"""
|
||||
# Create first DB
|
||||
cli.inv('tdb1')
|
||||
cli.invoke('inv', 'add',
|
||||
'-n', 'Test DB1',
|
||||
'-on', 'ACME DB1',
|
||||
'-oi', 'acme-id',
|
||||
'-tu', 'https://example.com',
|
||||
'-tt', '3c66a6ad-22de-4db6-ac46-d8982522ec40',
|
||||
'--common')
|
||||
|
||||
# Create an user for first DB
|
||||
cli.invoke('user', 'add', 'foo@foo.com', '-a', 'Foo', '-c', 'ES', '-p', 'Such password')
|
||||
|
||||
with tdb1.app_context():
|
||||
# There is a row for the inventory
|
||||
inv = Inventory.query.one() # type: Inventory
|
||||
assert inv.id == 'tdb1'
|
||||
assert inv.name == 'Test DB1'
|
||||
assert inv.tag_provider == URL('https://example.com')
|
||||
assert inv.tag_token == UUID('3c66a6ad-22de-4db6-ac46-d8982522ec40')
|
||||
assert db.has_schema('tdb1')
|
||||
org = Organization.query.one() # type: Organization
|
||||
# assert inv.org_id == org.id
|
||||
assert org.name == 'ACME DB1'
|
||||
assert org.tax_id == 'acme-id'
|
||||
user = User.query.one() # type: User
|
||||
assert user.email == 'foo@foo.com'
|
||||
|
||||
cli.inv('tdb2')
|
||||
# Create a second DB
|
||||
# Note how we don't create common anymore
|
||||
cli.invoke('inv', 'add',
|
||||
'-n', 'Test DB2',
|
||||
'-on', 'ACME DB2',
|
||||
'-oi', 'acme-id-2',
|
||||
'-tu', 'https://example.com',
|
||||
'-tt', 'fbad1c08-ffdc-4a61-be49-464962c186a8')
|
||||
# Create an user for with access for both DB
|
||||
cli.invoke('user', 'add', 'bar@bar.com', '-a', 'Bar', '-p', 'Wow password')
|
||||
|
||||
with tdb2.app_context():
|
||||
inventories = Inventory.query.all() # type: List[Inventory]
|
||||
assert len(inventories) == 2
|
||||
assert inventories[0].id == 'tdb1'
|
||||
assert inventories[1].id == 'tdb2'
|
||||
assert db.has_schema('tdb2')
|
||||
org_db2 = Organization.query.one()
|
||||
assert org_db2 != org
|
||||
assert org_db2.name == 'ACME DB2'
|
||||
users = User.query.all() # type: List[User]
|
||||
assert users[0].email == 'foo@foo.com'
|
||||
assert users[1].email == 'bar@bar.com'
|
||||
|
||||
# Delete tdb1
|
||||
cli.inv('tdb1')
|
||||
cli.invoke('inv', 'del', '--yes')
|
||||
|
||||
with tdb2.app_context():
|
||||
# There is only tdb2 as inventory
|
||||
inv = Inventory.query.one() # type: Inventory
|
||||
assert inv.id == 'tdb2'
|
||||
# User foo@foo.com is deleted because it only
|
||||
# existed in tdb1, but not bar@bar.com which existed
|
||||
# in another inventory too (tdb2)
|
||||
user = User.query.one() # type: User
|
||||
assert user.email == 'bar@bar.com'
|
||||
assert not db.has_schema('tdb1')
|
||||
assert db.has_schema('tdb2')
|
||||
|
||||
|
||||
def test_create_existing_inventory(cli, tdb1):
|
||||
"""Tries to create twice the same inventory."""
|
||||
cli.inv('tdb1')
|
||||
cli.invoke('inv', 'add', '--common')
|
||||
with tdb1.app_context():
|
||||
assert db.has_schema('tdb1')
|
||||
with pytest.raises(AssertionError, message='Schema tdb1 already exists.'):
|
||||
cli.invoke('inv', 'add', '--common')
|
|
@ -75,6 +75,7 @@ def test_lot_modify_patch_endpoint_and_delete(user: UserClient):
|
|||
l_after, _ = user.get(res=Lot, item=l['id'])
|
||||
assert l_after['name'] == 'bar'
|
||||
assert l_after['description'] == 'bax'
|
||||
user.patch({'description': 'bax'}, res=Lot, item=l['id'], status=204)
|
||||
user.delete(res=Lot, item=l['id'], status=204)
|
||||
user.get(res=Lot, item=l['id'], status=404)
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ Excluded cases in tests
|
|||
-
|
||||
|
||||
"""
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -33,7 +34,7 @@ def test_rate_data_storage_rate():
|
|||
|
||||
data_storage_rate = DataStorageRate().compute([hdd_1969], WorkbenchRate())
|
||||
|
||||
assert round(data_storage_rate, 2) == 4.02, 'DataStorageRate returns incorrect value(rate)'
|
||||
assert math.isclose(data_storage_rate, 4.02, rel_tol=0.001), 'DataStorageRate returns incorrect value(rate)'
|
||||
|
||||
hdd_3054 = HardDrive(size=476940)
|
||||
hdd_3054.events_one.add(BenchmarkDataStorage(read_speed=158, write_speed=34.7))
|
||||
|
@ -41,21 +42,21 @@ def test_rate_data_storage_rate():
|
|||
# calculate DataStorage Rate
|
||||
data_storage_rate = DataStorageRate().compute([hdd_3054], WorkbenchRate())
|
||||
|
||||
assert round(data_storage_rate, 2) == 4.07, 'DataStorageRate returns incorrect value(rate)'
|
||||
assert math.isclose(data_storage_rate, 4.07, rel_tol=0.001), 'DataStorageRate returns incorrect value(rate)'
|
||||
|
||||
hdd_81 = HardDrive(size=76319)
|
||||
hdd_81.events_one.add(BenchmarkDataStorage(read_speed=72.2, write_speed=24.3))
|
||||
|
||||
data_storage_rate = DataStorageRate().compute([hdd_81], WorkbenchRate())
|
||||
|
||||
assert round(data_storage_rate, 2) == 2.61, 'DataStorageRate returns incorrect value(rate)'
|
||||
assert math.isclose(data_storage_rate, 2.61, rel_tol=0.001), 'DataStorageRate returns incorrect value(rate)'
|
||||
|
||||
hdd_1556 = HardDrive(size=152587)
|
||||
hdd_1556.events_one.add(BenchmarkDataStorage(read_speed=78.1, write_speed=24.4))
|
||||
|
||||
data_storage_rate = DataStorageRate().compute([hdd_1556], WorkbenchRate())
|
||||
|
||||
assert round(data_storage_rate, 2) == 3.70, 'DataStorageRate returns incorrect value(rate)'
|
||||
assert math.isclose(data_storage_rate, 3.70, rel_tol=0.001), 'DataStorageRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
def test_rate_data_storage_size_is_null():
|
||||
|
@ -95,7 +96,8 @@ def test_rate_ram_rate():
|
|||
|
||||
ram_rate = RamRate().compute([ram1], WorkbenchRate())
|
||||
|
||||
assert round(ram_rate, 2) == 2.02, 'RamRate returns incorrect value(rate)'
|
||||
# todo rel_tol >= 0.002
|
||||
assert math.isclose(ram_rate, 2.02, rel_tol=0.002), 'RamRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
def test_rate_ram_rate_2modules():
|
||||
|
@ -109,7 +111,7 @@ def test_rate_ram_rate_2modules():
|
|||
|
||||
ram_rate = RamRate().compute([ram1, ram2], WorkbenchRate())
|
||||
|
||||
assert round(ram_rate, 2) == 3.79, 'RamRate returns incorrect value(rate)'
|
||||
assert math.isclose(ram_rate, 3.79, rel_tol=0.001), 'RamRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
def test_rate_ram_rate_4modules():
|
||||
|
@ -125,7 +127,8 @@ def test_rate_ram_rate_4modules():
|
|||
|
||||
ram_rate = RamRate().compute([ram1, ram2, ram3, ram4], WorkbenchRate())
|
||||
|
||||
assert round(ram_rate, 2) == 1.99, 'RamRate returns incorrect value(rate)'
|
||||
# todo rel_tol >= 0.002
|
||||
assert math.isclose(ram_rate, 1.993, rel_tol=0.001), 'RamRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
def test_rate_ram_module_size_is_0():
|
||||
|
@ -149,13 +152,14 @@ def test_rate_ram_speed_is_null():
|
|||
|
||||
ram_rate = RamRate().compute([ram0], WorkbenchRate())
|
||||
|
||||
assert round(ram_rate, 2) == 1.85, 'RamRate returns incorrect value(rate)'
|
||||
assert math.isclose(ram_rate, 1.85, rel_tol=0.002), 'RamRate returns incorrect value(rate)'
|
||||
|
||||
ram0 = RamModule(size=1024, speed=None)
|
||||
|
||||
ram_rate = RamRate().compute([ram0], WorkbenchRate())
|
||||
|
||||
assert round(ram_rate, 2) == 1.25, 'RamRate returns incorrect value(rate)'
|
||||
# todo rel_tol >= 0.004
|
||||
assert math.isclose(ram_rate, 1.25, rel_tol=0.004), 'RamRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
def test_rate_no_ram_module():
|
||||
|
@ -182,7 +186,7 @@ def test_rate_processor_rate():
|
|||
|
||||
processor_rate = ProcessorRate().compute(cpu, WorkbenchRate())
|
||||
|
||||
assert processor_rate == 1, 'ProcessorRate returns incorrect value(rate)'
|
||||
assert math.isclose(processor_rate, 1, rel_tol=0.001), 'ProcessorRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
def test_rate_processor_rate_2cores():
|
||||
|
@ -197,31 +201,31 @@ def test_rate_processor_rate_2cores():
|
|||
|
||||
processor_rate = ProcessorRate().compute(cpu, WorkbenchRate())
|
||||
|
||||
assert round(processor_rate, 2) == 3.95, 'ProcessorRate returns incorrect value(rate)'
|
||||
assert math.isclose(processor_rate, 3.95, rel_tol=0.001), 'ProcessorRate returns incorrect value(rate)'
|
||||
|
||||
cpu = Processor(cores=2, speed=3.3)
|
||||
cpu.events_one.add(BenchmarkProcessor(rate=26339.48))
|
||||
|
||||
processor_rate = ProcessorRate().compute(cpu, WorkbenchRate())
|
||||
|
||||
assert round(processor_rate, 2) == 3.93, 'ProcessorRate returns incorrect value(rate)'
|
||||
# todo rel_tol >= 0.002
|
||||
assert math.isclose(processor_rate, 3.93, rel_tol=0.002), 'ProcessorRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Debug test')
|
||||
def test_rate_processor_with_null_cores():
|
||||
"""
|
||||
Test with processor device have null number of cores
|
||||
"""
|
||||
cpu = Processor(cores=None, speed=3.3)
|
||||
cpu.events_one.add(BenchmarkProcessor(rate=0))
|
||||
# todo try without BenchmarkProcessor, StopIteration problem
|
||||
cpu.events_one.add(BenchmarkProcessor())
|
||||
|
||||
processor_rate = ProcessorRate().compute(cpu, WorkbenchRate())
|
||||
|
||||
# todo result is not 1 != 1.376 .. check what's wrong
|
||||
assert processor_rate == 1, 'ProcessorRate returns incorrect value(rate)'
|
||||
# todo rel_tol >= 0.003
|
||||
assert math.isclose(processor_rate, 1.38, rel_tol=0.003), 'ProcessorRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Debug test')
|
||||
def test_rate_processor_with_null_speed():
|
||||
"""
|
||||
Test with processor device have null speed value
|
||||
|
@ -231,7 +235,7 @@ def test_rate_processor_with_null_speed():
|
|||
|
||||
processor_rate = ProcessorRate().compute(cpu, WorkbenchRate())
|
||||
|
||||
assert round(processor_rate, 2) == 1.06, 'ProcessorRate returns incorrect value(rate)'
|
||||
assert math.isclose(processor_rate, 1.06, rel_tol=0.001), 'ProcessorRate returns incorrect value(rate)'
|
||||
|
||||
|
||||
def test_rate_computer_rate():
|
||||
|
@ -325,13 +329,13 @@ def test_rate_computer_rate():
|
|||
# Compute all components rates and general rating
|
||||
Rate().compute(pc_test, rate_pc)
|
||||
|
||||
assert round(rate_pc.ram, 2) == 3.79
|
||||
assert math.isclose(rate_pc.ram, 3.79, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.data_storage, 2) == 4.02
|
||||
assert math.isclose(rate_pc.data_storage, 4.02, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.processor, 2) == 3.95
|
||||
assert math.isclose(rate_pc.processor, 3.95, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.rating, 2) == 4.61
|
||||
assert math.isclose(rate_pc.rating, 4.61, rel_tol=0.001)
|
||||
|
||||
# Create a new Computer with components characteristics of pc with id = 1201
|
||||
pc_test = Desktop(chassis=ComputerChassis.Tower)
|
||||
|
@ -350,13 +354,13 @@ def test_rate_computer_rate():
|
|||
# Compute all components rates and general rating
|
||||
Rate().compute(pc_test, rate_pc)
|
||||
|
||||
assert round(rate_pc.ram, 2) == 2.02
|
||||
assert math.isclose(rate_pc.ram, 2.02, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.data_storage, 2) == 4.07
|
||||
assert math.isclose(rate_pc.data_storage, 4.07, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.processor, 2) == 3.93
|
||||
assert math.isclose(rate_pc.processor, 3.93, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.rating, 2) == 3.48
|
||||
assert math.isclose(rate_pc.rating, 3.48, rel_tol=0.001)
|
||||
|
||||
# Create a new Computer with components characteristics of pc with id = 79
|
||||
pc_test = Desktop(chassis=ComputerChassis.Tower)
|
||||
|
@ -378,13 +382,13 @@ def test_rate_computer_rate():
|
|||
# Compute all components rates and general rating
|
||||
Rate().compute(pc_test, rate_pc)
|
||||
|
||||
assert round(rate_pc.ram, 2) == 1.99
|
||||
assert math.isclose(rate_pc.ram, 1.99, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.data_storage, 2) == 2.61
|
||||
assert math.isclose(rate_pc.data_storage, 2.61, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.processor, 2) == 1
|
||||
assert math.isclose(rate_pc.processor, 1, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.rating, 2) == 1.58
|
||||
assert math.isclose(rate_pc.rating, 1.58, rel_tol=0.001)
|
||||
|
||||
# Create a new Computer with components characteristics of pc with id = 798
|
||||
pc_test = Desktop(chassis=ComputerChassis.Tower)
|
||||
|
@ -403,13 +407,13 @@ def test_rate_computer_rate():
|
|||
# Compute all components rates and general rating
|
||||
Rate().compute(pc_test, rate_pc)
|
||||
|
||||
assert round(rate_pc.ram, 2) == 1
|
||||
assert math.isclose(rate_pc.ram, 1, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.data_storage, 2) == 3.7
|
||||
assert math.isclose(rate_pc.data_storage, 3.7, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.processor, 2) == 4.09
|
||||
assert math.isclose(rate_pc.processor, 4.09, rel_tol=0.001)
|
||||
|
||||
assert round(rate_pc.rating, 2) == 2.5
|
||||
assert math.isclose(rate_pc.rating, 2.5, rel_tol=0.001)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Data Storage rate actually requires a DSSBenchmark')
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from datetime import datetime, timedelta, timezone
|
||||
from operator import itemgetter
|
||||
from typing import List, Tuple
|
||||
from uuid import uuid4
|
||||
|
||||
|
@ -12,7 +13,8 @@ from ereuse_devicehub.db import db
|
|||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.resources.device import models as m
|
||||
from ereuse_devicehub.resources.device.exceptions import NeedsId
|
||||
from ereuse_devicehub.resources.device.sync import MismatchBetweenTagsAndHid
|
||||
from ereuse_devicehub.resources.device.sync import MismatchBetweenProperties, \
|
||||
MismatchBetweenTagsAndHid
|
||||
from ereuse_devicehub.resources.enums import ComputerChassis, SnapshotSoftware
|
||||
from ereuse_devicehub.resources.event.models import AggregateRate, BenchmarkProcessor, \
|
||||
EraseSectors, Event, Snapshot, SnapshotRequest, WorkbenchRate
|
||||
|
@ -78,13 +80,17 @@ def test_snapshot_post(user: UserClient):
|
|||
assert 'events' not in snapshot['device']
|
||||
assert 'author' not in snapshot['device']
|
||||
device, _ = user.get(res=m.Device, item=snapshot['device']['id'])
|
||||
key = itemgetter('serialNumber')
|
||||
snapshot['components'].sort(key=key)
|
||||
device['components'].sort(key=key)
|
||||
assert snapshot['components'] == device['components']
|
||||
|
||||
assert tuple(c['type'] for c in snapshot['components']) == (m.GraphicCard.t, m.RamModule.t,
|
||||
m.Processor.t)
|
||||
assert {c['type'] for c in snapshot['components']} == {m.GraphicCard.t, m.RamModule.t,
|
||||
m.Processor.t}
|
||||
rate = next(e for e in snapshot['events'] if e['type'] == WorkbenchRate.t)
|
||||
rate, _ = user.get(res=Event, item=rate['id'])
|
||||
assert rate['device']['id'] == snapshot['device']['id']
|
||||
rate['components'].sort(key=key)
|
||||
assert rate['components'] == snapshot['components']
|
||||
assert rate['snapshot']['id'] == snapshot['id']
|
||||
|
||||
|
@ -246,10 +252,8 @@ def test_snapshot_tag_inner_tag_mismatch_between_tags_and_hid(user: UserClient,
|
|||
user.post(pc2, res=Snapshot, status=MismatchBetweenTagsAndHid)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='There is no attribute checking for tag-matching devices')
|
||||
def test_snapshot_different_properties_same_tags(user: UserClient, tag_id: str):
|
||||
"""
|
||||
Tests a snapshot performed to device 1 with tag A and then to
|
||||
"""Tests a snapshot performed to device 1 with tag A and then to
|
||||
device 2 with tag B. Both don't have HID but are different type.
|
||||
Devicehub must fail the Snapshot.
|
||||
"""
|
||||
|
@ -262,9 +266,9 @@ def test_snapshot_different_properties_same_tags(user: UserClient, tag_id: str):
|
|||
pc2 = file('basic.snapshot')
|
||||
pc2['uuid'] = uuid4()
|
||||
pc2['device']['tags'] = pc1['device']['tags']
|
||||
del pc2['device'][
|
||||
'model'] # pc2 model is unknown but pc1 model is set = different characteristic
|
||||
user.post(pc2, res=Snapshot, status=422)
|
||||
# pc2 model is unknown but pc1 model is set = different property
|
||||
del pc2['device']['model']
|
||||
user.post(pc2, res=Snapshot, status=MismatchBetweenProperties)
|
||||
|
||||
|
||||
def test_snapshot_upload_twice_uuid_error(user: UserClient):
|
||||
|
@ -289,12 +293,14 @@ def test_snapshot_component_containing_components(user: UserClient):
|
|||
user.post(s, res=Snapshot, status=ValidationError)
|
||||
|
||||
|
||||
def test_erase_privacy(user: UserClient):
|
||||
def test_erase_privacy_standards(user: UserClient):
|
||||
"""Tests a Snapshot with EraseSectors and the resulting
|
||||
privacy properties.
|
||||
"""
|
||||
s = file('erase-sectors.snapshot')
|
||||
assert '2018-06-01T09:12:06+02:00' == s['components'][0]['events'][0]['endTime']
|
||||
snapshot = snapshot_and_check(user, s, (EraseSectors.t,), perform_second_snapshot=True)
|
||||
assert '2018-06-01T07:12:06+00:00' == snapshot['events'][0]['endTime']
|
||||
storage, *_ = snapshot['components']
|
||||
assert storage['type'] == 'SolidStateDrive', 'Components must be ordered by input order'
|
||||
storage, _ = user.get(res=m.Device, item=storage['id']) # Let's get storage events too
|
||||
|
@ -302,18 +308,24 @@ def test_erase_privacy(user: UserClient):
|
|||
erasure1, _snapshot1, erasure2, _snapshot2 = storage['events']
|
||||
assert erasure1['type'] == erasure2['type'] == 'EraseSectors'
|
||||
assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot'
|
||||
assert snapshot == user.get(res=Event, item=_snapshot2['id'])[0]
|
||||
get_snapshot, _ = user.get(res=Event, item=_snapshot2['id'])
|
||||
assert get_snapshot['events'][0]['endTime'] == '2018-06-01T07:12:06+00:00'
|
||||
assert snapshot == get_snapshot
|
||||
erasure, _ = user.get(res=Event, item=erasure1['id'])
|
||||
assert len(erasure['steps']) == 2
|
||||
assert erasure['steps'][0]['startTime'] == '2018-06-01T08:15:00+00:00'
|
||||
assert erasure['steps'][0]['endTime'] == '2018-06-01T09:16:00+00:00'
|
||||
assert erasure['steps'][1]['startTime'] == '2018-06-01T08:16:00+00:00'
|
||||
assert erasure['steps'][1]['endTime'] == '2018-06-01T09:17:00+00:00'
|
||||
assert erasure['steps'][0]['startTime'] == '2018-06-01T06:15:00+00:00'
|
||||
assert erasure['steps'][0]['endTime'] == '2018-06-01T07:16:00+00:00'
|
||||
assert erasure['steps'][1]['startTime'] == '2018-06-01T06:16:00+00:00'
|
||||
assert erasure['steps'][1]['endTime'] == '2018-06-01T07:17:00+00:00'
|
||||
assert erasure['device']['id'] == storage['id']
|
||||
for step in erasure['steps']:
|
||||
assert step['type'] == 'StepZero'
|
||||
assert step['severity'] == 'Info'
|
||||
assert 'num' not in step
|
||||
step1, step2 = erasure['steps']
|
||||
assert step1['type'] == 'StepZero'
|
||||
assert step1['severity'] == 'Info'
|
||||
assert 'num' not in step1
|
||||
assert step2['type'] == 'StepRandom'
|
||||
assert step2['severity'] == 'Info'
|
||||
assert 'num' not in step2
|
||||
assert ['HMG_IS5'] == erasure['standards']
|
||||
assert storage['privacy']['type'] == 'EraseSectors'
|
||||
pc, _ = user.get(res=m.Device, item=snapshot['device']['id'])
|
||||
assert pc['privacy'] == [storage['privacy']]
|
||||
|
@ -323,7 +335,7 @@ def test_erase_privacy(user: UserClient):
|
|||
s['components'][0]['events'][0]['severity'] = 'Error'
|
||||
snapshot, _ = user.post(s, res=Snapshot)
|
||||
storage, _ = user.get(res=m.Device, item=storage['id'])
|
||||
assert storage['hid'] == 'c1mr-c1s-c1ml'
|
||||
assert storage['hid'] == 'solidstatedrive-c1mr-c1ml-c1s'
|
||||
assert storage['privacy']['type'] == 'EraseSectors'
|
||||
pc, _ = user.get(res=m.Device, item=snapshot['device']['id'])
|
||||
assert pc['privacy'] == [storage['privacy']]
|
||||
|
@ -346,10 +358,12 @@ def test_snapshot_computer_monitor(user: UserClient):
|
|||
# todo check that ManualRate has generated an AggregateRate
|
||||
|
||||
|
||||
def test_snapshot_mobile_smartphone(user: UserClient):
|
||||
def test_snapshot_mobile_smartphone_imei_manual_rate(user: UserClient):
|
||||
s = file('smartphone.snapshot')
|
||||
snapshot_and_check(user, s, event_types=('ManualRate',))
|
||||
# todo check that ManualRate has generated an AggregateRate
|
||||
snapshot = snapshot_and_check(user, s, event_types=('ManualRate',))
|
||||
mobile, _ = user.get(res=m.Device, item=snapshot['device']['id'])
|
||||
assert mobile['imei'] == 3568680000414120
|
||||
# todo check that manual rate has been created
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Test not developed')
|
||||
|
@ -385,6 +399,9 @@ def assert_similar_components(components1: List[dict], components2: List[dict]):
|
|||
similar than the components in components2.
|
||||
"""
|
||||
assert len(components1) == len(components2)
|
||||
key = itemgetter('serialNumber')
|
||||
components1.sort(key=key)
|
||||
components2.sort(key=key)
|
||||
for c1, c2 in zip(components1, components2):
|
||||
assert_similar_device(c1, c2)
|
||||
|
||||
|
@ -436,3 +453,14 @@ def test_snapshot_keyboard(user: UserClient):
|
|||
snapshot = snapshot_and_check(user, s, event_types=('ManualRate',))
|
||||
keyboard = snapshot['device']
|
||||
assert keyboard['layout'] == 'ES'
|
||||
|
||||
|
||||
def test_pc_rating_rate_none(user: UserClient):
|
||||
"""Tests a Snapshot with EraseSectors."""
|
||||
s = file('desktop-9644w8n-lenovo-0169622.snapshot')
|
||||
snapshot, _ = user.post(res=Snapshot, data=s)
|
||||
|
||||
|
||||
def test_pc_2(user: UserClient):
|
||||
s = file('laptop-hp_255_g3_notebook-hewlett-packard-cnd52270fw.snapshot')
|
||||
snapshot, _ = user.post(res=Snapshot, data=s)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import pathlib
|
||||
|
||||
import pytest
|
||||
import requests_mock
|
||||
from boltons.urlutils import URL
|
||||
from ereuse_utils.session import DevicehubClient
|
||||
from pytest import raises
|
||||
from teal.db import MultipleResourcesFound, ResourceNotFound, UniqueViolation
|
||||
from teal.marshmallow import ValidationError
|
||||
|
@ -135,7 +137,7 @@ def test_tag_get_device_from_tag_endpoint_multiple_tags(app: Devicehub, user: Us
|
|||
def test_tag_create_tags_cli(app: Devicehub, user: UserClient):
|
||||
"""Checks creating tags with the CLI endpoint."""
|
||||
runner = app.test_cli_runner()
|
||||
runner.invoke(args=['create-tag', 'id1'], catch_exceptions=False)
|
||||
runner.invoke('tag', 'add', 'id1')
|
||||
with app.app_context():
|
||||
tag = Tag.query.one() # type: Tag
|
||||
assert tag.id == 'id1'
|
||||
|
@ -146,8 +148,7 @@ def test_tag_create_etags_cli(app: Devicehub, user: UserClient):
|
|||
"""Creates an eTag through the CLI."""
|
||||
# todo what happens to organization?
|
||||
runner = app.test_cli_runner()
|
||||
runner.invoke(args=['create-tag', '-p', 'https://t.ereuse.org', '-s', 'foo', 'DT-BARBAR'],
|
||||
catch_exceptions=False)
|
||||
runner.invoke('tag', 'add', '-p', 'https://t.ereuse.org', '-s', 'foo', 'DT-BARBAR')
|
||||
with app.app_context():
|
||||
tag = Tag.query.one() # type: Tag
|
||||
assert tag.id == 'dt-barbar'
|
||||
|
@ -220,8 +221,7 @@ def test_tag_create_tags_cli_csv(app: Devicehub, user: UserClient):
|
|||
"""Checks creating tags with the CLI endpoint using a CSV."""
|
||||
csv = pathlib.Path(__file__).parent / 'files' / 'tags-cli.csv'
|
||||
runner = app.test_cli_runner()
|
||||
runner.invoke(args=['create-tags-csv', str(csv)],
|
||||
catch_exceptions=False)
|
||||
runner.invoke('tag', 'add-csv', str(csv))
|
||||
with app.app_context():
|
||||
t1 = Tag.from_an_id('id1').one()
|
||||
t2 = Tag.from_an_id('sec1').one()
|
||||
|
@ -232,3 +232,57 @@ def test_tag_multiple_secondary_org(user: UserClient):
|
|||
"""Ensures two secondary ids cannot be part of the same Org."""
|
||||
user.post({'id': 'foo', 'secondary': 'bar'}, res=Tag)
|
||||
user.post({'id': 'foo1', 'secondary': 'bar'}, res=Tag, status=UniqueViolation)
|
||||
|
||||
|
||||
def test_crate_num_regular_tags(user: UserClient, requests_mock: requests_mock.mocker.Mocker):
|
||||
"""Create regular tags. This is done using a tag provider that
|
||||
returns IDs. These tags are printable.
|
||||
"""
|
||||
requests_mock.post('https://example.com/',
|
||||
# request
|
||||
request_headers={
|
||||
'Authorization': 'Basic {}'.format(DevicehubClient.encode_token(
|
||||
'52dacef0-6bcb-4919-bfed-f10d2c96ecee'))
|
||||
},
|
||||
# response
|
||||
json=['tag1id', 'tag2id'],
|
||||
status_code=201)
|
||||
data, _ = user.post({}, res=Tag, query=[('num', 2)])
|
||||
assert data['items'][0]['id'] == 'tag1id'
|
||||
assert data['items'][0]['printable'], 'Tags made this way are printable'
|
||||
assert data['items'][1]['id'] == 'tag2id'
|
||||
assert data['items'][1]['printable']
|
||||
|
||||
|
||||
def test_get_tags_endpoint(user: UserClient, app: Devicehub,
|
||||
requests_mock: requests_mock.mocker.Mocker):
|
||||
"""Performs GET /tags after creating 3 tags, 2 printable and one
|
||||
not. Only the printable ones are returned.
|
||||
"""
|
||||
# Prepare test
|
||||
with app.app_context():
|
||||
org = Organization(name='bar', tax_id='bartax')
|
||||
tag = Tag(id='bar-1', org=org, provider=URL('http://foo.bar'))
|
||||
db.session.add(tag)
|
||||
db.session.commit()
|
||||
assert not tag.printable
|
||||
|
||||
requests_mock.post('https://example.com/',
|
||||
# request
|
||||
request_headers={
|
||||
'Authorization': 'Basic {}'.format(DevicehubClient.encode_token(
|
||||
'52dacef0-6bcb-4919-bfed-f10d2c96ecee'))
|
||||
},
|
||||
# response
|
||||
json=['tag1id', 'tag2id'],
|
||||
status_code=201)
|
||||
user.post({}, res=Tag, query=[('num', 2)])
|
||||
|
||||
# Test itself
|
||||
data, _ = user.get(res=Tag)
|
||||
assert len(data['items']) == 2, 'Only 2 tags are printable, thus retreived'
|
||||
# Order is created descending
|
||||
assert data['items'][0]['id'] == 'tag2id'
|
||||
assert data['items'][0]['printable']
|
||||
assert data['items'][1]['id'] == 'tag1id'
|
||||
assert data['items'][1]['printable'], 'Tags made this way are printable'
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from base64 import b64decode
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
@ -7,6 +6,7 @@ from teal.enums import Country
|
|||
from teal.marshmallow import ValidationError
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from ereuse_devicehub import auth
|
||||
from ereuse_devicehub.client import Client
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
|
@ -74,14 +74,15 @@ def test_login_success(client: Client, app: Devicehub):
|
|||
with app.app_context():
|
||||
create_user()
|
||||
user, _ = client.post({'email': 'foo@foo.com', 'password': 'foo'},
|
||||
uri='/users/login',
|
||||
uri='/users/login/',
|
||||
status=200)
|
||||
assert user['email'] == 'foo@foo.com'
|
||||
assert UUID(b64decode(user['token'].encode()).decode()[:-1])
|
||||
assert UUID(auth.Auth.decode(user['token']))
|
||||
assert 'password' not in user
|
||||
assert user['individuals'][0]['name'] == 'Timmy'
|
||||
assert user['individuals'][0]['type'] == 'Person'
|
||||
assert len(user['individuals']) == 1
|
||||
assert user['inventories'][0]['id'] == 'test'
|
||||
|
||||
|
||||
def test_login_failure(client: Client, app: Devicehub):
|
||||
|
@ -90,12 +91,17 @@ def test_login_failure(client: Client, app: Devicehub):
|
|||
with app.app_context():
|
||||
create_user()
|
||||
client.post({'email': 'foo@foo.com', 'password': 'wrong pass'},
|
||||
uri='/users/login',
|
||||
uri='/users/login/',
|
||||
status=WrongCredentials)
|
||||
# Wrong URI
|
||||
client.post({}, uri='/wrong-uri', status=NotFound)
|
||||
# Malformed data
|
||||
client.post({}, uri='/users/login', status=ValidationError)
|
||||
client.post({}, uri='/users/login/', status=ValidationError)
|
||||
client.post({'email': 'this is not an email', 'password': 'nope'},
|
||||
uri='/users/login',
|
||||
uri='/users/login/',
|
||||
status=ValidationError)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Test not developed')
|
||||
def test_user_at_least_one_inventory():
|
||||
pass
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue